diff --git a/force-app/main/default/applications/LWC_Recipes_Console_App.app-meta.xml b/force-app/main/default/applications/LWC_Recipes_Console_App.app-meta.xml new file mode 100644 index 000000000..1e065a263 --- /dev/null +++ b/force-app/main/default/applications/LWC_Recipes_Console_App.app-meta.xml @@ -0,0 +1,95 @@ + + + + #0070D2 + false + + Small + Large + false + false + false + + Console + Hello + Datatable + Apex + standard-Contact + Composition + Events + Parent_to_Child + Message_Service + Refresh_View + Light_DOM + Dynamic_Interactions + Aura_Interoperability + Wire + GraphQL + Navigation + Record_Picker + Misc_Techniques + X3rd_Party_Libs + Workspace_API_Tab_Launcher + Lightning + LWC_Recipes_Console_App_UtilityBar + + + Apex + + + Aura_Interoperability + + + Composition + + + Datatable + + + Dynamic_Interactions + + + Events + + + GraphQL + + + Hello + + + Light_DOM + + + Message_Service + + + Misc_Techniques + + + Navigation + + + Parent_to_Child + + + Record_Picker + + + Refresh_View + + + Wire + + + Workspace_API_Tab_Launcher + + + X3rd_Party_Libs + + + AccountId + standard-Contact + + + diff --git a/force-app/main/default/flexipages/LWC_Recipes_Console_App_UtilityBar.flexipage-meta.xml b/force-app/main/default/flexipages/LWC_Recipes_Console_App_UtilityBar.flexipage-meta.xml new file mode 100644 index 000000000..31f6003ed --- /dev/null +++ b/force-app/main/default/flexipages/LWC_Recipes_Console_App_UtilityBar.flexipage-meta.xml @@ -0,0 +1,56 @@ + + + + + + + eager + decorator + false + + + height + decorator + 480 + + + icon + decorator + fallback + + + label + decorator + workspaceAPICloseTab + + + scrollable + decorator + true + + + width + decorator + 340 + + workspaceAPICloseTab + workspaceAPICloseTab + + + utilityItems + Region + + + backgroundComponents + Background + + LWC Recipes Console App UtilityBar + + UtilityBar + diff --git a/force-app/main/default/flexipages/Workspace_API.flexipage-meta.xml b/force-app/main/default/flexipages/Workspace_API.flexipage-meta.xml new file mode 100644 index 000000000..c37d022e2 --- /dev/null +++ b/force-app/main/default/flexipages/Workspace_API.flexipage-meta.xml @@ -0,0 +1,62 @@ + + + + + + workspaceAPIOpenTab + c_workspaceAPIOpenTab + + + + + workspaceAPISetTabIcon + c_workspaceAPISetTabIcon + + + + + workspaceAPIDisableTabClose + c_workspaceAPIDisableTabClose + + + region1 + Region + + + + + workspaceAPIOpenSubtab + c_workspaceAPIOpenSubtab + + + + + workspaceAPIHighlightTab + c_workspaceAPIHighlightTab + + + region2 + Region + + + + + workspaceAPISetTabLabel + c_workspaceAPISetTabLabel + + + + + workspaceAPIFocusTab + c_workspaceAPIFocusTab + + + region3 + Region + + Workspace API + + AppPage + diff --git a/force-app/main/default/flexipages/Workspace_API_Tab_Launcher.flexipage-meta.xml b/force-app/main/default/flexipages/Workspace_API_Tab_Launcher.flexipage-meta.xml new file mode 100644 index 000000000..4fe36fd89 --- /dev/null +++ b/force-app/main/default/flexipages/Workspace_API_Tab_Launcher.flexipage-meta.xml @@ -0,0 +1,18 @@ + + + + + + workspaceAPI + c_workspaceAPI + + + region1 + Region + + Workspace API + + AppPage + diff --git a/force-app/main/default/lwc/workspaceAPI/__tests__/workspaceAPI.test.js b/force-app/main/default/lwc/workspaceAPI/__tests__/workspaceAPI.test.js new file mode 100644 index 000000000..7868ef5fd --- /dev/null +++ b/force-app/main/default/lwc/workspaceAPI/__tests__/workspaceAPI.test.js @@ -0,0 +1,45 @@ +import { createElement } from 'lwc'; +import WorkspaceAPI from 'c/workspaceAPI'; +import { getNavigateCalledWith } from 'lightning/navigation'; + +describe('c-workspace-api', () => { + afterEach(() => { + // The jsdom instance is shared across test cases in a single file so reset the DOM + while (document.body.firstChild) { + document.body.removeChild(document.body.firstChild); + } + // Prevent data saved on mocks from leaking between tests + jest.clearAllMocks(); + }); + + it('navigates to Workspace API page when Take me there! button clicked', async () => { + const API_NAME = 'Workspace_API'; + const INPUT_TYPE = 'standard__navItemPage'; + + // Create component + const element = createElement('c-workspace-api', { + is: WorkspaceAPI + }); + document.body.appendChild(element); + + // Click button + const buttonEl = element.shadowRoot.querySelector('lightning-button'); + buttonEl.click(); + + // Verify the component under test called the correct navigate event + // type and sent the expected recordId defined above + const { pageReference } = getNavigateCalledWith(); + expect(pageReference.type).toBe(INPUT_TYPE); + expect(pageReference.attributes.apiName).toBe(API_NAME); + }); + + it('is accessible', async () => { + const element = createElement('c-workspace-api', { + is: WorkspaceAPI + }); + document.body.appendChild(element); + + // Check accessibility + await expect(element).toBeAccessible(); + }); +}); diff --git a/force-app/main/default/lwc/workspaceAPI/workspaceAPI.html b/force-app/main/default/lwc/workspaceAPI/workspaceAPI.html new file mode 100644 index 000000000..deae51fc3 --- /dev/null +++ b/force-app/main/default/lwc/workspaceAPI/workspaceAPI.html @@ -0,0 +1,18 @@ + diff --git a/force-app/main/default/lwc/workspaceAPI/workspaceAPI.js b/force-app/main/default/lwc/workspaceAPI/workspaceAPI.js new file mode 100644 index 000000000..e1f947cdb --- /dev/null +++ b/force-app/main/default/lwc/workspaceAPI/workspaceAPI.js @@ -0,0 +1,16 @@ +import { LightningElement, wire } from 'lwc'; +import { NavigationMixin } from 'lightning/navigation'; +import getSingleContact from '@salesforce/apex/ContactController.getSingleContact'; + +export default class Lds extends NavigationMixin(LightningElement) { + @wire(getSingleContact) contact; + + navigateToWorkspaceAPIExamples() { + this[NavigationMixin.Navigate]({ + type: 'standard__navItemPage', + attributes: { + apiName: 'Workspace_API' + } + }); + } +} diff --git a/force-app/main/default/lwc/workspaceAPI/workspaceAPI.js-meta.xml b/force-app/main/default/lwc/workspaceAPI/workspaceAPI.js-meta.xml new file mode 100644 index 000000000..b67524848 --- /dev/null +++ b/force-app/main/default/lwc/workspaceAPI/workspaceAPI.js-meta.xml @@ -0,0 +1,10 @@ + + + 59.0 + true + + lightning__AppPage + lightning__RecordPage + lightning__HomePage + + diff --git a/force-app/main/default/lwc/workspaceAPICloseTab/__tests__/workspaceAPICloseTab.test.js b/force-app/main/default/lwc/workspaceAPICloseTab/__tests__/workspaceAPICloseTab.test.js new file mode 100644 index 000000000..c92f3e3f9 --- /dev/null +++ b/force-app/main/default/lwc/workspaceAPICloseTab/__tests__/workspaceAPICloseTab.test.js @@ -0,0 +1,54 @@ +import { createElement } from 'lwc'; +import { + IsConsoleNavigation, + getFocusedTabInfo, + closeTab, + FOCUSED_TAB +} from 'lightning/platformWorkspaceApi'; +import WorkspaceAPICloseTab from 'c/workspaceAPICloseTab'; + +describe('c-workspace-api-close-tab', () => { + afterEach(() => { + // The jsdom instance is shared across test cases in a single file so reset the DOM + while (document.body.firstChild) { + document.body.removeChild(document.body.firstChild); + } + }); + + // Helper function to wait until the microtask queue is empty. This is needed for promise + // timing when calling async functions + async function flushPromises() { + return Promise.resolve(); + } + + it('Calls the related platformWorkspaceApi methods', async () => { + // Create component + const element = createElement('c-workspace-api-close-tab', { + is: WorkspaceAPICloseTab + }); + document.body.appendChild(element); + + IsConsoleNavigation.emit(true); + + // Query lightning-button component element + const buttonEl = element.shadowRoot.querySelector('lightning-button'); + buttonEl.click(); + + await flushPromises(); + + // Compare if related platformWorkspaceApi functions have been called + expect(getFocusedTabInfo).toHaveBeenCalled(); + expect(closeTab).toHaveBeenCalledWith(FOCUSED_TAB); + }); + + it('is accessible', async () => { + // Create component + const element = createElement('c-workspace-api-close-tab', { + is: WorkspaceAPICloseTab + }); + document.body.appendChild(element); + + // Check accessibility + await expect(element).toBeAccessible(); + }); +}); diff --git a/force-app/main/default/lwc/workspaceAPICloseTab/workspaceAPICloseTab.html b/force-app/main/default/lwc/workspaceAPICloseTab/workspaceAPICloseTab.html new file mode 100644 index 000000000..c25fb977c --- /dev/null +++ b/force-app/main/default/lwc/workspaceAPICloseTab/workspaceAPICloseTab.html @@ -0,0 +1,14 @@ + diff --git a/force-app/main/default/lwc/workspaceAPICloseTab/workspaceAPICloseTab.js b/force-app/main/default/lwc/workspaceAPICloseTab/workspaceAPICloseTab.js new file mode 100644 index 000000000..92c74514d --- /dev/null +++ b/force-app/main/default/lwc/workspaceAPICloseTab/workspaceAPICloseTab.js @@ -0,0 +1,18 @@ +import { LightningElement, wire } from 'lwc'; +import { + closeTab, + IsConsoleNavigation, + getFocusedTabInfo +} from 'lightning/platformWorkspaceApi'; + +export default class WorkspaceAPICloseTab extends LightningElement { + @wire(IsConsoleNavigation) isConsoleNavigation; + + async closeTab() { + if (!this.isConsoleNavigation) { + return; + } + const { tabId } = await getFocusedTabInfo(); + await closeTab(tabId); + } +} diff --git a/force-app/main/default/lwc/workspaceAPICloseTab/workspaceAPICloseTab.js-meta.xml b/force-app/main/default/lwc/workspaceAPICloseTab/workspaceAPICloseTab.js-meta.xml new file mode 100644 index 000000000..7918bdb82 --- /dev/null +++ b/force-app/main/default/lwc/workspaceAPICloseTab/workspaceAPICloseTab.js-meta.xml @@ -0,0 +1,8 @@ + + + 60.0 + true + + lightning__UtilityBar + + diff --git a/force-app/main/default/lwc/workspaceAPIDisableTabClose/__tests__/workspaceAPIDisableTabClose.test.js b/force-app/main/default/lwc/workspaceAPIDisableTabClose/__tests__/workspaceAPIDisableTabClose.test.js new file mode 100644 index 000000000..f0cc39b7f --- /dev/null +++ b/force-app/main/default/lwc/workspaceAPIDisableTabClose/__tests__/workspaceAPIDisableTabClose.test.js @@ -0,0 +1,58 @@ +import { createElement } from 'lwc'; +import { + disableTabClose, + IsConsoleNavigation, + getFocusedTabInfo, + FOCUSED_TAB +} from 'lightning/platformWorkspaceApi'; + +import WorkspaceAPIDisableTabClose from 'c/workspaceAPIDisableTabClose'; + +describe('c-workspace-api-disable-tab-close', () => { + afterEach(() => { + // The jsdom instance is shared across test cases in a single file so reset the DOM + while (document.body.firstChild) { + document.body.removeChild(document.body.firstChild); + } + }); + + // Helper function to wait until the microtask queue is empty. This is needed for promise + // timing when calling async functions + async function flushPromises() { + return Promise.resolve(); + } + + it('Calls the related platformWorkspaceApi methods', async () => { + // Create component + const element = createElement('c-workspace-api-disable-tab-close', { + is: WorkspaceAPIDisableTabClose + }); + document.body.appendChild(element); + + IsConsoleNavigation.emit(true); + + // Query lightning-input component element + const inputEl = element.shadowRoot.querySelector('lightning-input'); + const toggleValue = true; + inputEl.dispatchEvent( + new CustomEvent('change', { detail: { checked: toggleValue } }) + ); + + await flushPromises(); + + // Compare if related platformWorkspaceApi functions have been called + expect(getFocusedTabInfo).toHaveBeenCalled(); + expect(disableTabClose).toHaveBeenCalledWith(FOCUSED_TAB, toggleValue); + }); + + it('is accessible', async () => { + // Create component + const element = createElement('c-workspace-api-disable-tab-close', { + is: WorkspaceAPIDisableTabClose + }); + document.body.appendChild(element); + + // Check accessibility + await expect(element).toBeAccessible(); + }); +}); diff --git a/force-app/main/default/lwc/workspaceAPIDisableTabClose/workspaceAPIDisableTabClose.html b/force-app/main/default/lwc/workspaceAPIDisableTabClose/workspaceAPIDisableTabClose.html new file mode 100644 index 000000000..8a803de4d --- /dev/null +++ b/force-app/main/default/lwc/workspaceAPIDisableTabClose/workspaceAPIDisableTabClose.html @@ -0,0 +1,18 @@ + diff --git a/force-app/main/default/lwc/workspaceAPIDisableTabClose/workspaceAPIDisableTabClose.js b/force-app/main/default/lwc/workspaceAPIDisableTabClose/workspaceAPIDisableTabClose.js new file mode 100644 index 000000000..4a85fb332 --- /dev/null +++ b/force-app/main/default/lwc/workspaceAPIDisableTabClose/workspaceAPIDisableTabClose.js @@ -0,0 +1,19 @@ +import { LightningElement, wire } from 'lwc'; +import { + disableTabClose, + IsConsoleNavigation, + getFocusedTabInfo +} from 'lightning/platformWorkspaceApi'; + +export default class WorkspaceAPIDisableTabClose extends LightningElement { + @wire(IsConsoleNavigation) isConsoleNavigation; + + async disableTabClose(event) { + if (!this.isConsoleNavigation) { + return; + } + const close = event.detail.checked; + const { tabId } = await getFocusedTabInfo(); + await disableTabClose(tabId, close); + } +} diff --git a/force-app/main/default/lwc/workspaceAPIDisableTabClose/workspaceAPIDisableTabClose.js-meta.xml b/force-app/main/default/lwc/workspaceAPIDisableTabClose/workspaceAPIDisableTabClose.js-meta.xml new file mode 100644 index 000000000..075c6d873 --- /dev/null +++ b/force-app/main/default/lwc/workspaceAPIDisableTabClose/workspaceAPIDisableTabClose.js-meta.xml @@ -0,0 +1,8 @@ + + + 60.0 + true + + lightning__AppPage + + diff --git a/force-app/main/default/lwc/workspaceAPIFocusTab/__tests__/workspaceAPIFocusTab.test.js b/force-app/main/default/lwc/workspaceAPIFocusTab/__tests__/workspaceAPIFocusTab.test.js new file mode 100644 index 000000000..1e7b670c4 --- /dev/null +++ b/force-app/main/default/lwc/workspaceAPIFocusTab/__tests__/workspaceAPIFocusTab.test.js @@ -0,0 +1,61 @@ +import { createElement } from 'lwc'; +import { + focusTab, + IsConsoleNavigation, + getFocusedTabInfo, + getAllTabInfo, + FOCUSED_TAB +} from 'lightning/platformWorkspaceApi'; +import WorkspaceAPIFocusTab from 'c/workspaceAPIFocusTab'; + +describe('c-workspace-api-focus-tab', () => { + afterEach(() => { + // The jsdom instance is shared across test cases in a single file so reset the DOM + while (document.body.firstChild) { + document.body.removeChild(document.body.firstChild); + } + }); + + // Helper function to wait until the microtask queue is empty. This is needed for promise + // timing when calling async functions + async function flushPromises() { + return Promise.resolve(); + } + + it('Calls the related platformWorkspaceApi methods', async () => { + // Create component + const element = createElement('c-workspace-api-focus-tab', { + is: WorkspaceAPIFocusTab + }); + document.body.appendChild(element); + + IsConsoleNavigation.emit(true); + + // Query lightning-button component element + const buttonEl = element.shadowRoot.querySelector('lightning-button'); + buttonEl.click(); + + await flushPromises(); + + // Compare if related platformWorkspaceApi functions have been called + expect(getFocusedTabInfo).toHaveBeenCalled(); + expect(getAllTabInfo).toHaveBeenCalled(); + const allTabs = await getAllTabInfo(); + const selectedTabIndex = allTabs.findIndex( + (possibleNextTab) => possibleNextTab.tabId === FOCUSED_TAB + ); + const nextTabId = allTabs[selectedTabIndex + 1].tabId; + expect(focusTab).toHaveBeenCalledWith(nextTabId); + }); + + it('is accessible', async () => { + // Create component + const element = createElement('c-workspace-api-focus-tab', { + is: WorkspaceAPIFocusTab + }); + document.body.appendChild(element); + + // Check accessibility + await expect(element).toBeAccessible(); + }); +}); diff --git a/force-app/main/default/lwc/workspaceAPIFocusTab/workspaceAPIFocusTab.html b/force-app/main/default/lwc/workspaceAPIFocusTab/workspaceAPIFocusTab.html new file mode 100644 index 000000000..e7d16de8b --- /dev/null +++ b/force-app/main/default/lwc/workspaceAPIFocusTab/workspaceAPIFocusTab.html @@ -0,0 +1,14 @@ + diff --git a/force-app/main/default/lwc/workspaceAPIFocusTab/workspaceAPIFocusTab.js b/force-app/main/default/lwc/workspaceAPIFocusTab/workspaceAPIFocusTab.js new file mode 100644 index 000000000..7edf31c33 --- /dev/null +++ b/force-app/main/default/lwc/workspaceAPIFocusTab/workspaceAPIFocusTab.js @@ -0,0 +1,25 @@ +import { LightningElement, wire } from 'lwc'; +import { + focusTab, + IsConsoleNavigation, + getFocusedTabInfo, + getAllTabInfo +} from 'lightning/platformWorkspaceApi'; + +export default class WorkspaceAPIFocusTab extends LightningElement { + @wire(IsConsoleNavigation) isConsoleNavigation; + + async focusNextTab() { + if (!this.isConsoleNavigation) { + return; + } + const { tabId } = await getFocusedTabInfo(); + const allTabs = await getAllTabInfo(); + const selectedTabIndex = allTabs.findIndex( + (possibleNextTab) => possibleNextTab.tabId === tabId + ); + const nextTabId = allTabs[selectedTabIndex + 1].tabId; + + await focusTab(nextTabId); + } +} diff --git a/force-app/main/default/lwc/workspaceAPIFocusTab/workspaceAPIFocusTab.js-meta.xml b/force-app/main/default/lwc/workspaceAPIFocusTab/workspaceAPIFocusTab.js-meta.xml new file mode 100644 index 000000000..075c6d873 --- /dev/null +++ b/force-app/main/default/lwc/workspaceAPIFocusTab/workspaceAPIFocusTab.js-meta.xml @@ -0,0 +1,8 @@ + + + 60.0 + true + + lightning__AppPage + + diff --git a/force-app/main/default/lwc/workspaceAPIHighlightTab/__tests__/workspaceAPIHighlightTab.test.js b/force-app/main/default/lwc/workspaceAPIHighlightTab/__tests__/workspaceAPIHighlightTab.test.js new file mode 100644 index 000000000..ae99b66a7 --- /dev/null +++ b/force-app/main/default/lwc/workspaceAPIHighlightTab/__tests__/workspaceAPIHighlightTab.test.js @@ -0,0 +1,59 @@ +import { createElement } from 'lwc'; +import WorkspaceAPIHighlightTab from 'c/workspaceAPIHighlightTab'; +import { + IsConsoleNavigation, + getFocusedTabInfo, + setTabHighlighted, + FOCUSED_TAB +} from 'lightning/platformWorkspaceApi'; + +describe('c-workspace-api-highlight-tab', () => { + afterEach(() => { + // The jsdom instance is shared across test cases in a single file so reset the DOM + while (document.body.firstChild) { + document.body.removeChild(document.body.firstChild); + } + }); + + // Helper function to wait until the microtask queue is empty. This is needed for promise + // timing when calling async functions + async function flushPromises() { + return Promise.resolve(); + } + + it('Calls the related platformWorkspaceApi methods', async () => { + // Create component + const element = createElement('c-workspace-api-highlight-tab', { + is: WorkspaceAPIHighlightTab + }); + document.body.appendChild(element); + + IsConsoleNavigation.emit(true); + + // Query lightning-input component element + const inputEl = element.shadowRoot.querySelector('lightning-input'); + inputEl.dispatchEvent( + new CustomEvent('change', { detail: { checked: true } }) + ); + + await flushPromises(); + + // Compare if related platformWorkspaceApi functions have been called + expect(getFocusedTabInfo).toHaveBeenCalled(); + expect(setTabHighlighted).toHaveBeenCalledWith(FOCUSED_TAB, true, { + pulse: true, + state: 'success' + }); + }); + + it('is accessible', async () => { + // Create component + const element = createElement('c-workspace-api-highlight-tab', { + is: WorkspaceAPIHighlightTab + }); + document.body.appendChild(element); + + // Check accessibility + await expect(element).toBeAccessible(); + }); +}); diff --git a/force-app/main/default/lwc/workspaceAPIHighlightTab/workspaceAPIHighlightTab.html b/force-app/main/default/lwc/workspaceAPIHighlightTab/workspaceAPIHighlightTab.html new file mode 100644 index 000000000..2307bae35 --- /dev/null +++ b/force-app/main/default/lwc/workspaceAPIHighlightTab/workspaceAPIHighlightTab.html @@ -0,0 +1,18 @@ + diff --git a/force-app/main/default/lwc/workspaceAPIHighlightTab/workspaceAPIHighlightTab.js b/force-app/main/default/lwc/workspaceAPIHighlightTab/workspaceAPIHighlightTab.js new file mode 100644 index 000000000..6b57d2c3c --- /dev/null +++ b/force-app/main/default/lwc/workspaceAPIHighlightTab/workspaceAPIHighlightTab.js @@ -0,0 +1,22 @@ +import { LightningElement, wire } from 'lwc'; +import { + IsConsoleNavigation, + getFocusedTabInfo, + setTabHighlighted +} from 'lightning/platformWorkspaceApi'; + +export default class WorkspaceAPIHighlightTab extends LightningElement { + @wire(IsConsoleNavigation) isConsoleNavigation; + + async highlightTab(event) { + if (!this.isConsoleNavigation) { + return; + } + const highlighted = event.detail.checked; + const { tabId } = await getFocusedTabInfo(); + setTabHighlighted(tabId, highlighted, { + pulse: true, + state: 'success' + }); + } +} diff --git a/force-app/main/default/lwc/workspaceAPIHighlightTab/workspaceAPIHighlightTab.js-meta.xml b/force-app/main/default/lwc/workspaceAPIHighlightTab/workspaceAPIHighlightTab.js-meta.xml new file mode 100644 index 000000000..075c6d873 --- /dev/null +++ b/force-app/main/default/lwc/workspaceAPIHighlightTab/workspaceAPIHighlightTab.js-meta.xml @@ -0,0 +1,8 @@ + + + 60.0 + true + + lightning__AppPage + + diff --git a/force-app/main/default/lwc/workspaceAPIOpenSubtab/__tests__/workspaceAPIOpenSubtab.test.js b/force-app/main/default/lwc/workspaceAPIOpenSubtab/__tests__/workspaceAPIOpenSubtab.test.js new file mode 100644 index 000000000..303cbfaaa --- /dev/null +++ b/force-app/main/default/lwc/workspaceAPIOpenSubtab/__tests__/workspaceAPIOpenSubtab.test.js @@ -0,0 +1,63 @@ +import { createElement } from 'lwc'; +import WorkspaceAPIOpenSubtab from 'c/workspaceAPIOpenSubtab'; +import { + IsConsoleNavigation, + EnclosingTabId, + openSubtab, + ENCLOSING_TAB_ID +} from 'lightning/platformWorkspaceApi'; + +describe('c-workspace-api-open-subtab', () => { + afterEach(() => { + // The jsdom instance is shared across test cases in a single file so reset the DOM + while (document.body.firstChild) { + document.body.removeChild(document.body.firstChild); + } + }); + + // Helper function to wait until the microtask queue is empty. This is needed for promise + // timing when calling async functions + async function flushPromises() { + return Promise.resolve(); + } + + it('Calls the related platformWorkspaceApi methods', async () => { + // Create component + const element = createElement('c-workspace-api-open-subtab', { + is: WorkspaceAPIOpenSubtab + }); + document.body.appendChild(element); + + const enclosingTabId = 'tab0'; + IsConsoleNavigation.emit(true); + EnclosingTabId.emit(enclosingTabId); + + // Query lightning-button component element + const buttonEl = element.shadowRoot.querySelector('lightning-button'); + buttonEl.click(); + + await flushPromises(); + + // Compare if related platformWorkspaceApi functions have been called + expect(openSubtab).toHaveBeenCalledWith(ENCLOSING_TAB_ID, { + pageReference: { + type: 'standard__objectPage', + attributes: { + objectApiName: 'Account', + actionName: 'list' + } + } + }); + }); + + it('is accessible', async () => { + // Create component + const element = createElement('c-workspace-api-open-subtab', { + is: WorkspaceAPIOpenSubtab + }); + document.body.appendChild(element); + + // Check accessibility + await expect(element).toBeAccessible(); + }); +}); diff --git a/force-app/main/default/lwc/workspaceAPIOpenSubtab/workspaceAPIOpenSubtab.html b/force-app/main/default/lwc/workspaceAPIOpenSubtab/workspaceAPIOpenSubtab.html new file mode 100644 index 000000000..2b0562d50 --- /dev/null +++ b/force-app/main/default/lwc/workspaceAPIOpenSubtab/workspaceAPIOpenSubtab.html @@ -0,0 +1,17 @@ + diff --git a/force-app/main/default/lwc/workspaceAPIOpenSubtab/workspaceAPIOpenSubtab.js b/force-app/main/default/lwc/workspaceAPIOpenSubtab/workspaceAPIOpenSubtab.js new file mode 100644 index 000000000..a86272251 --- /dev/null +++ b/force-app/main/default/lwc/workspaceAPIOpenSubtab/workspaceAPIOpenSubtab.js @@ -0,0 +1,26 @@ +import { LightningElement, wire } from 'lwc'; +import { + IsConsoleNavigation, + EnclosingTabId, + openSubtab +} from 'lightning/platformWorkspaceApi'; + +export default class WorkspaceAPIOpenSubtab extends LightningElement { + @wire(IsConsoleNavigation) isConsoleNavigation; + @wire(EnclosingTabId) enclosingTabId; + + findEnclosingTabAndOpenSubtab() { + if (!this.isConsoleNavigation || !this.enclosingTabId) { + return; + } + openSubtab(this.enclosingTabId, { + pageReference: { + type: 'standard__objectPage', + attributes: { + objectApiName: 'Account', + actionName: 'list' + } + } + }); + } +} diff --git a/force-app/main/default/lwc/workspaceAPIOpenSubtab/workspaceAPIOpenSubtab.js-meta.xml b/force-app/main/default/lwc/workspaceAPIOpenSubtab/workspaceAPIOpenSubtab.js-meta.xml new file mode 100644 index 000000000..075c6d873 --- /dev/null +++ b/force-app/main/default/lwc/workspaceAPIOpenSubtab/workspaceAPIOpenSubtab.js-meta.xml @@ -0,0 +1,8 @@ + + + 60.0 + true + + lightning__AppPage + + diff --git a/force-app/main/default/lwc/workspaceAPIOpenTab/__tests__/workspaceAPIOpenTab.test.js b/force-app/main/default/lwc/workspaceAPIOpenTab/__tests__/workspaceAPIOpenTab.test.js new file mode 100644 index 000000000..c5cb48307 --- /dev/null +++ b/force-app/main/default/lwc/workspaceAPIOpenTab/__tests__/workspaceAPIOpenTab.test.js @@ -0,0 +1,58 @@ +import { createElement } from 'lwc'; +import WorkspaceAPIOpenTab from 'c/workspaceAPIOpenTab'; +import { IsConsoleNavigation, openTab } from 'lightning/platformWorkspaceApi'; + +describe('c-workspace-api-open-tab', () => { + afterEach(() => { + // The jsdom instance is shared across test cases in a single file so reset the DOM + while (document.body.firstChild) { + document.body.removeChild(document.body.firstChild); + } + }); + + // Helper function to wait until the microtask queue is empty. This is needed for promise + // timing when calling async functions + async function flushPromises() { + return Promise.resolve(); + } + + it('Calls the related platformWorkspaceApi methods', async () => { + // Create component + const element = createElement('c-workspace-api-open-tab', { + is: WorkspaceAPIOpenTab + }); + document.body.appendChild(element); + + IsConsoleNavigation.emit(true); + + // Query lightning-button component element + const buttonEl = element.shadowRoot.querySelector('lightning-button'); + buttonEl.click(); + + await flushPromises(); + + // Compare if related platformWorkspaceApi functions have been called + expect(openTab).toHaveBeenCalledWith({ + pageReference: { + type: 'standard__objectPage', + attributes: { + objectApiName: 'Contact', + actionName: 'list' + } + }, + focus: true, + label: 'Contacts List' + }); + }); + + it('is accessible', async () => { + // Create component + const element = createElement('c-workspace-api-open-tab', { + is: WorkspaceAPIOpenTab + }); + document.body.appendChild(element); + + // Check accessibility + await expect(element).toBeAccessible(); + }); +}); diff --git a/force-app/main/default/lwc/workspaceAPIOpenTab/workspaceAPIOpenTab.html b/force-app/main/default/lwc/workspaceAPIOpenTab/workspaceAPIOpenTab.html new file mode 100644 index 000000000..b3cd03cd9 --- /dev/null +++ b/force-app/main/default/lwc/workspaceAPIOpenTab/workspaceAPIOpenTab.html @@ -0,0 +1,14 @@ + diff --git a/force-app/main/default/lwc/workspaceAPIOpenTab/workspaceAPIOpenTab.js b/force-app/main/default/lwc/workspaceAPIOpenTab/workspaceAPIOpenTab.js new file mode 100644 index 000000000..7947281cc --- /dev/null +++ b/force-app/main/default/lwc/workspaceAPIOpenTab/workspaceAPIOpenTab.js @@ -0,0 +1,23 @@ +import { LightningElement, wire } from 'lwc'; +import { IsConsoleNavigation, openTab } from 'lightning/platformWorkspaceApi'; + +export default class WorkspaceAPIOpenTab extends LightningElement { + @wire(IsConsoleNavigation) isConsoleNavigation; + + async openTab() { + if (!this.isConsoleNavigation) { + return; + } + await openTab({ + pageReference: { + type: 'standard__objectPage', + attributes: { + objectApiName: 'Contact', + actionName: 'list' + } + }, + focus: true, + label: 'Contacts List' + }); + } +} diff --git a/force-app/main/default/lwc/workspaceAPIOpenTab/workspaceAPIOpenTab.js-meta.xml b/force-app/main/default/lwc/workspaceAPIOpenTab/workspaceAPIOpenTab.js-meta.xml new file mode 100644 index 000000000..075c6d873 --- /dev/null +++ b/force-app/main/default/lwc/workspaceAPIOpenTab/workspaceAPIOpenTab.js-meta.xml @@ -0,0 +1,8 @@ + + + 60.0 + true + + lightning__AppPage + + diff --git a/force-app/main/default/lwc/workspaceAPIRefreshTab/__tests__/workspaceAPIRefreshTab.test.js b/force-app/main/default/lwc/workspaceAPIRefreshTab/__tests__/workspaceAPIRefreshTab.test.js new file mode 100644 index 000000000..f74a49bd2 --- /dev/null +++ b/force-app/main/default/lwc/workspaceAPIRefreshTab/__tests__/workspaceAPIRefreshTab.test.js @@ -0,0 +1,56 @@ +import { createElement } from 'lwc'; +import { + IsConsoleNavigation, + getFocusedTabInfo, + refreshTab, + FOCUSED_TAB +} from 'lightning/platformWorkspaceApi'; +import WorkspaceAPIRefreshTab from 'c/workspaceAPIRefreshTab'; + +describe('c-workspace-api-refresh-tab', () => { + afterEach(() => { + // The jsdom instance is shared across test cases in a single file so reset the DOM + while (document.body.firstChild) { + document.body.removeChild(document.body.firstChild); + } + }); + + // Helper function to wait until the microtask queue is empty. This is needed for promise + // timing when calling async functions + async function flushPromises() { + return Promise.resolve(); + } + + it('Calls the related platformWorkspaceApi methods', async () => { + // Create component + const element = createElement('c-workspace-api-refresh-tab', { + is: WorkspaceAPIRefreshTab + }); + document.body.appendChild(element); + + IsConsoleNavigation.emit(true); + + // Query lightning-button component element + const buttonEl = element.shadowRoot.querySelector('lightning-button'); + buttonEl.click(); + + await flushPromises(); + + // Compare if related platformWorkspaceApi functions have been called + expect(getFocusedTabInfo).toHaveBeenCalled(); + expect(refreshTab).toHaveBeenCalledWith(FOCUSED_TAB, { + includeAllSubtabs: true + }); + }); + + it('is accessible', async () => { + // Create component + const element = createElement('c-workspace-api-refresh-tab', { + is: WorkspaceAPIRefreshTab + }); + document.body.appendChild(element); + + // Check accessibility + await expect(element).toBeAccessible(); + }); +}); diff --git a/force-app/main/default/lwc/workspaceAPIRefreshTab/workspaceAPIRefreshTab.html b/force-app/main/default/lwc/workspaceAPIRefreshTab/workspaceAPIRefreshTab.html new file mode 100644 index 000000000..b5b21c666 --- /dev/null +++ b/force-app/main/default/lwc/workspaceAPIRefreshTab/workspaceAPIRefreshTab.html @@ -0,0 +1,14 @@ + diff --git a/force-app/main/default/lwc/workspaceAPIRefreshTab/workspaceAPIRefreshTab.js b/force-app/main/default/lwc/workspaceAPIRefreshTab/workspaceAPIRefreshTab.js new file mode 100644 index 000000000..e97f71bae --- /dev/null +++ b/force-app/main/default/lwc/workspaceAPIRefreshTab/workspaceAPIRefreshTab.js @@ -0,0 +1,20 @@ +import { LightningElement, wire } from 'lwc'; +import { + IsConsoleNavigation, + getFocusedTabInfo, + refreshTab +} from 'lightning/platformWorkspaceApi'; + +export default class WorkspaceAPIRefreshTab extends LightningElement { + @wire(IsConsoleNavigation) isConsoleNavigation; + + async refreshTab() { + if (!this.isConsoleNavigation) { + return; + } + const { tabId } = await getFocusedTabInfo(); + await refreshTab(tabId, { + includeAllSubtabs: true + }); + } +} diff --git a/force-app/main/default/lwc/workspaceAPIRefreshTab/workspaceAPIRefreshTab.js-meta.xml b/force-app/main/default/lwc/workspaceAPIRefreshTab/workspaceAPIRefreshTab.js-meta.xml new file mode 100644 index 000000000..075c6d873 --- /dev/null +++ b/force-app/main/default/lwc/workspaceAPIRefreshTab/workspaceAPIRefreshTab.js-meta.xml @@ -0,0 +1,8 @@ + + + 60.0 + true + + lightning__AppPage + + diff --git a/force-app/main/default/lwc/workspaceAPISetTabIcon/__tests__/workspaceAPISetTabIcon.test.js b/force-app/main/default/lwc/workspaceAPISetTabIcon/__tests__/workspaceAPISetTabIcon.test.js new file mode 100644 index 000000000..2b271bb96 --- /dev/null +++ b/force-app/main/default/lwc/workspaceAPISetTabIcon/__tests__/workspaceAPISetTabIcon.test.js @@ -0,0 +1,58 @@ +import { createElement } from 'lwc'; +import WorkspaceAPISetTabIcon from 'c/workspaceAPISetTabIcon'; +import { + IsConsoleNavigation, + getFocusedTabInfo, + setTabIcon, + FOCUSED_TAB +} from 'lightning/platformWorkspaceApi'; + +describe('c-workspace-api-set-tab-icon', () => { + afterEach(() => { + // The jsdom instance is shared across test cases in a single file so reset the DOM + while (document.body.firstChild) { + document.body.removeChild(document.body.firstChild); + } + }); + + // Helper function to wait until the microtask queue is empty. This is needed for promise + // timing when calling async functions + async function flushPromises() { + return Promise.resolve(); + } + + it('Calls the related platformWorkspaceApi methods', async () => { + // Create component + const element = createElement('c-workspace-api-set-tab-icon', { + is: WorkspaceAPISetTabIcon + }); + document.body.appendChild(element); + + const TAB_ICON = 'utility:animal_and_nature'; + const TAB_ICON_ALT_TEXT = 'Animal and Nature'; + IsConsoleNavigation.emit(true); + + // Query lightning-button component element + const buttonEl = element.shadowRoot.querySelector('lightning-button'); + buttonEl.click(); + + await flushPromises(); + + // Compare if related platformWorkspaceApi functions have been called + expect(getFocusedTabInfo).toHaveBeenCalled(); + expect(setTabIcon).toHaveBeenCalledWith(FOCUSED_TAB, TAB_ICON, { + iconAlt: TAB_ICON_ALT_TEXT + }); + }); + + it('is accessible', async () => { + // Create component + const element = createElement('c-workspace-api-set-tab-icon', { + is: WorkspaceAPISetTabIcon + }); + document.body.appendChild(element); + + // Check accessibility + await expect(element).toBeAccessible(); + }); +}); diff --git a/force-app/main/default/lwc/workspaceAPISetTabIcon/workspaceAPISetTabIcon.html b/force-app/main/default/lwc/workspaceAPISetTabIcon/workspaceAPISetTabIcon.html new file mode 100644 index 000000000..0b653d35a --- /dev/null +++ b/force-app/main/default/lwc/workspaceAPISetTabIcon/workspaceAPISetTabIcon.html @@ -0,0 +1,14 @@ + diff --git a/force-app/main/default/lwc/workspaceAPISetTabIcon/workspaceAPISetTabIcon.js b/force-app/main/default/lwc/workspaceAPISetTabIcon/workspaceAPISetTabIcon.js new file mode 100644 index 000000000..fedd43ae5 --- /dev/null +++ b/force-app/main/default/lwc/workspaceAPISetTabIcon/workspaceAPISetTabIcon.js @@ -0,0 +1,24 @@ +import { LightningElement, wire } from 'lwc'; +import { + IsConsoleNavigation, + getFocusedTabInfo, + setTabIcon +} from 'lightning/platformWorkspaceApi'; + +const TAB_ICON = 'utility:animal_and_nature'; +const TAB_ICON_ALT_TEXT = 'Animal and Nature'; + +export default class WorkspaceAPISetTabIcon extends LightningElement { + @wire(IsConsoleNavigation) isConsoleNavigation; + + async setTabIcon() { + if (!this.isConsoleNavigation) { + return; + } + + const { tabId } = await getFocusedTabInfo(); + setTabIcon(tabId, TAB_ICON, { + iconAlt: TAB_ICON_ALT_TEXT + }); + } +} diff --git a/force-app/main/default/lwc/workspaceAPISetTabIcon/workspaceAPISetTabIcon.js-meta.xml b/force-app/main/default/lwc/workspaceAPISetTabIcon/workspaceAPISetTabIcon.js-meta.xml new file mode 100644 index 000000000..075c6d873 --- /dev/null +++ b/force-app/main/default/lwc/workspaceAPISetTabIcon/workspaceAPISetTabIcon.js-meta.xml @@ -0,0 +1,8 @@ + + + 60.0 + true + + lightning__AppPage + + diff --git a/force-app/main/default/lwc/workspaceAPISetTabLabel/__tests__/workspaceAPISetTabLabel.test.js b/force-app/main/default/lwc/workspaceAPISetTabLabel/__tests__/workspaceAPISetTabLabel.test.js new file mode 100644 index 000000000..f84d8ddf1 --- /dev/null +++ b/force-app/main/default/lwc/workspaceAPISetTabLabel/__tests__/workspaceAPISetTabLabel.test.js @@ -0,0 +1,55 @@ +import { createElement } from 'lwc'; +import WorkspaceAPISetTabLabel from 'c/workspaceAPISetTabLabel'; +import { + IsConsoleNavigation, + getFocusedTabInfo, + setTabLabel, + FOCUSED_TAB +} from 'lightning/platformWorkspaceApi'; + +describe('c-workspace-api-set-tab-label', () => { + afterEach(() => { + // The jsdom instance is shared across test cases in a single file so reset the DOM + while (document.body.firstChild) { + document.body.removeChild(document.body.firstChild); + } + }); + + // Helper function to wait until the microtask queue is empty. This is needed for promise + // timing when calling async functions + async function flushPromises() { + return Promise.resolve(); + } + + it('Calls the related platformWorkspaceApi methods', async () => { + // Create component + const element = createElement('c-workspace-api-set-tab-label', { + is: WorkspaceAPISetTabLabel + }); + document.body.appendChild(element); + + const TAB_LABEL = 'Awesome Label'; + IsConsoleNavigation.emit(true); + + // Query lightning-button component element + const buttonEl = element.shadowRoot.querySelector('lightning-button'); + buttonEl.click(); + + await flushPromises(); + + // Compare if related platformWorkspaceApi functions have been called + expect(getFocusedTabInfo).toHaveBeenCalled(); + expect(setTabLabel).toHaveBeenCalledWith(FOCUSED_TAB, TAB_LABEL); + }); + + it('is accessible', async () => { + // Create component + const element = createElement('c-workspace-api-set-tab-label', { + is: WorkspaceAPISetTabLabel + }); + document.body.appendChild(element); + + // Check accessibility + await expect(element).toBeAccessible(); + }); +}); diff --git a/force-app/main/default/lwc/workspaceAPISetTabLabel/workspaceAPISetTabLabel.html b/force-app/main/default/lwc/workspaceAPISetTabLabel/workspaceAPISetTabLabel.html new file mode 100644 index 000000000..a731b776e --- /dev/null +++ b/force-app/main/default/lwc/workspaceAPISetTabLabel/workspaceAPISetTabLabel.html @@ -0,0 +1,17 @@ + diff --git a/force-app/main/default/lwc/workspaceAPISetTabLabel/workspaceAPISetTabLabel.js b/force-app/main/default/lwc/workspaceAPISetTabLabel/workspaceAPISetTabLabel.js new file mode 100644 index 000000000..190314168 --- /dev/null +++ b/force-app/main/default/lwc/workspaceAPISetTabLabel/workspaceAPISetTabLabel.js @@ -0,0 +1,20 @@ +import { LightningElement, wire } from 'lwc'; +import { + IsConsoleNavigation, + getFocusedTabInfo, + setTabLabel +} from 'lightning/platformWorkspaceApi'; + +const TAB_LABEL = 'Awesome Label'; + +export default class WorkspaceAPISetTabLabel extends LightningElement { + @wire(IsConsoleNavigation) isConsoleNavigation; + + async setTabLabel() { + if (!this.isConsoleNavigation) { + return; + } + const { tabId } = await getFocusedTabInfo(); + setTabLabel(tabId, TAB_LABEL); + } +} diff --git a/force-app/main/default/lwc/workspaceAPISetTabLabel/workspaceAPISetTabLabel.js-meta.xml b/force-app/main/default/lwc/workspaceAPISetTabLabel/workspaceAPISetTabLabel.js-meta.xml new file mode 100644 index 000000000..075c6d873 --- /dev/null +++ b/force-app/main/default/lwc/workspaceAPISetTabLabel/workspaceAPISetTabLabel.js-meta.xml @@ -0,0 +1,8 @@ + + + 60.0 + true + + lightning__AppPage + + diff --git a/force-app/main/default/permissionsets/recipes.permissionset-meta.xml b/force-app/main/default/permissionsets/recipes.permissionset-meta.xml index 0f19c1cab..1c1b7e408 100644 --- a/force-app/main/default/permissionsets/recipes.permissionset-meta.xml +++ b/force-app/main/default/permissionsets/recipes.permissionset-meta.xml @@ -4,6 +4,10 @@ LWC_Recipes true + + LWC_Recipes_Console_App + true + AccountController true @@ -224,6 +228,14 @@ Wire Visible + + Workspace_API + Visible + + + Workspace_API_Tab_Launcher + Visible + GraphQL Visible diff --git a/force-app/main/default/tabs/Workspace_API.tab-meta.xml b/force-app/main/default/tabs/Workspace_API.tab-meta.xml new file mode 100644 index 000000000..675482603 --- /dev/null +++ b/force-app/main/default/tabs/Workspace_API.tab-meta.xml @@ -0,0 +1,7 @@ + + + Created by Lightning App Builder + Workspace_API + + Custom37: Bridge + diff --git a/force-app/main/default/tabs/Workspace_API_Tab_Launcher.tab-meta.xml b/force-app/main/default/tabs/Workspace_API_Tab_Launcher.tab-meta.xml new file mode 100644 index 000000000..7da4030fa --- /dev/null +++ b/force-app/main/default/tabs/Workspace_API_Tab_Launcher.tab-meta.xml @@ -0,0 +1,7 @@ + + + Created by Lightning App Builder + Workspace_API_Tab_Launcher + + Custom37: Bridge + diff --git a/force-app/test/jest-mocks/lightning/platformWorkspaceApi.js b/force-app/test/jest-mocks/lightning/platformWorkspaceApi.js new file mode 100644 index 000000000..51724967e --- /dev/null +++ b/force-app/test/jest-mocks/lightning/platformWorkspaceApi.js @@ -0,0 +1,22 @@ +import { createTestWireAdapter } from '@salesforce/wire-service-jest-util'; + +// This mock assumes two tabs are open, and the first one is focused +export const FOCUSED_TAB = 'tab0'; +export const ENCLOSING_TAB_ID = 'tab0'; +export const closeTab = jest.fn().mockResolvedValue(true); +export const disableTabClose = jest.fn().mockResolvedValue(true); +export const focusTab = jest.fn().mockResolvedValue(true); +export const getAllTabInfo = jest + .fn() + .mockResolvedValue([{ tabId: 'tab0' }, { tabId: 'tab1' }]); +export const getFocusedTabInfo = jest.fn().mockResolvedValue({ tabId: 'tab0' }); +export const getTabInfo = jest.fn().mockResolvedValue({ tabId: 'tab0' }); +export const openSubtab = jest.fn().mockResolvedValue(true); +export const openTab = jest.fn().mockResolvedValue(true); +export const refreshTab = jest.fn().mockResolvedValue(true); +export const setTabHighlighted = jest.fn().mockResolvedValue(true); +export const setTabIcon = jest.fn().mockResolvedValue(true); +export const setTabLabel = jest.fn().mockResolvedValue(true); + +export const EnclosingTabId = createTestWireAdapter(jest.fn()); +export const IsConsoleNavigation = createTestWireAdapter(jest.fn()); diff --git a/jest.config.js b/jest.config.js index c5c2f7a1d..8cd1d73de 100644 --- a/jest.config.js +++ b/jest.config.js @@ -24,7 +24,9 @@ module.exports = { '^lightning/modal$': '/force-app/test/jest-mocks/lightning/modal', '^lightning/refresh$': - '/force-app/test/jest-mocks/lightning/refresh' + '/force-app/test/jest-mocks/lightning/refresh', + '^lightning/platformWorkspaceApi$': + '/force-app/test/jest-mocks/lightning/platformWorkspaceApi' }, setupFiles: ['jest-canvas-mock'], setupFilesAfterEnv,