diff --git a/how-to/integrate-with-ms365/client/src/ms365-integration.ts b/how-to/integrate-with-ms365/client/src/ms365-integration.ts index 13f1a6b626..b8f0faf687 100644 --- a/how-to/integrate-with-ms365/client/src/ms365-integration.ts +++ b/how-to/integrate-with-ms365/client/src/ms365-integration.ts @@ -167,11 +167,6 @@ export class Microsoft365Integration { */ private _ms365Connection?: Microsoft365Connection; - /** - * The debounce timer id. - */ - private _debounceTimerId?: number; - /** * Any errors during connection. */ @@ -354,24 +349,24 @@ export class Microsoft365Integration { } /** - * Get a list of search results based on the query and filters. + * Get entries to show while the integration is searching. * @param query The query to search for. - * @param filters The filters to apply. * @param lastResponse The last search response used for updating existing results. * @param options Options for the search query. - * @param options.queryMinLength The minimum length of the query before showing results. - * @param options.queryAgainst The field to search against. + * @param options.queryMinLength The minimum length before a query is actioned. + * @param options.queryAgainst The fields in the data to query against. + * @param options.isSuggestion Is the query from a suggestion. * @returns The list of results and new filters. */ - public async getSearchResults( + public async getSearchResultsProgress?( query: string, - filters: CLIFilter[], lastResponse: HomeSearchListenerResponse, - options?: { - queryMinLength?: number; - queryAgainst?: string[]; + options: { + queryMinLength: number; + queryAgainst: string[]; + isSuggestion?: boolean; } - ): Promise { + ): Promise { if (!this._ms365Connection && this._integrationHelpers) { const themeClient = await this._integrationHelpers.getThemeClient(); this._connectLastResponse = lastResponse; @@ -385,16 +380,32 @@ export class Microsoft365Integration { results.push(connectResult); } } - return { - results - }; + return results; } - if (this._debounceTimerId) { - window.clearTimeout(this._debounceTimerId); - this._debounceTimerId = undefined; - } + const minLength = options?.queryMinLength ?? 3; + return query.length >= minLength ? [this.createSearchingResult()] : [] + } + /** + * Get a list of search results based on the query and filters. + * @param query The query to search for. + * @param filters The filters to apply. + * @param lastResponse The last search response used for updating existing results. + * @param options Options for the search query. + * @param options.queryMinLength The minimum length of the query before showing results. + * @param options.queryAgainst The field to search against. + * @returns The list of results and new filters. + */ + public async getSearchResults( + query: string, + filters: CLIFilter[], + lastResponse: HomeSearchListenerResponse, + options?: { + queryMinLength?: number; + queryAgainst?: string[]; + } + ): Promise { const isRecent = query === "/recent"; const defaultFilters: Microsoft365ObjectTypes[] = isRecent ? ["File"] @@ -402,227 +413,225 @@ export class Microsoft365Integration { const minLength = options?.queryMinLength ?? 3; - this._debounceTimerId = window.setTimeout(async () => { - if (this._ms365Connection && this._integrationHelpers) { - const themeClient = await this._integrationHelpers.getThemeClient(); - const palette = await themeClient.getPalette(); - - try { - // If query starts with ms just do a passthrough to the graph API - if ( - !this._settings?.disableGraphExplorer && - query.startsWith(`/${this._settings?.graphExplorerPrefix}/`) - ) { - const path = query.replace(`/${this._settings?.graphExplorerPrefix}/`, ""); - if (path.length > 0) { - const fullPath = `/v1.0/${path}`; - - this._logger?.info("Graph API Request", fullPath); + if (this._ms365Connection && this._integrationHelpers) { + const themeClient = await this._integrationHelpers.getThemeClient(); + const palette = await themeClient.getPalette(); - const response = await this._ms365Connection.executeApiRequest(fullPath); - const jsonResult = await this.createGraphJsonResult( - this._integrationHelpers.templateHelpers, - palette, - response - ); - lastResponse.respond([jsonResult]); - } - } else if (isRecent || (query.length >= minLength && !query.startsWith("/"))) { - const ms365Filter = filters?.find((f) => f.id === Microsoft365Integration._MS365_FILTERS); + try { + // If query starts with ms just do a passthrough to the graph API + if ( + !this._settings?.disableGraphExplorer && + query.startsWith(`/${this._settings?.graphExplorerPrefix}/`) + ) { + const path = query.replace(`/${this._settings?.graphExplorerPrefix}/`, ""); + if (path.length > 0) { + const fullPath = `/v1.0/${path}`; + + this._logger?.info("Graph API Request", fullPath); + + const response = await this._ms365Connection.executeApiRequest(fullPath); + const jsonResult = await this.createGraphJsonResult( + this._integrationHelpers.templateHelpers, + palette, + response + ); + lastResponse.respond([jsonResult]); + } + } else if (isRecent || (query.length >= minLength && !query.startsWith("/"))) { + const ms365Filter = filters?.find((f) => f.id === Microsoft365Integration._MS365_FILTERS); - let includeOptions: Microsoft365ObjectTypes[] = [...defaultFilters]; + let includeOptions: Microsoft365ObjectTypes[] = [...defaultFilters]; - if (ms365Filter?.options && Array.isArray(ms365Filter.options)) { - includeOptions = ms365Filter.options - .filter((o) => o.isSelected) - .map((o) => o.value as Microsoft365ObjectTypes); - } + if (ms365Filter?.options && Array.isArray(ms365Filter.options)) { + includeOptions = ms365Filter.options + .filter((o) => o.isSelected) + .map((o) => o.value as Microsoft365ObjectTypes); + } - const batchRequests: GraphBatchRequest[] = []; - - if (includeOptions.includes("User")) { - const userSearchFields: (keyof User)[] = [ - "displayName", - "givenName", - "surname", - "department", - "jobTitle", - "mobilePhone" - ]; - const userSearchQuery = userSearchFields.map((s) => `"${s}:${query}"`).join(" OR "); - - batchRequests.push({ - id: "User", - method: "GET", - url: `/users?$search=${encodeURIComponent(userSearchQuery)}&$top=10`, - headers: { - ConsistencyLevel: "eventual" - } - }); - } - if (includeOptions.includes("Contact")) { - const contactSearchQuery = `"${query}"`; - batchRequests.push({ - id: "Contact", - method: "GET", - url: `/me/contacts?$search=${encodeURIComponent(contactSearchQuery)}&$top=10` - }); - } - if (includeOptions.includes("Message")) { - const messageSearchQuery = `"${query}"`; - batchRequests.push({ - id: "Message", - method: "GET", - url: `/me/messages?$select=sender,subject,bodyPreview,receivedDateTime,webLink&$search=${encodeURIComponent( - messageSearchQuery - )}&$top=10` - }); - } - if (includeOptions.includes("Event")) { - batchRequests.push({ - id: "Event", - url: "/search/query", - method: "POST", - body: { - requests: [ - { - entityTypes: ["event"], - query: { - queryString: query - }, - from: 0, - size: 10 - } - ] - }, - headers: { - "Content-Type": "application/json" - } - }); - } - if (includeOptions.includes("ChatMessage")) { - batchRequests.push({ - id: "ChatMessage", - url: "/search/query", - method: "POST", - body: { - requests: [ - { - entityTypes: ["chatMessage"], - query: { - queryString: query - }, - from: 0, - size: 10 - } - ] - }, - headers: { - "Content-Type": "application/json" - } - }); - } + const batchRequests: GraphBatchRequest[] = []; + + if (includeOptions.includes("User")) { + const userSearchFields: (keyof User)[] = [ + "displayName", + "givenName", + "surname", + "department", + "jobTitle", + "mobilePhone" + ]; + const userSearchQuery = userSearchFields.map((s) => `"${s}:${query}"`).join(" OR "); + + batchRequests.push({ + id: "User", + method: "GET", + url: `/users?$search=${encodeURIComponent(userSearchQuery)}&$top=10`, + headers: { + ConsistencyLevel: "eventual" + } + }); + } + if (includeOptions.includes("Contact")) { + const contactSearchQuery = `"${query}"`; + batchRequests.push({ + id: "Contact", + method: "GET", + url: `/me/contacts?$search=${encodeURIComponent(contactSearchQuery)}&$top=10` + }); + } + if (includeOptions.includes("Message")) { + const messageSearchQuery = `"${query}"`; + batchRequests.push({ + id: "Message", + method: "GET", + url: `/me/messages?$select=sender,subject,bodyPreview,receivedDateTime,webLink&$search=${encodeURIComponent( + messageSearchQuery + )}&$top=10` + }); + } + if (includeOptions.includes("Event")) { + batchRequests.push({ + id: "Event", + url: "/search/query", + method: "POST", + body: { + requests: [ + { + entityTypes: ["event"], + query: { + queryString: query + }, + from: 0, + size: 10 + } + ] + }, + headers: { + "Content-Type": "application/json" + } + }); + } + if (includeOptions.includes("ChatMessage")) { + batchRequests.push({ + id: "ChatMessage", + url: "/search/query", + method: "POST", + body: { + requests: [ + { + entityTypes: ["chatMessage"], + query: { + queryString: query + }, + from: 0, + size: 10 + } + ] + }, + headers: { + "Content-Type": "application/json" + } + }); + } - if (includeOptions.includes("File")) { - const fileSearchQuery = `'${query}'`; - batchRequests.push({ - id: "File", - url: isRecent - ? "/me/drive/recent" - : `/me/drive/root/search(q=${encodeURIComponent( - fileSearchQuery - )})?$top=10&$orderby=lastModifiedDateTime desc&$expand=thumbnails`, - method: "GET" - }); - } + if (includeOptions.includes("File")) { + const fileSearchQuery = `'${query}'`; + batchRequests.push({ + id: "File", + url: isRecent + ? "/me/drive/recent" + : `/me/drive/root/search(q=${encodeURIComponent( + fileSearchQuery + )})?$top=10&$orderby=lastModifiedDateTime desc&$expand=thumbnails`, + method: "GET" + }); + } - const homeResults: HomeSearchResult[] = await this.sendBatchQuery( - query, - includeOptions, - batchRequests - ); + const homeResults: HomeSearchResult[] = await this.sendBatchQuery( + query, + includeOptions, + batchRequests + ); - if (includeOptions.includes("Team") || includeOptions.includes("Channel")) { - const lowerQuery = query.toLowerCase(); - - for (const teamAndChannels of this._teamsAndChannelsCache) { - if ( - includeOptions.includes("Team") && - (teamAndChannels.team.displayName?.toLowerCase().includes(lowerQuery) ?? - teamAndChannels.team.description?.toLowerCase().includes(lowerQuery)) - ) { - homeResults.push( - await this.createLoadingResult( - this._integrationHelpers.templateHelpers, - palette, - teamAndChannels.team, - "displayName", - "Team" - ) - ); - } - if (includeOptions.includes("Channel")) { - for (const channel of teamAndChannels.channels) { - if ( - channel.displayName?.toLowerCase().includes(lowerQuery) ?? - channel.description?.toLowerCase().includes(lowerQuery) - ) { - homeResults.push( - await this.createLoadingResult( - this._integrationHelpers.templateHelpers, - palette, - { - ...channel, - team: teamAndChannels.team - }, - "displayName", - "Channel" - ) - ); - } + if (includeOptions.includes("Team") || includeOptions.includes("Channel")) { + const lowerQuery = query.toLowerCase(); + + for (const teamAndChannels of this._teamsAndChannelsCache) { + if ( + includeOptions.includes("Team") && + (teamAndChannels.team.displayName?.toLowerCase().includes(lowerQuery) ?? + teamAndChannels.team.description?.toLowerCase().includes(lowerQuery)) + ) { + homeResults.push( + await this.createLoadingResult( + this._integrationHelpers.templateHelpers, + palette, + teamAndChannels.team, + "displayName", + "Team" + ) + ); + } + if (includeOptions.includes("Channel")) { + for (const channel of teamAndChannels.channels) { + if ( + channel.displayName?.toLowerCase().includes(lowerQuery) ?? + channel.description?.toLowerCase().includes(lowerQuery) + ) { + homeResults.push( + await this.createLoadingResult( + this._integrationHelpers.templateHelpers, + palette, + { + ...channel, + team: teamAndChannels.team + }, + "displayName", + "Channel" + ) + ); } } } } + } - lastResponse.respond(homeResults); + lastResponse.respond(homeResults); - const resultTypes: Set = new Set(); - for (const searchResult of homeResults) { - if (searchResult.label) { - resultTypes.add(searchResult.label); - } + const resultTypes: Set = new Set(); + for (const searchResult of homeResults) { + if (searchResult.label) { + resultTypes.add(searchResult.label); } - - const newFilters = resultTypes.entries(); - lastResponse.updateContext({ - filters: [ - { - id: Microsoft365Integration._MS365_FILTERS, - title: "Microsoft 365", - options: [...newFilters].map((f) => ({ - value: f[0], - isSelected: true - })) - } - ] - }); } - } catch (err) { - const message = err instanceof Error ? err.message : err; - lastResponse.respond([ - await this.createGraphJsonResult(this._integrationHelpers.templateHelpers, palette, { - status: 400, - data: message - }) - ]); + + const newFilters = resultTypes.entries(); + lastResponse.updateContext({ + filters: [ + { + id: Microsoft365Integration._MS365_FILTERS, + title: "Microsoft 365", + options: [...newFilters].map((f) => ({ + value: f[0], + isSelected: true + })) + } + ] + }); } + } catch (err) { + const message = err instanceof Error ? err.message : err; + lastResponse.respond([ + await this.createGraphJsonResult(this._integrationHelpers.templateHelpers, palette, { + status: 400, + data: message + }) + ]); } - lastResponse.revoke(`${this._definition?.id}-searching`); - }, 500); + } + lastResponse.revoke(`${this._definition?.id}-searching`); return { - results: query.length >= minLength ? [this.createSearchingResult()] : [] + results: [] }; } @@ -794,8 +803,7 @@ export class Microsoft365Integration { if (this._ms365Connection && actionData.emails) { this._logger?.info("Teams Meeting", this._ms365Connection.currentUser.mail, actionData.emails); await fin.System.openUrlWithBrowser( - `${Microsoft365Integration._TEAMS_PROTOCOL}/l/meeting/new?attendees=${ - this._ms365Connection.currentUser.mail + `${Microsoft365Integration._TEAMS_PROTOCOL}/l/meeting/new?attendees=${this._ms365Connection.currentUser.mail },${actionData.emails.join(",")}` ); return true; @@ -1467,31 +1475,31 @@ export class Microsoft365Integration { imageKey: string; imageAltText: string; }[] = [ - { - titleKey: "callTitle", - action: Microsoft365Integration._ACTION_TEAMS_CALL, - imageKey: "callImage", - imageAltText: "Teams Call" - }, - { - titleKey: "emailTitle", - action: Microsoft365Integration._ACTION_OUTLOOK_EMAIL, - imageKey: "emailImage", - imageAltText: "E-mail" - }, - { - titleKey: "meetingTitle", - action: Microsoft365Integration._ACTION_TEAMS_MEETING, - imageKey: "meetingImage", - imageAltText: "Meeting" - }, - { - titleKey: "chatTitle", - action: Microsoft365Integration._ACTION_TEAMS_CHAT, - imageKey: "chatImage", - imageAltText: "Chat" - } - ]; + { + titleKey: "callTitle", + action: Microsoft365Integration._ACTION_TEAMS_CALL, + imageKey: "callImage", + imageAltText: "Teams Call" + }, + { + titleKey: "emailTitle", + action: Microsoft365Integration._ACTION_OUTLOOK_EMAIL, + imageKey: "emailImage", + imageAltText: "E-mail" + }, + { + titleKey: "meetingTitle", + action: Microsoft365Integration._ACTION_TEAMS_MEETING, + imageKey: "meetingImage", + imageAltText: "Meeting" + }, + { + titleKey: "chatTitle", + action: Microsoft365Integration._ACTION_TEAMS_CHAT, + imageKey: "chatImage", + imageAltText: "Chat" + } + ]; return { key: `${this._definition?.id}-${user.id}`, @@ -1826,13 +1834,13 @@ export class Microsoft365Integration { imageKey: string; imageAltText: string; }[] = [ - { - titleKey: "openTitle", - action: Microsoft365Integration._ACTION_OPEN, - imageKey: "openImage", - imageAltText: "Open" - } - ]; + { + titleKey: "openTitle", + action: Microsoft365Integration._ACTION_OPEN, + imageKey: "openImage", + imageAltText: "Open" + } + ]; return { key: `${this._definition?.id}-${message.id}`, @@ -1958,13 +1966,13 @@ export class Microsoft365Integration { imageKey: string; imageAltText: string; }[] = [ - { - titleKey: "openTitle", - action: Microsoft365Integration._ACTION_OPEN, - imageKey: "openImage", - imageAltText: "Open" - } - ]; + { + titleKey: "openTitle", + action: Microsoft365Integration._ACTION_OPEN, + imageKey: "openImage", + imageAltText: "Open" + } + ]; return { key: `${this._definition?.id}-${event.id}`, @@ -2095,13 +2103,13 @@ export class Microsoft365Integration { imageKey: string; imageAltText: string; }[] = [ - { - titleKey: "openTitle", - action: Microsoft365Integration._ACTION_TEAMS_CHAT, - imageKey: "openImage", - imageAltText: "Open" - } - ]; + { + titleKey: "openTitle", + action: Microsoft365Integration._ACTION_TEAMS_CHAT, + imageKey: "openImage", + imageAltText: "Open" + } + ]; return { key: `${this._definition?.id}-${chatMessage.id}`, @@ -2203,31 +2211,31 @@ export class Microsoft365Integration { imageKey: string; imageAltText: string; }[] = [ - { - titleKey: "openTitle", - action: Microsoft365Integration._ACTION_TEAMS_CALL, - imageKey: "openImage", - imageAltText: "Open" - }, - { - titleKey: "emailTitle", - action: Microsoft365Integration._ACTION_OUTLOOK_EMAIL, - imageKey: "emailImage", - imageAltText: "Email" - }, - { - titleKey: "meetingTitle", - action: Microsoft365Integration._ACTION_TEAMS_MEETING, - imageKey: "meetingImage", - imageAltText: "Meeting" - }, - { - titleKey: "chatTitle", - action: Microsoft365Integration._ACTION_TEAMS_CHAT, - imageKey: "chatImage", - imageAltText: "Chat" - } - ]; + { + titleKey: "openTitle", + action: Microsoft365Integration._ACTION_TEAMS_CALL, + imageKey: "openImage", + imageAltText: "Open" + }, + { + titleKey: "emailTitle", + action: Microsoft365Integration._ACTION_OUTLOOK_EMAIL, + imageKey: "emailImage", + imageAltText: "Email" + }, + { + titleKey: "meetingTitle", + action: Microsoft365Integration._ACTION_TEAMS_MEETING, + imageKey: "meetingImage", + imageAltText: "Meeting" + }, + { + titleKey: "chatTitle", + action: Microsoft365Integration._ACTION_TEAMS_CHAT, + imageKey: "chatImage", + imageAltText: "Chat" + } + ]; return { key: `${this._definition?.id}-${team.id}`, @@ -2332,31 +2340,31 @@ export class Microsoft365Integration { imageKey: string; imageAltText: string; }[] = [ - { - titleKey: "openTitle", - action: Microsoft365Integration._ACTION_TEAMS_CALL, - imageKey: "openImage", - imageAltText: "Open" - }, - { - titleKey: "emailTitle", - action: Microsoft365Integration._ACTION_OUTLOOK_EMAIL, - imageKey: "emailImage", - imageAltText: "Email" - }, - { - titleKey: "meetingTitle", - action: Microsoft365Integration._ACTION_TEAMS_MEETING, - imageKey: "meetingImage", - imageAltText: "Meeting" - }, - { - titleKey: "chatTitle", - action: Microsoft365Integration._ACTION_TEAMS_CHAT, - imageKey: "chatImage", - imageAltText: "Chat" - } - ]; + { + titleKey: "openTitle", + action: Microsoft365Integration._ACTION_TEAMS_CALL, + imageKey: "openImage", + imageAltText: "Open" + }, + { + titleKey: "emailTitle", + action: Microsoft365Integration._ACTION_OUTLOOK_EMAIL, + imageKey: "emailImage", + imageAltText: "Email" + }, + { + titleKey: "meetingTitle", + action: Microsoft365Integration._ACTION_TEAMS_MEETING, + imageKey: "meetingImage", + imageAltText: "Meeting" + }, + { + titleKey: "chatTitle", + action: Microsoft365Integration._ACTION_TEAMS_CHAT, + imageKey: "chatImage", + imageAltText: "Chat" + } + ]; return { key: `${this._definition?.id}-${channel.id}`, @@ -2466,13 +2474,13 @@ export class Microsoft365Integration { imageKey: string; imageAltText: string; }[] = [ - { - titleKey: "openTitle", - action: Microsoft365Integration._ACTION_OPEN, - imageKey: "openImage", - imageAltText: "Open" - } - ]; + { + titleKey: "openTitle", + action: Microsoft365Integration._ACTION_OPEN, + imageKey: "openImage", + imageAltText: "Open" + } + ]; const isFolder = this.driveItemIsFolder(driveItem); const typeName = isFolder ? "Folder" : "File"; diff --git a/how-to/integrate-with-salesforce/client/src/salesforce-integration.ts b/how-to/integrate-with-salesforce/client/src/salesforce-integration.ts index e1735b6b70..78dab9b2af 100644 --- a/how-to/integrate-with-salesforce/client/src/salesforce-integration.ts +++ b/how-to/integrate-with-salesforce/client/src/salesforce-integration.ts @@ -110,11 +110,6 @@ export class SalesforceIntegration { */ private _salesForceConnection: SalesforceConnection | undefined; - /** - * The debounce timer id. - */ - private _debounceTimerId?: number; - /** * Logger for logging info. * @internal @@ -364,6 +359,31 @@ export class SalesforceIntegration { return false; } + /** + * Get entries to show while the integration is searching. + * @param query The query to search for. + * @param lastResponse The last search response used for updating existing results. + * @param options Options for the search query. + * @param options.queryMinLength The minimum length before a query is actioned. + * @param options.queryAgainst The fields in the data to query against. + * @param options.isSuggestion Is the query from a suggestion. + * @returns The list of results and new filters. + */ + public async getSearchResultsProgress?( + query: string, + lastResponse: HomeSearchListenerResponse, + options: { + queryMinLength: number; + queryAgainst: string[]; + isSuggestion?: boolean; + } + ): Promise { + const homeResults = await this.getDefaultEntries(query); + + const minLength = options?.queryMinLength ?? 3; + return homeResults.concat(query.length >= minLength ? [this.createSearchingResult()] : []); + } + /** * Get a list of search results based on the query and filters. * @param query The query to search for. @@ -383,87 +403,76 @@ export class SalesforceIntegration { queryAgainst?: string[]; } ): Promise { - const homeResults = await this.getDefaultEntries(query); - this._lastResponse = lastResponse; const minLength = options?.queryMinLength ?? 3; - if (this._debounceTimerId) { - window.clearTimeout(this._debounceTimerId); - this._debounceTimerId = undefined; - } - - this._debounceTimerId = window.setTimeout(async () => { - if (this._lastResponse) { - if (this._salesForceConnection && query.length >= minLength && !query.startsWith("/")) { - let selectedObjects: string[] = this._mappings.map((m) => m.label ?? ""); - if (Array.isArray(filters) && filters.length > 0) { - const objectsFilter = filters.find((x) => x.id === SalesforceIntegration._OBJECTS_FILTER_ID); - if (objectsFilter) { - selectedObjects = ( - Array.isArray(objectsFilter.options) ? objectsFilter.options : [objectsFilter.options] - ) - .filter((x) => Boolean(x.isSelected)) - .map((x) => x.value); - } - } + if (this._salesForceConnection && query.length >= minLength && !query.startsWith("/")) { + let selectedObjects: string[] = this._mappings.map((m) => m.label ?? ""); + if (Array.isArray(filters) && filters.length > 0) { + const objectsFilter = filters.find((x) => x.id === SalesforceIntegration._OBJECTS_FILTER_ID); + if (objectsFilter) { + selectedObjects = ( + Array.isArray(objectsFilter.options) ? objectsFilter.options : [objectsFilter.options] + ) + .filter((x) => Boolean(x.isSelected)) + .map((x) => x.value); + } + } - try { - const apiSearchResults = await this.getApiSearchResults(query, selectedObjects); + try { + const apiSearchResults = await this.getApiSearchResults(query, selectedObjects); - const maps: { [source: string]: SalesforceMapping } = {}; - for (const mapping of this._mappings) { - maps[mapping.sourceType] = mapping; - } + const maps: { [source: string]: SalesforceMapping } = {}; + for (const mapping of this._mappings) { + maps[mapping.sourceType] = mapping; + } - const searchResults = await Promise.all( - apiSearchResults.results.map(async (r) => - this.buildTemplate( - r, - // we clone this so reference deletions don't affect the original - this.objectClone(maps[r.attributes.type]), - CLITemplate.Loading - ) - ) - ); + const searchResults = await Promise.all( + apiSearchResults.results.map(async (r) => + this.buildTemplate( + r, + // we clone this so reference deletions don't affect the original + this.objectClone(maps[r.attributes.type]), + CLITemplate.Loading + ) + ) + ); - this._lastResponse.respond(searchResults); + this._lastResponse.respond(searchResults); - const resultTypes: Set = new Set(); - for (const searchResult of searchResults) { - if (searchResult.label) { - resultTypes.add(searchResult.label); - } - } + const resultTypes: Set = new Set(); + for (const searchResult of searchResults) { + if (searchResult.label) { + resultTypes.add(searchResult.label); + } + } - const newFilters = resultTypes.entries(); - this._lastResponse.updateContext({ - filters: [ - { - id: SalesforceIntegration._OBJECTS_FILTER_ID as string, - title: "Salesforce", - options: [...newFilters].map((f) => ({ - value: f[0], - isSelected: true - })) - } - ] - }); - } catch (err) { - await this.closeConnection(); - if (err instanceof AuthorizationError) { - this._lastResponse.respond([this.getReconnectSearchResult(query, filters)]); + const newFilters = resultTypes.entries(); + this._lastResponse.updateContext({ + filters: [ + { + id: SalesforceIntegration._OBJECTS_FILTER_ID as string, + title: "Salesforce", + options: [...newFilters].map((f) => ({ + value: f[0], + isSelected: true + })) } - this._logger?.error("Error retrieving Salesforce search results", err); - } + ] + }); + } catch (err) { + await this.closeConnection(); + if (err instanceof AuthorizationError) { + this._lastResponse.respond([this.getReconnectSearchResult(query, filters)]); } - this._lastResponse.revoke(`${this._definition?.id}-searching`); + this._logger?.error("Error retrieving Salesforce search results", err); } - }, 500); + } + this._lastResponse.revoke(`${this._definition?.id}-searching`); return { - results: homeResults.concat(query.length >= minLength ? [this.createSearchingResult()] : []) + results: [] }; } diff --git a/how-to/integrate-with-servicenow/client/src/servicenow-integration.ts b/how-to/integrate-with-servicenow/client/src/servicenow-integration.ts index 6dabcfee89..e7e0d3d329 100644 --- a/how-to/integrate-with-servicenow/client/src/servicenow-integration.ts +++ b/how-to/integrate-with-servicenow/client/src/servicenow-integration.ts @@ -116,11 +116,6 @@ export class ServiceNowIntegration { */ private _serviceNowConnection?: ServiceNowConnection; - /** - * The debounce timer id. - */ - private _debounceTimerId?: number; - /** * Any errors during connection. */ @@ -213,24 +208,24 @@ export class ServiceNowIntegration { } /** - * Get a list of search results based on the query and filters. + * Get entries to show while the integration is searching. * @param query The query to search for. - * @param filters The filters to apply. * @param lastResponse The last search response used for updating existing results. * @param options Options for the search query. - * @param options.queryMinLength The minimum length of the query before showing results. - * @param options.queryAgainst The field to search against. + * @param options.queryMinLength The minimum length before a query is actioned. + * @param options.queryAgainst The fields in the data to query against. + * @param options.isSuggestion Is the query from a suggestion. * @returns The list of results and new filters. */ - public async getSearchResults( + public async getSearchResultsProgress?( query: string, - filters: CLIFilter[], lastResponse: HomeSearchListenerResponse, - options?: { - queryMinLength?: number; - queryAgainst?: string[]; + options: { + queryMinLength: number; + queryAgainst: string[]; + isSuggestion?: boolean; } - ): Promise { + ): Promise { if (!this._serviceNowConnection && this._integrationHelpers) { this._connectLastResponse = lastResponse; const results = []; @@ -245,18 +240,9 @@ export class ServiceNowIntegration { results.push(connectResult); } } - return { - results - }; - } - - if (this._debounceTimerId) { - window.clearTimeout(this._debounceTimerId); - this._debounceTimerId = undefined; + return results; } - const defaultFilters: ServiceNowObjectTypes[] = ["Contact", "Account", "Case", "Task", "Incident"]; - const minLength = options?.queryMinLength ?? 3; const apps: HomeSearchResult[] = []; @@ -267,180 +253,204 @@ export class ServiceNowIntegration { apps.push(await this.createAppResult(this._integrationHelpers?.templateHelpers, palette)); } - this._debounceTimerId = window.setTimeout(async () => { - if (this._serviceNowConnection && this._integrationHelpers) { - try { - if (query.length >= minLength && !query.startsWith("/")) { - const serviceNowFilter = filters?.find((f) => f.id === ServiceNowIntegration._FILTERS); + return apps.concat(query.length >= minLength ? [this.createSearchingResult()] : []) + } + + /** + * Get a list of search results based on the query and filters. + * @param query The query to search for. + * @param filters The filters to apply. + * @param lastResponse The last search response used for updating existing results. + * @param options Options for the search query. + * @param options.queryMinLength The minimum length of the query before showing results. + * @param options.queryAgainst The field to search against. + * @returns The list of results and new filters. + */ + public async getSearchResults( + query: string, + filters: CLIFilter[], + lastResponse: HomeSearchListenerResponse, + options?: { + queryMinLength?: number; + queryAgainst?: string[]; + } + ): Promise { + const defaultFilters: ServiceNowObjectTypes[] = ["Contact", "Account", "Case", "Task", "Incident"]; - let includeOptions: ServiceNowObjectTypes[] = [...defaultFilters]; + const minLength = options?.queryMinLength ?? 3; - if (serviceNowFilter?.options && Array.isArray(serviceNowFilter.options)) { - includeOptions = serviceNowFilter.options - .filter((o) => o.isSelected) - .map((o) => o.value as ServiceNowObjectTypes); - } + if (this._serviceNowConnection && this._integrationHelpers) { + try { + if (query.length >= minLength && !query.startsWith("/")) { + const serviceNowFilter = filters?.find((f) => f.id === ServiceNowIntegration._FILTERS); - const homeResults: HomeSearchResult[] = []; - - const batchRequest: ServiceNowBatchRequest = { - // eslint-disable-next-line camelcase - batch_request_id: "1", - // eslint-disable-next-line camelcase - rest_requests: [] - }; - - if (includeOptions.includes("Contact")) { - batchRequest.rest_requests.push({ - id: "Contact", - method: "GET", - url: `/api/now/v2/table/${ServiceNowIntegration._TABLE_NAMES.Contact}?${this.buildSearchQuery( - query, - ["name"], - ["sys_id", "name"], - 10 - )}` - }); - } + let includeOptions: ServiceNowObjectTypes[] = [...defaultFilters]; - if (includeOptions.includes("Account")) { - batchRequest.rest_requests.push({ - id: "Account", - method: "GET", - url: `/api/now/v2/table/${ServiceNowIntegration._TABLE_NAMES.Account}?${this.buildSearchQuery( - query, - ["name"], - ["sys_id", "name"], - 10 - )}` - }); - } + if (serviceNowFilter?.options && Array.isArray(serviceNowFilter.options)) { + includeOptions = serviceNowFilter.options + .filter((o) => o.isSelected) + .map((o) => o.value as ServiceNowObjectTypes); + } - if (includeOptions.includes("Case")) { - batchRequest.rest_requests.push({ - id: "Case", - method: "GET", - url: `/api/now/v2/table/${ServiceNowIntegration._TABLE_NAMES.Case}?${this.buildSearchQuery( - query, - ["number", "case", "short_description"], - ["sys_id", "number", "short_description"], - 10 - )}` - }); - } + const homeResults: HomeSearchResult[] = []; + + const batchRequest: ServiceNowBatchRequest = { + // eslint-disable-next-line camelcase + batch_request_id: "1", + // eslint-disable-next-line camelcase + rest_requests: [] + }; + + if (includeOptions.includes("Contact")) { + batchRequest.rest_requests.push({ + id: "Contact", + method: "GET", + url: `/api/now/v2/table/${ServiceNowIntegration._TABLE_NAMES.Contact}?${this.buildSearchQuery( + query, + ["name"], + ["sys_id", "name"], + 10 + )}` + }); + } - if (includeOptions.includes("Task")) { - batchRequest.rest_requests.push({ - id: "Task", - method: "GET", - url: `/api/now/v2/table/${ServiceNowIntegration._TABLE_NAMES.Task}?${this.buildSearchQuery( - query, - ["number", "short_description"], - ["sys_id", "number", "short_description"], - 10 - )}` - }); - } + if (includeOptions.includes("Account")) { + batchRequest.rest_requests.push({ + id: "Account", + method: "GET", + url: `/api/now/v2/table/${ServiceNowIntegration._TABLE_NAMES.Account}?${this.buildSearchQuery( + query, + ["name"], + ["sys_id", "name"], + 10 + )}` + }); + } - if (includeOptions.includes("Incident")) { - batchRequest.rest_requests.push({ - id: "Incident", - method: "GET", - url: `/api/now/v2/table/${ - ServiceNowIntegration._TABLE_NAMES.Incident - }?${this.buildSearchQuery( - query, - ["number", "short_description"], - ["sys_id", "number", "short_description"], - 10 - )}` - }); - } + if (includeOptions.includes("Case")) { + batchRequest.rest_requests.push({ + id: "Case", + method: "GET", + url: `/api/now/v2/table/${ServiceNowIntegration._TABLE_NAMES.Case}?${this.buildSearchQuery( + query, + ["number", "case", "short_description"], + ["sys_id", "number", "short_description"], + 10 + )}` + }); + } - const results: { - [id: string]: ServiceNowEntities.Core.BaseEntity[]; - } = {}; - - for (const request of batchRequest.rest_requests) { - const response = await this._serviceNowConnection.executeApiRequest( - request.url, - request.method, - request.body, - false, - request.headers - ); + if (includeOptions.includes("Task")) { + batchRequest.rest_requests.push({ + id: "Task", + method: "GET", + url: `/api/now/v2/table/${ServiceNowIntegration._TABLE_NAMES.Task}?${this.buildSearchQuery( + query, + ["number", "short_description"], + ["sys_id", "number", "short_description"], + 10 + )}` + }); + } - if (response.status === 200 && Array.isArray(response.data)) { - results[request.id] = response.data; - } + if (includeOptions.includes("Incident")) { + batchRequest.rest_requests.push({ + id: "Incident", + method: "GET", + url: `/api/now/v2/table/${ + ServiceNowIntegration._TABLE_NAMES.Incident + }?${this.buildSearchQuery( + query, + ["number", "short_description"], + ["sys_id", "number", "short_description"], + 10 + )}` + }); + } + + const results: { + [id: string]: ServiceNowEntities.Core.BaseEntity[]; + } = {}; + + for (const request of batchRequest.rest_requests) { + const response = await this._serviceNowConnection.executeApiRequest( + request.url, + request.method, + request.body, + false, + request.headers + ); + + if (response.status === 200 && Array.isArray(response.data)) { + results[request.id] = response.data; } + } - if (results.Contact) { - for (const contact of results.Contact as ServiceNowEntities.Core.Contact[]) { - homeResults.push(await this.createLoadingResult(contact, "name", undefined, "Contact")); - } + if (results.Contact) { + for (const contact of results.Contact as ServiceNowEntities.Core.Contact[]) { + homeResults.push(await this.createLoadingResult(contact, "name", undefined, "Contact")); } - if (results.Account) { - for (const account of results.Account as ServiceNowEntities.CSM.Account[]) { - homeResults.push(await this.createLoadingResult(account, "name", undefined, "Account")); - } + } + if (results.Account) { + for (const account of results.Account as ServiceNowEntities.CSM.Account[]) { + homeResults.push(await this.createLoadingResult(account, "name", undefined, "Account")); } - if (results.Case) { - for (const cs of results.Case as ServiceNowEntities.CSM.Case[]) { - homeResults.push(await this.createLoadingResult(cs, "short_description", "number", "Case")); - } + } + if (results.Case) { + for (const cs of results.Case as ServiceNowEntities.CSM.Case[]) { + homeResults.push(await this.createLoadingResult(cs, "short_description", "number", "Case")); } - if (results.Task) { - for (const task of results.Task as ServiceNowEntities.CSM.Task[]) { - homeResults.push(await this.createLoadingResult(task, "short_description", "number", "Task")); - } + } + if (results.Task) { + for (const task of results.Task as ServiceNowEntities.CSM.Task[]) { + homeResults.push(await this.createLoadingResult(task, "short_description", "number", "Task")); } - if (results.Incident) { - for (const incident of results.Incident as ServiceNowEntities.Core.Incident[]) { - homeResults.push( - await this.createLoadingResult(incident, "short_description", "number", "Incident") - ); - } + } + if (results.Incident) { + for (const incident of results.Incident as ServiceNowEntities.Core.Incident[]) { + homeResults.push( + await this.createLoadingResult(incident, "short_description", "number", "Incident") + ); } + } - lastResponse.respond(homeResults); + lastResponse.respond(homeResults); - const resultTypes: Set = new Set(); - for (const searchResult of homeResults) { - if (searchResult.data?.objType) { - resultTypes.add(searchResult.data?.objType); - } + const resultTypes: Set = new Set(); + for (const searchResult of homeResults) { + if (searchResult.data?.objType) { + resultTypes.add(searchResult.data?.objType); } - - const newFilters = resultTypes.entries(); - lastResponse.updateContext({ - filters: [ - { - id: ServiceNowIntegration._FILTERS as string, - title: "Service Now", - options: [...newFilters].map((f) => ({ - value: f[0], - isSelected: true - })) - } - ] - }); - } - } catch (err) { - if (err instanceof AuthTokenExpiredError) { - this._logger?.error("Auth token expired, reconnecting"); - this._serviceNowConnection = undefined; - await this.connectToServiceNow(); - } else { - this._logger?.error(this.formatError(err)); } + + const newFilters = resultTypes.entries(); + lastResponse.updateContext({ + filters: [ + { + id: ServiceNowIntegration._FILTERS as string, + title: "Service Now", + options: [...newFilters].map((f) => ({ + value: f[0], + isSelected: true + })) + } + ] + }); + } + } catch (err) { + if (err instanceof AuthTokenExpiredError) { + this._logger?.error("Auth token expired, reconnecting"); + this._serviceNowConnection = undefined; + await this.connectToServiceNow(); + } else { + this._logger?.error(this.formatError(err)); } } - lastResponse.revoke(`${this._definition?.id}-searching`); - }, 500); + } + lastResponse.revoke(`${this._definition?.id}-searching`); return { - results: apps.concat(query.length >= minLength ? [this.createSearchingResult()] : []) + results: [] }; } diff --git a/how-to/workspace-platform-starter/client/src/framework/integrations.ts b/how-to/workspace-platform-starter/client/src/framework/integrations.ts index 0c029ce0f9..a6d4c92298 100644 --- a/how-to/workspace-platform-starter/client/src/framework/integrations.ts +++ b/how-to/workspace-platform-starter/client/src/framework/integrations.ts @@ -191,6 +191,59 @@ export async function getSearchResults( }; } +/** + * Get the search results from all the integration providers. + * @param query The query to get the search results for. + * @param lastResponse The last search response used for updating existing results. + * @param selectedSources Selected sources filters list. + * @param options Options for the search query. + * @param options.queryMinLength The minimum length before a query is actioned. + * @param options.queryAgainst The fields in the data to query against. + * @param options.isSuggestion Is the query from a suggestion. + * @returns The search results and new filters. + */ +export async function getSearchResultsProgress( + query: string, + lastResponse: HomeSearchListenerResponse, + selectedSources: string[], + options: { + queryMinLength: number; + queryAgainst: string[]; + isSuggestion?: boolean; + } +): Promise { + let homeResponse: HomeSearchResult[] = []; + + if (!isEmpty(integrationProviderOptions)) { + const promises: Promise[] = []; + for (const integrationModule of integrationModules) { + if ( + integrationModule.isInitialized && + integrationModule.implementation.getSearchResultsProgress && + ((integrationModule.definition.excludeFromSourceFilter ?? selectedSources.length === 0) || + selectedSources.includes(integrationModule.definition.title)) + ) { + promises.push( + integrationModule.implementation.getSearchResultsProgress(query, lastResponse, options) + ); + } + } + + const promiseResults = await Promise.allSettled(promises); + for (const promiseResult of promiseResults) { + if (promiseResult.status === "fulfilled") { + if (Array.isArray(promiseResult.value)) { + homeResponse = homeResponse.concat(promiseResult.value); + } + } else { + logger.error(promiseResult.reason); + } + } + } + + return homeResponse; +} + /** * Get the help search entries for all the integration providers. * @returns The list of help entries. diff --git a/how-to/workspace-platform-starter/client/src/framework/shapes/integrations-shapes.ts b/how-to/workspace-platform-starter/client/src/framework/shapes/integrations-shapes.ts index 347e481678..d1f96873e0 100644 --- a/how-to/workspace-platform-starter/client/src/framework/shapes/integrations-shapes.ts +++ b/how-to/workspace-platform-starter/client/src/framework/shapes/integrations-shapes.ts @@ -115,6 +115,26 @@ export interface IntegrationModule extends ModuleImplementation; + /** + * Get entries to show while the integration is searching. + * @param query The query to search for. + * @param lastResponse The last search response used for updating existing results. + * @param options Options for the search query. + * @param options.queryMinLength The minimum length before a query is actioned. + * @param options.queryAgainst The fields in the data to query against. + * @param options.isSuggestion Is the query from a suggestion. + * @returns The list of results and new filters. + */ + getSearchResultsProgress?( + query: string, + lastResponse: HomeSearchListenerResponse, + options: { + queryMinLength: number; + queryAgainst: string[]; + isSuggestion?: boolean; + } + ): Promise; + /** * Get a list of the static help entries. * @returns The list of help entries. diff --git a/how-to/workspace-platform-starter/client/src/framework/workspace/home.ts b/how-to/workspace-platform-starter/client/src/framework/workspace/home.ts index ffc21f32b1..afabe26551 100644 --- a/how-to/workspace-platform-starter/client/src/framework/workspace/home.ts +++ b/how-to/workspace-platform-starter/client/src/framework/workspace/home.ts @@ -9,7 +9,12 @@ import { type HomeSearchListenerResponse, type HomeSearchResponse } from "@openfin/workspace"; -import { getHelpSearchEntries, getSearchResults, itemSelection } from "../integrations"; +import { + getHelpSearchEntries, + getSearchResults, + getSearchResultsProgress, + itemSelection +} from "../integrations"; import { createLogger } from "../logger-provider"; import type { HomeProviderOptions } from "../shapes/home-shapes"; import { isEmpty } from "../utils"; @@ -22,6 +27,7 @@ const logger = createLogger("Home"); let homeProviderOptions: HomeProviderOptions | undefined; let registrationInfo: HomeRegistration | undefined; let lastResponse: HomeSearchListenerResponse; +let debounceTimerId: number | undefined; /** * Register the home component. @@ -118,31 +124,41 @@ async function onUserInput( return searchResults; } + if (debounceTimerId) { + window.clearTimeout(debounceTimerId); + debounceTimerId = undefined; + } + if (!isEmpty(lastResponse)) { lastResponse.close(); } lastResponse = response; lastResponse.open(); - // Perform the search async and return immediately with a dummy filter, so the UI does not "bounce" - window.setTimeout(async () => { - try { - const selectedFilters: CLIFilter[] = request?.context?.selectedFilters ?? []; + const selectedFilters: CLIFilter[] = request?.context?.selectedFilters ?? []; - let selectedSourceFilterOptions: string[] = []; - if (enableSourceFilter && selectedFilters) { - const sourceFilter = selectedFilters.find((f) => f.id === HOME_SOURCE_FILTERS); - if (sourceFilter) { - if (Array.isArray(sourceFilter.options)) { - selectedSourceFilterOptions = sourceFilter.options - .filter((o) => o.isSelected) - .map((o) => o.value); - } else if (sourceFilter.options.isSelected) { - selectedSourceFilterOptions.push(sourceFilter.options.value); - } - } + let selectedSourceFilterOptions: string[] = []; + if (enableSourceFilter && selectedFilters) { + const sourceFilter = selectedFilters.find((f) => f.id === HOME_SOURCE_FILTERS); + if (sourceFilter) { + if (Array.isArray(sourceFilter.options)) { + selectedSourceFilterOptions = sourceFilter.options.filter((o) => o.isSelected).map((o) => o.value); + } else if (sourceFilter.options.isSelected) { + selectedSourceFilterOptions.push(sourceFilter.options.value); } + } + } + const queryOptions = { + queryMinLength: homeProviderOptions?.queryMinLength ?? 3, + queryAgainst: homeProviderOptions?.queryAgainst ?? ["title"], + isSuggestion: request.context?.isSuggestion ?? false + }; + + // Debounce the keyboard input, this also means that the method returns + // immediately with a dummy filter, so the UI does not "bounce" + debounceTimerId = window.setTimeout(async () => { + try { logger.info("Search results requested."); const finalFilters: CLIFilter[] = []; let sourceFilter: CLIFilter | undefined; @@ -187,11 +203,7 @@ async function onUserInput( } }, selectedSourceFilterOptions, - { - queryMinLength: homeProviderOptions?.queryMinLength ?? 3, - queryAgainst: homeProviderOptions?.queryAgainst ?? ["title"], - isSuggestion: request.context?.isSuggestion ?? false - } + queryOptions ); if (!Array.isArray(searchResults?.results)) { @@ -237,7 +249,14 @@ async function onUserInput( } catch (err) { logger.error("Exception while getting search list results", err); } - }, 0); + }, 200); + + const integrationProgressResults = await getSearchResultsProgress( + request.query, + lastResponse, + selectedSourceFilterOptions, + queryOptions + ); return { results: [ @@ -248,7 +267,8 @@ async function onUserInput( actions: [], template: CLITemplate.Loading, templateContent: "" - } + }, + ...integrationProgressResults ], context: { filters: enableSourceFilter ? [createEmptySourceFilter()] : []