From 423a3788a8ff5c004d90cb099cedcd620b95d3f4 Mon Sep 17 00:00:00 2001 From: mathieu-lessard Date: Fri, 3 May 2024 12:21:51 -0700 Subject: [PATCH 01/95] Added mapping for uuid (#74) Co-authored-by: Mathieu Lessard --- scripts/metadata.js | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/metadata.js b/scripts/metadata.js index f9368c5a..ccc53b8b 100644 --- a/scripts/metadata.js +++ b/scripts/metadata.js @@ -12,6 +12,7 @@ import { fetchCached } from './fetch-util.js'; import { getBaseConfigPath, getMetadataConfigs } from './site-config.js'; const SEARCH_FIELD_TO_POLARIS_API_MAP = { + uuid: 'assetId', 'dc-format': 'repositoryMetadata.dc:format', 'repo-name': 'repositoryMetadata.repo:name', 'repo-createDate': 'repositoryMetadata.repo:createDate', From f7c918ac04d3e860ee150d735d5e482b4c864ad0 Mon Sep 17 00:00:00 2001 From: Christopher Heintzman Date: Wed, 8 May 2024 17:03:05 -0400 Subject: [PATCH 02/95] Removed Prefix from Displayed UUID Value on Assets (#76) --- scripts/metadata-html-builder.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/scripts/metadata-html-builder.js b/scripts/metadata-html-builder.js index 8ae18086..92da6528 100644 --- a/scripts/metadata-html-builder.js +++ b/scripts/metadata-html-builder.js @@ -139,6 +139,12 @@ export async function fetchMetadataAndCreateHTML(metadataViewConfig, assetData, if (assetData === undefined || assetJSON === undefined) { assetJSON = await getAssetMetadata(getAssetIdFromURL()); } + // Remove "urn:aaid:aem:" prefix from displayed 'assetId' value + if (assetJSON.assetId) { + // A deep copy of the assetJSON object is created to avoid modifying the original object + assetJSON = JSON.parse(JSON.stringify(assetJSON)); + assetJSON.assetId = assetJSON.assetId.replace('urn:aaid:aem:', ''); + } const metadataContainer = document.createElement('div'); metadataContainer.classList.add('metadata-container'); From 61f761195d0d96550ce39419ab25c6a20cdea7ce Mon Sep 17 00:00:00 2001 From: Christopher Heintzman Date: Thu, 9 May 2024 14:34:50 -0400 Subject: [PATCH 03/95] Modified formatAssetMetadata() to allow custom dc-format Labels (#79) --- scripts/metadata.js | 5 ----- 1 file changed, 5 deletions(-) diff --git a/scripts/metadata.js b/scripts/metadata.js index ccc53b8b..dd0d036a 100644 --- a/scripts/metadata.js +++ b/scripts/metadata.js @@ -387,11 +387,6 @@ export function formatAssetMetadata(propertyName, metadataValue) { return DATA_TYPES.tags(metadataValue, propertyName); } - // file types - if (['dc-format'].includes(propertyName)) { - return PREDEFINED_METADATA_FIELDS.format.format(metadataValue); - } - // dates if (isDate(propertyName, metadataValue)) { return formatDate(metadataValue); From 777b5cbed5b653adff26b38ef0ea1ed1c991c572 Mon Sep 17 00:00:00 2001 From: mathieu-lessard Date: Fri, 17 May 2024 10:38:22 -0700 Subject: [PATCH 04/95] Removed "required" and custom values for fields (#86) Co-authored-by: Mathieu Lessard --- contenthub/hydration/hydration-utils.js | 21 +++++++-------------- 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/contenthub/hydration/hydration-utils.js b/contenthub/hydration/hydration-utils.js index 61471d8c..d2c3912f 100644 --- a/contenthub/hydration/hydration-utils.js +++ b/contenthub/hydration/hydration-utils.js @@ -29,10 +29,6 @@ export function formIsComplete(metadataSchema, formValues) { if(incomplete) return false; - if(formValues['gmo:campaignName']){ - if(!formValues['gmo:programName']) return false; - } - if(formValues['gmo:licensedContent'] !== 'no'){ if(!formValues['gmo:usageTerms']) return false; if(formValues['gmo:licensedContent'] === 'yes-expires' && !formValues['gmo:licenseExpiryDate']) return false; @@ -116,7 +112,8 @@ export function getMetadataSchema(facetOptions){ label: 'Product', placeholder: 'Select one or more', required: true, - element: 'tags', + element: 'dropdown', + multipleSelection: true, dropdownOptions: [ { name: 'N/A', id: 'na' }, { name: 'Acrobat Export PDF', id: 'acrobat-export-pdf' }, @@ -217,13 +214,7 @@ export function getMetadataSchema(facetOptions){ return value.toLowerCase().split(' ').every((val) => name.includes(val)); } ) - }, - required: true, - requires: [{ - property: 'gmo:campaignName', - expectedValue: '', - operator: '!==' - }] + } }, { mapToProperty: 'gmo:deliverableType', @@ -360,7 +351,8 @@ export function getMetadataSchema(facetOptions){ mapToProperty: 'gmo:ddomStage', label: 'DDOM Stage', placeholder: 'Select one or more', - element: 'tags', + element: 'dropdown', + multipleSelection: true, dropdownOptions: [ { id: 'discover', @@ -388,7 +380,8 @@ export function getMetadataSchema(facetOptions){ mapToProperty: 'gmo:p0TargetMarketGeo', label: 'Target Market', placeholder: 'Select one or more', - element: 'tags', + element: 'dropdown', + multipleSelection: true, dropdownOptions: [ { id: 'apac', From e1748ca349d43d2b1919cbf83153f3f63b203428 Mon Sep 17 00:00:00 2001 From: Christopher Heintzman Date: Thu, 23 May 2024 13:23:25 -0400 Subject: [PATCH 05/95] Added getBaseConfigPath to Collections Back Button (#89) --- blocks/adp-collection-header/adp-collection-header.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/blocks/adp-collection-header/adp-collection-header.js b/blocks/adp-collection-header/adp-collection-header.js index 7bea4d74..0d496732 100644 --- a/blocks/adp-collection-header/adp-collection-header.js +++ b/blocks/adp-collection-header/adp-collection-header.js @@ -2,6 +2,7 @@ import { getCollection, getCollectionIdFromURL, deleteCollection } from '../../s import createConfirmDialog from '../../scripts/confirm-dialog.js'; import { decorateIcons } from '../../scripts/lib-franklin.js'; import { createLinkHref, navigateTo } from '../../scripts/shared.js'; +import { getBaseConfigPath } from '../../scripts/site-config.js'; import { selectAllAssets, deselectAllAssets, @@ -13,7 +14,7 @@ function createCollectionInfoHeader(collectionInfoHeader, collection) { collectionInfoHeader.innerHTML = `
- +
From 9c80f01ab1f85c7be83d0ee9b199c2657a4bec9e Mon Sep 17 00:00:00 2001 From: Shivani Gupta <61603050+shiv-gup@users.noreply.github.com> Date: Mon, 3 Jun 2024 14:43:13 -0400 Subject: [PATCH 06/95] Releasing HCV Dashboard code (#106) * 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 * Release 05.09.2024 (#78) * Added mapping for uuid (#74) Co-authored-by: Mathieu Lessard * Removed Prefix from Displayed UUID Value on Assets (#76) --------- Co-authored-by: Mathieu Lessard Co-authored-by: Christopher Heintzman * 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: Michael Dickson Co-authored-by: Samruddhi <150183547+staware30@users.noreply.github.com> Co-authored-by: mdickson-adbe <95774602+mdickson-adbe@users.noreply.github.com> Co-authored-by: Tyrone Tse Co-authored-by: Shivani gupta Co-authored-by: mathieu-lessard Co-authored-by: Mathieu Lessard Co-authored-by: Christopher Heintzman 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 From 112dc3a1ef7f51d2b36ba0fca0b80346ca69fbd0 Mon Sep 17 00:00:00 2001 From: TyroneAEM <147942284+TyroneAEM@users.noreply.github.com> Date: Mon, 3 Jun 2024 19:51:46 -0500 Subject: [PATCH 07/95] ASSETS-88912 : [Issue] Last page number is not matching with total number of pages (#105) * 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 * Release 05.09.2024 (#78) * Added mapping for uuid (#74) Co-authored-by: Mathieu Lessard * Removed Prefix from Displayed UUID Value on Assets (#76) --------- Co-authored-by: Mathieu Lessard Co-authored-by: Christopher Heintzman * 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 * Change default items per page from 4 to 8 Changed undefined message for Campaign to Marketing Moment Not Available * Remove debugging from graphql.js * Deleted function debug_console() --------- Co-authored-by: Shivani Gupta <61603050+shiv-gup@users.noreply.github.com> Co-authored-by: Michael Dickson Co-authored-by: Samruddhi <150183547+staware30@users.noreply.github.com> Co-authored-by: mdickson-adbe <95774602+mdickson-adbe@users.noreply.github.com> Co-authored-by: Shivani gupta Co-authored-by: mathieu-lessard Co-authored-by: Mathieu Lessard Co-authored-by: Christopher Heintzman Co-authored-by: Shivani gupta --- blocks/gmo-program-list/gmo-program-list.js | 19 +++++-------------- scripts/graphql.js | 6 ++---- scripts/shared-program.js | 6 +++--- 3 files changed, 10 insertions(+), 21 deletions(-) diff --git a/blocks/gmo-program-list/gmo-program-list.js b/blocks/gmo-program-list/gmo-program-list.js index d1b94f1a..71806d5f 100644 --- a/blocks/gmo-program-list/gmo-program-list.js +++ b/blocks/gmo-program-list/gmo-program-list.js @@ -33,11 +33,13 @@ const headerConfig = [ } ] +const DEFAULT_ITEMS_PER_PAGE = 8; //Global variables used by helper functions let currentPageInfo = {}; let cursorArray = []; let currentPage = 1; -let currentNumberPerPage = 4; +let currentNumberPerPage = DEFAULT_ITEMS_PER_PAGE; + let currentGraphqlFilter = {}; //Get Campaign Count for pagination let campaignCount = await graphqlCampaignCount(); @@ -62,7 +64,7 @@ document.addEventListener('gmoCampaignListBlock', async function() { currentPageInfo = {}; cursorArray = []; currentPage = 1; - currentNumberPerPage = 4; + currentNumberPerPage = DEFAULT_ITEMS_PER_PAGE; decorate( block, currentNumberPerPage, '', false, false, currentGraphqlFilter); @@ -120,16 +122,6 @@ export default async function decorate(block, numPerPage = currentNumberPerPage, 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); } @@ -193,7 +185,7 @@ async function buildCampaignList(campaigns, numPerPage) { Program Name
- ${checkBlankString(campaign.node.campaignName)} + ${checkBlankString(campaign.node.campaignName,'Marketing Moment Not Available')} Marketing Moment
`; @@ -351,7 +343,6 @@ function buildListFooter(rows, rowsPerPage) { const footerPerPageDropdown = document.createElement('select'); footerPerPageDropdown.id = 'per-page'; footerPerPageDropdown.innerHTML = ` - diff --git a/scripts/graphql.js b/scripts/graphql.js index 504685ba..72f6ceaa 100644 --- a/scripts/graphql.js +++ b/scripts/graphql.js @@ -216,9 +216,7 @@ export function generateFilterJSON(filterParams) { // 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; } @@ -245,4 +243,4 @@ export async function executeQuery(queryString) { console.error('Error fetching data: ', error); throw error; // Rethrow or handle error as appropriate }); -}; \ No newline at end of file +}; diff --git a/scripts/shared-program.js b/scripts/shared-program.js index f6a2e610..39e8db93 100644 --- a/scripts/shared-program.js +++ b/scripts/shared-program.js @@ -46,9 +46,9 @@ export async function getProductMapping(product) { /* * Check for undefined/blank property and supply 'Not Available' if no data */ -export function checkBlankString(string) { - if (string == undefined || string == '' ) { - return 'Not Available'; +export function checkBlankString(string, notAvailableText = 'Not Available') { + if (string == undefined || string == '') { + return notAvailableText; } else { return string; } From 62f579f6dd71c7deb11e9b19c98a48669db3707d Mon Sep 17 00:00:00 2001 From: mdickson-adbe <95774602+mdickson-adbe@users.noreply.github.com> Date: Tue, 4 Jun 2024 16:52:52 -0400 Subject: [PATCH 08/95] Performance refactoring (#107) * performance refactoring - improve thumbnail performance by makin async - format date property to yyyy-mm-dd - fix margin issue in program header - refactor function that builds header dropdown for increased performance * resolve bug with event listeners not attaching --- .../gmo-program-header/gmo-program-header.css | 2 +- .../gmo-program-header/gmo-program-header.js | 43 ++++++++-------- blocks/gmo-program-list/gmo-program-list.css | 17 +++++-- blocks/gmo-program-list/gmo-program-list.js | 50 ++++++++++++------- scripts/graphql.js | 30 ----------- 5 files changed, 64 insertions(+), 78 deletions(-) diff --git a/blocks/gmo-program-header/gmo-program-header.css b/blocks/gmo-program-header/gmo-program-header.css index ac30d581..e00c20fb 100644 --- a/blocks/gmo-program-header/gmo-program-header.css +++ b/blocks/gmo-program-header/gmo-program-header.css @@ -1,4 +1,4 @@ -.gmo-campaign-header.block { +.gmo-program-header.block { display: flex; flex-direction: column; margin-top: 20px; diff --git a/blocks/gmo-program-header/gmo-program-header.js b/blocks/gmo-program-header/gmo-program-header.js index 98e15246..d7a9e46d 100644 --- a/blocks/gmo-program-header/gmo-program-header.js +++ b/blocks/gmo-program-header/gmo-program-header.js @@ -2,9 +2,6 @@ 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 = `
@@ -128,32 +125,29 @@ export default async function decorate(block) { sendGmoCampaignListBlockEvent(); }); - await initializeDropdowns(); - attachEventListeners(); + initializeDropdowns(); decorateIcons(block); + document.addEventListener('click', handleClickOutside); } async function initializeDropdowns() { // Business Line List + graphqlQueryNameList('getBusinessLine').then((response) => { + populateDropdown(response, 'dropdownBusinessOptions', 'businessLine'); + }); - const businessLineResponse = await graphqlQueryNameList('getBusinessLine'); - const businessLines = businessLineResponse.data.jsonByPath.item.json.options; - populateDropdown(businessLines, 'dropdownBusinessOptions', 'businessLine'); + // Geo List + graphqlQueryNameList('getGeoList').then((response) => { + populateDropdown(response, 'dropdownGeoOptions', 'p0TargetGeo'); + }); // Status List const statusResponse = await statusMapping; - const statuses = statusResponse.data.jsonByPath.item.json.options; - populateDropdown(statuses, 'dropdownStatusOptions', 'status'); + populateDropdown(statusResponse, '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'); + populateDropdown(productResponse, 'dropdownProductOptions', 'productOffering'); } // Function to attach event listeners @@ -173,12 +167,10 @@ function attachEventListeners() { if (resetFiltersBtn) { resetFiltersBtn.addEventListener('click', resetFiltersClickHandler); } - - // Add event listener for clicks outside of dropdowns - document.addEventListener('click', handleClickOutside); } -function populateDropdown(options, dropdownId, type) { +function populateDropdown(response, dropdownId, type) { + const options = response.data?.jsonByPath ? response.data.jsonByPath.item.json.options : response; let dropdownContent = document.getElementById(dropdownId); dropdownContent.innerHTML = ''; options.forEach((option, index) => { @@ -189,13 +181,18 @@ function populateDropdown(options, dropdownId, type) { anchor.dataset.type = type; anchor.className = "dropoption"; anchor.textContent = option.text; + anchor.addEventListener('click', dropOptionClickHandler); dropdownContent.appendChild(anchor); }); + // add event listener to button + const button = dropdownContent.parentElement.querySelector(".dropdown-button"); + button.addEventListener('click', dropdownButtonClickHandler); } // Function to filter products based on selected business line function filterProductsByBusinessLine(businessLine) { - const filteredProducts = allProducts.filter(product => + const products = productList.data.jsonByPath.item.json.options; + const filteredProducts = products.filter(product => product['business-line'].includes(businessLine) ); populateDropdown(filteredProducts, 'dropdownProductOptions', 'productOffering'); @@ -324,7 +321,7 @@ function resetAllFilters() { function resetProductsDropDown(){ // Populate all products into Products dropdown - populateDropdown(allProducts, 'dropdownProductOptions', 'productOffering'); + populateDropdown(productList, 'dropdownProductOptions', 'productOffering'); // Reset product offering filters removeSelectedProductOfferingFilters(); attachEventListeners(); diff --git a/blocks/gmo-program-list/gmo-program-list.css b/blocks/gmo-program-list/gmo-program-list.css index 479d9922..1942d063 100644 --- a/blocks/gmo-program-list/gmo-program-list.css +++ b/blocks/gmo-program-list/gmo-program-list.css @@ -26,9 +26,20 @@ body { display: flex; font-weight: bold; margin-bottom: 20px; - + position: sticky; padding-top: 5px; } +.list-items { + display: flex; + flex-direction: column; + overflow-y: scroll; + height: 65vh; + -ms-overflow-style: none; + scrollbar-width: none; + &::-webkit-scrollbar { + display: none; + } +} .column-header-wrapper { display: flex; } @@ -48,10 +59,6 @@ body { height: 14px; width: 14px; } -.list-items { - display: flex; - flex-direction: column; -} .campaign-row { display: flex; font-size: 14px; diff --git a/blocks/gmo-program-list/gmo-program-list.js b/blocks/gmo-program-list/gmo-program-list.js index 71806d5f..b86c0396 100644 --- a/blocks/gmo-program-list/gmo-program-list.js +++ b/blocks/gmo-program-list/gmo-program-list.js @@ -150,6 +150,10 @@ async function buildCampaignList(campaigns, numPerPage) { for (const campaign of campaigns) { const index = campaigns.indexOf(campaign); const campaignRow = document.createElement('div'); + const programName = campaign.node.programName; + const campaignName = campaign.node.campaignName; + const programRef = campaign.node.programReferenceNumber; + campaignRow.classList.add('campaign-row'); if ((index + 1) > numPerPage) campaignRow.classList.add('hidden'); @@ -163,35 +167,27 @@ async function buildCampaignList(campaigns, numPerPage) { 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); + campaignIcon.dataset.programname = programName; + campaignIcon.dataset.campaignname = campaignName; + campaignIcon.dataset.reference = programRef; + addThumbnail(campaignIcon, programName, campaignName); campaignIconLink.appendChild(campaignIcon); - const campaignName = document.createElement('div'); - campaignName.classList.add('campaign-name-wrapper', 'vertical-center'); + const campaignNameWrapper = document.createElement('div'); + campaignNameWrapper.classList.add('campaign-name-wrapper', 'vertical-center'); - campaignName.innerHTML = ` + campaignNameWrapper.innerHTML = `
- ${checkBlankString(campaign.node.programName)} + ${checkBlankString(programName)} Program Name
- ${checkBlankString(campaign.node.campaignName,'Marketing Moment Not Available')} + ${checkBlankString(campaignName,'Marketing Moment Not Available')} Marketing Moment
`; campaignInfoWrapper.appendChild(campaignIconLink); - campaignInfoWrapper.appendChild(campaignName); + campaignInfoWrapper.appendChild(campaignNameWrapper); const campaignOverviewWrapper = document.createElement('div'); campaignOverviewWrapper.classList.add('column-2', 'campaign-description-wrapper', 'vertical-center'); @@ -203,7 +199,7 @@ async function buildCampaignList(campaigns, numPerPage) { campaignOverviewWrapper.appendChild(campaignOverview); const campaignLaunch = document.createElement('div'); - campaignLaunch.textContent = checkBlankString(campaign.node.launchDate); + campaignLaunch.textContent = dateFormat(campaign.node.launchDate); campaignLaunch.classList.add('column-3', 'campaign-launch-date', 'vertical-center'); campaignLaunch.dataset.property = 'launch'; @@ -238,6 +234,17 @@ function buildStatus(statusWrapper, campaign) { return statusWrapper; } +async function addThumbnail(parentElement, programName, campaignName) { + searchAsset(programName, campaignName).then((response) => { + if (response && (Object.hasOwn(response, 'imageUrl') && Object.hasOwn(response, 'imageAltText'))) { + const iconImage = document.createElement('img'); + iconImage.src = response?.imageUrl; + iconImage.alt = response?.imageAltText; + parentElement.appendChild(iconImage); + } + }) +} + async function buildProduct(product) { const productParent = document.createElement('div'); const productMapping = await getProductMapping(product); @@ -470,3 +477,8 @@ function sortColumn(dir, property) { container.appendChild(row); }); } + +function dateFormat(dateString) { + const formattedDate = dateString ? dateString.split('T')[0] : 'Not Available'; + return formattedDate; +} \ No newline at end of file diff --git a/scripts/graphql.js b/scripts/graphql.js index 72f6ceaa..9ed80bce 100644 --- a/scripts/graphql.js +++ b/scripts/graphql.js @@ -129,36 +129,6 @@ export async function graphqlCampaignByName(campaignName) { }); } -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; From 476410161909dc6d1f332aca2f8e61741a40e230 Mon Sep 17 00:00:00 2001 From: mdickson-adbe <95774602+mdickson-adbe@users.noreply.github.com> Date: Wed, 5 Jun 2024 13:30:37 -0400 Subject: [PATCH 09/95] add programID parameter to deliverable query (#109) * add programID parameter to deliverable query * quick null check for status color * remove console log --- .../gmo-program-details.js | 6 +++--- blocks/gmo-program-list/gmo-program-list.js | 21 +++++++++++++------ 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/blocks/gmo-program-details/gmo-program-details.js b/blocks/gmo-program-details/gmo-program-details.js index 5347cb4a..55522099 100644 --- a/blocks/gmo-program-details/gmo-program-details.js +++ b/blocks/gmo-program-details/gmo-program-details.js @@ -7,7 +7,7 @@ import { searchAsset } from '../../scripts/assets.js'; let blockConfig; const programName = getQueryVariable('programName'); -const programRefNumber = getQueryVariable('programReferenceNumber'); +const programID = getQueryVariable('programID'); const deliverableMappings = resolveMappings("getDeliverableTypeMapping"); const platformMappings = resolveMappings("getPlatformsMapping"); @@ -15,9 +15,9 @@ export default async function decorate(block) { const encodedSemi = encodeURIComponent(';'); const encodedProgram = encodeURIComponent(programName); - const programQueryString = `getProgramDetails${encodedSemi}programName=${encodedProgram}${encodedSemi}programReferenceNumber=${encodeURIComponent(programRefNumber)}`; + const programQueryString = `getProgramDetails${encodedSemi}programName=${encodedProgram}${encodedSemi}programID=${encodeURIComponent(programID)}`; const programData = await executeQuery(programQueryString); - const deliverableQueryString = `getProgramDeliverables${encodedSemi}programName=${encodedProgram}`; + const deliverableQueryString = `getProgramDeliverables${encodedSemi}programName=${encodedProgram}${encodedSemi}programID=${encodeURIComponent(programID)}`; const deliverables = await executeQuery(deliverableQueryString); const p0TargetMarketArea = programData.data.programList.items[0].p0TargetMarketArea; diff --git a/blocks/gmo-program-list/gmo-program-list.js b/blocks/gmo-program-list/gmo-program-list.js index b86c0396..76265c26 100644 --- a/blocks/gmo-program-list/gmo-program-list.js +++ b/blocks/gmo-program-list/gmo-program-list.js @@ -152,7 +152,7 @@ async function buildCampaignList(campaigns, numPerPage) { const campaignRow = document.createElement('div'); const programName = campaign.node.programName; const campaignName = campaign.node.campaignName; - const programRef = campaign.node.programReferenceNumber; + const programID = campaign.node.programID ? campaign.node.programID : ""; campaignRow.classList.add('campaign-row'); if ((index + 1) > numPerPage) campaignRow.classList.add('hidden'); @@ -161,15 +161,15 @@ async function buildCampaignList(campaigns, numPerPage) { 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 : ""}` + let campaignDetailsLink = host + `/${detailsPage}?programName=${programName}&`; + campaignDetailsLink += `programID=${programID}` campaignIconLink.href = campaignDetailsLink; const campaignIcon = document.createElement('div'); campaignIcon.classList.add('campaign-icon'); campaignIcon.dataset.programname = programName; campaignIcon.dataset.campaignname = campaignName; - campaignIcon.dataset.reference = programRef; + campaignIcon.dataset.programid = programID; addThumbnail(campaignIcon, programName, campaignName); campaignIconLink.appendChild(campaignIcon); const campaignNameWrapper = document.createElement('div'); @@ -225,9 +225,18 @@ function buildStatus(statusWrapper, campaign) { 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; + + let statusText, statusColor; + if (statusMatch.length > 0) { + statusText = statusMatch[0].text; + statusColor = statusMatch[0]["color-code"]; + } else { + statusText = statusStr; + statusColor = "BABABA"; + } + campaignStatus.textContent = statusText; - campaignStatus.style.backgroundColor = "#" + statusMatch[0]["color-code"]; + campaignStatus.style.backgroundColor = "#" + statusColor; campaignStatus.classList.add('status'); campaignStatus.dataset.property = 'status'; statusWrapper.appendChild(campaignStatus); From fcc7312307938f8a7f8abc059d49279af3b6279f Mon Sep 17 00:00:00 2001 From: mdickson-adbe <95774602+mdickson-adbe@users.noreply.github.com> Date: Wed, 5 Jun 2024 17:45:32 -0400 Subject: [PATCH 10/95] Update query names, handle formatting (#110) * update query names * handle null deliverable types * make dateFormat function available to detail block * remove unused function * remove console log messages --- blocks/gmo-program-details/gmo-program-details.js | 13 ++++++++----- blocks/gmo-program-list/gmo-program-list.js | 7 +------ scripts/graphql.js | 4 ++-- scripts/shared-program.js | 5 +++++ 4 files changed, 16 insertions(+), 13 deletions(-) diff --git a/blocks/gmo-program-details/gmo-program-details.js b/blocks/gmo-program-details/gmo-program-details.js index 55522099..ff05b20f 100644 --- a/blocks/gmo-program-details/gmo-program-details.js +++ b/blocks/gmo-program-details/gmo-program-details.js @@ -1,7 +1,7 @@ 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 { resolveMappings, filterArray, getProductMapping, checkBlankString, dateFormat } from '../../scripts/shared-program.js'; import { getBaseConfigPath } from '../../scripts/site-config.js'; import { searchAsset } from '../../scripts/assets.js'; @@ -376,7 +376,10 @@ 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'); + // we want the 'null' deliverableType to be part of this set for filtering + const uniqueCatSet = new Set(); + deliverableList.forEach(object => { uniqueCatSet.add(object['deliverableType']) }) + const uniqueCategories = Array.from(uniqueCatSet); let emptyCategory = false; uniqueCategories.forEach(async (category) => { // build header row @@ -466,8 +469,8 @@ async function buildTableRow(deliverableJson, kpi, createHidden) { const status = (deliverableJson.deliverableStatusUpdate == null) ? "Not Available" : deliverableJson.deliverableStatusUpdate + "%"; const statusPct = (deliverableJson.deliverableStatusUpdate == null) ? "0%" : deliverableJson.deliverableStatusUpdate + "%"; dataRow.innerHTML = ` -
${deliverableJson.deliverableName}
-
${typeLabel}
+
${checkBlankString(deliverableJson.deliverableName)}
+
${checkBlankString(typeLabel)}
-
${checkBlankString(deliverableJson.taskCompletionDate)}
+
${dateFormat(deliverableJson.taskCompletionDate)}
${deliverableJson.previousTaskCompletionDate ? '
Revised from ' + deliverableJson.previousTaskCompletionDate + '
': ""}
${checkBlankString(deliverableJson.driver)}
diff --git a/blocks/gmo-program-list/gmo-program-list.js b/blocks/gmo-program-list/gmo-program-list.js index 76265c26..4ec0a174 100644 --- a/blocks/gmo-program-list/gmo-program-list.js +++ b/blocks/gmo-program-list/gmo-program-list.js @@ -1,7 +1,7 @@ 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 { getProductMapping, checkBlankString, statusMapping, dateFormat } from '../../scripts/shared-program.js' import { getBaseConfigPath } from '../../scripts/site-config.js'; import { searchAsset } from '../../scripts/assets.js'; @@ -485,9 +485,4 @@ function sortColumn(dir, property) { sortArray.forEach(({ row }, index) => { container.appendChild(row); }); -} - -function dateFormat(dateString) { - const formattedDate = dateString ? dateString.split('T')[0] : 'Not Available'; - return formattedDate; } \ No newline at end of file diff --git a/scripts/graphql.js b/scripts/graphql.js index 9ed80bce..0d59b62f 100644 --- a/scripts/graphql.js +++ b/scripts/graphql.js @@ -31,7 +31,7 @@ export async function graphqlQueryNameList(queryNameList) { } export async function graphqlCampaignCount(filter = {}) { - const queryName = 'getCampaignNameFilter'; + const queryName = 'getTotalPrograms'; const encodedSemiColon = encodeURIComponent(';'); const encodedFilter = encodeURIComponent(JSON.stringify(filter)); const graphqlEndpoint = `${baseApiUrl}/${projectId}/${queryName}${encodedSemiColon}filter=${encodedFilter}`; @@ -103,7 +103,7 @@ export async function graphqlAllCampaignsFilter(first,cursor,filter) { } export async function graphqlCampaignByName(campaignName) { - const queryName = 'getCampaignNames'; + const queryName = 'getCampaignNamesFilter'; const encodedCampaignName = encodeURIComponent(campaignName); const encodedSemiColon = encodeURIComponent(';'); //persisted query URLs have to be encoded together with the first semicolon diff --git a/scripts/shared-program.js b/scripts/shared-program.js index 39e8db93..e71afcf0 100644 --- a/scripts/shared-program.js +++ b/scripts/shared-program.js @@ -53,3 +53,8 @@ export function checkBlankString(string, notAvailableText = 'Not Available') { return string; } } + +export function dateFormat(dateString) { + const formattedDate = dateString ? dateString.split('T')[0] : 'Not Available'; + return formattedDate; +} \ No newline at end of file From 92329625130b49fde73c6b031e3e8931e8fedf5a Mon Sep 17 00:00:00 2001 From: mdickson-adbe <95774602+mdickson-adbe@users.noreply.github.com> Date: Wed, 5 Jun 2024 18:30:28 -0400 Subject: [PATCH 11/95] fix typo in graphql query name (#111) --- scripts/graphql.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/graphql.js b/scripts/graphql.js index 0d59b62f..fd20c366 100644 --- a/scripts/graphql.js +++ b/scripts/graphql.js @@ -103,7 +103,7 @@ export async function graphqlAllCampaignsFilter(first,cursor,filter) { } export async function graphqlCampaignByName(campaignName) { - const queryName = 'getCampaignNamesFilter'; + const queryName = 'getCampaignNameFilter'; const encodedCampaignName = encodeURIComponent(campaignName); const encodedSemiColon = encodeURIComponent(';'); //persisted query URLs have to be encoded together with the first semicolon From 887a1e77afd349bc830bcf67cfac725e16a48b25 Mon Sep 17 00:00:00 2001 From: TyroneAEM <147942284+TyroneAEM@users.noreply.github.com> Date: Wed, 5 Jun 2024 17:46:14 -0500 Subject: [PATCH 12/95] ASSETS-88914 : Fix Pagination Frontend Code (#108) * Changed the default number items per page to 4 Added function debounce(func, wait) to delay the Previous and Next page to wait 0.5 seconds between each click, to stop the user clicking too fast and breaking pagination * Decrease debounce timeout to 200 milliseconds (0.2 secs) * Added check that next page cannot go past last page Added check that prev page cannot go back past page 1 * fix(pagination): Ensure proper handling of next/prev buttons and page boundaries - Added totalPages calculation and checks to ensure "Next" button is disabled on the last page - Updated nextPage function to correctly enable/disable pagination buttons - Improved prevPage function to ensure it navigates back to the first page correctly - Applied debounce to prevent rapid clicking issues - Refactored logic for enabling/disabling pagination buttons based on current page state * - Changed default items per page to 8 - Removed 4 items per page from filter - Removed cusorArray, and replaced it with currentPageInfo.previousCursor = currentPageInfo.currentCursor; - Set next cursor to data.programPaginated.pageInfo.endCursor; - Function prevPage now uses currentPage.previousCursor as the cursor parameter value, and calculating the cursor value to use from cursorArray is no longer needed. * Increased the click delay to 500 milliseconds * Disabled the Previous and Next Button as soon as they are clicked, to prevent the user multiple clicking the button. --- blocks/gmo-program-list/gmo-program-list.js | 105 ++++++++++---------- 1 file changed, 55 insertions(+), 50 deletions(-) diff --git a/blocks/gmo-program-list/gmo-program-list.js b/blocks/gmo-program-list/gmo-program-list.js index 4ec0a174..8a3c6f18 100644 --- a/blocks/gmo-program-list/gmo-program-list.js +++ b/blocks/gmo-program-list/gmo-program-list.js @@ -36,11 +36,10 @@ const headerConfig = [ const DEFAULT_ITEMS_PER_PAGE = 8; //Global variables used by helper functions let currentPageInfo = {}; -let cursorArray = []; let currentPage = 1; let currentNumberPerPage = DEFAULT_ITEMS_PER_PAGE; - let currentGraphqlFilter = {}; +let totalPages = 0; //Get Campaign Count for pagination let campaignCount = await graphqlCampaignCount(); let blockConfig; @@ -62,7 +61,6 @@ document.addEventListener('gmoCampaignListBlock', async function() { //Trigger loading the gmo-campaign-block //Reset page variables currentPageInfo = {}; - cursorArray = []; currentPage = 1; currentNumberPerPage = DEFAULT_ITEMS_PER_PAGE; @@ -75,26 +73,23 @@ export default async function decorate(block, numPerPage = currentNumberPerPage, if (blockConfig == undefined) blockConfig = readBlockConfig(block); const campaignPaginatedResponse = await graphqlAllCampaignsFilter(numPerPage, cursor,graphQLFilter); const campaigns = campaignPaginatedResponse.data.programPaginated.edges; + + //Set previous cursor to currentCursor + currentPageInfo.previousCursor = currentPageInfo.currentCursor; + 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); + currentPageInfo.nextCursor = currentPageInfo.endCursor === undefined ? campaigns[campaigns.length - 1].cursor : currentPageInfo.endCursor; } - else if (nextPage){ - campaigns.forEach(item => { - cursorArray.push(item.cursor); - }); - } currentPageInfo.itemCount = campaigns.length; + // Calculate total number of pages + totalPages = Math.ceil(campaignCount / currentNumberPerPage); + const listHeaders = buildListHeaders(headerConfig); const listItems = await buildCampaignList(campaigns, numPerPage); const listFooter = buildListFooter(campaignCount, numPerPage); @@ -107,20 +102,21 @@ export default async function decorate(block, numPerPage = currentNumberPerPage, listContainer.appendChild(listHeaders); listContainer.appendChild(listItems); listContainer.appendChild(listFooter); - //Show Hide Previous and Next Page buttons + // 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'); + if (currentPage > 1) { + footerPrev.classList.add('active'); } else { - footerPrev.classList.remove('active'); + footerPrev.classList.remove('active'); } - if (currentPageInfo.hasNextPage){ - footerNext.classList.add('active'); + if (currentPage < totalPages) { + footerNext.classList.add('active'); } else { - footerNext.classList.remove('active'); + footerNext.classList.remove('active'); } + decorateIcons(block); } @@ -315,6 +311,7 @@ function buildListHeaders(headerConfig) { function buildListFooter(rows, rowsPerPage) { const pages = Math.ceil(rows / rowsPerPage); + totalPages = pages; const footerWrapper = document.createElement('div'); footerWrapper.classList.add('list-footer', 'footer-wrapper'); footerWrapper.dataset.pages = pages; @@ -329,9 +326,13 @@ function buildListFooter(rows, rowsPerPage) { const footerPrev = document.createElement('div'); footerPrev.classList.add('footer-pagination-button', 'prev'); footerPrev.textContent = 'Prev'; + footerPrev.addEventListener('click', (event) => { + // Disable the button + footerPrev.classList.remove('active'); + footerPrev.classList.add('disabled'); prevPage(event.target); - }) + }); const footerPageBtnsWrapper = document.createElement('div'); footerPageBtnsWrapper.classList.add('footer-pages-wrapper'); @@ -340,9 +341,13 @@ function buildListFooter(rows, rowsPerPage) { //Show current page buildCurrentPageDivElement(currentPage, footerPageBtnsWrapper); - footerNext.addEventListener('click', (event) => { + footerNext.addEventListener('click', (event) => { + // Disable the button + footerNext.classList.remove('active'); + footerNext.classList.add('disabled'); nextPage(event.target); - }) + }); + footerNext.textContent = 'Next'; footerPagination.appendChild(footerPrev); footerPagination.appendChild(footerPageBtnsWrapper); @@ -411,35 +416,34 @@ function repaginate(dropdown) { } 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'); + if (currentPage < totalPages) { + currentPage++; + const block = document.querySelector('.gmo-program-list.block'); + decorate(block, currentNumberPerPage, currentPageInfo.nextCursor, false, true, currentGraphqlFilter); + + const prevBtn = document.querySelector('.footer-pagination-button.prev'); + prevBtn.classList.add('active'); + + if (currentPage === totalPages) { + nextBtn.classList.remove('active'); + } else { + nextBtn.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'); + if (currentPage > 1) { + currentPage--; + const block = document.querySelector('.gmo-program-list.block'); + decorate(block, currentNumberPerPage, currentPage.previousCursor, true, false, currentGraphqlFilter); + const nextBtn = document.querySelector('.footer-pagination-button.next'); + nextBtn.classList.add('active'); + if (currentPage === 1) { + prevBtn.classList.remove('active'); + } else { + prevBtn.classList.add('active'); + } } } @@ -485,4 +489,5 @@ function sortColumn(dir, property) { sortArray.forEach(({ row }, index) => { container.appendChild(row); }); -} \ No newline at end of file +} + From b272d9ca6399dcf3a59704dee6a068bb9c23b753 Mon Sep 17 00:00:00 2001 From: TyroneAEM <147942284+TyroneAEM@users.noreply.github.com> Date: Thu, 6 Jun 2024 10:19:25 -0500 Subject: [PATCH 13/95] Restored previous logic using cursorArray to manage the calculation of the cursor for the previous page. (#113) --- blocks/gmo-program-list/gmo-program-list.js | 23 ++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/blocks/gmo-program-list/gmo-program-list.js b/blocks/gmo-program-list/gmo-program-list.js index 8a3c6f18..4899cfe8 100644 --- a/blocks/gmo-program-list/gmo-program-list.js +++ b/blocks/gmo-program-list/gmo-program-list.js @@ -36,6 +36,7 @@ const headerConfig = [ const DEFAULT_ITEMS_PER_PAGE = 8; //Global variables used by helper functions let currentPageInfo = {}; +let cursorArray = []; let currentPage = 1; let currentNumberPerPage = DEFAULT_ITEMS_PER_PAGE; let currentGraphqlFilter = {}; @@ -61,6 +62,7 @@ document.addEventListener('gmoCampaignListBlock', async function() { //Trigger loading the gmo-campaign-block //Reset page variables currentPageInfo = {}; + cursorArray = []; currentPage = 1; currentNumberPerPage = DEFAULT_ITEMS_PER_PAGE; @@ -73,10 +75,6 @@ export default async function decorate(block, numPerPage = currentNumberPerPage, if (blockConfig == undefined) blockConfig = readBlockConfig(block); const campaignPaginatedResponse = await graphqlAllCampaignsFilter(numPerPage, cursor,graphQLFilter); const campaigns = campaignPaginatedResponse.data.programPaginated.edges; - - //Set previous cursor to currentCursor - currentPageInfo.previousCursor = currentPageInfo.currentCursor; - currentPageInfo = campaignPaginatedResponse.data.programPaginated.pageInfo; //Current cursor used in previous page logic currentPageInfo.currentCursor = cursor; @@ -85,6 +83,17 @@ export default async function decorate(block, numPerPage = currentNumberPerPage, currentPageInfo.nextCursor = currentPageInfo.endCursor === undefined ? campaigns[campaigns.length - 1].cursor : currentPageInfo.endCursor; } + if (!previousPage && !nextPage) + { + cursorArray = campaigns.map(item => item.cursor); + } + else if (nextPage){ + + campaigns.forEach(item => { + cursorArray.push(item.cursor); + }); + } + currentPageInfo.itemCount = campaigns.length; // Calculate total number of pages @@ -436,7 +445,11 @@ function prevPage(prevBtn) { if (currentPage > 1) { currentPage--; const block = document.querySelector('.gmo-program-list.block'); - decorate(block, currentNumberPerPage, currentPage.previousCursor, true, false, currentGraphqlFilter); + + const currentCursor = currentPageInfo.currentCursor; + //Calculate cursor for previous page + const indexCursor = cursorArray.indexOf(currentCursor) - currentNumberPerPage; + decorate(block, currentNumberPerPage, cursorArray[indexCursor], true, false,currentGraphqlFilter); const nextBtn = document.querySelector('.footer-pagination-button.next'); nextBtn.classList.add('active'); if (currentPage === 1) { From d12919814aea5e911db2640bc8d56a60dac7bbf1 Mon Sep 17 00:00:00 2001 From: mdickson-adbe <95774602+mdickson-adbe@users.noreply.github.com> Date: Fri, 7 Jun 2024 12:24:48 -0400 Subject: [PATCH 14/95] Adjust height of deliverables tab on program-details block (#114) * minor css changes for program details * adjust deliverables table height * add min height to deliverables table * remove commented css * Sort deliverable type array before rows are made --- .../gmo-program-details.css | 7 +++++-- .../gmo-program-details/gmo-program-details.js | 18 ++++++++++++++++-- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/blocks/gmo-program-details/gmo-program-details.css b/blocks/gmo-program-details/gmo-program-details.css index c653aafd..e39c5510 100644 --- a/blocks/gmo-program-details/gmo-program-details.css +++ b/blocks/gmo-program-details/gmo-program-details.css @@ -1,3 +1,6 @@ +:root { + border-bottom: none; +} body { background-color: rgb(247, 246, 246); } @@ -43,7 +46,6 @@ body { border-radius: 6px; box-shadow: 0px 3px 6px #0000000D; margin-top: 20px; - min-height: 800px; padding: 20px; } } @@ -319,7 +321,8 @@ body { } } .table-content { - height: 45vh; + max-height: 45vh; + min-height: 7vh; overflow-y: auto; &::-webkit-scrollbar { display: none; diff --git a/blocks/gmo-program-details/gmo-program-details.js b/blocks/gmo-program-details/gmo-program-details.js index ff05b20f..262d20e6 100644 --- a/blocks/gmo-program-details/gmo-program-details.js +++ b/blocks/gmo-program-details/gmo-program-details.js @@ -375,13 +375,14 @@ function formatDate(dateString) { async function buildTable(jsonResponse) { const deliverableList = jsonResponse.data.deliverableList.items; const programKpi = jsonResponse.data.programList?.items.primaryKpi; - const rows = document.createElement('div'); + let rows = document.createElement('div'); // we want the 'null' deliverableType to be part of this set for filtering const uniqueCatSet = new Set(); deliverableList.forEach(object => { uniqueCatSet.add(object['deliverableType']) }) const uniqueCategories = Array.from(uniqueCatSet); + const sortedCategories = sortDeliverableTypes(uniqueCategories); let emptyCategory = false; - uniqueCategories.forEach(async (category) => { + sortedCategories.forEach(async (category) => { // build header row let headerRow; const matchingCampaigns = deliverableList.filter(deliverable => deliverable.deliverableType === category); @@ -425,6 +426,19 @@ function dateSort(parent) { }) } +function sortDeliverableTypes(arr) { + return arr.sort((a, b) => { + // If a is null and b is not null, a should come after b + if (a === null && b !== null) return 1; + // If b is null and a is not null, b should come after a + if (a !== null && b === null) return -1; + // If both a and b are null, they are equal in terms of sorting + if (a === null && b === null) return 0; + // If neither a nor b are null, sort them alphabetically + return a.localeCompare(b); + }); +} + async function lookupType(rawText, mappingType) { const mappings = (mappingType === 'deliverable-type') ? await deliverableMappings : await platformMappings; const typeMatch = mappings.filter(item => item.value === rawText); From ba077db29db46db7b7c97208a78a693778c2b774 Mon Sep 17 00:00:00 2001 From: Samruddhi <150183547+staware30@users.noreply.github.com> Date: Tue, 11 Jun 2024 08:58:33 -0700 Subject: [PATCH 15/95] DXI-26587- Modify landing page for v3 Hub, so Signin goes to CH/v4 (#116) --- blocks/gmo-landing-page/gmo-landing-page.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/blocks/gmo-landing-page/gmo-landing-page.js b/blocks/gmo-landing-page/gmo-landing-page.js index b6da5a47..d2060aec 100644 --- a/blocks/gmo-landing-page/gmo-landing-page.js +++ b/blocks/gmo-landing-page/gmo-landing-page.js @@ -44,7 +44,7 @@ export default async function decorate(block) { const host = location.origin; const signInMsg = getSignInMsg(block); const config = readBlockConfig(block); - const redirect = host + config?.mainpage; + const redirect = config?.redirect ?? (host + config?.mainpage); block.innerHTML=`
From ae27a9c7b46c30d9e3d5c5ee0eb8a2ca8dd11a10 Mon Sep 17 00:00:00 2001 From: TyroneAEM <147942284+TyroneAEM@users.noreply.github.com> Date: Tue, 11 Jun 2024 16:26:06 -0500 Subject: [PATCH 16/95] Replace throwing 404 error when the asset is not found, with asset.svg icon (#119) --- scripts/polaris.js | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/scripts/polaris.js b/scripts/polaris.js index 1b081b40..d94f6a20 100644 --- a/scripts/polaris.js +++ b/scripts/polaris.js @@ -101,13 +101,10 @@ export async function authorizeURL(url) { const response = await fetch(url, options); + if (!response.ok) { - // Handle specific HTTP errors - if (response.status === 404) { - throw new Error('Not Found (404)'); - } else { - throw new Error(`HTTP error! Status: ${response.status}`); - } + // Return the full URL to the asset icon if the response is not OK, as the asset was not found + return new URL('/icons/asset.svg', window.location.origin).href; } const imageBlob = await response.blob(); From 525d50a2a72fb678652a3e86778230f2a54db024 Mon Sep 17 00:00:00 2001 From: mdickson-adbe <95774602+mdickson-adbe@users.noreply.github.com> Date: Wed, 12 Jun 2024 11:38:58 -0400 Subject: [PATCH 17/95] Make content fragment path for WF mappings configurable (#118) * refactoring to use single graphql endpoint for map * refactor 'mapping' functions - refactor mapping retrieval functions in all three hcv blocks * Remove comments * remove unused function stub * resolve bug in header filter refresh * remove unused import --- .../gmo-program-details.js | 9 ++--- .../gmo-program-header/gmo-program-header.js | 11 +++--- blocks/gmo-program-list/gmo-program-list.js | 3 +- scripts/graphql.js | 27 +------------- scripts/shared-program.js | 37 +++++++++++-------- scripts/site-config.js | 15 ++++++++ 6 files changed, 47 insertions(+), 55 deletions(-) diff --git a/blocks/gmo-program-details/gmo-program-details.js b/blocks/gmo-program-details/gmo-program-details.js index 262d20e6..679fc029 100644 --- a/blocks/gmo-program-details/gmo-program-details.js +++ b/blocks/gmo-program-details/gmo-program-details.js @@ -1,15 +1,15 @@ 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, dateFormat } from '../../scripts/shared-program.js'; +import { filterArray, getProductMapping, checkBlankString, dateFormat, statusMapping, getMappingArray } 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 programID = getQueryVariable('programID'); -const deliverableMappings = resolveMappings("getDeliverableTypeMapping"); -const platformMappings = resolveMappings("getPlatformsMapping"); +const deliverableMappings = getMappingArray('deliverableType'); +const platformMappings = getMappingArray('platforms'); export default async function decorate(block) { @@ -333,8 +333,7 @@ function buildArtifactLinks(program) { 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 statusMatch = filterArray(statusMapping, 'value', status); const statusText = statusMatch ? statusMatch[0].text : status; const statusHex = statusMatch[0]["color-code"]; statusDiv.textContent = statusText; diff --git a/blocks/gmo-program-header/gmo-program-header.js b/blocks/gmo-program-header/gmo-program-header.js index d7a9e46d..ab99e4f3 100644 --- a/blocks/gmo-program-header/gmo-program-header.js +++ b/blocks/gmo-program-header/gmo-program-header.js @@ -1,6 +1,6 @@ import { decorateIcons } from '../../scripts/lib-franklin.js'; -import { graphqlQueryNameList, graphqlCampaignByName } from '../../scripts/graphql.js'; -import { statusMapping, productList } from '../../scripts/shared-program.js'; +import { graphqlCampaignByName } from '../../scripts/graphql.js'; +import { statusMapping, productList, getMappingArray } from '../../scripts/shared-program.js'; export default async function decorate(block) { block.innerHTML = ` @@ -132,12 +132,12 @@ export default async function decorate(block) { async function initializeDropdowns() { // Business Line List - graphqlQueryNameList('getBusinessLine').then((response) => { + getMappingArray('businessLine').then((response) => { populateDropdown(response, 'dropdownBusinessOptions', 'businessLine'); }); // Geo List - graphqlQueryNameList('getGeoList').then((response) => { + getMappingArray('geoList').then((response) => { populateDropdown(response, 'dropdownGeoOptions', 'p0TargetGeo'); }); @@ -191,8 +191,7 @@ function populateDropdown(response, dropdownId, type) { // Function to filter products based on selected business line function filterProductsByBusinessLine(businessLine) { - const products = productList.data.jsonByPath.item.json.options; - const filteredProducts = products.filter(product => + const filteredProducts = productList.filter(product => product['business-line'].includes(businessLine) ); populateDropdown(filteredProducts, 'dropdownProductOptions', 'productOffering'); diff --git a/blocks/gmo-program-list/gmo-program-list.js b/blocks/gmo-program-list/gmo-program-list.js index 4899cfe8..04a15a3a 100644 --- a/blocks/gmo-program-list/gmo-program-list.js +++ b/blocks/gmo-program-list/gmo-program-list.js @@ -228,8 +228,7 @@ async function buildCampaignList(campaigns, numPerPage) { 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 statusMatch = statusMapping.filter(item => item.value === statusStr); let statusText, statusColor; if (statusMatch.length > 0) { diff --git a/scripts/graphql.js b/scripts/graphql.js index fd20c366..c4d733f4 100644 --- a/scripts/graphql.js +++ b/scripts/graphql.js @@ -5,31 +5,6 @@ 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 = 'getTotalPrograms'; const encodedSemiColon = encodeURIComponent(';'); @@ -213,4 +188,4 @@ export async function executeQuery(queryString) { console.error('Error fetching data: ', error); throw error; // Rethrow or handle error as appropriate }); -}; +}; \ No newline at end of file diff --git a/scripts/shared-program.js b/scripts/shared-program.js index e71afcf0..c76dda30 100644 --- a/scripts/shared-program.js +++ b/scripts/shared-program.js @@ -1,18 +1,10 @@ -import { graphqlQueryNameList } from "./graphql.js"; -import { getProductIconMapping, getBaseConfigPath } from './site-config.js'; +import { executeQuery } from "./graphql.js"; +import { getProductIconMapping, getBaseConfigPath, getQueryPaths } 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; -} +const cfMapping = getQueryPaths(); +export let statusMapping = await getMappingArray('status'); +export let productList = await getMappingArray('products'); /** * Filter provided array based on provided key/value pair @@ -32,9 +24,8 @@ export async function getProductMapping(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); + if (productList == undefined) productList = await getMappingArray('products'); + const productsMatch = filterArray(productList, 'value', product); const productsText = productsMatch ? productsMatch[0].text : product; return { @@ -57,4 +48,18 @@ export function checkBlankString(string, notAvailableText = 'Not Available') { export function dateFormat(dateString) { const formattedDate = dateString ? dateString.split('T')[0] : 'Not Available'; return formattedDate; +} + +function getCFPath(cfArray, type) { + const cfMatch = cfArray.filter(item => item['type'] === type); + const cfPath = cfMatch.length > 0 ? cfMatch[0].path : null; + return cfPath; +} + +export async function getMappingArray(type) { + const mappingCf = getCFPath(await cfMapping, type); + const mappings = executeQuery(`getMappings${encodeURIComponent(';')}path=${encodeURIComponent(mappingCf)}`).then((response) => { + return response.data.jsonByPath.item.json.options; + }) + return mappings; } \ No newline at end of file diff --git a/scripts/site-config.js b/scripts/site-config.js index 6c9694ff..e4c86b94 100644 --- a/scripts/site-config.js +++ b/scripts/site-config.js @@ -379,4 +379,19 @@ export async function getProductIconMapping() { console.log("Unable to retrieve site-config.json"); } return iconArray; +} + +/** + * @returns {Array} with mapping-type and the path to its content fragment. + */ +export async function getQueryPaths() { + let mapping = []; + const response = await getConfig('site-config.json'); + for (const entry of response['query-fragments'].data || []) { + mapping.push({ + type: entry['mapping-Type'], + path: entry['path'] + }); + } + return mapping; } \ No newline at end of file From 69d6ec3401a2bd751f1b2e8a766bd6761f0c1d74 Mon Sep 17 00:00:00 2001 From: mathieu-lessard Date: Wed, 12 Jun 2024 10:27:06 -0700 Subject: [PATCH 18/95] Fixed issue where header didn't load if the user didn't have a profile yet (#121) Co-authored-by: Mathieu Lessard --- scripts/security.js | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/scripts/security.js b/scripts/security.js index edbe7458..c74c8023 100644 --- a/scripts/security.js +++ b/scripts/security.js @@ -71,15 +71,22 @@ export async function getUserProfile() { async function getCCCollabProfile() { const bearerToken = await getBearerToken(); const url = await getCcCollabUrl(); - return await fetchCached( - `https://${url}`, - { - method: 'GET', - headers: { - Authorization: bearerToken, + + //If this fails, we want to return something. fetchCached will halt. + try{ + return await fetchCached( + `https://${url}`, + { + method: 'GET', + headers: { + Authorization: bearerToken, + }, }, - }, - ); + ); + } catch (error) { + // fetchCached will log the error if it fails + return null; + } } export async function getAvatarUrl() { From 1ea6d9bb6083ecd6ba12571cd03ca565d322223d Mon Sep 17 00:00:00 2001 From: mdickson-adbe <95774602+mdickson-adbe@users.noreply.github.com> Date: Wed, 12 Jun 2024 14:09:38 -0400 Subject: [PATCH 19/95] add checks for null 'review link' and 'final asset' (#122) --- blocks/gmo-program-details/gmo-program-details.js | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/blocks/gmo-program-details/gmo-program-details.js b/blocks/gmo-program-details/gmo-program-details.js index 679fc029..cf717066 100644 --- a/blocks/gmo-program-details/gmo-program-details.js +++ b/blocks/gmo-program-details/gmo-program-details.js @@ -486,9 +486,10 @@ async function buildTableRow(deliverableJson, kpi, createHidden) {
${checkBlankString(typeLabel)}
+ ${deliverableJson.linkedFolderLink ? 'Final Asset ': "Not Available"}
@@ -508,14 +509,6 @@ async function buildTableRow(deliverableJson, kpi, createHidden) {
${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; } From 6c551774f67785fdb267765fdfc70a079f461e31 Mon Sep 17 00:00:00 2001 From: mdickson-adbe <95774602+mdickson-adbe@users.noreply.github.com> Date: Wed, 12 Jun 2024 16:53:26 -0400 Subject: [PATCH 20/95] Refactor header and query variable extraction (#123) * add checks for null 'review link' and 'final asset' * Add msg for programs with no data available * refactor header so it can be used with no data * wrap msg with div for styling * refactor query variable extraction - had to account for ampersand in program name --- .../gmo-program-details.css | 3 + .../gmo-program-details.js | 108 +++++++++++++----- 2 files changed, 81 insertions(+), 30 deletions(-) diff --git a/blocks/gmo-program-details/gmo-program-details.css b/blocks/gmo-program-details/gmo-program-details.css index e39c5510..fb2c1b9f 100644 --- a/blocks/gmo-program-details/gmo-program-details.css +++ b/blocks/gmo-program-details/gmo-program-details.css @@ -47,6 +47,9 @@ body { box-shadow: 0px 3px 6px #0000000D; margin-top: 20px; padding: 20px; + & > .no-data-msg { + margin-top: 20px; + } } } diff --git a/blocks/gmo-program-details/gmo-program-details.js b/blocks/gmo-program-details/gmo-program-details.js index cf717066..ededa381 100644 --- a/blocks/gmo-program-details/gmo-program-details.js +++ b/blocks/gmo-program-details/gmo-program-details.js @@ -1,63 +1,64 @@ import { decorateIcons, readBlockConfig } from '../../scripts/lib-franklin.js'; -import { getQueryVariable } from '../../scripts/shared.js'; import { executeQuery } from '../../scripts/graphql.js'; import { filterArray, getProductMapping, checkBlankString, dateFormat, statusMapping, getMappingArray } 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 programID = getQueryVariable('programID'); +const queryVars = extractQueryVars(); +const programName = queryVars.programName; +const programID = queryVars.programID; const deliverableMappings = getMappingArray('deliverableType'); const platformMappings = getMappingArray('platforms'); export default async function decorate(block) { - const encodedSemi = encodeURIComponent(';'); const encodedProgram = encodeURIComponent(programName); const programQueryString = `getProgramDetails${encodedSemi}programName=${encodedProgram}${encodedSemi}programID=${encodeURIComponent(programID)}`; const programData = await executeQuery(programQueryString); + const program = programData.data.programList.items[0]; + blockConfig = readBlockConfig(block); + const header = buildHeader(program, queryVars).outerHTML; + if (!program) { + block.innerHTML = ` +
+ + Back +
+
+ ${header} +
No data available.
+
+ ` + decorateIcons(block); + enableBackBtn(block, blockConfig); + return; + } + const deliverableQueryString = `getProgramDeliverables${encodedSemi}programName=${encodedProgram}${encodedSemi}programID=${encodeURIComponent(programID)}`; const deliverables = await executeQuery(deliverableQueryString); - const p0TargetMarketArea = programData.data.programList.items[0].p0TargetMarketArea; - const p1TargetMarketArea = programData.data.programList.items[0].p1TargetMarketArea; + const p0TargetMarketArea = program.p0TargetMarketArea; + const p1TargetMarketArea = program.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} -
-
-
+ ${header}
Overview
Deliverables
@@ -164,11 +165,7 @@ export default async function decorate(block) { 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}`; - }) + enableBackBtn(block, blockConfig); block.querySelectorAll('.read-more').forEach((button) => { button.addEventListener('click', (event) => { const readMore = event.target; @@ -187,6 +184,35 @@ export default async function decorate(block) { buildStatus(program.status); } +function enableBackBtn(block, blockConfig) { + block.querySelector('.back-button').addEventListener('click', () => { + const host = location.origin + getBaseConfigPath(); + const listPage = blockConfig.listpage; + document.location.href = host + `/${listPage}`; + }) +} + +function buildHeader(program, queryVars) { + const headerWrapper = document.createElement('div'); + headerWrapper.classList.add('details-header-wrapper'); + const date = program && program.launchDate ? `
` + + `Launch date${formatDate(program.launchDate)}
` : ""; + const programName = program ? program.programName : queryVars.programName; + const campaignName = program && program.campaignName ? '
' + program.campaignName + '
': ""; + headerWrapper.innerHTML = ` +
+
+
+
+ ${programName} +
+ ${campaignName} + ${date} +
+ ` + return headerWrapper; +} + /** * Extracts unique values from a specified property within an array of objects. * @@ -564,3 +590,25 @@ function attachListener(htmlElement) { }) }) } + +function extractQueryVars() { + const urlStr = window.location.href; + const pnRegex = /.*programName=(.*?)&programID=(.*)/; + const match = urlStr.match(pnRegex); + if (match && match[1] && match[2]) { + const pName = decodeURIComponent(match[1]); + let pID = decodeURIComponent(match[2]) + if (pID.endsWith('#')) { + pID = pID.slice(0, -1); + } + return { + programName: pName, + programID: pID + } + } else { + return { + programName: 'Program Name Not Available', + programID: 'Program ID Not Available' + } + } +} \ No newline at end of file From af3e26bef2d69d57fb8ea977216c2e110003f632 Mon Sep 17 00:00:00 2001 From: mathieu-lessard Date: Wed, 12 Jun 2024 14:35:59 -0700 Subject: [PATCH 21/95] Updated Product List (#117) * Product list updated * N/A value and label now match * Fixed formatting --------- Co-authored-by: Mathieu Lessard --- contenthub/hydration/hydration-utils.js | 140 +++++++++++++++--------- 1 file changed, 86 insertions(+), 54 deletions(-) diff --git a/contenthub/hydration/hydration-utils.js b/contenthub/hydration/hydration-utils.js index d2c3912f..f0b128da 100644 --- a/contenthub/hydration/hydration-utils.js +++ b/contenthub/hydration/hydration-utils.js @@ -115,60 +115,92 @@ export function getMetadataSchema(facetOptions){ element: 'dropdown', multipleSelection: true, dropdownOptions: [ - { name: 'N/A', id: 'na' }, - { name: 'Acrobat Export PDF', id: 'acrobat-export-pdf' }, - { name: 'Acrobat PDF Pack', id: 'acrobat-pdf-pack' }, - { name: 'Acrobat Pro', id: 'acrobat-pro' }, - { name: 'Acrobat Reader', id: 'acrobat-reader' }, - { name: 'Acrobat Sign Mobile', id: 'acrobat-sign-mobile' }, - { name: 'Acrobat Sign', id: 'acrobat-sign' }, - { name: 'Acrobat standard', id: 'acrobat-standard' }, - { name: 'Adobe Color', id: 'adobe-color' }, - { name: 'Adobe Express', id: 'adobe-express' }, - { name: 'Adobe Fonts', id: 'adobe-fonts' }, - { name: 'Adobe Scan', id: 'adobe-scan' }, - { name: 'Aero', id: 'aero' }, - { name: 'After Effects', id: 'after-effects' }, - { name: 'Animate', id: 'animate' }, - { name: 'Audition', id: 'audition' }, - { name: 'Behance', id: 'behance' }, - { name: 'Bridge', id: 'bridge' }, - { name: 'Capture', id: 'capture' }, - { name: 'Character Animator', id: 'character-animator' }, - { name: 'Cloud Service', id: 'cloud-service' }, - { name: 'Content Server', id: 'content-server' }, - { name: 'Creative Cloud All Apps', id: 'creative-cloud-all-apps' }, - { name: 'Digital Editions', id: 'digital-editions' }, - { name: 'Dreamweaver', id: 'dreamweaver' }, - { name: 'Fill Sign', id: 'fill-sign' }, - { name: 'Firefly', id: 'firefly' }, - { name: 'Frame.io', id: 'frame-io' }, - { name: 'Fresco', id: 'fresco' }, - { name: 'Http Dynamic Streaming', id: 'http-dynamic-streaming' }, - { name: 'Illustrator', id: 'illustrator' }, - { name: 'InCopy', id: 'incopy' }, - { name: 'InDesign Server', id: 'indesign-server' }, - { name: 'InDesign', id: 'indesign' }, - { name: 'Lightroom Classic', id: 'lightroom-classic' }, - { name: 'Lightroom', id: 'lightroom' }, - { name: 'Media Encoder', id: 'media-encoder' }, - { name: 'Media Server 5 Extended', id: 'media-server-5-extended' }, - { name: 'Media Server 5 on Amazon Web Services', id: 'media-server-5-on-amazon-web-services' }, - { name: 'Media Server 5 Professional', id: 'media-server-5-professional' }, - { name: 'Media Server 5 Standard', id: 'media-server-5-standard' }, - { name: 'Mixamo', id: 'mixamo' }, - { name: 'Photoshop Express', id: 'photoshop-express' }, - { name: 'Photoshop', id: 'photoshop' }, - { name: 'Portfolio', id: 'portfolio' }, - { name: 'Premiere Elements', id: 'premiere-elements' }, - { name: 'Premiere Pro', id: 'premiere-pro' }, - { name: 'Premiere Rush', id: 'premiere-rush' }, - { name: 'Stock', id: 'stock' }, - { name: 'Substance 3D Designer', id: 'substance-3d-designer' }, - { name: 'Substance 3D Modeler', id: 'substance-3d-modeler' }, - { name: 'Substance 3D Painter', id: 'substance-3d-painter' }, - { name: 'Substance 3D Sampler', id: 'substance-3d-sampler' }, - { name: 'Substance 3D Stager', id: 'substance-3d-stager' }, + { name: 'N/A', id: 'N/A' }, + { name: 'Acrobat', id: 'Acrobat' }, + { name: 'Acrobat Export PDF', id: 'Acrobat Export PDF' }, + { name: 'Acrobat PDF Pack', id: 'Acrobat PDF Pack' }, + { name: 'Acrobat Pro', id: 'Acrobat Pro' }, + { name: 'Acrobat Reader', id: 'Acrobat Reader' }, + { name: 'Acrobat Sign', id: 'Acrobat Sign' }, + { name: 'Acrobat Sign (Mobile)', id: 'Acrobat Sign (Mobile)' }, + { name: 'Acrobat Standard', id: 'Acrobat Standard' }, + { name: 'Adobe Color', id: 'Adobe Color' }, + { name: 'Adobe Express', id: 'Adobe Express' }, + { name: 'Adobe Fonts', id: 'Adobe Fonts' }, + { name: 'Adobe Fresco', id: 'Adobe Fresco' }, + { name: 'Adobe Scan', id: 'Adobe Scan' }, + { name: 'Adobe Stock', id: 'Adobe Stock' }, + { name: 'Advertising', id: 'Advertising' }, + { name: 'AEC', id: 'AEC' }, + { name: 'AEM Assets', id: 'AEM Assets' }, + { name: 'AEM Forms', id: 'AEM Forms' }, + { name: 'AEM Other (retired)', id: 'AEM Other (retired)' }, + { name: 'AEM Sites', id: 'AEM Sites' }, + { name: 'AEP', id: 'AEP' }, + { name: 'Aero', id: 'Aero' }, + { name: 'After Effects', id: 'After Effects' }, + { name: 'Analytics', id: 'Analytics' }, + { name: 'Animate', id: 'Animate' }, + { name: 'Audience Manager', id: 'Audience Manager' }, + { name: 'Audition', id: 'Audition' }, + { name: 'Behance', id: 'Behance' }, + { name: 'Bizible', id: 'Bizible' }, + { name: 'Bridge', id: 'Bridge' }, + { name: 'Campaign', id: 'Campaign' }, + { name: 'Capture', id: 'Capture' }, + { name: 'Character Animator', id: 'Character Animator' }, + { name: 'Cloud Service', id: 'Cloud Service' }, + { name: 'Commerce', id: 'Commerce' }, + { name: 'Connect', id: 'Connect' }, + { name: 'Content Server', id: 'Content Server' }, + { name: 'Creative', id: 'Creative' }, + { name: 'Creative Cloud All Apps', id: 'Creative Cloud All Apps' }, + { name: 'Digital Editions', id: 'Digital Editions' }, + { name: 'Dreamweaver', id: 'Dreamweaver' }, + { name: 'DX General', id: 'DX General' }, + { name: 'DX Video', id: 'DX Video' }, + { name: 'Fill & Sign', id: 'Fill & Sign' }, + { name: 'Firefly', id: 'Firefly' }, + { name: 'Frame.io', id: 'Frame.io' }, + { name: 'HTTP Dynamic Streaming', id: 'HTTP Dynamic Streaming' }, + { name: 'Illustrator', id: 'Illustrator' }, + { name: 'InCopy', id: 'InCopy' }, + { name: 'InDesign', id: 'InDesign' }, + { name: 'InDesign Server', id: 'InDesign Server' }, + { name: 'Journey Analytics', id: 'Journey Analytics' }, + { name: 'Journey Optimizer', id: 'Journey Optimizer' }, + { name: 'Lightroom', id: 'Lightroom' }, + { name: 'Lightroom Classic', id: 'Lightroom Classic' }, + { name: 'Magento OpenSource', id: 'Magento OpenSource' }, + { name: 'Marketo', id: 'Marketo' }, + { name: 'Media Encoder', id: 'Media Encoder' }, + { name: 'Media Server 5 Extended', id: 'Media Server 5 Extended' }, + { name: 'Media Server 5 on Amazon Web Services', id: 'Media Server 5 on Amazon Web Services' }, + { name: 'Media Server 5 Professional', id: 'Media Server 5 Professional' }, + { name: 'Media Server 5 Standard', id: 'Media Server 5 Standard' }, + { name: 'Mixamo', id: 'Mixamo' }, + { name: 'Multi-product', id: 'Multi-product' }, + { name: 'Photoshop', id: 'Photoshop' }, + { name: 'Photoshop Express', id: 'Photoshop Express' }, + { name: 'Portfolio', id: 'Portfolio' }, + { name: 'PPBU', id: 'PPBU' }, + { name: 'PPBU (Primetime)', id: 'PPBU (Primetime)' }, + { name: 'Premiere Elements', id: 'Premiere Elements' }, + { name: 'Premiere Pro', id: 'Premiere Pro' }, + { name: 'Premier Support', id: 'Premier Support' }, + { name: 'RT CDP', id: 'RT CDP' }, + { name: 'Sensei (retired)', id: 'Sensei (retired)' }, + { name: 'Services - Digital Performance (retired)', id: 'Services - Digital Performance (retired)' }, + { name: 'Services - Other Consulting Services (retired)', id: 'Services - Other Consulting Services (retired)' }, + { name: 'Sign', id: 'Sign' }, + { name: 'Stock', id: 'Stock' }, + { name: 'Substance 3D Designer', id: 'Substance 3D Designer' }, + { name: 'Substance 3D Modeler', id: 'Substance 3D Modeler' }, + { name: 'Substance 3D Painter', id: 'Substance 3D Painter' }, + { name: 'Substance 3D Sampler', id: 'Substance 3D Sampler' }, + { name: 'Substance 3D Stager', id: 'Substance 3D Stager' }, + { name: 'Target', id: 'Target' }, + { name: 'Workfront', id: 'Workfront' }, ], }, { From 780be9745b491e4397371c0a908249b111631978 Mon Sep 17 00:00:00 2001 From: TyroneAEM <147942284+TyroneAEM@users.noreply.github.com> Date: Thu, 13 Jun 2024 16:13:56 -0500 Subject: [PATCH 22/95] Restored calling the function attatchEventListerners() at the end of the function decorate(block) (#124) --- blocks/gmo-program-header/gmo-program-header.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/blocks/gmo-program-header/gmo-program-header.js b/blocks/gmo-program-header/gmo-program-header.js index ab99e4f3..295e046b 100644 --- a/blocks/gmo-program-header/gmo-program-header.js +++ b/blocks/gmo-program-header/gmo-program-header.js @@ -126,6 +126,8 @@ export default async function decorate(block) { }); initializeDropdowns(); + // Attach event listeners for the dropdowns and reset filters + attachEventListeners(); decorateIcons(block); document.addEventListener('click', handleClickOutside); } From f33f4aa2dfc7b3fb3eb9c28d65574424bdee0b20 Mon Sep 17 00:00:00 2001 From: TyroneAEM <147942284+TyroneAEM@users.noreply.github.com> Date: Fri, 14 Jun 2024 16:08:21 -0500 Subject: [PATCH 23/95] Fix text wrapping issue in Project Owner column for smaller viewports (#125) - Adjusted CSS to ensure text in the "Project Owner" column wraps properly - Set row height to auto and adjusted line-height for better spacing - Added media query for responsive width adjustments - Ensured consistent display and improved readability across different viewport sizes --- .../gmo-program-details.css | 47 +++++++++++++++++-- 1 file changed, 42 insertions(+), 5 deletions(-) diff --git a/blocks/gmo-program-details/gmo-program-details.css b/blocks/gmo-program-details/gmo-program-details.css index fb2c1b9f..20a3b139 100644 --- a/blocks/gmo-program-details/gmo-program-details.css +++ b/blocks/gmo-program-details/gmo-program-details.css @@ -1,9 +1,11 @@ :root { border-bottom: none; } + body { background-color: rgb(247, 246, 246); } + .back-button { margin-top: 20px; background-color: #FFFFFF; @@ -26,6 +28,7 @@ body { font-weight: bold; } } + .gmo-program-details.block { & .h1 { font: normal normal bold 18px/27px Adobe Clean; @@ -133,11 +136,13 @@ body { } } } + .tab.two-column { display: flex; flex-direction: row; justify-content: space-between; } + .overview-wrapper { display: flex; flex-direction: column; @@ -168,6 +173,7 @@ body { cursor: pointer; } } + .kpis-wrapper { margin-top: 30px; & ul { @@ -181,6 +187,7 @@ body { margin-top: 15px; } } + .use-cases-wrapper { margin-bottom: 30px; } @@ -218,6 +225,7 @@ body { flex-direction: column; margin-top: 20px; } + .links-wrapper { margin-top: 30px; & .links { @@ -231,6 +239,7 @@ body { color: #0D66D0; } } + .infocards-wrapper { margin-right: 40px; & .card { @@ -244,6 +253,7 @@ body { } } } + .card { & .card-heading { margin-top: 10px; @@ -259,6 +269,7 @@ body { font-size: 14px; } } + .milestone, .card-content { font: normal normal normal 14px/21px Adobe Clean; & .icon { @@ -271,6 +282,7 @@ body { margin-bottom: 5px; } } + .deliverables { & > .page-heading { display: flex; @@ -306,6 +318,7 @@ body { } } } + .table-wrapper { font: normal normal bold 14px/21px Adobe Clean; & > .table-header { @@ -323,6 +336,7 @@ body { top: 0; } } + .table-content { max-height: 45vh; min-height: 7vh; @@ -331,10 +345,12 @@ body { display: none; } } + .inactive { display: none !important; visibility: hidden; } + .row { height: 56px; &:not(:last-child) { @@ -343,10 +359,11 @@ body { &.datarow { display: flex; align-items: center; - height: 88px; + height: auto; /* Set to auto to accommodate varying heights */ font-weight: 200; & .property { - line-height: 88px; + line-height: normal; /* Adjust line-height to normal */ + padding: 10px 0; /* Add padding for better spacing */ } &:first-child, &:nth-child(2) { border-top: 2px solid #F4F4F4; @@ -359,7 +376,7 @@ body { & .date-wrapper { display: flex; flex-direction: column; - justify-content:center; + justify-content: center; & > .completion-date, .revised-date { line-height: 21px; } @@ -400,6 +417,7 @@ body { justify-content: center; } } + .subheader { &:nth-child(2) { border-top: 2px solid #F4F4F4; @@ -444,12 +462,13 @@ body { border-radius: 3px; } } - } + .header-row3:hover .date-tooltip { visibility: visible; opacity: 1; } + .date-tooltip { background-color: #D3D3D3; border-radius: 2px; @@ -462,32 +481,50 @@ body { font: normal normal normal 12px/17px Adobe Clean; margin-left: 100px; } + .column1 { margin-left: 45px; } + .column1 { width: 200px; } + .column3 { width: 140px; } -.column4, & .column5, & .column6 { + +.column4, .column5, .column6 { width: 110px; } + .column2 { width: 130px; } + .column7 { width: 170px; } + .column8 { width: 120px; } + .column9 { width: 150px; + word-wrap: break-word; + white-space: normal; + line-height: 1.2; } + .table-column { &:not(:last-child) { margin-right: 85px; } } + +@media (max-width: 1255px) { + .column9 { + width: 100px; + } +} From 61f4dd6ae1455a52e27001c42d6a398a50a60d81 Mon Sep 17 00:00:00 2001 From: TyroneAEM <147942284+TyroneAEM@users.noreply.github.com> Date: Tue, 18 Jun 2024 15:59:53 -0500 Subject: [PATCH 24/95] ASSETS-88920 : Cosmetic Updates from MVP Feedback (#126) * feat: Add Geo column to program list and adjust column widths for proper alignment - Updated `gmo-program-list.js` to include Geo column in the header configuration and campaign list - Adjusted column widths in `gmo-program-list.css` to ensure proper alignment under each column heading - Add function `formatGeos` to format Geo array as comma-delimited list, that is displayed in the Geo column * - Added a tooltip to the "Total Approved Assets" field in the Deliverables tab. - Tooltip text provides instructions to view assets: "To view the assets, go to the 'All Asset' search page and use Program and Campaign name facet to filter the assets." - Styled the tooltip to match the color and background of the Launch Date tooltip. - Ensured the tooltip text appears on one line and positioned it correctly. - Updated the "Total Asset" label to "Total Approved Asset" - Updated the "Deliverable Name" column name to "Deliverable Task Name" * - Added the Driver detail in Overview Tab next to Launch date like "Project Owner: Aina Tchoshanova" - Update the Project "Owner column" label to "Task Owner" --- .../gmo-program-details.css | 56 +++++++++++++++++++ .../gmo-program-details.js | 31 ++++++++-- blocks/gmo-program-list/gmo-program-list.css | 17 ++++-- blocks/gmo-program-list/gmo-program-list.js | 16 ++++++ 4 files changed, 108 insertions(+), 12 deletions(-) diff --git a/blocks/gmo-program-details/gmo-program-details.css b/blocks/gmo-program-details/gmo-program-details.css index 20a3b139..d38b1af2 100644 --- a/blocks/gmo-program-details/gmo-program-details.css +++ b/blocks/gmo-program-details/gmo-program-details.css @@ -102,6 +102,20 @@ body { } } +.details-header-wrapper .header-row3 { + display: flex; + align-items: center; +} + +.details-header-wrapper .header-row3 .icon-calendar { + margin-right: 10px; +} + +.details-header-wrapper .header-row3 .driver-text { + font: normal normal normal 14px/21px Adobe Clean; + margin-left: 55px; +} + .campaign-img { width: 80px; height: 80px; @@ -319,6 +333,47 @@ body { } } +.total-assets-tooltip { + position: relative; + display: inline-block; + cursor: pointer; +} + +.total-assets-tooltip .tooltiptext { + visibility: hidden; + white-space: nowrap; /* Ensure text appears on one line */ + background-color: #D3D3D3; /* Match the background color */ + color: black; /* Match the text color */ + text-align: left; /* Left-align the text */ + border-radius: 4px; + padding: 5px 10px; /* Adjust padding as needed */ + position: absolute; + z-index: 1; + top: 50%; /* Center vertically */ + left: 100%; /* Position to the right of the element */ + transform: translateY(-50%); /* Center vertically */ + margin-left: 10px; /* Adjust margin to match the screenshot */ + opacity: 0; + transition: opacity 0.3s; + box-shadow: 0px 3px 6px rgba(0, 0, 0, 0.1); /* Add shadow for better visibility */ +} + +.total-assets-tooltip .tooltiptext::after { + content: ""; + position: absolute; + top: 50%; /* Center the arrow vertically */ + left: -5px; /* Adjust to position the arrow */ + transform: translateY(-50%); /* Center vertically */ + border-width: 5px; + border-style: solid; + border-color: transparent #D3D3D3 transparent transparent; /* Arrow color matches tooltip background */ +} + +.total-assets-tooltip:hover .tooltiptext { + visibility: visible; + opacity: 1; +} + .table-wrapper { font: normal normal bold 14px/21px Adobe Clean; & > .table-header { @@ -528,3 +583,4 @@ body { width: 100px; } } + diff --git a/blocks/gmo-program-details/gmo-program-details.js b/blocks/gmo-program-details/gmo-program-details.js index ededa381..a950bbb3 100644 --- a/blocks/gmo-program-details/gmo-program-details.js +++ b/blocks/gmo-program-details/gmo-program-details.js @@ -125,21 +125,22 @@ export default async function decorate(block) {
${artifactLinks} -
-
Total Assets
+
+
Total Approved Assets
+ To view the assets, go to the "All Asset" search page and use Program and Campaign name facet to filter the assets
-
Deliverable Name
+
Deliverable Task Name
Deliverable Type
Platforms
Review Link
Final Asset
Status Update
Completion Date
-
Project Owner
+
Task Owner
@@ -192,6 +193,13 @@ function enableBackBtn(block, blockConfig) { }) } +function buildDriverField(driverName) { + const driverSpan = document.createElement('span'); + driverSpan.classList.add('driver-text'); + driverSpan.innerHTML = `Project Owner: ${driverName}`; + return driverSpan; +} + function buildHeader(program, queryVars) { const headerWrapper = document.createElement('div'); headerWrapper.classList.add('details-header-wrapper'); @@ -199,6 +207,14 @@ function buildHeader(program, queryVars) { `Launch date${formatDate(program.launchDate)}
` : ""; const programName = program ? program.programName : queryVars.programName; const campaignName = program && program.campaignName ? '
' + program.campaignName + '
': ""; + + const driver = program && program.driver ? program.driver : "Not Available"; + let driverField = ''; + + if (program){ + driverField=buildDriverField(driver).outerHTML; + } + headerWrapper.innerHTML = `
@@ -207,7 +223,10 @@ function buildHeader(program, queryVars) { ${programName}
${campaignName} - ${date} +
+ ${date} + ${driverField} +
` return headerWrapper; @@ -611,4 +630,4 @@ function extractQueryVars() { programID: 'Program ID Not Available' } } -} \ No newline at end of file +} diff --git a/blocks/gmo-program-list/gmo-program-list.css b/blocks/gmo-program-list/gmo-program-list.css index a417c0c9..f2351932 100644 --- a/blocks/gmo-program-list/gmo-program-list.css +++ b/blocks/gmo-program-list/gmo-program-list.css @@ -204,28 +204,33 @@ select { border-radius: 4px; } .column-1 { - width: 28%; + width: 18%; margin-right: 1%; } .column-2 { - width: 40%; + width: 25%; margin-right: 1%; } .column-3 { - width: 12%; + width: 15%; margin-right: 1%; } .column-4 { - width: 16%; + width: 14%; margin-right: 1%; } .column-5 { - width: 13%; + width: 10%; margin-right: 1%; } .column-6 { - width: 14%; + width: 10%; + margin-right: 1%; } +.column-7 { + width: 5%; +} + .status { width: 80px; text-align: center; diff --git a/blocks/gmo-program-list/gmo-program-list.js b/blocks/gmo-program-list/gmo-program-list.js index 04a15a3a..83563dc9 100644 --- a/blocks/gmo-program-list/gmo-program-list.js +++ b/blocks/gmo-program-list/gmo-program-list.js @@ -30,6 +30,11 @@ const headerConfig = [ 'name': 'Status', 'attribute': 'status', 'sortable': false + }, + { + 'name': 'Geo', + 'attribute': 'geo', + 'sortable': false } ] @@ -214,17 +219,28 @@ async function buildCampaignList(campaigns, numPerPage) { var campaignStatusWrapper = document.createElement('div'); campaignStatusWrapper.classList.add('status-wrapper', 'column-6', 'vertical-center'); campaignStatusWrapper = buildStatus(campaignStatusWrapper, campaign); + + const campaignGeo = document.createElement('div'); + campaignGeo.textContent = formatGeos(campaign.node.p0TargetGeo); + campaignGeo.classList.add('column-7', 'vertical-center'); + campaignGeo.dataset.property = 'geo'; + campaignRow.appendChild(campaignInfoWrapper); campaignRow.appendChild(campaignOverviewWrapper); campaignRow.appendChild(campaignLaunch); campaignRow.appendChild(campaignProducts); campaignRow.appendChild(campaignStatusWrapper); + campaignRow.appendChild(campaignGeo); listWrapper.appendChild(campaignRow); } return listWrapper; } +function formatGeos(geoArray) { + return geoArray.map(geo => geo.toUpperCase()).join(', '); +} + function buildStatus(statusWrapper, campaign) { const campaignStatus = document.createElement('div'); const statusStr = checkBlankString(campaign.node.status); From 0a413be9b76d9b025c23012696d05bea712f1a29 Mon Sep 17 00:00:00 2001 From: TyroneAEM <147942284+TyroneAEM@users.noreply.github.com> Date: Tue, 25 Jun 2024 10:07:34 -0500 Subject: [PATCH 25/95] ASSETS-88921 : Marketing Moments Clickable Title and Underdevelopment Icon for ProgramNames/Campaigns (#128) * feat: Change cursor to pointer for campaign name label and text - Added CSS to change cursor to pointer on hover for .campaign-name-label and .campaign-name span. feat: Add click event to campaign name label and text - Updated buildCampaignList function to add click event listener to campaign name label and text. - Clicking on the campaign name label or text now navigates to campaignDetailsLink. - Ensured the appearance of the label and text remains unchanged while adding navigation functionality. feat: Rename "Review Link" to "QA Files" in the "Deliverable" task tab - Updated Program Details column text/header to QA files - Updated CSS class from review-link to qa-files * - feature : When a campaign does not have an image assigned display the underdevelopment icon Updated function searchAsset(programName, campaignName, imageWidth = 80) to get the underdevelopment icon from site-config worksheet shared-branding worksheet --- blocks/gmo-program-details/gmo-program-details.css | 2 +- blocks/gmo-program-details/gmo-program-details.js | 8 ++++---- blocks/gmo-program-list/gmo-program-list.css | 1 + blocks/gmo-program-list/gmo-program-list.js | 13 ++++++++++++- scripts/assets.js | 8 ++++++-- 5 files changed, 24 insertions(+), 8 deletions(-) diff --git a/blocks/gmo-program-details/gmo-program-details.css b/blocks/gmo-program-details/gmo-program-details.css index d38b1af2..733ef284 100644 --- a/blocks/gmo-program-details/gmo-program-details.css +++ b/blocks/gmo-program-details/gmo-program-details.css @@ -423,7 +423,7 @@ body { &:first-child, &:nth-child(2) { border-top: 2px solid #F4F4F4; } - & .deliverable-name, .platforms, .deliverable-type, .review-link, .kpi { + & .deliverable-name, .platforms, .deliverable-type, .qa-files, .kpi { overflow: hidden; text-overflow: ellipsis; line-height: 16px; diff --git a/blocks/gmo-program-details/gmo-program-details.js b/blocks/gmo-program-details/gmo-program-details.js index 061c0a69..e493bcd7 100644 --- a/blocks/gmo-program-details/gmo-program-details.js +++ b/blocks/gmo-program-details/gmo-program-details.js @@ -136,7 +136,7 @@ export default async function decorate(block) {
Deliverable Task Name
Deliverable Type
Platforms
-
Review Link
+
QA Files
Final Asset
Status Update
Completion Date
@@ -530,8 +530,8 @@ async function buildTableRow(deliverableJson, kpi, createHidden) {
${checkBlankString(deliverableJson.deliverableName)}
${checkBlankString(typeLabel)}
- `; + + // Wait for deliverables data + const deliverables = await executeQuery(deliverableQueryString); + + const uniqueDeliverableTypes = getUniqueItems(programData.data.deliverableList.items, 'deliverableType'); + const uniquePlatforms = getUniqueItems(programData.data.deliverableList.items, 'platforms'); + 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); - } + buildFieldScopes('deliverable-type', uniqueDeliverableTypes, block); + buildFieldScopes('platforms', uniquePlatforms, block); + + const table = await buildTable(await deliverables).then(async (rows) => { + return rows; + }); - block.querySelector('.tab-wrapper').addEventListener('click', (event) => { + // Batch Dom Updates + const tableRoot = block.querySelector('.table-content'); + const fragment = document.createDocumentFragment(); + fragment.appendChild(table); + tableRoot.appendChild(fragment); + + buildStatus(program.status); + + // Optimize Event Listeners: Added debouncing to event listeners to prevent performance issues. + const debounce = (func, delay) => { + let timeout; + return (...args) => { + clearTimeout(timeout); + timeout = setTimeout(() => func(...args), delay); + }; + }; + + block.querySelector('.tab-wrapper').addEventListener('click', debounce((event) => { switchTab(event.target); - }) + }, 300)); + enableBackBtn(block, blockConfig); block.querySelectorAll('.read-more').forEach((button) => { button.addEventListener('click', (event) => { @@ -175,14 +212,7 @@ export default async function decorate(block) { }); }); 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); + } function enableBackBtn(block, blockConfig) { @@ -248,9 +278,11 @@ function getUniqueItems(items, property) { )]; } -function insertImageIntoCampaignImg(block,imageObject) { +function insertImageIntoCampaignImg(block, imageObject) { const campaignImgDiv = block.querySelector('.campaign-img'); const imgElement = document.createElement('img'); + //Lazy load images + imgElement.loading = 'lazy'; imgElement.src = imageObject.imageUrl; imgElement.alt = imageObject.imageAltText; campaignImgDiv.appendChild(imgElement); @@ -259,7 +291,7 @@ function insertImageIntoCampaignImg(block,imageObject) { 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; @@ -412,7 +444,7 @@ function formatDate(dateString) { // Formatting the date into mm/dd/yyyy format const formattedDate = mm + '/' + dd + '/' + yyyy; - + return formattedDate; } @@ -502,7 +534,7 @@ async function buildHeaderRow(category, headerType, isInactive, matchCount) { const headerRow = document.createElement('div'); headerRow.classList.add('row', 'collapsible', 'header'); let divopen; - if (headerType === 'subcategory') { + if (headerType === 'subcategory') { headerRow.classList.add('subheader'); divopen = '
'; } else { @@ -579,7 +611,7 @@ function sortRows(rows) { 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) { From 309fbd974b0b98a4f05ddc0f24f695b7580cfd50 Mon Sep 17 00:00:00 2001 From: TyroneAEM <147942284+TyroneAEM@users.noreply.github.com> Date: Mon, 8 Jul 2024 14:49:37 -0500 Subject: [PATCH 28/95] assets.js (#132) Added helper function async function getUnderdevelopmentIcon() to display the underdevelopment icon. Also display the under development icon when programName and campaignName is null gmo-program-details.js When programName and campaignName is null display the under development icon --- .../gmo-program-details.js | 15 +++++++++++-- scripts/assets.js | 21 ++++++++++++++----- 2 files changed, 29 insertions(+), 7 deletions(-) diff --git a/blocks/gmo-program-details/gmo-program-details.js b/blocks/gmo-program-details/gmo-program-details.js index 8feafeb6..09f34aca 100644 --- a/blocks/gmo-program-details/gmo-program-details.js +++ b/blocks/gmo-program-details/gmo-program-details.js @@ -54,7 +54,7 @@ export default async function decorate(block) { decorateIcons(block); } else - { + { //programName and campaignName is null block.innerHTML = `
@@ -64,7 +64,18 @@ export default async function decorate(block) { ${header}
No data available.
- ` + `; + try { + //programName and campaignName is null display under development icon + imageObject = await searchAsset(null, null); + if (imageObject) { + insertImageIntoCampaignImg(block, imageObject); + totalassets = imageObject.assetCount; + } + } catch (error) { + console.error("Failed to load campaign image:", error); + } + decorateIcons(block); enableBackBtn(block, blockConfig); return; diff --git a/scripts/assets.js b/scripts/assets.js index ea159d72..7d213422 100644 --- a/scripts/assets.js +++ b/scripts/assets.js @@ -33,6 +33,12 @@ const getFilters = () => { return `is_pur-expirationDate = 0 OR pur-expirationDate > ${currentEpoch}`; }; +async function getUnderdevelopmentIcon() { + const configPath = getBaseConfigPath(); + const brandingConfig = await getBrandingConfig(); + const underdevelopmentIconPath = `${configPath}/${brandingConfig.underdevelopmentIcon}`.replace(/\/\//g, '/'); + return { imageUrl: underdevelopmentIconPath, imageAltText: 'Under Development', assetCount: 0 }; +} /** * Search Asset for programName and campaignName parameters. @@ -49,6 +55,13 @@ export async function searchAsset(programName, campaignName, imageWidth = 80) { const indexName = await getSearchIndex(); + + if (programName==null && campaignName==null) + { + // Display Underdevelopment Icon + return await getUnderdevelopmentIcon(); + } + // Initialize the facetFilters array const facetFilters = []; if (programName) { // Check if programName is not null @@ -96,11 +109,8 @@ export async function searchAsset(programName, campaignName, imageWidth = 80) { } else { - //Display Underdevelopment Icon - const configPath = getBaseConfigPath(); - const brandingConfig = await getBrandingConfig(); - const underdevelopmentIconPath = `${configPath}/${brandingConfig.underdevelopmentIcon}`.replace(/\/\//g, '/'); - return {imageUrl : underdevelopmentIconPath, imageAltText: 'Under Development', assetCount: 0}; + // Display Underdevelopment Icon + return await getUnderdevelopmentIcon(); } } // Handle other response codes @@ -110,3 +120,4 @@ export async function searchAsset(programName, campaignName, imageWidth = 80) { throw error; } } + From 18ddd12c8fef2fbcb9c56b54735e907f421f17ad Mon Sep 17 00:00:00 2001 From: Tyrone Tse Date: Thu, 11 Jul 2024 10:19:18 -0500 Subject: [PATCH 29/95] gmo-program-details.js PR Code review change to --- blocks/gmo-program-details/gmo-program-details.js | 2 +- scripts/assets.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/blocks/gmo-program-details/gmo-program-details.js b/blocks/gmo-program-details/gmo-program-details.js index 09f34aca..89e776c1 100644 --- a/blocks/gmo-program-details/gmo-program-details.js +++ b/blocks/gmo-program-details/gmo-program-details.js @@ -39,7 +39,7 @@ export default async function decorate(block) { // Update the header with the actual data block.querySelector('.placeholder-header').outerHTML = header; - let imageObject = null; + let imageObject = {imageUrl : '', imageAltText: '', assetCount: 0}; let totalassets = 0; if (program) { try { diff --git a/scripts/assets.js b/scripts/assets.js index 7d213422..c4b5fb58 100644 --- a/scripts/assets.js +++ b/scripts/assets.js @@ -56,7 +56,7 @@ export async function searchAsset(programName, campaignName, imageWidth = 80) { const indexName = await getSearchIndex(); - if (programName==null && campaignName==null) + if (!programName && !campaignName) { // Display Underdevelopment Icon return await getUnderdevelopmentIcon(); From 4f775473589652d1777019f5be16f5d79a06c3fb Mon Sep 17 00:00:00 2001 From: Tyrone Tse Date: Thu, 11 Jul 2024 10:20:06 -0500 Subject: [PATCH 30/95] Revert "gmo-program-details.js" This reverts commit 18ddd12c8fef2fbcb9c56b54735e907f421f17ad. --- blocks/gmo-program-details/gmo-program-details.js | 2 +- scripts/assets.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/blocks/gmo-program-details/gmo-program-details.js b/blocks/gmo-program-details/gmo-program-details.js index 89e776c1..09f34aca 100644 --- a/blocks/gmo-program-details/gmo-program-details.js +++ b/blocks/gmo-program-details/gmo-program-details.js @@ -39,7 +39,7 @@ export default async function decorate(block) { // Update the header with the actual data block.querySelector('.placeholder-header').outerHTML = header; - let imageObject = {imageUrl : '', imageAltText: '', assetCount: 0}; + let imageObject = null; let totalassets = 0; if (program) { try { diff --git a/scripts/assets.js b/scripts/assets.js index c4b5fb58..7d213422 100644 --- a/scripts/assets.js +++ b/scripts/assets.js @@ -56,7 +56,7 @@ export async function searchAsset(programName, campaignName, imageWidth = 80) { const indexName = await getSearchIndex(); - if (!programName && !campaignName) + if (programName==null && campaignName==null) { // Display Underdevelopment Icon return await getUnderdevelopmentIcon(); From ffa3451a35396d24a2a9d67ed79de52b6f4a5ea1 Mon Sep 17 00:00:00 2001 From: Tyrone Tse Date: Thu, 11 Jul 2024 10:25:26 -0500 Subject: [PATCH 31/95] gmo-program-details.js PR code review change to let imageObject = {imageUrl : '', imageAltText: '', assetCount: 0}; assets.js PR code review change to if (!progamName && !campaginName) --- blocks/gmo-program-details/gmo-program-details.js | 2 +- scripts/assets.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/blocks/gmo-program-details/gmo-program-details.js b/blocks/gmo-program-details/gmo-program-details.js index 09f34aca..89e776c1 100644 --- a/blocks/gmo-program-details/gmo-program-details.js +++ b/blocks/gmo-program-details/gmo-program-details.js @@ -39,7 +39,7 @@ export default async function decorate(block) { // Update the header with the actual data block.querySelector('.placeholder-header').outerHTML = header; - let imageObject = null; + let imageObject = {imageUrl : '', imageAltText: '', assetCount: 0}; let totalassets = 0; if (program) { try { diff --git a/scripts/assets.js b/scripts/assets.js index 7d213422..e4902c59 100644 --- a/scripts/assets.js +++ b/scripts/assets.js @@ -56,7 +56,7 @@ export async function searchAsset(programName, campaignName, imageWidth = 80) { const indexName = await getSearchIndex(); - if (programName==null && campaignName==null) + if (!progamName && !campaginName) { // Display Underdevelopment Icon return await getUnderdevelopmentIcon(); From e16de1cb15c61eea56ce3e7b43b55d9d7bb5cf86 Mon Sep 17 00:00:00 2001 From: Tyrone Tse Date: Thu, 11 Jul 2024 12:11:40 -0500 Subject: [PATCH 32/95] Resolved typo in variable programName from if (!progamName && !campaginName) to if (!programName && !campaignName) --- scripts/assets.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/assets.js b/scripts/assets.js index e4902c59..c4b5fb58 100644 --- a/scripts/assets.js +++ b/scripts/assets.js @@ -56,7 +56,7 @@ export async function searchAsset(programName, campaignName, imageWidth = 80) { const indexName = await getSearchIndex(); - if (!progamName && !campaginName) + if (!programName && !campaignName) { // Display Underdevelopment Icon return await getUnderdevelopmentIcon(); From 5151195b90ff5edfc55055c3747f1b04a18ace06 Mon Sep 17 00:00:00 2001 From: TyroneAEM <147942284+TyroneAEM@users.noreply.github.com> Date: Thu, 11 Jul 2024 16:31:53 -0500 Subject: [PATCH 33/95] feat: Improve page load speed and add lazy loading for images (#134) - Implement debounce for search input to reduce the number of API calls. - Refactor the `decorate` function for better readability and performance. - Add lazy loading for images using `IntersectionObserver` to load images only when they come into view. - Move pagination button toggling to a separate `togglePaginationButtons` function. - Ensure strict comparison (`===`) for block configuration check. --- blocks/gmo-program-list/gmo-program-list.js | 67 +++++++++++++++------ 1 file changed, 48 insertions(+), 19 deletions(-) diff --git a/blocks/gmo-program-list/gmo-program-list.js b/blocks/gmo-program-list/gmo-program-list.js index 0635f963..e26f3d61 100644 --- a/blocks/gmo-program-list/gmo-program-list.js +++ b/blocks/gmo-program-list/gmo-program-list.js @@ -50,8 +50,19 @@ let totalPages = 0; 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() { +// Debounce function to reduce the number of calls +function debounce(func, delay) { + let debounceTimer; + return function() { + const context = this; + const args = arguments; + clearTimeout(debounceTimer); + debounceTimer = setTimeout(() => func.apply(context, args), delay); + }; +} + +//Custom event gmoCampaignListBlock with debounce to allow the gmo-campaign-header to trigger the gmo-program-list to update +document.addEventListener('gmoCampaignListBlock', debounce(async function() { //Build graphq filter that is passed to the graphql persisted queries const graphQLFilterArray = getFilterValues(); const searchInputValue = document.getElementById('campaign-search').value; @@ -64,16 +75,15 @@ document.addEventListener('gmoCampaignListBlock', async function() { 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 = DEFAULT_ITEMS_PER_PAGE; - + //Trigger loading the gmo-campaign-block decorate( block, currentNumberPerPage, '', false, false, currentGraphqlFilter); - -}); +}, 300)); export default async function decorate(block, numPerPage = currentNumberPerPage, cursor = '', previousPage = false, nextPage = false, graphQLFilter = {}) { @@ -117,8 +127,32 @@ export default async function decorate(block, numPerPage = currentNumberPerPage, listContainer.appendChild(listItems); listContainer.appendChild(listFooter); // Show Hide Previous and Next Page buttons - const footerNext = document.querySelector('.footer-pagination-button.next'); + togglePaginationButtons(); + + decorateIcons(block); + + // Lazy loading for images + document.addEventListener('DOMContentLoaded', function() { + if ('IntersectionObserver' in window) { + const lazyImages = document.querySelectorAll('.lazy'); + const observer = new IntersectionObserver((entries, observer) => { + entries.forEach(entry => { + if (entry.isIntersecting) { + const img = entry.target; + img.src = img.dataset.src; + img.classList.remove('lazy'); + observer.unobserve(img); + } + }); + }); + lazyImages.forEach(img => observer.observe(img)); + } + }); +} + +function togglePaginationButtons() { const footerPrev = document.querySelector('.footer-pagination-button.prev'); + const footerNext = document.querySelector('.footer-pagination-button.next'); if (currentPage > 1) { footerPrev.classList.add('active'); } else { @@ -130,9 +164,6 @@ export default async function decorate(block, numPerPage = currentNumberPerPage, } else { footerNext.classList.remove('active'); } - - decorateIcons(block); - } function getFilterValues(){ @@ -275,14 +306,13 @@ function buildStatus(statusWrapper, campaign) { } async function addThumbnail(parentElement, programName, campaignName) { - searchAsset(programName, campaignName).then((response) => { - if (response && (Object.hasOwn(response, 'imageUrl') && Object.hasOwn(response, 'imageAltText'))) { - const iconImage = document.createElement('img'); - iconImage.src = response?.imageUrl; - iconImage.alt = response?.imageAltText; - parentElement.appendChild(iconImage); - } - }) + const response = await searchAsset(programName, campaignName); + if (response?.imageUrl && response?.imageAltText) { + const iconImage = document.createElement('img'); + iconImage.src = response.imageUrl; + iconImage.alt = response.imageAltText; + parentElement.appendChild(iconImage); + } } async function buildProduct(product) { @@ -529,4 +559,3 @@ function sortColumn(dir, property) { container.appendChild(row); }); } - From a40ff9e95bdea6590be68c22c0a2e7ac812208d7 Mon Sep 17 00:00:00 2001 From: TyroneAEM <147942284+TyroneAEM@users.noreply.github.com> Date: Fri, 12 Jul 2024 08:42:42 -0500 Subject: [PATCH 34/95] ASSETS-88926 : Review and Improve Page Performance of Marketing Moment Landing page (Push Failed) (#137) * feat: Improve page load speed and add lazy loading for images - Implement debounce for search input to reduce the number of API calls. - Refactor the `decorate` function for better readability and performance. - Add lazy loading for images using `IntersectionObserver` to load images only when they come into view. - Move pagination button toggling to a separate `togglePaginationButtons` function. - Ensure strict comparison (`===`) for block configuration check. * feat: Improve page load speed and add lazy loading for images - Removed debounce for search - Added lazy loading for images using `IntersectionObserver` to load images only when they come into view. --- blocks/gmo-program-list/gmo-program-list.js | 17 ++--------------- 1 file changed, 2 insertions(+), 15 deletions(-) diff --git a/blocks/gmo-program-list/gmo-program-list.js b/blocks/gmo-program-list/gmo-program-list.js index e26f3d61..9b331872 100644 --- a/blocks/gmo-program-list/gmo-program-list.js +++ b/blocks/gmo-program-list/gmo-program-list.js @@ -50,19 +50,7 @@ let totalPages = 0; let campaignCount = await graphqlCampaignCount(); let blockConfig; -// Debounce function to reduce the number of calls -function debounce(func, delay) { - let debounceTimer; - return function() { - const context = this; - const args = arguments; - clearTimeout(debounceTimer); - debounceTimer = setTimeout(() => func.apply(context, args), delay); - }; -} - -//Custom event gmoCampaignListBlock with debounce to allow the gmo-campaign-header to trigger the gmo-program-list to update -document.addEventListener('gmoCampaignListBlock', debounce(async function() { +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; @@ -83,8 +71,7 @@ document.addEventListener('gmoCampaignListBlock', debounce(async function() { currentNumberPerPage = DEFAULT_ITEMS_PER_PAGE; //Trigger loading the gmo-campaign-block decorate( block, currentNumberPerPage, '', false, false, currentGraphqlFilter); -}, 300)); - +}); export default async function decorate(block, numPerPage = currentNumberPerPage, cursor = '', previousPage = false, nextPage = false, graphQLFilter = {}) { if (blockConfig == undefined) blockConfig = readBlockConfig(block); From bd7b9a9f8f53819cd4592d98a11bcc7bbd13308c Mon Sep 17 00:00:00 2001 From: TyroneAEM <147942284+TyroneAEM@users.noreply.github.com> Date: Mon, 15 Jul 2024 09:15:31 -0500 Subject: [PATCH 35/95] ASSETS-88927 : Toggle Read More Button (#138) * feat: Add Read More/Read Less toggle functionality - Added event listener for Read More buttons to toggle text visibility in Marketing Goal and Product Value sections. - Updated the decorate function to handle Read More/Read Less functionality. * style: Add hide-overflow class for text truncation - Added .hide-overflow class to handle text truncation with ellipsis. - Ensured compatibility with the Read More/Read Less functionality. --- blocks/gmo-program-details/gmo-program-details.css | 7 +++++++ blocks/gmo-program-details/gmo-program-details.js | 6 +++++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/blocks/gmo-program-details/gmo-program-details.css b/blocks/gmo-program-details/gmo-program-details.css index 733ef284..86239780 100644 --- a/blocks/gmo-program-details/gmo-program-details.css +++ b/blocks/gmo-program-details/gmo-program-details.css @@ -584,3 +584,10 @@ body { } } +.hide-overflow { + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 3; + overflow: hidden; + text-overflow: ellipsis; +} diff --git a/blocks/gmo-program-details/gmo-program-details.js b/blocks/gmo-program-details/gmo-program-details.js index 89e776c1..2531bf5a 100644 --- a/blocks/gmo-program-details/gmo-program-details.js +++ b/blocks/gmo-program-details/gmo-program-details.js @@ -215,13 +215,17 @@ export default async function decorate(block) { }, 300)); enableBackBtn(block, blockConfig); + 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'); + const paragraph = parent.querySelector('.paragraph'); + paragraph.classList.toggle('hide-overflow'); + readMore.textContent = paragraph.classList.contains('hide-overflow') ? 'Read more' : 'Read less'; }); }); + decorateIcons(block); } From a668e8a0d43ec838bdc40fa0cf9c665687b8ce4e Mon Sep 17 00:00:00 2001 From: mathieu-lessard Date: Tue, 16 Jul 2024 11:48:56 -0700 Subject: [PATCH 36/95] Modified FAQ Nav (#139) Co-authored-by: Mathieu Lessard --- blocks/adp-header/adp-header.css | 10 ++++++++++ blocks/adp-header/adp-header.js | 6 +++--- scripts/scripts.js | 3 +++ styles/styles.css | 4 ++++ 4 files changed, 20 insertions(+), 3 deletions(-) diff --git a/blocks/adp-header/adp-header.css b/blocks/adp-header/adp-header.css index bfd8516f..52585a73 100644 --- a/blocks/adp-header/adp-header.css +++ b/blocks/adp-header/adp-header.css @@ -514,6 +514,10 @@ header nav .nav-bottom { width: 100%; } +header.hide-navigation-tabs nav .nav-bottom { + display: none; +} + header nav { display: grid; grid-template-areas: @@ -526,6 +530,12 @@ header nav { box-shadow: 0 0 4px rgba(233 233 233 / 100%); } +header.hide-navigation-tabs nav { + grid-template-areas: + "nav-top"; + grid-template-rows: calc(var(--nav-height) - var(--header-banner-height)); +} + .contenthub header nav { background: var(--contenthub-header-background); border-bottom: unset; diff --git a/blocks/adp-header/adp-header.js b/blocks/adp-header/adp-header.js index b3eafe7c..16c4da45 100644 --- a/blocks/adp-header/adp-header.js +++ b/blocks/adp-header/adp-header.js @@ -106,14 +106,14 @@ function toggleMenu(nav, navSections, forceExpanded = null) { */ export default async function decorate(block) { block.textContent = ''; - + const logoUrl = document.querySelector('head meta[name="logo-link"]')?.getAttribute('content'); // decorate nav DOM const nav = document.createElement('nav'); nav.id = 'nav'; nav.innerHTML = `