From 5c38e5cf0b611ea3ff8fbae05c256c3342fdcd2a Mon Sep 17 00:00:00 2001 From: mdickson-adbe <95774602+mdickson-adbe@users.noreply.github.com> Date: Mon, 3 Jun 2024 14:53:42 -0400 Subject: [PATCH] Holistic Campaign View (HCV) Dashboard (#104) * ASSETS-88894 GRAPHQL Persisted Query Code (#53) * GRAPHQL POC Demo code to call a Persisted GraphQL query for Content Fragments * Updated the code call the GraphQL Persisted query from the QA AEM Author server, and also to authenticate using the JWT Bearer token * Renamed file test-graphql.js to graphql.js Added function graphqlAllCampaign() to get call GraphQL persisted query for All Campains Added function graphqlCampaignByName(campaignName) to get call GraphQL persisted query to get campaign by campaign name. Added function getGraphqlEndpoint() to get the value of property aemGraphqlEndpoint from the config file admin-config.json * Changed queryName to getAllCampaings * Rollback to 3/15/24 Commit a519a5155c3dd57f0c54ea8bd336cd0a231cffab before adding GraphQL test code * Delete test graphql code * ASSETS-88895 : Show HCV report pages to limited users (#55) * security.js updated function checkUserAccess for users that are members of the imsUserGroup, that if a page has the property reporting-access then access to the page is only granted if the user is member of te group defined by the property imsReportingGroup in the file admin-config.xlsx site-config.js : Updated the function getQuickLinkConfig to show the quick link to users who are members of the new column Group in shared-quicklinks tab of file site-config.xlsx * securiity.js : Deleted function isReportingAccessPage() as it is not needed any more. Updated function checkUserAccess() : Update code for non public pages, check if the current page path is in the pages returned by the function getQuickLinks. If it does not exist in quick links, then the user does not have access to the page. The functionQuickLinks already has logic to check if a page is only accessible by users of a page group. Renamed function checkGroupAccess(adminConfigGroupPropertyName) to checkPageGroupAccess(adminConfigGroupPropertyName) * Campaign List block for Marketing Dashboard page (#56) * initial commit, new blocks/files * rename files, start implementing * continuing to implement/adjust styling * numerous changes - finish building bones of list component - build bones of pagination/footer - add icons for products (will likely need more) - build javascript for pagination (cleanup needed) - build javascript for sorting list --------- Co-authored-by: Michael Dickson * updated hydration-utils.js (#54) MH: Added Firefly product to AA Modal 'Product' Field List * Assets 98990 - Dynamic campaign list (#61) * initial commit, new blocks/files * rename files, start implementing * continuing to implement/adjust styling * numerous changes - finish building bones of list component - build bones of pagination/footer - add icons for products (will likely need more) - build javascript for pagination (cleanup needed) - build javascript for sorting list * finish pagination * Updates to css/js to better align to mockup * ASSETS-88895 : Updated function getQuickLinkConfig() to still work if the row.Group is undefined or does not exist (#60) * security.js updated function checkUserAccess for users that are members of the imsUserGroup, that if a page has the property reporting-access then access to the page is only granted if the user is member of te group defined by the property imsReportingGroup in the file admin-config.xlsx site-config.js : Updated the function getQuickLinkConfig to show the quick link to users who are members of the new column Group in shared-quicklinks tab of file site-config.xlsx * securiity.js : Deleted function isReportingAccessPage() as it is not needed any more. Updated function checkUserAccess() : Update code for non public pages, check if the current page path is in the pages returned by the function getQuickLinks. If it does not exist in quick links, then the user does not have access to the page. The functionQuickLinks already has logic to check if a page is only accessible by users of a page group. Renamed function checkGroupAccess(adminConfigGroupPropertyName) to checkPageGroupAccess(adminConfigGroupPropertyName) * Updated function getQuickLinkConfig() to still work if the row.Group is undefined or does not exist * Added function export async function graphqlFilterOnMarketingInitiative(marketingInitiative) (#64) Which calls the persisted query gmo/filter-on-marketing-initiative with example parameters { "marketingInitiative": "FY24Q1-Q2_AdobeExpress_level Up" } https://author-p108396-e1046543.adobeaemcloud.com/graphql/execute.json/gmo/filter-on-marketing-initiative%3BmarketingInitiative=FY24Q1-Q2_AdobeExpress_level%20Up * Campaign List filters display (#65) * initial html structure setup * refactor from select to div implementation Select with multiselect displays as a list, not a dropdown * finish refactor, enable visual functionality - added all javascript needed for visual functionality - finished refactoring structure * Assets 98990 (#66) * initial commit, new blocks/files * rename files, start implementing * continuing to implement/adjust styling * numerous changes - finish building bones of list component - build bones of pagination/footer - add icons for products (will likely need more) - build javascript for pagination (cleanup needed) - build javascript for sorting list * finish pagination * Updates to css/js to better align to mockup * minor updates - update graphql query to have hardcoded offset/limit (to resolve error) - remove comments - add placeholder 'refresh date' message and css - move css for main body to accomodate above message * add firefly icon --------- Co-authored-by: Tyrone Tse * ASSETS-88899 : [Issue] Collection Detail Page is Redirecting to No-Access Page (#68) * Added function hideQuickLinks() * Added the hide field to the shared-quicklinks config/worksheet /blocks/adp-header/adp-header.js : Updated the code to only show links if hide!=='true' /scripts/security.js : Updated function checkUserAccess to get the current URL without the parameters for /collection/ and /share/ /scripts/site-config.js : Updated function to include the hide field * Refactored the If statement logic * Assets 98992- Campaign details/overview (#69) * initial block structure * start mocking up structure * many changes - finish overview tab mockup - (mostly) finish deliverables tab * fix css- add top/bottom borders to rows * delete invalid metadata from new svgs * updates for mvp - Rename and hide various elements of page - Refactor tablebuilding function to deal with items that are missing categorization properties * hide total assets * ASSETS-88900 : Update Pagination for gmo-campiagn-list Block (#70) * gmo-campagin-list refactored to use persisted query function function graphqlCampaignPaginated(first,cursor) to get a page of Campaigns at a time * Refactored the Navigation code for Previous Page, Next Page, Select Number of Items on a Page. Refactored the code for Repaginate for when the number of items page is changed Show current page status * graphql.js : Updated function graphqlAllCampaigns(first,cursor) to call getAllCampaigns persisted query with parameters first and cursor Deleted function graphqlCampaignPaginated(first,cursor) Updated gmo-campaign-list/gmo-campaign-list.js : To use persisted query graphqlAllCampaigns(first,cursor) and refactored the pagination to call the decorate function to call the graphql query each time to get the next or previous page * Fixed logic in calculating the nextCursor * Removed all debug console.log statements * Removed testCampaigns array of test campaign data, which now has been replaced by data from graphql * Renamed the variable testConfig to headerConfig * Campaign details block (#71) * initial block structure * start mocking up structure * many changes - finish overview tab mockup - (mostly) finish deliverables tab * fix css- add top/bottom borders to rows * delete invalid metadata from new svgs * updates for mvp - Rename and hide various elements of page - Refactor tablebuilding function to deal with items that are missing categorization properties * hide total assets * add status bar display Also adjusted demo data slightly to include due date and lead * update css/html to align with mockup and mvp * resolve border bug * implemented requested changes * ASSETS-88901 : (Backend) Campaign Page: Filters (#72) * graphql.js : Added functions graphqlProductList and graphqlStatusList gmo-campaign-header.js : Updated code to build Status dropdown from function graphqlStatusList Updated code to build Product dropdown from function graphqlProductList * /blocks/gmo-campaign-list/gmo-campaign-list.js => Added custom event listener for custom event gmoCampaignListBlock which allows the event gmoCampaignListBlock to be called from the block component gmo-campaign-header.js /blocks/gmo-campaign-header/gmo-campaign-header.js : Created new function sendGmoCampaignListBlockEvent() that calls the custom event gmoCampaignListBlock from the gmo-campaign-header. This will trigger the custom event in the /blocks/gmo-campaign-list to call the graphql persisted query getAllCampaigns with a filter based on the selected values from the dropdown lists for Business Line Status and Product * graphql.js : Added test function graphAllCampaignsFilterfirst,cursor,filter) : Which has the filter parameter which is the graphQL filter object Added function generateFilterJSON(filterParams) which generates the graphQL filter object from an Array of parameters. /blocks/gmo-campaign-header/gmo-campaign-header.js : Added event on the campaign-search field, to trigger the sendGmoCampaignBlockEvent after 3 characters are typed, later this will be replaced by a Campaign Suggested List generated as the user types /blocks/gmo-campaign-list/gmo-campaign-list.js : Added code build the graphQL filter object from the Campaign Search fields and Drop Downs Updated the function decorate to have new parameter graphQLFilter So that the function graphqlAllCampaignsFilter(numPerPage, cursor,graphQLFilter) can be called * Adde autocomplete list CSS, JS and HMTL for the campaign search field. * graphql.js Update function graphCampaignByName to use persisted query getCampaignNames used for autocomplete list for Campaign Name search gmo-campaign-list.js : Changed campaignName search to use filter operater : '=' /gmo-campaign-header.js : Updated campaign search autocompleteList to trigger sendCampaignListBlockEvent to make autocomplete search work * gmo-campaign-header.js : Updated function resetAllFilters() to call function sendGmoCampaignListBlockEvent(); gmo-campaign-list.js : Updated campaignCount to call function graphqlCampaignCount(graphqlFilter) with graphqlFilter graphql.js : Updated function graphCampaignCount : Added filter parameter, and call persisted query getCampaignNameFilter * Removed console.log messages * Fixed bug with currentPageInfo.nextCursor * Fixed bug in calculation of cursor to use for the previous page logic * graphql.js : Deleted function graphqlAllCampaigns(first,cursor) , replaced function graphqlStatusList() and graphqlProductList() with function graphqlQueryNameList(queryNameList) Variables baseApiUrl and projectId are now global/class level variables. gmo-campaign-header.js : Close dropdown list when a value is selected * update campaign with program --------- Co-authored-by: Shivani gupta * ASSETS-88901 : Campaign Header: Dropdown List Only Allow Single Value to Be Selected (#73) * graphql.js : Added functions graphqlProductList and graphqlStatusList gmo-campaign-header.js : Updated code to build Status dropdown from function graphqlStatusList Updated code to build Product dropdown from function graphqlProductList * /blocks/gmo-campaign-list/gmo-campaign-list.js => Added custom event listener for custom event gmoCampaignListBlock which allows the event gmoCampaignListBlock to be called from the block component gmo-campaign-header.js /blocks/gmo-campaign-header/gmo-campaign-header.js : Created new function sendGmoCampaignListBlockEvent() that calls the custom event gmoCampaignListBlock from the gmo-campaign-header. This will trigger the custom event in the /blocks/gmo-campaign-list to call the graphql persisted query getAllCampaigns with a filter based on the selected values from the dropdown lists for Business Line Status and Product * graphql.js : Added test function graphAllCampaignsFilterfirst,cursor,filter) : Which has the filter parameter which is the graphQL filter object Added function generateFilterJSON(filterParams) which generates the graphQL filter object from an Array of parameters. /blocks/gmo-campaign-header/gmo-campaign-header.js : Added event on the campaign-search field, to trigger the sendGmoCampaignBlockEvent after 3 characters are typed, later this will be replaced by a Campaign Suggested List generated as the user types /blocks/gmo-campaign-list/gmo-campaign-list.js : Added code build the graphQL filter object from the Campaign Search fields and Drop Downs Updated the function decorate to have new parameter graphQLFilter So that the function graphqlAllCampaignsFilter(numPerPage, cursor,graphQLFilter) can be called * Adde autocomplete list CSS, JS and HMTL for the campaign search field. * graphql.js Update function graphCampaignByName to use persisted query getCampaignNames used for autocomplete list for Campaign Name search gmo-campaign-list.js : Changed campaignName search to use filter operater : '=' /gmo-campaign-header.js : Updated campaign search autocompleteList to trigger sendCampaignListBlockEvent to make autocomplete search work * gmo-campaign-header.js : Updated function resetAllFilters() to call function sendGmoCampaignListBlockEvent(); gmo-campaign-list.js : Updated campaignCount to call function graphqlCampaignCount(graphqlFilter) with graphqlFilter graphql.js : Updated function graphCampaignCount : Added filter parameter, and call persisted query getCampaignNameFilter * Removed console.log messages * Fixed bug with currentPageInfo.nextCursor * Fixed bug in calculation of cursor to use for the previous page logic * graphql.js : Deleted function graphqlAllCampaigns(first,cursor) , replaced function graphqlStatusList() and graphqlProductList() with function graphqlQueryNameList(queryNameList) Variables baseApiUrl and projectId are now global/class level variables. gmo-campaign-header.js : Close dropdown list when a value is selected * update campaign with program * Updated function toggleOption : To only allow a single option to be selected in the dropdown list --------- Co-authored-by: Shivani gupta * Assets 98993 (#75) * many updates * finish dynamic properties - all properties should be dynamic based on graphql data - refactored some common lookups to a shared javascript file - updated overflow for overview/description - made status, products more presentable - more elegant handling of empty audience and kpi lists * test rename svg to resolve issue * finish updating icon names and mapping * resolve pr comments * ASSETS-88902 : Add Target Geo Filter in the Landing Page (update all dropdown lists to use updated graphql queries) (#77) * Add hardcoded Geo(graphy) dropdown list filter to Campaign Header * Fixed Previous Page pagination logic for calculating the cursor for the Previous page * Updated Products and Status Dropdown Lists code to use updated graphql persisted queries * Assets 98994 (#81) * many updates * finish dynamic properties - all properties should be dynamic based on graphql data - refactored some common lookups to a shared javascript file - updated overflow for overview/description - made status, products more presentable - more elegant handling of empty audience and kpi lists * test rename svg to resolve issue * finish updating icon names and mapping * initial changeover from static to dynamic data * adjustments based on feedback * additional changes based on feedback * minor bugfix, null checks * squash final bug with read more * final touches * combine two graphql functions with duplicated code * bugfixes per pr review * ASSETS-88902 : Make the Business Line Dropdown List filter values in the Product Dropdown List filter (#82) * Add hardcoded Geo(graphy) dropdown list filter to Campaign Header * Fixed Previous Page pagination logic for calculating the cursor for the Previous page * Updated Products and Status Dropdown Lists code to use updated graphql persisted queries * Updated Business Line dropdown list to be populated by graphql persisted query getBusinessLine. Updated Geo dropdown list to be populated by graphql persisted query getGeoList * Updated the Business Line dropdown list to filter the Products List when a Business Line is selected. When a Business Line option is deselected then the Products List shows all products. * Reduced the sample dropdown list options * make links configurable, remove extraneous logs (#83) * ASSETS-88903 : [Issue] Product Name and Label are Undefined (#84) * Refactored the function buildProduct(product) to handle the condition when a product is not defined in the JSON object productMappings[product] which is defined in /scripts/shared-campaigns.js * Removed comment * ASSETS-88904 : Update Asset Thumbnail for Campaign List Entries (#85) * Refactored the function buildProduct(product) to handle the condition when a product is not defined in the JSON object productMappings[product] which is defined in /scripts/shared-campaigns.js * Added function searchAsset(programName, campaignName) to get the asset URL * Changed alt text to use assets repo-name property * Renamed/Moved AssetsDatasource.js to /scripts/assets gmo-campaign-list.js : Added logic to only allow blockConfig to be set on initial call to function decorate(block ... otherwise the values from blockConfig are overwritten when paginating to next page. gmo-campaign-details.js : Added campaign Image gmo-campaign-details.css : Updated CSS to display campaign Image * gmo-campaign-list.js : Deleted comment /scripts/assets.js : Updated to use createSearchEndpoint /scripts/scripts.js : Added export to export function createSearchEndpoint() { * Fixed bug when product is not defined in productMappings in /scripts/shared-campaigns.js * gmo-campaign-list.js : Removed iconImage.alt = "Failed to load image"; /scripts/assets.js : Updated facetFilters to an Array of Stings instead of Array of objects * Assets 98996 (#88) * initial grouping functionality * add sorting, label mappings * readd expand/collapse chevrons, add counts * add sort by date in groups * add important links dynamic generation * resolve bug with detailpage link * adjust width on links in deliverables tab * merge 12024 in and adjust width for links * update property name for revised completion date * implement graphql query for status mapping * ASSETS-88905 : Add Fields to the Overview Tab (#87) * Added Target Market Area lists function function createKPI(kpi) is renamed createLI(li) Added function buildTargetMarketAreaList(p0TargetMarketArea,p1TargetMarketArea) { * gmo-campaign-details.css : CSS fixes for scope-tag to not wrap gmo-campaign-details.js : Added Target Market Area and Platforms refactored async function buildChannelScope(scopeTypeId, scopes, block) to be able to display data for data based for a specified CSS ID * Added function getUniqueItems(items, property) to get unique values for deliverableType and platforms * Renamed function async function buildChannelScope(scopeTypeId, scopes, block) to async function buildFieldScopes(scopeTypeId, scopes, block) * Added global variable globalGraphFliter (#92) Previous and Next Buttons now call function decorate(block, numPerPage = currentNumberPerPage, cursor = '', previousPage = false, nextPage = false, graphQLFilter = {}) with graphFilter = currentGraphqlFilter * Assets 98996 (#93) * initial grouping functionality * add sorting, label mappings * readd expand/collapse chevrons, add counts * add sort by date in groups * add important links dynamic generation * resolve bug with detailpage link * adjust width on links in deliverables tab * merge 12024 in and adjust width for links * update property name for revised completion date * implement graphql query for status mapping * add deliverabletype graphql - todo: refactor, some superfluous function(s) can be removed * refactor product list to use graphql * fix deliverable type tags, fix multiline text * fix sort on marketing moments column * removing merge artifact * resolve PR comments * platforms mapping with graphql (#94) - also cleaned up extraneous/defunct code * Refactor sort icons (#95) * platforms mapping with graphql - also cleaned up extraneous/defunct code * refactor chevrons for column sort * add title attribs * ASSETS-88908 : Remove Console Error in Marketing Dashboard page and Campaign Details when no image is found (#96) * assets.js : Refactored code to eliminate console.log error gmo-campaign-list.js : Removed console.error("No campaign image found:", error); * Eliminated JavaScript errors when image is not found * Display total assets = 0 when the campaign image does not exist * Code cleanup (#98) * platforms mapping with graphql - also cleaned up extraneous/defunct code * refactor chevrons for column sort * add title attribs * remove kpi column * rename blocks * cleaning up redundant code * continuing cleanup * fix pagination bug * fix css bug in audience card * Updated product mapping label to text "Not Available" when the product mapping label is null (#99) * Code cleanup part 2 (#100) * platforms mapping with graphql - also cleaned up extraneous/defunct code * refactor chevrons for column sort * add title attribs * remove kpi column * rename blocks * cleaning up redundant code * continuing cleanup * fix pagination bug * fix css bug in audience card * cleanup, readd missing row - remove commented code - remove commented html - readd campaign name to overview tab if available * resolve undone null check for product label * handle bad response from product icon map * Added function closeAllDropDowns() (#101) Added function handleClickOutside(event) Updated function attachEventListeners() // Add event listener for clicks outside of dropdowns document.addEventListener('click', handleClickOutside); Added function resetProductsDropDown(); Updated function resetAllFilters() to call function resetProductsDropDown(); * Sticky deliverables header, update graphql endpoints (#102) * platforms mapping with graphql - also cleaned up extraneous/defunct code * refactor chevrons for column sort * add title attribs * remove kpi column * rename blocks * cleaning up redundant code * continuing cleanup * fix pagination bug * fix css bug in audience card * cleanup, readd missing row - remove commented code - remove commented html - readd campaign name to overview tab if available * resolve undone null check for product label * handle bad response from product icon map * update query endpoints, css tweak on deliverables * Added tool tip for Program Name and Campaign Name (#103) * remove duplicate icons --------- Co-authored-by: TyroneAEM <147942284+TyroneAEM@users.noreply.github.com> Co-authored-by: Shivani Gupta <61603050+shiv-gup@users.noreply.github.com> Co-authored-by: Samruddhi <150183547+staware30@users.noreply.github.com> Co-authored-by: Tyrone Tse Co-authored-by: Shivani gupta Co-authored-by: Shivani gupta --- blocks/adp-header/adp-header.js | 32 +- .../gmo-program-details.css | 487 +++++++++++++++ .../gmo-program-details.js | 557 ++++++++++++++++++ .../gmo-program-header/gmo-program-header.css | 199 +++++++ .../gmo-program-header/gmo-program-header.js | 376 ++++++++++++ blocks/gmo-program-list/gmo-program-list.css | 277 +++++++++ blocks/gmo-program-list/gmo-program-list.js | 481 +++++++++++++++ icons/collapse.svg | 6 + icons/trophy.svg | 7 + scripts/assets.js | 108 ++++ scripts/graphql.js | 248 ++++++++ scripts/scripts.js | 2 +- scripts/security.js | 33 +- scripts/shared-program.js | 55 ++ scripts/site-config.js | 31 +- 15 files changed, 2873 insertions(+), 26 deletions(-) create mode 100644 blocks/gmo-program-details/gmo-program-details.css create mode 100644 blocks/gmo-program-details/gmo-program-details.js create mode 100644 blocks/gmo-program-header/gmo-program-header.css create mode 100644 blocks/gmo-program-header/gmo-program-header.js create mode 100644 blocks/gmo-program-list/gmo-program-list.css create mode 100644 blocks/gmo-program-list/gmo-program-list.js create mode 100644 icons/collapse.svg create mode 100644 icons/trophy.svg create mode 100644 scripts/assets.js create mode 100644 scripts/graphql.js create mode 100644 scripts/shared-program.js diff --git a/blocks/adp-header/adp-header.js b/blocks/adp-header/adp-header.js index e25e6719..b3eafe7c 100644 --- a/blocks/adp-header/adp-header.js +++ b/blocks/adp-header/adp-header.js @@ -325,22 +325,24 @@ async function initQuickLinks() { const quickLinks = document.querySelector('.adp-header .nav-bottom .quick-links'); // decorate quick links quickLinksConfig.forEach((item) => { - const itemEl = document.createElement('div'); - itemEl.className = 'item'; - const itemLinkEl = document.createElement('a'); - if (item.page.startsWith('/') && isUrlPathNonRoot()) { - itemLinkEl.href = createLinkHref(getBaseConfigPath() + item.page); - } else { - itemLinkEl.href = createLinkHref(item.page); - } - itemLinkEl.dataset.page = item.page; - if (item.page.startsWith('http')) { - itemLinkEl.target = '_blank'; - itemLinkEl.rel = 'noopener'; + if (item.hide!=='true'){ + const itemEl = document.createElement('div'); + itemEl.className = 'item'; + const itemLinkEl = document.createElement('a'); + if (item.page.startsWith('/') && isUrlPathNonRoot()) { + itemLinkEl.href = createLinkHref(getBaseConfigPath() + item.page); + } else { + itemLinkEl.href = createLinkHref(item.page); + } + itemLinkEl.dataset.page = item.page; + if (item.page.startsWith('http')) { + itemLinkEl.target = '_blank'; + itemLinkEl.rel = 'noopener'; + } + itemLinkEl.textContent = item.title; + itemEl.append(itemLinkEl); + quickLinks.append(itemEl); } - itemLinkEl.textContent = item.title; - itemEl.append(itemLinkEl); - quickLinks.append(itemEl); }); if (await checkAddAssetsAccess()) { diff --git a/blocks/gmo-program-details/gmo-program-details.css b/blocks/gmo-program-details/gmo-program-details.css new file mode 100644 index 00000000..c653aafd --- /dev/null +++ b/blocks/gmo-program-details/gmo-program-details.css @@ -0,0 +1,487 @@ +body { + background-color: rgb(247, 246, 246); +} +.back-button { + margin-top: 20px; + background-color: #FFFFFF; + width: 65px; + height: 32px; + display: flex; + padding-left: 10px; + padding-right: 10px; + align-items: center; + border: 1px solid #D3D3D3; + border-radius: 4px; + cursor: pointer; + & > .icon { + width: 20px; + height: 30px; + } + & > .back-label { + margin-left: 15px; + font: normal normal normal 14px/17px Adobe Clean; + font-weight: bold; + } +} +.gmo-program-details.block { + & .h1 { + font: normal normal bold 18px/27px Adobe Clean; + } + & .h2 { + font: normal normal bold 16px/20px Adobe Clean; + } + & .h3 { + font: normal normal bold 14px/21px Adobe Clean; + } + & .subtitle { + font: normal normal normal 14px/21px Adobe Clean; + } + & > .main-body-wrapper { + display: flex; + flex-direction: column; + background: #FFFFFF; + border-radius: 6px; + box-shadow: 0px 3px 6px #0000000D; + margin-top: 20px; + min-height: 800px; + padding: 20px; + } +} + +.details-header-wrapper { + display: flex; + & > .header-title { + margin-left: 20px; + display: flex; + flex-direction: column; + justify-content: space-evenly; + & .icon-calendar { + width: 17px; + } + } + & > .campaign-img { + background-color: #e1e1e1; + } + & .header-row1 { + display: flex; + align-items: center; + flex-wrap: wrap; + text-align: center; + line-height: 22px; + } + & .header-row2 { + display: flex; + align-items: center; + } + & .header-row3 { + display: flex; + align-items: center; + } + & .campaign-status { + border-radius: 4px; + color: black; + filter: saturate(0.5); + font: normal normal normal 12px/15px Adobe Clean; + height: 24px; + line-height: 24px; + width: 72px; + margin-left: 10px; + text-align: center; + } + & .campaign-date { + font: normal normal normal 14px/21px Adobe Clean; + margin-left: 10px; + } +} + +.campaign-img { + width: 80px; + height: 80px; + margin-right: 14px; + background: #e2e2e2; + border-radius: 18px; +} + +.campaign-img img { + width: 100%; + height: 100%; + object-fit: cover; + border-radius: 18px; +} + +.tab-wrapper { + display: flex; + justify-content: space-between; + flex-wrap: wrap; + align-content: center; + width: 160px; + height: 50px; + font: normal normal normal 14px/17px Adobe Clean; + border-bottom: 2px solid #EAEAEA; + & > .tabBtn { + color: #747474; + height: 100%; + line-height: 50px; + &.active { + color: #323232; + border-bottom: 2px solid #323232; + } + } +} +.tab.two-column { + display: flex; + flex-direction: row; + justify-content: space-between; +} +.overview-wrapper { + display: flex; + flex-direction: column; + width: 800px; + & .overview-heading { + margin-top: 20px; + margin-bottom: 20px; + } + & .product-overview-wrapper { + margin-bottom: 20px; + } + & .description, .overview { + margin-top: 10px; + font: normal normal normal 14px/21px Adobe Clean; + letter-spacing: 0px; + color: black; + &.hide-overflow { + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 3; + overflow: hidden; + } + } + & .button.no-bg { + font: normal normal normal 14px/17px Adobe Clean; + margin-top: 10px; + color: #505050; + cursor: pointer; + } +} +.kpis-wrapper { + margin-top: 30px; + & ul { + columns: 2; + -webkit-columns: 2; + -moz-columns: 2; + font: normal normal normal 14px/21px Adobe Clean; + } + & div { + font-size: 14px; + margin-top: 15px; + } +} +.use-cases-wrapper { + margin-bottom: 30px; +} + +.use-cases-wrapper, .channel-scope-wrapper { + display: flex; + flex-direction: column; + margin-top: 30px; + + & .tags-wrapper { + display: flex; + flex-wrap: wrap; /* Ensure tags wrap properly */ + margin-top: 10px; + } + + & .use-case-tag, .scope-tag { + font: normal normal normal 14px/17px Adobe Clean; + color: #505050; + border: 1px solid #D3D3D3; + border-radius: 4px; + padding: 0 10px; /* Ensure no padding at the bottom */ + line-height: 32px; + height: 32px; /* Ensure consistent height */ + display: flex; + align-items: center; /* Vertically center the text */ + margin-bottom: 5px; /* Add margin at the bottom */ + &:not(:last-child) { + margin-right: 5px; + } + } +} + +.main-message-wrapper { + display: flex; + flex-direction: column; + margin-top: 20px; +} +.links-wrapper { + margin-top: 30px; + & .links { + display: flex; + justify-content: space-between; + margin-top: 10px; + } + & .campaign-link { + font: normal normal normal 14px/21px Adobe Clean; + letter-spacing: 0px; + color: #0D66D0; + } +} +.infocards-wrapper { + margin-right: 40px; + & .card { + width: 360px; + border: 1px solid #D3D3D3; + border-radius: 4px; + background-color: #FFFFFF; + padding: 0px 15px 10px 15px; + &:not(:last-child) { + margin-bottom: 30px; + } + } +} +.card { + & .card-heading { + margin-top: 10px; + margin-bottom: 10px; + } + &.scope { + font: normal normal normal 14px/21px Adobe Clean; + & > ul { + padding-left: 15px; + } + } + &.audiences > div { + font-size: 14px; + } +} +.milestone, .card-content { + font: normal normal normal 14px/21px Adobe Clean; + & .icon { + height: 18px; + margin-right: 5px; + } + display: flex; + align-items: center; + &:not(:last-child) { + margin-bottom: 5px; + } +} +.deliverables { + & > .page-heading { + display: flex; + margin-top: 20px; + margin-bottom: 25px; + & > .total-assets { + display: flex; + flex-direction: column; + justify-content: space-between; + & > span { + font: normal normal normal 14px/21px Adobe Clean; + } + } + } + .links-wrapper { + display: flex; + flex-direction: column; + max-width: 90%; + margin-right: 100px; + margin-top: unset; + & .links { + display: flex; + justify-content: space-between; + margin-top: 10px; + } + & .campaign-link { + font: normal normal normal 14px/21px Adobe Clean; + letter-spacing: 0px; + color: #0D66D0; + &:not(:last-child) { + margin-right: 30px; + } + } + } +} +.table-wrapper { + font: normal normal bold 14px/21px Adobe Clean; + & > .table-header { + background: #f7f6f6 0% 0% no-repeat padding-box; + margin-left: -20px; + margin-right: -20px; + padding-left: 20px; + padding-right: 20px; + height: 64px; + color: #505050; + display: flex; + align-items: center; + position: sticky; + position: -webkit-sticky; + top: 0; + } +} +.table-content { + height: 45vh; + overflow-y: auto; + &::-webkit-scrollbar { + display: none; + } +} +.inactive { + display: none !important; + visibility: hidden; +} +.row { + height: 56px; + &:not(:last-child) { + border-bottom: 2px solid #F4F4F4; + } + &.datarow { + display: flex; + align-items: center; + height: 88px; + font-weight: 200; + & .property { + line-height: 88px; + } + &:first-child, &:nth-child(2) { + border-top: 2px solid #F4F4F4; + } + & .deliverable-name, .platforms, .deliverable-type, .review-link, .kpi { + overflow: hidden; + text-overflow: ellipsis; + line-height: 16px; + } + & .date-wrapper { + display: flex; + flex-direction: column; + justify-content:center; + & > .completion-date, .revised-date { + line-height: 21px; + } + & > .revised-date { + color: #959595; + font: italic normal normal 11px/21px Adobe Clean; + } + } + } + &.collapsible.header { + height: unset; + display: flex; + flex-direction: column; + line-height: 56px; + & > .heading-wrapper { + display: flex; + margin-left: 15px; + align-items: center; + &.subheading { + margin-left: 25px; + } + & > .icon-next, & > .icon-collapse { + height: 10px; + width: 10px; + } + & > .headertext { + margin-left: 10px; + } + } + } + + & .property { + line-height: 56px; + } + & .justify-center { + display: flex; + flex-direction: column; + justify-content: center; + } +} +.subheader { + &:nth-child(2) { + border-top: 2px solid #F4F4F4; + } + & > .row:nth-child(2) { + border-top: 2px solid #F4F4F4; + } +} + +.status-wrapper { + height: 56px; + display: flex; + flex-direction: column; + justify-content: center; + & > .status-heading { + display: flex; + justify-content: space-between; + height: 12px; + font: normal normal normal 12px/15px Adobe Clean; + + } + & > .status-bar-wrapper { + position: relative; + & > .status-bar-underlay { + background-color: #D3D3D3; + width: 100%; + margin-top: 15px; + height: 6px; + margin-bottom: 5px; + left: 0; + top: 0; + position: absolute; + border-radius: 3px; + } + & > .status-bar { + margin-top: 15px; + height: 6px; + background-color: #2680EB; + margin-bottom: 5px; + z-index: 1; + position: relative; + border-radius: 3px; + } + } + +} +.header-row3:hover .date-tooltip { + visibility: visible; + opacity: 1; +} +.date-tooltip { + background-color: #D3D3D3; + border-radius: 2px; + visibility: hidden; + z-index: 1; + position: absolute; + text-align: center; + width: 100px; + padding: 2px 0; + font: normal normal normal 12px/17px Adobe Clean; + margin-left: 100px; +} +.column1 { + margin-left: 45px; +} +.column1 { + width: 200px; +} +.column3 { + width: 140px; +} +.column4, & .column5, & .column6 { + width: 110px; +} +.column2 { + width: 130px; +} +.column7 { + width: 170px; +} +.column8 { + width: 120px; +} +.column9 { + width: 150px; +} +.table-column { + &:not(:last-child) { + margin-right: 85px; + } +} diff --git a/blocks/gmo-program-details/gmo-program-details.js b/blocks/gmo-program-details/gmo-program-details.js new file mode 100644 index 00000000..5347cb4a --- /dev/null +++ b/blocks/gmo-program-details/gmo-program-details.js @@ -0,0 +1,557 @@ +import { decorateIcons, readBlockConfig } from '../../scripts/lib-franklin.js'; +import { getQueryVariable } from '../../scripts/shared.js'; +import { executeQuery } from '../../scripts/graphql.js'; +import { resolveMappings, filterArray, getProductMapping, checkBlankString } from '../../scripts/shared-program.js'; +import { getBaseConfigPath } from '../../scripts/site-config.js'; +import { searchAsset } from '../../scripts/assets.js'; + +let blockConfig; +const programName = getQueryVariable('programName'); +const programRefNumber = getQueryVariable('programReferenceNumber'); +const deliverableMappings = resolveMappings("getDeliverableTypeMapping"); +const platformMappings = resolveMappings("getPlatformsMapping"); + +export default async function decorate(block) { + + const encodedSemi = encodeURIComponent(';'); + const encodedProgram = encodeURIComponent(programName); + const programQueryString = `getProgramDetails${encodedSemi}programName=${encodedProgram}${encodedSemi}programReferenceNumber=${encodeURIComponent(programRefNumber)}`; + const programData = await executeQuery(programQueryString); + const deliverableQueryString = `getProgramDeliverables${encodedSemi}programName=${encodedProgram}`; + const deliverables = await executeQuery(deliverableQueryString); + + const p0TargetMarketArea = programData.data.programList.items[0].p0TargetMarketArea; + const p1TargetMarketArea = programData.data.programList.items[0].p1TargetMarketArea; + + // Extract unique deliverable types + const uniqueDeliverableTypes = getUniqueItems(programData.data.deliverableList.items, 'deliverableType'); + // Extract unique platforms (flattened from arrays within each item) + const uniquePlatforms = getUniqueItems(programData.data.deliverableList.items, 'platforms'); + + const program = programData.data.programList.items[0]; + const kpis = buildKPIList(program).outerHTML; + + const targetMarketAreas = buildTargetMarketAreaList(p0TargetMarketArea,p1TargetMarketArea).outerHTML; + + const audiences = buildAudienceList(program).outerHTML; + const date = formatDate(program.launchDate); + const artifactLinks = buildArtifactLinks(program).outerHTML; + blockConfig = readBlockConfig(block); + block.innerHTML = ` +
+ + Back +
+
+
+
+
+
+
+ ${program.programName} +
+ ${program.campaignName ? '
' + program.campaignName + '
': ""} +
+ + Launch date + ${date} +
+
+
+
+
Overview
+
Deliverables
+
Calendar
+
+
+
+ At a Glance +
+ Marketing Goal +
${checkBlankString(program.marketingGoal.plaintext)}
+
Read more
+
+
+ Product Value +
${checkBlankString(program.productValue.plaintext)}
+
Read more
+
+
+ KPIs to Measure Success + ${kpis} +
+
+ Target Market Area + ${targetMarketAreas} +
+
+ Hero Use Cases +
+
Text to Image
+
Use Case 2
+
+
+
+ Main Message + + A major genAI release of the Photoshop beta app that delivers new and enhanced generative AI capabilities. + +
+ +
+ Deliverable Type +
+
+
+ +
+ Platforms +
+
+
+ ${artifactLinks} +
+
+
+
Products
+
+
+
Audiences
+ ${audiences} +
+
+
+
+
+ ${artifactLinks} +
+
Total Assets
+ +
+
+
+
+
Deliverable Name
+
Deliverable Type
+
Platforms
+
Review Link
+
Final Asset
+
Status Update
+
Completion Date
+
Project Owner
+
+
+
+
+
+
+ `; + buildProductCard(program); + try { + const imageObject = await searchAsset(program.programName, program.campaignName); + if (imageObject){ + insertImageIntoCampaignImg(block,imageObject); + document.getElementById('totalassets').textContent = imageObject.assetCount; + } + else + { + document.getElementById('totalassets').textContent = 0; + } + } catch (error) { + console.error("Failed to load campaign image:", error); + } + + block.querySelector('.tab-wrapper').addEventListener('click', (event) => { + switchTab(event.target); + }) + block.querySelector('.back-button').addEventListener('click', () => { + const host = location.origin + getBaseConfigPath(); + const listPage = blockConfig.listpage; + document.location.href = host + `/${listPage}`; + }) + block.querySelectorAll('.read-more').forEach((button) => { + button.addEventListener('click', (event) => { + const readMore = event.target; + const parent = readMore.parentElement; + parent.querySelector('.paragraph').classList.toggle('hide-overflow'); + }); + }); + decorateIcons(block); + buildFieldScopes('deliverable-type',uniqueDeliverableTypes, block); + buildFieldScopes('platforms',uniquePlatforms, block); + const table = await buildTable(await deliverables).then(async (rows) => { + return rows; + }) + const tableRoot = block.querySelector('.table-content'); + tableRoot.appendChild(table); + buildStatus(program.status); +} + +/** + * Extracts unique values from a specified property within an array of objects. + * + * @param {Array} items - The array of objects to extract values from. + * @param {string} property - The property name to extract values from each object. + * @returns {Array} - An array of unique values extracted from the specified property. + * + * This function flattens arrays of values if the property contains arrays within each object, + * filters out null and undefined values, and returns a unique set of these values. + */ +function getUniqueItems(items, property) { + return [...new Set(items.flatMap(item => item[property]) + .filter(value => value !== null && value !== undefined) + )]; +} + +function insertImageIntoCampaignImg(block,imageObject) { + const campaignImgDiv = block.querySelector('.campaign-img'); + const imgElement = document.createElement('img'); + imgElement.src = imageObject.imageUrl; + imgElement.alt = imageObject.imageAltText; + campaignImgDiv.appendChild(imgElement); +} + +function switchTab(tab) { + if (tab.classList.contains('active') || tab.classList.contains('tab-wrapper')) { + return; + } + document.querySelector('.tabBtn.active').classList.toggle('active'); + document.querySelector(`.tab:not(.inactive)`).classList.toggle('inactive'); + const targetTab = tab.dataset.target; + const tabElement = document.getElementById(targetTab); + tabElement.classList.toggle('inactive'); + tab.classList.toggle('active'); +} + + +async function buildFieldScopes(scopeTypeId, scopes, block) { + if (scopes.length == 0) { + block.querySelector(`#${scopeTypeId}.channel-scope-wrapper`).classList.add('inactive'); + return; + } + const scopesParent = block.querySelector(`#${scopeTypeId}.channel-scope-wrapper .tags-wrapper`); + scopes.forEach(async (scope) => { + if (scope == null || scope == undefined || scope == '') return; + const tag = document.createElement('div'); + tag.classList.add('scope-tag'); + tag.textContent = await lookupType(scope, scopeTypeId); + scopesParent.appendChild(tag); + }); +} + +function buildKPIList(program) { + let kpiList = document.createElement('ul'); + program.primaryKpi?.forEach((kpi) => { + const kpiLi = createLI(kpi); + kpiList.appendChild(kpiLi); + }) + program.additionalKpi?.forEach((kpi) => { + const kpiLi = createLI(kpi); + kpiList.appendChild(kpiLi); + }) + if (kpiList.children.length == 0) { + kpiList.remove(); + kpiList = document.createElement('div'); + kpiList.textContent = "Not Available"; + } + return kpiList; +} + +function buildTargetMarketAreaList(p0TargetMarketArea,p1TargetMarketArea) { + let ulList = document.createElement('ul'); + p0TargetMarketArea?.forEach((item) => { + const itemLi = createLI(item); + ulList.appendChild(itemLi); + }); + + p1TargetMarketArea?.forEach((item) => { + const itemLi = createLI(item); + ulList.appendChild(itemLi); + }) + if (ulList.children.length == 0) { + ulList.remove(); + ulList = document.createElement('div'); + ulList.textContent = "Not Available"; + } + return ulList; +} + +function createLI(li) { + const liItem = document.createElement('li'); + const liText = parseString(li); + liItem.textContent = liText; + return liItem; +} + +async function buildProductCard(program) { + const productMapping = await getProductMapping(program.productOffering); + const productList = document.createElement('div'); + productList.classList.add('product', 'card-content'); + productList.innerHTML = ` + + ${checkBlankString(productMapping.label)} + ` + document.querySelector('.card.products').appendChild(productList); +} + +function buildAudienceList(program) { + const audienceList = document.createElement('div'); + program.primaryAudience?.forEach((audience) => { + const audienceDiv = createAudience(audience); + audienceList.appendChild(audienceDiv); + }) + program.additionalAudiences?.forEach((audience) => { + const audienceDiv = createAudience(audience); + audienceList.appendChild(audienceDiv); + }) + if (audienceList.children.length == 0) audienceList.textContent = "Not Available"; + return audienceList; +} + +function buildArtifactLinks(program) { + const artifactLinks = document.createElement('div'); + artifactLinks.classList.add('links-wrapper'); + artifactLinks.innerHTML = ` + Links to Important Artifacts + + `; + // see how many 'links' were made. if none, hide the section + const numLinks = artifactLinks.querySelectorAll('.campaign-link')?.length; + if (numLinks == 0) artifactLinks.classList.add('inactive'); + return artifactLinks; +} + +async function buildStatus(status) { + const statusDiv = document.createElement('div'); + statusDiv.classList.add('campaign-status'); + const statusArray = await resolveMappings("getStatusList"); + const statusMatch = filterArray(statusArray, 'value', status); + const statusText = statusMatch ? statusMatch[0].text : status; + const statusHex = statusMatch[0]["color-code"]; + statusDiv.textContent = statusText; + statusDiv.style.backgroundColor = "#" + statusHex; + document.querySelector('.header-row1').appendChild(statusDiv); +} + +function createAudience(audience) { + const text = parseString(audience); + const audienceDiv = document.createElement('div'); + audienceDiv.classList.add('audience', 'card-content'); + audienceDiv.innerHTML = ` + + ${text} + `; + return audienceDiv; +} + +function parseString(text) { + let parsed = text.replace(/-/g, ' ').split(' '); + parsed[0] = parsed[0].charAt(0).toUpperCase() + parsed[0].slice(1); + parsed = parsed.join(' '); + return parsed; +} + +function formatDate(dateString) { + const parts = dateString.split('-'); + const yyyy = parts[0]; + const mm = parts[1]; + const dd = parts[2]; + + // Formatting the date into mm/dd/yyyy format + const formattedDate = mm + '/' + dd + '/' + yyyy; + + return formattedDate; +} + +async function buildTable(jsonResponse) { + const deliverableList = jsonResponse.data.deliverableList.items; + const programKpi = jsonResponse.data.programList?.items.primaryKpi; + const rows = document.createElement('div'); + const uniqueCategories = getUniqueItems(deliverableList, 'deliverableType'); + let emptyCategory = false; + uniqueCategories.forEach(async (category) => { + // build header row + let headerRow; + const matchingCampaigns = deliverableList.filter(deliverable => deliverable.deliverableType === category); + const matchCount = matchingCampaigns.length; + if (category == null || category == undefined || category === '') { + emptyCategory = true; + headerRow = rows; + } else { + headerRow = await buildHeaderRow(category, 'header', false, matchCount); + attachListener(headerRow); + rows.appendChild(headerRow); + } + matchingCampaigns.forEach(async (campaign) => { + const tableRow = await buildTableRow(campaign, programKpi, !emptyCategory); + headerRow.appendChild(tableRow); + }) + // sort grouped rows by date + if (!emptyCategory) { + dateSort(headerRow); + } + emptyCategory = false; + }); + //sort the rows + sortRows(rows); + return rows; +} + +function dateSort(parent) { + const childNodes = Array.from(parent.getElementsByClassName('datarow')); + childNodes.sort((a, b) => { + const dateA = new Date(a.querySelector('.completion-date').innerHTML); + const dateB = new Date(b.querySelector('.completion-date').innerHTML); + // Check if dates are valid + if (isNaN(dateA.getTime()) || isNaN(dateB.getTime())) { + return 0; // Move on if date is invalid + } + return dateA - dateB; + }) + childNodes.forEach((node) => { + parent.appendChild(node); + }) +} + +async function lookupType(rawText, mappingType) { + const mappings = (mappingType === 'deliverable-type') ? await deliverableMappings : await platformMappings; + const typeMatch = mappings.filter(item => item.value === rawText); + const typeText = typeMatch.length > 0 ? typeMatch[0].text : rawText; + return typeText; +} + +/** + * @param {string} category - String value of the category property + * @param {string} headerType - Type of header. Either 'category' or 'subcategory' + * @param {boolean} isInactive - Determines whether or not the header will be hidden initially + * @param {number} matchCount - Number of matching items, will display beside the label + */ +async function buildHeaderRow(category, headerType, isInactive, matchCount) { + //look up friendly name for deliverable type + const typeLabel = await lookupType(category, 'deliverable-type'); + const headerRow = document.createElement('div'); + headerRow.classList.add('row', 'collapsible', 'header'); + let divopen; + if (headerType === 'subcategory') { + headerRow.classList.add('subheader'); + divopen = '
'; + } else { + divopen = '
'; + } + if (isInactive) headerRow.classList.add('inactive'); + headerRow.innerHTML = ` + ${divopen} + + +
${typeLabel} (${matchCount})
+
`; + return headerRow; +} + +async function buildTableRow(deliverableJson, kpi, createHidden) { + //look up friendly name for deliverable type + const typeLabel = await lookupType(deliverableJson.deliverableType, 'deliverable-type'); + const dataRow = document.createElement('div'); + dataRow.classList.add('row', 'datarow'); + if (createHidden) dataRow.classList.add('inactive'); + const status = (deliverableJson.deliverableStatusUpdate == null) ? "Not Available" : deliverableJson.deliverableStatusUpdate + "%"; + const statusPct = (deliverableJson.deliverableStatusUpdate == null) ? "0%" : deliverableJson.deliverableStatusUpdate + "%"; + dataRow.innerHTML = ` +
${deliverableJson.deliverableName}
+
${typeLabel}
+
+ +
+
+
+
+
+
Progress
+
${status}
+
+
+
+
+
+
+
+
+
${checkBlankString(deliverableJson.taskCompletionDate)}
+ ${deliverableJson.previousTaskCompletionDate ? '
Revised from ' + deliverableJson.previousTaskCompletionDate + '
': ""} +
+
${checkBlankString(deliverableJson.driver)}
+ `; + if (!(deliverableJson.linkedFolderLink == null)) { + const finalAssetLink = document.createElement('a'); + finalAssetLink.href = deliverableJson.linkedFolderLink; + finalAssetLink.classList.add('campaign-link'); + finalAssetLink.target = '_blank'; + finalAssetLink.textContent = "Final Asset"; + dataRow.querySelector('.column5').appendChild(finalAssetLink); + } + createPlatformString(deliverableJson.platforms, dataRow); + return dataRow; +} + +async function createPlatformString(platforms, htmlElem) { + let platformString = ''; + if (platforms && platforms.length > 0) { + for (const rawPlatform of platforms) { + const platform = await lookupType(rawPlatform, 'platform'); + platformString += platform + ', '; + } + platformString = platformString.slice(0, -2); + } else { + platformString = 'Not Available' + } + htmlElem.querySelector('.column3.platforms').textContent = platformString; +} + +function sortRows(rows) { + const rowParent = rows; + const nodes = Array.from(rowParent.childNodes); + // Sort child nodes by class name + nodes.sort((a, b) => { + var classA = a.classList ? a.classList.contains('datarow') : false; + var classB = b.classList ? b.classList.contains('datarow') : false; + + if (classA && !classB) { + return 1; + } else if (!classA && classB) { + return -1; + } else { + return 0; + } + }); + + // Rearrange child nodes + nodes.forEach((node) => { + rowParent.appendChild(node); + }); + return rowParent; +} + +function attachListener(htmlElement) { + htmlElement.querySelector('.heading-wrapper').addEventListener('click', (event) => { + const arrow = event.target; + const headerRow = arrow.closest('.row.collapsible'); + const rowChildren = headerRow.children; + headerRow.querySelector('.icon-next').classList.toggle('inactive'); + headerRow.querySelector('.icon-collapse').classList.toggle('inactive'); + Array.from(rowChildren).forEach((child) => { + //if child has 'row' class, then toggle 'visible' class + if (child.classList.contains('row')) child.classList.toggle('inactive'); + }) + }) +} diff --git a/blocks/gmo-program-header/gmo-program-header.css b/blocks/gmo-program-header/gmo-program-header.css new file mode 100644 index 00000000..ac30d581 --- /dev/null +++ b/blocks/gmo-program-header/gmo-program-header.css @@ -0,0 +1,199 @@ +.gmo-campaign-header.block { + display: flex; + flex-direction: column; + margin-top: 20px; +} +.inputs-wrapper { + display: flex; + height: 50px; +} +.search-wrapper { + background-color: #FFF; + margin-top: 18px; + margin-right: 10px; + border: 1px solid #D3d3d3; + border-radius: 4px; + display: flex; + align-items: center; + position: relative; /* Added to be the anchor for absolute positioning for autocomplete feature*/ + & > .icon { + background-color: #FFF; + height: 14px; + border-radius: 4px; + & > svg { + padding-bottom: 10px; + } + } +} +.campaign-search { + width: 400px; + height: 26px; + border: none; + &:focus { + outline: none; + } +} +.filter-wrapper { + height: 100%; + width: 176px; + display: flex; + flex-direction: column; + & label { + font: normal normal normal 12px/15px Adobe Clean; + display: block; + height: 18px; + color: #747474; + } + & .label { + font: normal normal normal 12px/15px Adobe Clean; + display: block; + height: 18px; + color: #747474; + } + &:not(:last-child) { + margin-right: 10px; + } +} +.filters { + font: normal normal normal 14px/17px Adobe Clean; + color: #505050; + letter-spacing: 0px; + width: 176px; + height: 32px; + &.categories { + width: 200px; + } + &.status { + width: 200px; + } +} + +.filter-dropdown { + position: relative; + display: inline-block; + font: normal normal normal 14px/17px Adobe Clean; + letter-spacing: 0px; + height: 32px; + background-color: #FFF; + width: 100%; + border: 1px solid #D3D3D3; + border-radius: 4px; + & > .dropdown-button { + height: 32px; + line-height: 32px; + padding-left: 10px; + display: flex; + justify-content: space-between; + & > .dropdown-label { + overflow: hidden; + } + & > .icon { + padding-top: 4px; + height: 16px; + } + } + & > .dropdown-content { + display: none; + position: absolute; + background-color: #f9f9f9; + + max-height: 200px; + overflow-y: auto; + border: 1px solid #ccc; + z-index: 1; + width: 174px; + } + &.active .dropdown-content { + display: block; + } +} + +.dropoption.selected { + background-color:#959595; +} +.icon.inactive { + display: none; + visibility: hidden; +} +.dropdown-content a { + display: block; + padding: 10px; + text-decoration: none; + color: #333; + &:hover { + background-color: #ddd; + } +} +.selections-wrapper { + margin-top: 10px; + min-height: 22px; + display: flex; + justify-content: space-between; + & > .selected-filters-list { + display: flex; + max-width: 90%; + flex-wrap: wrap; + & > .selected-filter { + background: #FFFFFF; + border: 1px solid #959595; + border-radius: 4px; + text-align: left; + font: normal normal normal 12px/15px Adobe Clean; + letter-spacing: 0px; + color: #747474; + height: 20px; + line-height: 20px; + padding: 0 7px 0 7px; + margin-right: 5px; + & > .label { + margin-right: 4px; + } + & > .icon-close { + height: 8px; + width: 6px; + } + } + } + & > .reset-filters { + font: normal normal normal 14px/17px Adobe Clean; + &.inactive { + display: none; + visibility: hidden; + } + } +} + +/* Add the following for autocomplete styling */ + +.autocomplete-items { + position: absolute; + top: 100%; + left: 0; + right: 0; + border: 1px solid #cccccc; + border-top: none; + z-index: 10; + background: #ffffff; + overflow-y: auto; + max-height: 200px; + border-bottom-left-radius: 4px; + border-bottom-right-radius: 4px; + box-shadow: 0 4px 6px rgba(0,0,0,0.1); +} + +.autocomplete-items div { + padding: 10px 15px; + cursor: pointer; + border-bottom: 1px solid #f0f0f0; + font-size: 14px; /* Adjust the font-size as needed */ + color: #333; /* Adjust the text color as needed */ + line-height: 1.4; /* Adjust line-height for better readability if necessary */ +} + +.autocomplete-items div:hover { + background-color: #e9e9e9; +} + +.autocomplete-items div:last-child { + border-bottom: none; +} diff --git a/blocks/gmo-program-header/gmo-program-header.js b/blocks/gmo-program-header/gmo-program-header.js new file mode 100644 index 00000000..98e15246 --- /dev/null +++ b/blocks/gmo-program-header/gmo-program-header.js @@ -0,0 +1,376 @@ +import { decorateIcons } from '../../scripts/lib-franklin.js'; +import { graphqlQueryNameList, graphqlCampaignByName } from '../../scripts/graphql.js'; +import { statusMapping, productList } from '../../scripts/shared-program.js'; + +// Declared at the top of the file, making it accessible to all functions within this file. +let allProducts = []; + +export default async function decorate(block) { + block.innerHTML = ` +
+
+ + + +
+
+
+
Business Line
+
+ + +
+
+
+
Status
+
+ + +
+
+ +
+
Products
+
+ + +
+
+ +
+
Geo
+
+ + +
+
+ +
+ +
+
+
+
Reset filters
+
+ + `; + + // autocomplete feature + const autocompleteList = document.getElementById('autocomplete-list'); + // Get the input element by its ID + const searchInput = document.getElementById('campaign-search'); + searchInput.addEventListener('input', async function() { + const value = this.value; + if (value) + { + const graphqlData = await graphqlCampaignByName(value); + //Get unique values + const searchItems = Array.from(new Set(graphqlData.data.programList.items.map(item => item.campaignName))); + autocomplete(value, searchItems); + } + else + { + //The value has been cleard so trigger the gmo-campaign-list block from the gmo-campaign-header + sendGmoCampaignListBlockEvent(); + } + }); + + function autocomplete(value, items) { + clearAutocomplete(); + if (!value) return; + const filteredItems = items.filter(item => item.toLowerCase().includes(value.toLowerCase())); + filteredItems.forEach(item => { + const entry = document.createElement('div'); + entry.innerHTML = item; + entry.addEventListener('click', function() { + searchInput.value = this.innerText; + clearAutocomplete(); + }); + autocompleteList.appendChild(entry); + }); + } + + function clearAutocomplete() { + while (autocompleteList.firstChild) { + autocompleteList.removeChild(autocompleteList.firstChild); + } + } + + // Listen for click events on the autocomplete list + autocompleteList.addEventListener('click', function(event) { + //Trigger the gmo-campaign-list block from the gmo-campaign-header + sendGmoCampaignListBlockEvent(); + }); + + // Listen for change events on the autocomplete list + autocompleteList.addEventListener('change', function(event) { + //Trigger the gmo-campaign-list block from the gmo-campaign-header + sendGmoCampaignListBlockEvent(); + }); + + await initializeDropdowns(); + attachEventListeners(); + decorateIcons(block); +} + +async function initializeDropdowns() { + // Business Line List + + const businessLineResponse = await graphqlQueryNameList('getBusinessLine'); + const businessLines = businessLineResponse.data.jsonByPath.item.json.options; + populateDropdown(businessLines, 'dropdownBusinessOptions', 'businessLine'); + + // Status List + const statusResponse = await statusMapping; + const statuses = statusResponse.data.jsonByPath.item.json.options; + populateDropdown(statuses, 'dropdownStatusOptions', 'status'); + + // Product List + const productResponse = await productList; + allProducts = productResponse.data.jsonByPath.item.json.options; + populateDropdown(allProducts, 'dropdownProductOptions', 'productOffering'); + + // Geo List + const geoResponse = await graphqlQueryNameList('getGeoList'); + const geos = geoResponse.data.jsonByPath.item.json.options; + populateDropdown(geos, 'dropdownGeoOptions', 'p0TargetGeo'); +} + +// Function to attach event listeners +function attachEventListeners() { + // First remove existing listeners to prevent duplicates + removeEventListeners(); + + document.querySelectorAll('.dropdown-button').forEach(button => { + button.addEventListener('click', dropdownButtonClickHandler); + }); + + document.querySelectorAll('.dropoption').forEach(option => { + option.addEventListener('click', dropOptionClickHandler); + }); + + const resetFiltersBtn = document.querySelector('.reset-filters'); + if (resetFiltersBtn) { + resetFiltersBtn.addEventListener('click', resetFiltersClickHandler); + } + + // Add event listener for clicks outside of dropdowns + document.addEventListener('click', handleClickOutside); +} + +function populateDropdown(options, dropdownId, type) { + let dropdownContent = document.getElementById(dropdownId); + dropdownContent.innerHTML = ''; + options.forEach((option, index) => { + let anchor = document.createElement('a'); + anchor.href = "#"; + anchor.id = `option${index + 1}`; + anchor.dataset.value = option.value; + anchor.dataset.type = type; + anchor.className = "dropoption"; + anchor.textContent = option.text; + dropdownContent.appendChild(anchor); + }); +} + +// Function to filter products based on selected business line +function filterProductsByBusinessLine(businessLine) { + const filteredProducts = allProducts.filter(product => + product['business-line'].includes(businessLine) + ); + populateDropdown(filteredProducts, 'dropdownProductOptions', 'productOffering'); +} + +// Function to remove productOffering filters +function removeSelectedProductOfferingFilters() { + // Select all div elements with class 'selected-filter' and data-type 'productOffering' + const filters = document.querySelectorAll('.selected-filter[data-type="productOffering"]'); + // Loop through the NodeList and remove each element + filters.forEach(filter => { + filter.parentNode.removeChild(filter); + }); +} + + +// Function to close all dropdowns +function closeAllDropdowns() { + document.querySelectorAll('.filter-dropdown.active').forEach(dropdown => { + dropdown.classList.remove('active'); + dropdown.querySelector('.icon-chevronDown').classList.remove('inactive'); + dropdown.querySelector('.icon-chevronUp').classList.add('inactive'); + }); +} + +// Function to handle clicks outside of dropdowns +function handleClickOutside(event) { + const isClickInsideDropdown = event.target.closest('.filter-dropdown'); + if (!isClickInsideDropdown) { + closeAllDropdowns(); + } +} + +function toggleDropdown(element) { + const dropdown = element.closest('.filter-dropdown'); + const iconChevronDown = dropdown.querySelector('.icon-chevronDown'); + const iconChevronUp = dropdown.querySelector('.icon-chevronUp'); + + iconChevronDown.classList.toggle('inactive'); + iconChevronUp.classList.toggle('inactive'); + dropdown.classList.toggle('active'); +} + +function toggleOption(optionValue, optionType) { + const currentlySelected = document.querySelector(`.dropoption.selected[data-type='${optionType}']`); + if (currentlySelected && currentlySelected.dataset.value !== optionValue) { + currentlySelected.classList.remove('selected'); // Remove the 'selected' class from the previously selected option + handleSelectedFilter(currentlySelected); // Update the UI to reflect this change + } + + const dropdownOption = document.querySelector(`[data-value='${optionValue}'][data-type='${optionType}']`); + const isSelected = dropdownOption.classList.contains('selected'); + if (!isSelected) { + dropdownOption.classList.add('selected'); + if (optionType === 'businessLine') { + // Filter products based on the selected business line + filterProductsByBusinessLine(optionValue); + // Reset product offering filters + removeSelectedProductOfferingFilters(); + attachEventListeners(); + } + } else { + dropdownOption.classList.remove('selected'); + if (optionType === 'businessLine') { + //Reset Products Dropdown + resetProductsDropDown(); + } + } + + handleSelectedFilter(dropdownOption); // Update the UI for the new selection + checkResetBtn(); // Check if the reset button should be active +} + +function handleSelectedFilter(option) { + const filterTagRoot = document.querySelector('.selected-filters-list'); + const filterValue = option.dataset.value; + const filterType = option.dataset.type; + if (option.classList.contains('selected')) { + const filterName = option.textContent; + const filterTag = document.createElement('div'); + filterTag.classList.add('selected-filter'); + const filterLabel = document.createElement('span'); + filterLabel.textContent = filterName; + filterLabel.classList.add('label'); + const closeOrig = document.querySelector('.icon.icon-close.inactive'); + const closeIcon = closeOrig.cloneNode(true); + closeIcon.classList.toggle('inactive'); + closeIcon.addEventListener('click', (event) => { + const filterTag = event.target.closest('.selected-filter'); + const optionValue = filterTag.dataset.value; + const optionType = filterTag.dataset.type; + toggleOption(optionValue, optionType); + }) + filterTag.appendChild(filterLabel); + filterTag.appendChild(closeIcon); + filterTag.dataset.type = filterType; + filterTag.dataset.value = filterValue; + filterTagRoot.appendChild(filterTag); + } else { + filterTagRoot.removeChild(document.querySelector(`.selected-filter[data-value='${filterValue}'][data-type='${filterType}']`)); + } + + //Trigger the gmo-campaign-list block from the gmo-campaign-header + sendGmoCampaignListBlockEvent(); +} + +function resetAllFilters() { + //Clear the campaignName search field + const searchInput = document.getElementById('campaign-search'); + searchInput.value = ''; + + const selectedFilters = document.querySelectorAll('.dropoption.selected'); + selectedFilters.forEach((element) => { + element.classList.toggle('selected'); + }) + const filterTagRoot = document.querySelector('.selected-filters-list'); + filterTagRoot.replaceChildren(); + checkResetBtn(); + + //Reset Products Dropdown + resetProductsDropDown(); + + //Trigger the gmo-campaign-list block from the gmo-campaign-header + sendGmoCampaignListBlockEvent(); +} + +function resetProductsDropDown(){ + // Populate all products into Products dropdown + populateDropdown(allProducts, 'dropdownProductOptions', 'productOffering'); + // Reset product offering filters + removeSelectedProductOfferingFilters(); + attachEventListeners(); +} + +function checkResetBtn() { + const selectedOptions = document.querySelectorAll('.dropoption.selected'); + const resetFiltersBtn = document.querySelector('.reset-filters'); + if (selectedOptions.length > 0) { + if (resetFiltersBtn.classList.contains('inactive')) resetFiltersBtn.classList.remove('inactive'); + } else { + resetFiltersBtn.classList.add('inactive'); + } +} + +// Define handlers as named functions to easily add and remove them +function dropdownButtonClickHandler(event) { + toggleDropdown(event.target); +} + +function dropOptionClickHandler(event) { + toggleOption(event.target.dataset.value, event.target.dataset.type); + toggleDropdown(event.target); // Closes the dropdown list +} + +// Function to remove event listeners +function removeEventListeners() { + document.querySelectorAll('.dropdown-button').forEach(button => { + button.removeEventListener('click', dropdownButtonClickHandler); + }); + + document.querySelectorAll('.dropoption').forEach(option => { + option.removeEventListener('click', dropOptionClickHandler); + }); + + const resetFiltersBtn = document.querySelector('.reset-filters'); + if (resetFiltersBtn) { + resetFiltersBtn.removeEventListener('click', resetFiltersClickHandler); + } +} + +function resetFiltersClickHandler() { + resetAllFilters(); +} + +function sendGmoCampaignListBlockEvent() { + const blockEvent = new CustomEvent('gmoCampaignListBlock'); + document.dispatchEvent(blockEvent); +} diff --git a/blocks/gmo-program-list/gmo-program-list.css b/blocks/gmo-program-list/gmo-program-list.css new file mode 100644 index 00000000..479d9922 --- /dev/null +++ b/blocks/gmo-program-list/gmo-program-list.css @@ -0,0 +1,277 @@ +body { + background-color: rgb(247, 246, 246); +} +.refresh-notification { + text-align: right; + font-size: 14px; +} +.list-container { + background-color: #FFF; + padding-left: 12px; + padding-right: 12px; + padding-bottom: 10px; + border-radius: 10px; +} +.gmo-campaign-list { + font-size: 16px; + padding-left: 10px; + padding-right: 10px; + margin-top: 25px; +} +.gmo-campaign-list .hidden { + visibility: hidden; + display: none; +} +.list-header { + display: flex; + font-weight: bold; + margin-bottom: 20px; + + padding-top: 5px; +} +.column-header-wrapper { + display: flex; +} +.column-label { + height: 32px; + line-height: 32px; + font-size: 14px; + text-align: center; + margin-right: 7px; +} +.column-sort-wrapper { + display: flex; + flex-direction: column; + justify-content: center; +} +.column-sort-wrapper .icon { + height: 14px; + width: 14px; +} +.list-items { + display: flex; + flex-direction: column; +} +.campaign-row { + display: flex; + font-size: 14px; + margin-bottom: 20px; + height: 80px; +} +.campaign-info-wrapper { + display: flex; + font-size: 14px; +} +.campaign-icon { + width: 80px; + height: 80px; + margin-right: 14px; + background: #e2e2e2; + border-radius: 18px; +} + +.campaign-icon img { + width: 100%; /* Ensure the image fills the entire width of its container */ + height: 100%; /* Ensure the image fills the entire height of its container */ + object-fit: cover; /* This will cover the area without distorting the aspect ratio */ + border-radius: 18px; /* Match the border-radius of the container */ +} + +.campaign-name-wrapper { + width: 220px; +} +.campaign-name-label { + font-weight: bold; +} +.campaign-name { + color: #959595; +} +.campaign-description-wrapper { + color: #505050; +} +.campaign-description { + line-height:20px; + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 4; + overflow: hidden; + text-overflow: ellipsis; +} +.campaign-launch-date { + color: black; + font-weight: bold; +} +.product-label { + color: black; + font-weight: bold; +} +.product-entry { + height: 22px; + display: flex; + align-items: center; + margin-bottom: 1px; +} +.product-entry .icon { + height: 20px; + margin-right: 2px; +} +.product-entry .product-label { + font-weight: bold; + line-height: 14px; +} +.list-footer { + height: 32px; + line-height: 32px; + font-size: 14px; + display: flex; + justify-content: space-between; + margin-top: 20px; + margin-bottom: 5px; +} +.footer-total { + font-weight: bold; + width: 300px; +} +.footer-pagination { + display: flex; +} +.footer-pagination-button { + font-size: 12px; + font-weight: bold; + text-align: center; + width: 55px; + height: 25px; + line-height: 25px; + border-radius: 15px; + background-color: #F4F4F4; + color: #BCBCBC; +} +.footer-pagination-button.inactive { + background-color: #F4F4F4; + color: #BCBCBC; + border: 2px solid #F4F4F4; +} +.footer-pagination-button.active { + background-color: #FFFFFF; + border: 2px solid #747474; + color: #747474; +} +.footer-pagination-button.prev { + margin-right: 25px; +} +.footer-pagination-pages { + line-height: 25px; + height: 25px; + font-size: 12px; + width: 23px; + text-align: center; + border-radius: 4px; +} +.footer-pagination-pages:not(:last-child) { + margin-right: 4px; +} +.footer-pages-wrapper { + display: flex; +} +.footer-pagination-pages.currentpage { + background-color: rgb(77, 77, 77); + color:#f6f6f6; +} +.footer-pagination-button.next { + margin-left: 25px; +} +.footer-perPage { + display: flex; + justify-content: flex-end; + width: 300px; +} +.footer-perPage-label { + font-size: 14px; + text-align: center; + margin-right: 8px; +} +.footer-perPage-dropdown { + width: 75px; +} +select { + border-radius: 4px; +} +.column-1 { + width: 28%; + margin-right: 1%; +} +.column-2 { + width: 40%; + margin-right: 1%; +} +.column-3 { + width: 12%; + margin-right: 1%; +} +.column-4 { + width: 16%; + margin-right: 1%; +} +.column-5 { + width: 13%; + margin-right: 1%; +} +.column-6 { + width: 14%; +} +.status { + width: 80px; + text-align: center; + border-radius: 3px; + height: 24px; + line-height: 24px; + filter: saturate(0.5); + color: black; +} +.status.green { + background-color: #E8FFF8; + color: #33AB84; +} +.status.red { + background-color: rgb(248, 181, 181); + color: rgba(150, 4, 4, 0.945); +} +.status.yellow { + background-color: rgb(248, 243, 181); + color: rgba(150, 138, 4, 0.945); +} +.vertical-center { + display: flex; + flex-direction: column; + justify-content: center; +} + +/* Tooltip styles */ +.campaign-name-label, .campaign-name { + position: relative; + display: inline-block; +} + +.campaign-name-label:hover .tooltip { + visibility: visible; + opacity: 1; +} + +.campaign-name:hover .tooltip { + visibility: visible; + opacity: 1; +} + +.tooltip { + background-color: #D3D3D3; + border-radius: 2px; + visibility: hidden; + z-index: 1; + position: absolute; + text-align: center; + width: 100px; + padding: 2px 0; + font: normal normal normal 12px/17px Adobe Clean; + margin-left: 20px; +} + diff --git a/blocks/gmo-program-list/gmo-program-list.js b/blocks/gmo-program-list/gmo-program-list.js new file mode 100644 index 00000000..d1b94f1a --- /dev/null +++ b/blocks/gmo-program-list/gmo-program-list.js @@ -0,0 +1,481 @@ +import { readBlockConfig } from '../../scripts/lib-franklin.js'; +import { decorateIcons } from '../../scripts/lib-franklin.js'; +import { graphqlAllCampaignsFilter, graphqlCampaignCount, generateFilterJSON } from '../../scripts/graphql.js'; +import { getProductMapping, checkBlankString, statusMapping } from '../../scripts/shared-program.js' +import { getBaseConfigPath } from '../../scripts/site-config.js'; +import { searchAsset } from '../../scripts/assets.js'; + +const headerConfig = [ + { + 'name': 'Marketing Moments', + 'attribute': 'campaign', + 'sortable': true + }, + { + 'name': 'Overview', + 'attribute': 'description', + 'sortable': false + }, + { + 'name': 'Launch Date', + 'attribute': 'launch', + 'sortable': true, + 'type': 'date' + }, + { + 'name': 'Products', + 'attribute': 'products' + }, + { + 'name': 'Status', + 'attribute': 'status', + 'sortable': false + } +] + +//Global variables used by helper functions +let currentPageInfo = {}; +let cursorArray = []; +let currentPage = 1; +let currentNumberPerPage = 4; +let currentGraphqlFilter = {}; +//Get Campaign Count for pagination +let campaignCount = await graphqlCampaignCount(); +let blockConfig; + +//Custom event gmoCampaignListBlock to allow the gmo-campaign-header to trigger the gmo-program-list to update +document.addEventListener('gmoCampaignListBlock', async function() { + //Build graphq filter that is passed to the graphql persisted queries + const graphQLFilterArray = getFilterValues(); + const searchInputValue = document.getElementById('campaign-search').value; + if (searchInputValue!=='') + { + graphQLFilterArray.push({type:'campaignName', value:searchInputValue, operator:'='}) + } + + currentGraphqlFilter= generateFilterJSON(graphQLFilterArray); + const block = document.querySelector('.gmo-program-list.block'); + //Get Campaign Count for pagination + campaignCount = await graphqlCampaignCount(currentGraphqlFilter); + //Trigger loading the gmo-campaign-block + //Reset page variables + currentPageInfo = {}; + cursorArray = []; + currentPage = 1; + currentNumberPerPage = 4; + + decorate( block, currentNumberPerPage, '', false, false, currentGraphqlFilter); + +}); + + +export default async function decorate(block, numPerPage = currentNumberPerPage, cursor = '', previousPage = false, nextPage = false, graphQLFilter = {}) { + if (blockConfig == undefined) blockConfig = readBlockConfig(block); + const campaignPaginatedResponse = await graphqlAllCampaignsFilter(numPerPage, cursor,graphQLFilter); + const campaigns = campaignPaginatedResponse.data.programPaginated.edges; + currentPageInfo = campaignPaginatedResponse.data.programPaginated.pageInfo; + //Current cursor used in previous page logic + currentPageInfo.currentCursor = cursor; + //Next Page + if (currentPageInfo.hasNextPage){ + currentPageInfo.nextCursor = campaigns[campaigns.length - 1].cursor; + } + + if (!previousPage && !nextPage) + { + cursorArray = campaigns.map(item => item.cursor); + } + else if (nextPage){ + + campaigns.forEach(item => { + cursorArray.push(item.cursor); + }); + } + currentPageInfo.itemCount = campaigns.length; + + const listHeaders = buildListHeaders(headerConfig); + const listItems = await buildCampaignList(campaigns, numPerPage); + const listFooter = buildListFooter(campaignCount, numPerPage); + + block.innerHTML = ` +
+
+
`; + const listContainer = block.querySelector('.list-container'); + listContainer.appendChild(listHeaders); + listContainer.appendChild(listItems); + listContainer.appendChild(listFooter); + //Show Hide Previous and Next Page buttons + const footerNext = document.querySelector('.footer-pagination-button.next'); + const footerPrev = document.querySelector('.footer-pagination-button.prev'); + if (currentPageInfo.hasPreviousPage){ + footerPrev.classList.add('active'); + } else { + footerPrev.classList.remove('active'); + } + + if (currentPageInfo.hasNextPage){ + footerNext.classList.add('active'); + } else { + footerNext.classList.remove('active'); + } + decorateIcons(block); + + //Debug Global Variables + //debug_console(); +} + +function debug_console(){ + console.log('currentPageInfo',currentPageInfo); + console.log('cursorArray',cursorArray); + console.log('currentPage',currentPage); + console.log('campaignCount',campaignCount); + +} + +function getFilterValues(){ + // Select all elements with the class 'selected-filter' + const filters = document.querySelectorAll('.selected-filter'); + // Create an array to hold the data-type and data-value attributes + const filterAttributes = []; + // Loop through each filter element and extract both 'data-type' and 'data-value' attributes + filters.forEach(filter => { + const dataType = filter.getAttribute('data-type'); + const dataValue = filter.getAttribute('data-value'); + filterAttributes.push({ type: dataType, value: dataValue, operator : "=" }); + }); + + return filterAttributes; +} + +async function buildCampaignList(campaigns, numPerPage) { + const listWrapper = document.createElement('div'); + listWrapper.classList.add('list-items'); + listWrapper.dataset.totalresults = campaigns.length; + const host = location.origin + getBaseConfigPath(); + const detailsPage = blockConfig.detailspage; + + for (const campaign of campaigns) { + const index = campaigns.indexOf(campaign); + const campaignRow = document.createElement('div'); + campaignRow.classList.add('campaign-row'); + if ((index + 1) > numPerPage) campaignRow.classList.add('hidden'); + + const campaignInfoWrapper = document.createElement('div'); + campaignInfoWrapper.classList.add('campaign-info-wrapper', 'column-1'); + + const campaignIconLink = document.createElement('a'); + let campaignDetailsLink = host + `/${detailsPage}?programName=${campaign.node.programName}&`; + campaignDetailsLink += `programReferenceNumber=${campaign.node.programReferenceNumber ? campaign.node.programReferenceNumber : ""}` + campaignIconLink.href = campaignDetailsLink; + + const campaignIcon = document.createElement('div'); + campaignIcon.classList.add('campaign-icon'); + campaignIcon.dataset.programname = campaign.node.programName; + campaignIcon.dataset.campaignname = campaign.node.campaignName; + //Add Icon Image + const iconImage = document.createElement('img'); + try { + const imageObject = await searchAsset(campaign.node.programName, campaign.node.campaignName); + iconImage.src = imageObject.imageUrl; + iconImage.alt = imageObject.imageAltText; + } catch (error) { + } + // Append the image to the campaignIcon div + campaignIcon.appendChild(iconImage); + campaignIconLink.appendChild(campaignIcon); + const campaignName = document.createElement('div'); + campaignName.classList.add('campaign-name-wrapper', 'vertical-center'); + + campaignName.innerHTML = ` +
+ ${checkBlankString(campaign.node.programName)} + Program Name +
+
+ ${checkBlankString(campaign.node.campaignName)} + Marketing Moment +
+ `; + + campaignInfoWrapper.appendChild(campaignIconLink); + campaignInfoWrapper.appendChild(campaignName); + + const campaignOverviewWrapper = document.createElement('div'); + campaignOverviewWrapper.classList.add('column-2', 'campaign-description-wrapper', 'vertical-center'); + + const campaignOverview = document.createElement('div'); + campaignOverview.textContent = checkBlankString(campaign.node.marketingGoal.plaintext); + campaignOverview.classList.add('campaign-description'); + campaignOverview.dataset.property = 'description'; + campaignOverviewWrapper.appendChild(campaignOverview); + + const campaignLaunch = document.createElement('div'); + campaignLaunch.textContent = checkBlankString(campaign.node.launchDate); + campaignLaunch.classList.add('column-3', 'campaign-launch-date', 'vertical-center'); + campaignLaunch.dataset.property = 'launch'; + + const campaignProducts = await buildProduct(checkBlankString(campaign.node.productOffering)); + campaignProducts.classList.add('column-4', 'vertical-center'); + + var campaignStatusWrapper = document.createElement('div'); + campaignStatusWrapper.classList.add('status-wrapper', 'column-6', 'vertical-center'); + campaignStatusWrapper = buildStatus(campaignStatusWrapper, campaign); + campaignRow.appendChild(campaignInfoWrapper); + campaignRow.appendChild(campaignOverviewWrapper); + campaignRow.appendChild(campaignLaunch); + campaignRow.appendChild(campaignProducts); + campaignRow.appendChild(campaignStatusWrapper); + + listWrapper.appendChild(campaignRow); + } + return listWrapper; +} + +function buildStatus(statusWrapper, campaign) { + const campaignStatus = document.createElement('div'); + const statusStr = checkBlankString(campaign.node.status); + const statusArray = statusMapping.data.jsonByPath.item.json.options; + const statusMatch = statusArray.filter(item => item.value === statusStr); + const statusText = statusMatch.length > 0 ? statusMatch[0].text : statusStr; + campaignStatus.textContent = statusText; + campaignStatus.style.backgroundColor = "#" + statusMatch[0]["color-code"]; + campaignStatus.classList.add('status'); + campaignStatus.dataset.property = 'status'; + statusWrapper.appendChild(campaignStatus); + return statusWrapper; +} + +async function buildProduct(product) { + const productParent = document.createElement('div'); + const productMapping = await getProductMapping(product); + const productEl = document.createElement('div'); + productEl.classList.add('product-entry'); + productEl.innerHTML = ` + + ${productMapping.label} + `; + productParent.appendChild(productEl); + return productParent; +} + +function buildListHeaders(headerConfig) { + const config = headerConfig; + const listHeaders = document.createElement('div'); + listHeaders.classList.add('list-header'); + let columnCounter = 1; + config.forEach((column) => { + const columnWrapper = document.createElement('div'); + columnWrapper.classList.add('column-header-wrapper'); + columnWrapper.classList.add(`column-${columnCounter}`); + const columnEl = document.createElement('div'); + columnEl.classList.add('column-label'); + columnEl.dataset.sortable = column.sortable; + columnEl.dataset.attribute = column.attribute; + columnEl.dataset.name = column.name; + columnEl.textContent = column.name; + + columnCounter++; + columnWrapper.appendChild(columnEl); + //sorting + if (column.sortable) { + const columnSort = document.createElement('div'); + columnSort.classList.add('column-sort-wrapper'); + const columnSortAsc = document.createElement('img'); + columnSortAsc.classList.add('column-sort-asc', 'icon'); + columnSortAsc.src = '/icons/chevronUp.svg'; + columnSortAsc.title = 'Sort (Ascending)' + columnSortAsc.addEventListener('click', () => { + sortColumn('asc', column.attribute); + }) + const columnSortDesc = document.createElement('img'); + columnSortDesc.classList.add('column-sort-desc', 'icon'); + columnSortDesc.src = '/icons/chevronDown.svg'; + columnSortDesc.title = 'Sort (Descending)'; + columnSortDesc.addEventListener('click', () => { + sortColumn('desc', column.attribute); + }) + columnSort.appendChild(columnSortAsc); + columnSort.appendChild(columnSortDesc); + columnWrapper.appendChild(columnSort); + } + //end sorting + listHeaders.appendChild(columnWrapper); + }) + return listHeaders; +} + +function buildListFooter(rows, rowsPerPage) { + const pages = Math.ceil(rows / rowsPerPage); + const footerWrapper = document.createElement('div'); + footerWrapper.classList.add('list-footer', 'footer-wrapper'); + footerWrapper.dataset.pages = pages; + const footerTotal = document.createElement('div'); + + footerTotal.textContent = `Page ${currentPage} of ${pages} -- ${rows} total results`; + footerTotal.classList.add('footer-total'); + + // pagination + const footerPagination = document.createElement('div'); + footerPagination.classList.add('footer-pagination'); + const footerPrev = document.createElement('div'); + footerPrev.classList.add('footer-pagination-button', 'prev'); + footerPrev.textContent = 'Prev'; + footerPrev.addEventListener('click', (event) => { + prevPage(event.target); + }) + + const footerPageBtnsWrapper = document.createElement('div'); + footerPageBtnsWrapper.classList.add('footer-pages-wrapper'); + const footerNext = document.createElement('div'); + footerNext.classList.add('footer-pagination-button', 'next'); + //Show current page + buildCurrentPageDivElement(currentPage, footerPageBtnsWrapper); + + footerNext.addEventListener('click', (event) => { + nextPage(event.target); + }) + footerNext.textContent = 'Next'; + footerPagination.appendChild(footerPrev); + footerPagination.appendChild(footerPageBtnsWrapper); + footerPagination.appendChild(footerNext); + // end pagination + + // per-page controls + const footerPerPage = document.createElement('div'); + footerPerPage.classList.add('footer-perPage'); + const footerPerPageLabel = document.createElement('div'); + footerPerPageLabel.textContent = 'Per Page'; + footerPerPageLabel.classList.add('footer-perPage-label'); + const footerPerPageDropdownWrapper = document.createElement('div'); + const footerPerPageDropdown = document.createElement('select'); + footerPerPageDropdown.id = 'per-page'; + footerPerPageDropdown.innerHTML = ` + + + + + + + + `; + + // Selecting the item based on the value of currentNumberPerPage + var options = footerPerPageDropdown.querySelectorAll('option'); + options.forEach(option => { + if (option.value === currentNumberPerPage.toString()) { + option.selected = true; + } + }); + + footerPerPageDropdown.addEventListener('change', (event) => { + repaginate(event.target); + }); + footerPerPageDropdownWrapper.appendChild(footerPerPageDropdown); + footerPerPageDropdownWrapper.classList.add('footer-perPage-dropdown'); + footerPerPage.appendChild(footerPerPageLabel); + footerPerPage.appendChild(footerPerPageDropdownWrapper); + // end per-page controls + + footerWrapper.appendChild(footerTotal); + footerWrapper.appendChild(footerPagination); + footerWrapper.appendChild(footerPerPage); + return footerWrapper; +} + +//Show current page +function buildCurrentPageDivElement(pageNumber,footerPageBtnsWrapper) +{ + const footerPageBtn = document.createElement('div'); + footerPageBtn.classList.add('footer-pagination-pages', 'currentpage'); + footerPageBtn.id = "current-page"; + footerPageBtn.textContent = pageNumber; + footerPageBtn.dataset.pagenumber = pageNumber; + footerPageBtnsWrapper.appendChild(footerPageBtn); +} + +function repaginate(dropdown) { + currentNumberPerPage = dropdown.value; + //Reset current page to 1 + currentPage = 1; + const block = document.querySelector('.gmo-program-list.block'); + //Reset cursor to '' + decorate(block, currentNumberPerPage, '', false, false); +} + +function nextPage(nextBtn) { + if (currentPageInfo.hasNextPage) { + //Calculate Next Page + currentPage++; + const block = document.querySelector('.gmo-program-list.block'); + decorate( block, currentNumberPerPage, currentPageInfo.nextCursor, false, true,currentGraphqlFilter); + if (!(nextBtn.classList.contains('active'))) { + return; + } + const prevBtn = document.querySelector('.footer-pagination-button.prev'); + prevBtn.classList.add('active'); + } +} + +function prevPage(prevBtn) { + if (currentPageInfo.hasPreviousPage) { + currentPage--; + const block = document.querySelector('.gmo-program-list.block'); + const currentCursor = currentPageInfo.currentCursor; + //Calculate cursor for previous page + const indexCursor = cursorArray.indexOf(currentCursor) - currentNumberPerPage; + decorate(block, currentNumberPerPage, cursorArray[indexCursor], true, false,currentGraphqlFilter); + if (!(prevBtn.classList.contains('active'))) { + return; + } + const nextBtn = document.querySelector('.footer-pagination-button.next'); + const currentPageBtn = document.querySelector('#current-page'); + const currentPageValue = parseInt(currentPageBtn.dataset.pagenumber); + const targetPage = (currentPageValue - 1); + nextBtn.classList.add('active'); + } +} + +function sortColumn(dir, property) { + const container = document.querySelector('.list-items'); + if (!container) { + console.error("Could not locate list container."); + return; + } + + const selector = '[data-property="' + property + '"]'; + const divs = document.querySelectorAll(selector); + const sortArray = []; + + divs.forEach(div => { + const textContent = div.textContent.trim(); + const row = div.closest('.campaign-row'); + sortArray.push({ textContent, row }); + }); + + if (property == 'launch') { + if (dir == 'asc') { + sortArray.sort((a,b) => { + a = a.textContent.split('/').reverse().join(''); + b = b.textContent.split('/').reverse().join(''); + return a.localeCompare(b); + }); + } else { + sortArray.sort((a,b) => { + a = a.textContent.split('/').reverse().join(''); + b = b.textContent.split('/').reverse().join(''); + return b.localeCompare(a); + }); + } + } else { + if (dir == 'asc') { + sortArray.sort((a, b) => a.textContent.localeCompare(b.textContent)); + } else { + sortArray.sort((a, b) => b.textContent.localeCompare(a.textContent)); + } + } + + sortArray.forEach(({ row }, index) => { + container.appendChild(row); + }); +} diff --git a/icons/collapse.svg b/icons/collapse.svg new file mode 100644 index 00000000..38a34b62 --- /dev/null +++ b/icons/collapse.svg @@ -0,0 +1,6 @@ + + + + + diff --git a/icons/trophy.svg b/icons/trophy.svg new file mode 100644 index 00000000..52e271ce --- /dev/null +++ b/icons/trophy.svg @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/scripts/assets.js b/scripts/assets.js new file mode 100644 index 00000000..37ec6233 --- /dev/null +++ b/scripts/assets.js @@ -0,0 +1,108 @@ +import { getBearerToken } from './security.js'; + +import { + getAssetHandlerApiKey, + getDeliveryEnvironment, + getBackendApiKey, + getSearchIndex, + initDeliveryEnvironment, + getOptimizedDeliveryUrl +} from './polaris.js'; + +import { getAdminConfig } from './site-config.js'; + +import { createSearchEndpoint, logError } from './scripts.js'; + +async function getRequestHeadersSearchAssets() { + const token = await getBearerToken(); + return { + 'Content-Type': 'application/json', + 'x-api-key': getBackendApiKey(), + Authorization: token, + 'x-adobe-accept-experimental': '1', + 'x-adp-request': 'search', + }; +} + +const getFilters = () => { + const currentDate = new Date(); + const currentEpoch = Math.floor(currentDate.getTime() / 1000); + // Algolia does not support filters based on mixed types; for example boolean & numeric field types + // is_pur-expirationDate is a boolean type; but aloglia's engine treats boolean false as 0 + // so we can use that to our advantage for numberic filters + return `is_pur-expirationDate = 0 OR pur-expirationDate > ${currentEpoch}`; +}; + + +/** + * Search Asset for programName and campaignName parameters. + * + * @param {string} - Program Name. + * @param {string} - Campaign Name. + * @returns {Image } Resolves with image object. + * @throws {Error} If an HTTP error or network error occurs. + */ + +export async function searchAsset(programName, campaignName, imageWidth = 80) { + const adminConfig = await getAdminConfig(); + const deliveryURL = await initDeliveryEnvironment(); + + const indexName = await getSearchIndex(); + + // Initialize the facetFilters array + const facetFilters = []; + if (programName) { // Check if programName is not null + facetFilters.push('gmo-programName :'+programName); + } + if (campaignName) { // Check if campaignName is not null + facetFilters.push('gmo-campaignName :'+ campaignName); + } + const data = { + requests: [ + { + indexName: indexName, + params: { + facetFilters: facetFilters, + filters: getFilters(), + highlightPostTag: '__/ais-highlight__', + highlightPreTag: '__ais-highlight__', + hitsPerPage: 1, + page: 0, + query: '', + tagFilters: '' + } + } + ] + }; + + const options = { + method: 'POST', + headers: await getRequestHeadersSearchAssets(), + body: JSON.stringify(data), + }; + + try { + const response = await fetch(createSearchEndpoint(), options); + // Handle response codes + if (response.status === 200) { + // Asset retrieved successfully + const responseBody = await response.json(); + const assetData = responseBody.results[0].hits[0]; + if (assetData) + { + const totalAssets = responseBody.results[0].nbHits; + const thumbnailURL = await getOptimizedDeliveryUrl(assetData.assetId, assetData['repo-name'], imageWidth); + return {imageUrl : thumbnailURL, imageAltText: assetData['repo-name'], assetCount: totalAssets}; + } + else + { + return null; + } + } + // Handle other response codes + throw new Error(`Failed to search asset: ${response.status} ${response.statusText}`); + } catch (error) { + logError('searchAsset', error); + throw error; + } +} diff --git a/scripts/graphql.js b/scripts/graphql.js new file mode 100644 index 00000000..504685ba --- /dev/null +++ b/scripts/graphql.js @@ -0,0 +1,248 @@ +import { getBearerToken } from './security.js'; +import { getAdminConfig } from './site-config.js'; +import { logError } from './scripts.js'; + +const baseApiUrl = `${await getGraphqlEndpoint()}/graphql/execute.json`; +const projectId = 'gmo'; + +export async function graphqlQueryNameList(queryNameList) { + const queryName = queryNameList; + //persisted query URLs have to be encoded together with the first semicolon + const graphqlEndpoint = `${baseApiUrl}/${projectId}/${queryName}`; + const jwtToken = await getBearerToken(); + + // Return the fetch promise chain so that it can be awaited outside + return fetch(graphqlEndpoint, { + method: 'GET', + headers: { + Authorization: jwtToken, + }, + }).then(response => { + if (!response.ok) { + throw new Error(`HTTP error! Status: ${response.status}`); + } + return response.json(); + }).then(data => { + return data; // Make sure to return the data so that the promise resolves with it + }).catch(error => { + console.error('Error fetching data: ', error); + throw error; // Rethrow or handle error as appropriate + }); +} + +export async function graphqlCampaignCount(filter = {}) { + const queryName = 'getCampaignNameFilter'; + const encodedSemiColon = encodeURIComponent(';'); + const encodedFilter = encodeURIComponent(JSON.stringify(filter)); + const graphqlEndpoint = `${baseApiUrl}/${projectId}/${queryName}${encodedSemiColon}filter=${encodedFilter}`; + const jwtToken = await getBearerToken(); + + try { + const options = { + method: 'GET', + headers: { + Authorization: jwtToken, + }, + }; + const response = await fetch(`${graphqlEndpoint}`, options); + // Handle response codes + if (response.status === 200) { + const responseBody = await response.json(); + return responseBody.data.programList.items.length; + } if (response.status === 404) { + // Handle 404 error + const errorResponse = await response.json(); + throw new Error(`Failed to get graphqlCampaignCount (404): ${errorResponse.detail}`); + } else { + // Handle other response codes + throw new Error(`Failed to retrieve graphqlCampaignCount: ${response.status} ${response.statusText}`); + } + } catch (error) { + // Handle network or other errors + logError('graphqlCampaignCount', error); + throw error; + } + +} + +export async function graphqlAllCampaignsFilter(first,cursor,filter) { + const queryName = 'getAllCampaigns'; + const encodedFirst = encodeURIComponent(first); + const encodedSemiColon = encodeURIComponent(';'); + const encodedCursor = encodeURIComponent(cursor); + const encodedFilter = encodeURIComponent(JSON.stringify(filter)); + const graphqlEndpoint = `${baseApiUrl}/${projectId}/${queryName}${encodedSemiColon}first=${encodedFirst}${encodedSemiColon}cursor=${encodedCursor}${encodedSemiColon}filter=${encodedFilter}`; + const jwtToken = await getBearerToken(); + + try { + const options = { + method: 'GET', + headers: { + Authorization: jwtToken, + }, + }; + const response = await fetch(`${graphqlEndpoint}`, options); + // Handle response codes + if (response.status === 200) { + const responseBody = await response.json(); + return responseBody; + } if (response.status === 404) { + // Handle 404 error + const errorResponse = await response.json(); + throw new Error(`Failed to get graphqlAllCampaignsFilter (404): ${errorResponse.detail}`); + } else { + // Handle other response codes + throw new Error(`Failed to retrieve graphqlAllCampaignsFilter: ${response.status} ${response.statusText}`); + } + } catch (error) { + // Handle network or other errors + logError('graphqlAllCampaignsFilter', error); + throw error; + } + +} + +export async function graphqlCampaignByName(campaignName) { + const queryName = 'getCampaignNames'; + const encodedCampaignName = encodeURIComponent(campaignName); + const encodedSemiColon = encodeURIComponent(';'); + //persisted query URLs have to be encoded together with the first semicolon + const graphqlEndpoint = `${baseApiUrl}/${projectId}/${queryName}${encodedSemiColon}campaignName=${encodedCampaignName}`; + const jwtToken = await getBearerToken(); + + // Return the fetch promise chain so that it can be awaited outside + return fetch(graphqlEndpoint, { + method: 'GET', + headers: { + Authorization: jwtToken, + }, + }).then(response => { + if (!response.ok) { + throw new Error(`HTTP error! Status: ${response.status}`); + } + return response.json(); + }).then(data => { + return data; // Make sure to return the data so that the promise resolves with it + }).catch(error => { + console.error('Error fetching data: ', error); + throw error; // Rethrow or handle error as appropriate + }); +} + +export async function graphqlFilterOnMarketingInitiative(marketingInitiative) { + + const baseApiUrl = `${await getGraphqlEndpoint()}/graphql/execute.json`; + const projectId = 'gmo'; + const queryName = 'filter-on-marketing-initiative'; + const encodedMarketingInitiative = encodeURIComponent(marketingInitiative); + const encodedSemiColon = encodeURIComponent(';'); + //persisted query URLs have to be encoded together with the first semicolon + const graphqlEndpoint = `${baseApiUrl}/${projectId}/${queryName}${encodedSemiColon}marketingInitiative=${encodedMarketingInitiative}`; + const jwtToken = await getBearerToken(); + + // Return the fetch promise chain so that it can be awaited outside + return fetch(graphqlEndpoint, { + method: 'GET', + headers: { + Authorization: jwtToken, + }, + }).then(response => { + if (!response.ok) { + throw new Error(`HTTP error! Status: ${response.status}`); + } + return response.json(); + }).then(data => { + return data; // Make sure to return the data so that the promise resolves with it + }).catch(error => { + console.error('Error fetching data: ', error); + throw error; // Rethrow or handle error as appropriate + }); +} + +async function getGraphqlEndpoint() { + const result = await getAdminConfig(); + return result.aemGraphqlEndpoint; +} + + + +/** + * Generates GraphQL Filter String from an array filterParams + * @param {Array of parameter objects} filterParams - + * Expected parameter format + * [ + * {type:"campignName", value:"Photo" ,operator : "CONTAINS"}, + * {type:"status", value:"Current" ,operator : "="} + * ] + * operator : "CONTAINS" generates + * "_expressions": [ + * { + * "value": "", + * "_operator": "CONTAINS", + * "_ignoreCase": true + * } + * ] + * operator : "=" generates + * "_expressions": [ + * { + * "value": "", + * } + * ] + * @returns {string} GraphQL Filter JSON Object. + */ + +export function generateFilterJSON(filterParams) { + + // Initialize an empty object to hold the final structure + const result = {}; + + // Iterate over each item in the filterParams array + filterParams.forEach(param => { + // Initialize the object for each parameter name if it doesn't already exist + if (!result[param.type]) { + result[param.type] = { _expressions: [] }; + } + + // Create the expression object based on the operator + const expression = { value: param.value }; + if (param.operator === "CONTAINS") { + expression._operator = "CONTAINS"; + expression._ignoreCase = true; + } + + // Add the expression object to the _expressions array for the corresponding parameter name + result[param.type]._expressions.push(expression); + }); + + // Convert the result object to JSON + const jsonResult = JSON.stringify(result,null,4); + // Logging the JSON to see the output + console.debug('Graphql filter',jsonResult); + console.debug('result', result); + return result; +} + +// general function for executing graphql queries +export async function executeQuery(queryString) { + const baseApiUrl = `${await getGraphqlEndpoint()}/graphql/execute.json`; + const projectId = 'gmo'; + const queryEndpoint = `${baseApiUrl}/${projectId}/${queryString}`; + const jwtToken = await getBearerToken(); + + return fetch(queryEndpoint, { + method: 'GET', + headers: { + Authorization: jwtToken, + }, + }).then(response => { + if (!response.ok) { + throw new Error(`HTTP error! Status: ${response.status}`); + } + return response.json(); + }).then(data => { + return data; // Make sure to return the data so that the promise resolves with it + }).catch(error => { + console.error('Error fetching data: ', error); + throw error; // Rethrow or handle error as appropriate + }); +}; \ No newline at end of file diff --git a/scripts/scripts.js b/scripts/scripts.js index f90040cf..de80ce00 100644 --- a/scripts/scripts.js +++ b/scripts/scripts.js @@ -127,7 +127,7 @@ async function loadScript(url, attrs) { }); } -function createSearchEndpoint() { +export function createSearchEndpoint() { return `${getDeliveryEnvironment()}/adobe/assets/search`; } diff --git a/scripts/security.js b/scripts/security.js index 659a7c98..edbe7458 100644 --- a/scripts/security.js +++ b/scripts/security.js @@ -1,6 +1,6 @@ import { fetchCached } from './fetch-util.js'; import { isUnifiedShellRuntimeAvailable, shell, user } from '../contenthub/unified-shell.js'; -import { getAdminConfig } from './site-config.js'; +import { getAdminConfig, getBrandingConfig, isContentHub, getQuickLinkConfig, getBaseConfigPath } from './site-config.js'; import { getSecurityGroupMemberships } from './security-imslib.js'; /** @@ -104,8 +104,22 @@ export async function checkUserAccess() { const imsLibSecurityModule = await import('./security-imslib.js'); if (isPublicPage()) { return true; - } - return await imsLibSecurityModule.isUserInSecurityGroup(imsUserGroup, await getBearerToken()); + } + const isIMSUser = await imsLibSecurityModule.isUserInSecurityGroup(imsUserGroup, await getBearerToken()); + if (isIMSUser) { + //Check if current page is present in the array of pages returned by function getQuickLinkConfig() + //Split the URL into parts + const currentUrlParts=window.location.pathname.replace(getBaseConfigPath((window.location.pathname)),'').split('/'); + //Current url beginning with / + const currentURL = '/'+currentUrlParts[1]; + const presentInQuickLinks = (await getQuickLinkConfig()).some((grp) => grp.page === (currentURL)); + + return presentInQuickLinks; + } + else + { //Not IMSUser + return isIMSUser; + } } } @@ -113,4 +127,15 @@ export async function checkAddAssetsAccess() { const adminConfig = await getAdminConfig(); const securityGroupMemberships = await getSecurityGroupMemberships(await getBearerToken()); return securityGroupMemberships.some((grp) => grp.groupName === adminConfig.imsAuthorGroup); -} \ No newline at end of file +} + +/** + * Checks Group Access for the group that is stored in the admin-config.xslx for + * for the property name in parameter adminConfigGroupPropertyName + * @returns {boolean} for access to the group + */ +export async function checkPageGroupAccess(adminConfigGroupPropertyName) { + const adminConfig = await getAdminConfig(); + const securityGroupMemberships = await getSecurityGroupMemberships(await getBearerToken()); + return securityGroupMemberships.some((grp) => grp.groupName === adminConfig[adminConfigGroupPropertyName]); +} diff --git a/scripts/shared-program.js b/scripts/shared-program.js new file mode 100644 index 00000000..f6a2e610 --- /dev/null +++ b/scripts/shared-program.js @@ -0,0 +1,55 @@ +import { graphqlQueryNameList } from "./graphql.js"; +import { getProductIconMapping, getBaseConfigPath } from './site-config.js'; + +let iconMapping; +export let statusMapping = await graphqlQueryNameList('getStatusList'); +export let productList = await graphqlQueryNameList('getProductList'); + +/* +* Executes graphql query for 'friendly' labels and returns array of the results +*/ +export async function resolveMappings(mappingType) { + const response = await graphqlQueryNameList(mappingType); + const mappingArray = response.data.jsonByPath.item.json.options; + return mappingArray; +} + +/** + * Filter provided array based on provided key/value pair + */ +export function filterArray(array, key, value) { + const arrayMatch = array.filter(match => match[key] === value); + return arrayMatch.length > 0 ? arrayMatch : null; +} + +export async function getProductMapping(product) { + let iconMatch; + const configPath = getBaseConfigPath(); + const defaultIcon = configPath + '/logo/products/default-app-icon.svg'; + if (iconMapping == undefined) iconMapping = await getProductIconMapping(); + if (iconMapping) { + iconMatch = filterArray(iconMapping, 'Product-offering', product); + } + const icon = iconMatch ? configPath + iconMatch[0]['Icon-path'] : defaultIcon; + + if (productList == undefined) productList = await graphqlQueryNameList('getProductList'); + const productsArray = productList.data.jsonByPath.item.json.options; + const productsMatch = filterArray(productsArray, 'value', product); + const productsText = productsMatch ? productsMatch[0].text : product; + + return { + label: productsText, + icon: icon + } +} + +/* +* Check for undefined/blank property and supply 'Not Available' if no data +*/ +export function checkBlankString(string) { + if (string == undefined || string == '' ) { + return 'Not Available'; + } else { + return string; + } +} diff --git a/scripts/site-config.js b/scripts/site-config.js index c906db24..6c9694ff 100644 --- a/scripts/site-config.js +++ b/scripts/site-config.js @@ -1,5 +1,6 @@ import { fetchCached } from './fetch-util.js'; import { toCamelCase } from './lib-franklin.js'; +import { checkPageGroupAccess } from './security.js'; const QA_BASE_PATH = 'qa'; const DRAFTS_BASE_PATH = 'drafts'; @@ -285,14 +286,18 @@ async function mapUserSettingsForId(configId, result) { export async function getQuickLinkConfig() { const result = []; const response = await getConfig('site-config.json'); - response.quicklinks?.data.forEach((row) => { + + for (const row of response.quicklinks?.data || []) { if (row.Title && row.Page) { - result.push({ - title: row.Title, - page: row.Page, - }); + if (!row.Group || (await checkPageGroupAccess(row.Group))) { + result.push({ + title: row.Title, + page: row.Page, + hide: row.Hide, + }); + } } - }); + } return result; } @@ -361,3 +366,17 @@ export async function getLicenseAgreementText() { }); return licenseAgreement; } + +/** + * @returns {Array} + */ +export async function getProductIconMapping() { + let iconArray; + try { + const response = await getConfig('site-config.json'); + iconArray = response['product-icons'].data; + } catch { + console.log("Unable to retrieve site-config.json"); + } + return iconArray; +} \ No newline at end of file