From 7c0ecc185dae3990df315967df0f1da89d9c8b74 Mon Sep 17 00:00:00 2001 From: Muhammadhu Abubakkar Date: Thu, 20 Jan 2022 17:37:12 +0000 Subject: [PATCH 01/41] Merged PR 1238: Sharepoint content caching service logic Before submitting this PR, please make ensure you've completed the following: - [ ] Your code builds clean without any errors **or warnings**. - [ ] All unused code has been deleted. - [ ] All TODOs in the code base have an associated ticket in ADO. - [ ] Your squash commit to main has a short descriptive commit message. Describe your PR: Added Sharepoint content caching service logic. Currently contains only the logic to cache place photo urls as entries of ExchangePlacePhoto List against unique key generated for Place's Sharepoint Id. If submitting to the FrontEnd UI, include a screenshot of the change: Related work items: #3462 --- .../Models/Enums/SharePointContentType.cs | 10 ++ Converge/Services/BuildingsMonoService.cs | 5 +- .../Services/CacheSharePointContentService.cs | 103 ++++++++++++++++++ Converge/Services/PlacesService.cs | 15 ++- Converge/Startup.cs | 1 + 5 files changed, 130 insertions(+), 4 deletions(-) create mode 100644 Converge/Models/Enums/SharePointContentType.cs create mode 100644 Converge/Services/CacheSharePointContentService.cs diff --git a/Converge/Models/Enums/SharePointContentType.cs b/Converge/Models/Enums/SharePointContentType.cs new file mode 100644 index 0000000..52843fa --- /dev/null +++ b/Converge/Models/Enums/SharePointContentType.cs @@ -0,0 +1,10 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace Converge.Models.Enums +{ + public enum SharePointContentType + { + ExchangePlacePhotoUrls, + } +} diff --git a/Converge/Services/BuildingsMonoService.cs b/Converge/Services/BuildingsMonoService.cs index 732b75e..f7b77ed 100644 --- a/Converge/Services/BuildingsMonoService.cs +++ b/Converge/Services/BuildingsMonoService.cs @@ -26,12 +26,13 @@ namespace Converge.Services public BuildingsMonoService(IConfiguration configuration, ILogger placesSvcLogger, AppGraphService appGraphSvc, - CachePlacesProviderService cacheProviderService) + CachePlacesProviderService cacheProviderService, + CacheSharePointContentService cacheSharePointContentService) { appGraphService = appGraphSvc; //Had to manually instantiate for addressing Singleton-classes-instantiation-issues during Dependency-injection. - placesService = new PlacesService(placesSvcLogger, configuration, appGraphService, new ScheduleService(appGraphService)); + placesService = new PlacesService(placesSvcLogger, configuration, appGraphService, new ScheduleService(appGraphService), cacheSharePointContentService); buildingsService = new BuildingsService(appGraphService, cacheProviderService, placesService, null); } diff --git a/Converge/Services/CacheSharePointContentService.cs b/Converge/Services/CacheSharePointContentService.cs new file mode 100644 index 0000000..31c440d --- /dev/null +++ b/Converge/Services/CacheSharePointContentService.cs @@ -0,0 +1,103 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Converge.Helpers; +using Converge.Models; +using Converge.Models.Enums; +using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.Logging; +using Microsoft.Graph; +using System; +using System.Collections.Generic; +using System.Text; + +namespace Converge.Services +{ + public class CacheSharePointContentService + { + private readonly IMemoryCache sharePointContentCache; + private readonly ILogger logger; + + private const string keyPlacePhotoUrls = "keyPlacePhotoUrls"; + private const int placePhotoUrlSize = 1; + + public CacheSharePointContentService(ILogger logger, + IMemoryCache sharePointContentCache) + { + this.logger = logger; + this.sharePointContentCache = sharePointContentCache; + } + + public static MemoryCacheEntryOptions CacheEntryOptionsDefaults() + { + // Structurally, similar idea based on CachePlacesProviderService + // CacheSharePointContentService will be existing regardless of appsettings configuration. + + return new MemoryCacheEntryOptions() + .SetPriority(CacheItemPriority.Normal) + .SetSlidingExpiration(TimeSpan.FromHours(24)); + } + + private string GetContentKeyAndString(SharePointContentType? contentType, out string contentTypeString) + { + contentTypeString = ""; + if (contentType.HasValue) + { + switch (contentType) + { + case SharePointContentType.ExchangePlacePhotoUrls: + contentTypeString = Enum.GetName(typeof(SharePointContentType), contentType.Value); + return keyPlacePhotoUrls; + } + } + return ""; + } + + private string DetermineCacheKeyForSharePointContent(SharePointContentType? sharePointContentType, string sharePointContentId) + { + string sharePointContentTypeString; + StringBuilder cacheKey = new StringBuilder(GetContentKeyAndString(sharePointContentType, out sharePointContentTypeString)); + cacheKey.Append($"&sharePointContentId={sharePointContentId}"); + cacheKey.Append($"&sharePointContentType={sharePointContentTypeString}"); + return cacheKey.ToString(); + } + + public List GetExchangePlacePhotoUrlsFromCache(string placeSharePointId) + { + try + { + List exchangePlacePhotos; + string itemCacheKey = DetermineCacheKeyForSharePointContent(SharePointContentType.ExchangePlacePhotoUrls, placeSharePointId); + if (!sharePointContentCache.TryGetValue(itemCacheKey, out exchangePlacePhotos)) + { + logger.LogInformation($"placeSharePointId: {placeSharePointId}, ExchangePlace photo urls not found in cache."); + return null; + } + logger.LogInformation($"placeSharePointId: {placeSharePointId}, ExchangePlace photo urls found in cache."); + return exchangePlacePhotos; + } + catch (Exception ex) + { + logger.LogError($"placeSharePointId: {placeSharePointId}, Error while retrieving ExchangePlace photo urls.", ex, ex.Message); + return null; + } + } + + public void AddExchangePlacePhotoUrlsToCache(string placeSharePointId, List exchangePlacePhotos) + { + try + { + logger.LogInformation($"placeSharePointId: {placeSharePointId}, creating unique cache entry key."); + string itemCacheKey = DetermineCacheKeyForSharePointContent(SharePointContentType.ExchangePlacePhotoUrls, placeSharePointId); + sharePointContentCache.Set(itemCacheKey, + exchangePlacePhotos, + CacheEntryOptionsDefaults().SetSize(placePhotoUrlSize)); + logger.LogInformation($"placeSharePointId: {placeSharePointId}, ExchangePlace photo urls successfully added to cache."); + } + catch (Exception ex) + { + logger.LogError($"placeSharePointId: {placeSharePointId}, Error while adding ExchangePlace photo urls.", ex, ex.Message); + } + } + } +} diff --git a/Converge/Services/PlacesService.cs b/Converge/Services/PlacesService.cs index e17f999..33cdc05 100644 --- a/Converge/Services/PlacesService.cs +++ b/Converge/Services/PlacesService.cs @@ -22,16 +22,19 @@ namespace Converge.Services private readonly IConfiguration configuration; private readonly AppGraphService appGraphService; private readonly ScheduleService scheduleService; + private readonly CacheSharePointContentService cacheSharePointContentService; public PlacesService(ILogger logger, IConfiguration configuration, AppGraphService appGraphService, - ScheduleService scheduleService) + ScheduleService scheduleService, + CacheSharePointContentService cacheSharePointContentService) { this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); this.configuration = configuration; this.appGraphService = appGraphService; this.scheduleService = scheduleService; + this.cacheSharePointContentService = cacheSharePointContentService; } public async Task GetMaxReserved(string upn, string start, string end) @@ -151,10 +154,16 @@ namespace Converge.Services public async Task> GetPlacePhotos(string placeSharePointID) { + List result = cacheSharePointContentService.GetExchangePlacePhotoUrlsFromCache(placeSharePointID); + if (result != null) + { + return result; + } string sharePointSiteId = configuration["SharePointSiteId"]; string sharePointPhotoListId = configuration["SharePointPhotoListId"]; List photosList = await appGraphService.GetList(sharePointSiteId, sharePointPhotoListId); - List result = new List(); + + result = new List(); if (photosList != null) { List photoItems = await appGraphService.GetPhotoItems(sharePointSiteId, photosList.Id, placeSharePointID); @@ -182,6 +191,8 @@ namespace Converge.Services } } } + + cacheSharePointContentService.AddExchangePlacePhotoUrlsToCache(placeSharePointID, result); return result; } } diff --git a/Converge/Startup.cs b/Converge/Startup.cs index 189f0a8..1a6106f 100644 --- a/Converge/Startup.cs +++ b/Converge/Startup.cs @@ -239,6 +239,7 @@ namespace Converge services.AddTransient(); services.AddTransient(); services.AddSingleton(); + services.AddSingleton(); } private void RegisterValidators(IServiceCollection services) From 503e7acd5d8fb6f7821dd9d3fccb8eb3035b3b41 Mon Sep 17 00:00:00 2001 From: Muhammadhu Abubakkar Date: Thu, 20 Jan 2022 21:31:06 +0000 Subject: [PATCH 02/41] Merged PR 1240: Validation fix for Venues to collaborate request. Before submitting this PR, please make ensure you've completed the following: - [ ] Your code builds clean without any errors **or warnings**. - [ ] All unused code has been deleted. - [ ] All TODOs in the code base have an associated ticket in ADO. - [ ] Your squash commit to main has a short descriptive commit message. Describe your PR: Changed validation against system time to utc time. If submitting to the FrontEnd UI, include a screenshot of the change: Validation fix for Venues to collaborate request. Related work items: #3841 --- Converge/Models/VenuesToCollaborateRequest.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Converge/Models/VenuesToCollaborateRequest.cs b/Converge/Models/VenuesToCollaborateRequest.cs index fff108a..47ed27f 100644 --- a/Converge/Models/VenuesToCollaborateRequest.cs +++ b/Converge/Models/VenuesToCollaborateRequest.cs @@ -45,7 +45,7 @@ namespace Converge.Models var errorBuilder = new StringBuilder(); errorBuilder = errorBuilder.Clear().Append("Start-time should be later than now."); - RuleFor(req => req.StartTime).GreaterThan(DateTime.Now).WithMessage(errorBuilder.ToString()); + RuleFor(req => req.StartTime).GreaterThan(DateTime.UtcNow.Date).WithMessage(errorBuilder.ToString()); errorBuilder = errorBuilder.Clear().Append("Start-time should be earlier than End-time."); RuleFor(req => req.StartTime).LessThan(x => x.EndTime).WithMessage(errorBuilder.ToString()); From 11db5c35652a5662b1c01f404bbed23e78159a35 Mon Sep 17 00:00:00 2001 From: Megan Slater Date: Thu, 20 Jan 2022 21:49:38 +0000 Subject: [PATCH 03/41] Consolidate duplicate API endpoints --- Converge/ClientApp/src/api/userService.ts | 13 +---- .../src/providers/TeammateFilterProvider.tsx | 52 ++++++++++++------- .../collaborate/components/NewEventModal.tsx | 6 +-- .../components/UserSearchDropdown.tsx | 8 +-- Converge/Controllers/UsersController.cs | 30 ++--------- 5 files changed, 45 insertions(+), 64 deletions(-) diff --git a/Converge/ClientApp/src/api/userService.ts b/Converge/ClientApp/src/api/userService.ts index 03336ab..a55a050 100644 --- a/Converge/ClientApp/src/api/userService.ts +++ b/Converge/ClientApp/src/api/userService.ts @@ -113,17 +113,6 @@ export const getMultiUserAvailabilityTimes = async ( }; export const searchUsers = async ( - searchString?: string, -): Promise => { - const axios = await getAxiosClient(); - if (!searchString) { - return []; - } - const request = await axios.get>(`/api/users/search/${searchString}`); - return request.data.result; -}; - -export const searchUsersByPage = async ( searchQuery?: string, options?: QueryOption[], ): Promise => { @@ -131,7 +120,7 @@ export const searchUsersByPage = async ( if (!searchQuery) { return { users: [], queryOptions: [] }; } - const request = await axios.get>("/api/users/searchAndPage", { + const request = await axios.get>("/api/users/search", { params: { searchString: searchQuery, QueryOptions: JSON.stringify(options), diff --git a/Converge/ClientApp/src/providers/TeammateFilterProvider.tsx b/Converge/ClientApp/src/providers/TeammateFilterProvider.tsx index b37c986..02c4a18 100644 --- a/Converge/ClientApp/src/providers/TeammateFilterProvider.tsx +++ b/Converge/ClientApp/src/providers/TeammateFilterProvider.tsx @@ -6,7 +6,7 @@ import * as MicrosoftGraph from "@microsoft/microsoft-graph-types"; import { getMyList, getPeople, getWorkgroup, } from "../api/meService"; -import { searchUsers, searchUsersByPage } from "../api/userService"; +import { searchUsers } from "../api/userService"; import TimeLimit from "../types/TimeLimit"; import { logEvent } from "../utilities/LogWrapper"; import { @@ -288,19 +288,27 @@ const TeammateFilterProvider: React.FC = ({ children }) => { case TeammateList.MyOrganization: requestMethod = getWorkgroup; break; - case TeammateList.All: - requestMethod = searchUsers; - break; default: throw new Error("Invalid list type requested."); } - requestMethod(searchString).then(async (teammates) => { - const payload = await Promise.all(teammates.map(async (teammate) => ({ - user: teammate, - }))); - dispatch({ type: TEAMMATES_RESPONSE, payload }); - }) - .catch(() => dispatch({ type: TEAMMATES_ERROR })); + if (requestMethod) { + requestMethod().then((teammates) => { + const payload = teammates.map((teammate) => ({ + user: teammate, + })); + dispatch({ type: TEAMMATES_RESPONSE, payload }); + }) + .catch(() => dispatch({ type: TEAMMATES_ERROR })); + } else { + searchUsers(searchString) + .then((response) => { + const payload = response.users.map((teammate) => ({ + user: teammate, + })); + dispatch({ type: TEAMMATES_RESPONSE, payload }); + }) + .catch(() => dispatch({ type: TEAMMATES_ERROR })); + } }; const setMoreTeammatesLoading = (buttonLoading: boolean) => { @@ -313,15 +321,19 @@ const TeammateFilterProvider: React.FC = ({ children }) => { teammatesPreset?: Teammate[], ) => { setMoreTeammatesLoading(true); - searchUsersByPage(searchString, qOptions).then(async (data) => { - const payload = await Promise.all(data.users.map(async (teammate) => ({ - user: teammate, - }))); - if (!teammatesPreset) dispatch({ type: TEAMMATES_RESPONSE, payload }); - else dispatch({ type: TEAMMATES_RESPONSE, payload: teammatesPreset.concat(payload) }); - dispatch({ type: UPDATE_SEARCH_QUERY_OPTIONS, payload: data.queryOptions }); - setMoreTeammatesLoading(false); - }) + searchUsers(searchString, qOptions) + .then((data) => { + const payload = data.users.map((teammate) => ({ + user: teammate, + })); + if (!teammatesPreset) { + dispatch({ type: TEAMMATES_RESPONSE, payload }); + } else { + dispatch({ type: TEAMMATES_RESPONSE, payload: teammatesPreset.concat(payload) }); + } + dispatch({ type: UPDATE_SEARCH_QUERY_OPTIONS, payload: data.queryOptions }); + setMoreTeammatesLoading(false); + }) .catch(() => { dispatch({ type: TEAMMATES_ERROR }); setMoreTeammatesLoading(false); diff --git a/Converge/ClientApp/src/tabs/collaborate/components/NewEventModal.tsx b/Converge/ClientApp/src/tabs/collaborate/components/NewEventModal.tsx index 4e7305e..007ae9d 100644 --- a/Converge/ClientApp/src/tabs/collaborate/components/NewEventModal.tsx +++ b/Converge/ClientApp/src/tabs/collaborate/components/NewEventModal.tsx @@ -170,13 +170,13 @@ const NewEventModal: React.FC = (props) => { if (data?.searchQuery) { setAttendeesLoading(true); searchUsers(data.searchQuery.toString()) - .then((users) => { + .then((response) => { setAttendeeItems( - users + response.users .filter((u) => !!u.displayName) .map((u) => u.displayName as string), ); - setFullUserData(fullUserData.concat(users)); + setFullUserData(fullUserData.concat(response.users)); }) .finally(() => setAttendeesLoading(false)); } else { diff --git a/Converge/ClientApp/src/tabs/collaborate/components/UserSearchDropdown.tsx b/Converge/ClientApp/src/tabs/collaborate/components/UserSearchDropdown.tsx index c8f1271..0f7bf07 100644 --- a/Converge/ClientApp/src/tabs/collaborate/components/UserSearchDropdown.tsx +++ b/Converge/ClientApp/src/tabs/collaborate/components/UserSearchDropdown.tsx @@ -85,15 +85,17 @@ const UserSearchDropdown:React.FC = (props) => { if (data?.searchQuery) { setLoading(true); searchUsers(data.searchQuery.toString()) - .then((users) => { - setInputItems(users.filter((u) => !!u.displayName).map((u) => u.displayName as string)); + .then((response) => { + setInputItems(response.users + .filter((u) => !!u.displayName) + .map((u) => u.displayName as string)); setDropdownUsers(dropdownUsers.filter((u) => { if (u.displayName) { return selectedUsers.includes(u.displayName) || defaultDropdownUsers?.find((ddu) => ddu.displayName === u.displayName); } return false; - }).concat(users.filter((u) => !!u.displayName))); + }).concat(response.users.filter((u) => !!u.displayName))); }).catch(() => setIsError(true)) .finally(() => setLoading(false)); } else { diff --git a/Converge/Controllers/UsersController.cs b/Converge/Controllers/UsersController.cs index 5d97e4c..aae5fbd 100644 --- a/Converge/Controllers/UsersController.cs +++ b/Converge/Controllers/UsersController.cs @@ -176,34 +176,12 @@ namespace Converge.Controllers /// /// Search Users that match the provided search string /// - /// + /// The string to user for user search + /// query optionsList of users whose DisplayName/UserPrincipalName starts with input string> [HttpGet] - [Route("search/{searchString}")] - public async Task>> SearchUsers(string searchString) - { - try - { - var userSearchResponse = await userGraphService.SearchUsers(searchString, null, User); - return userSearchResponse.Users; - } - catch (Exception ex) - { - logger.LogError(ex, $"Error while searching Users for '{searchString}'."); - throw; - } - } - - /// - /// Search Users that match the provided search string and - /// filter results based on the provided query options - /// - /// search string - /// query options - /// List of users whose DisplayName/UserPrincipalName starts with input string> - [HttpGet] - [Route("searchAndPage")] - public async Task> SearchUsersByPage(string searchString, string queryOptions) + [Route("search")] + public async Task> SearchUsers(string searchString, string queryOptions) { try { From 7087ea64392cfb1ab128265962813035c354b125 Mon Sep 17 00:00:00 2001 From: "Tej.Neelapala" Date: Fri, 21 Jan 2022 00:02:57 +0000 Subject: [PATCH 04/41] Merged PR 1242: Fix to bug # 3798 :: Uninstall-user functionality for background-job deletes all the users for -Dev Env. Before submitting this PR, please make ensure you've completed the following: - [ ] Your code builds clean without any errors **or warnings**. - [ ] All unused code has been deleted. - [ ] All TODOs in the code base have an associated ticket in ADO. - [ ] Your squash commit to main has a short descriptive commit message. Describe your PR: If submitting to the FrontEnd UI, include a screenshot of the change: Related work items: #3798 --- Converge/ClientApp/.env.example | 4 ---- Converge/ClientApp/buildManifest.js | 6 ++++-- Converge/Jobs/LocationPredictorJob.cs | 7 ++++++- Converge/Services/AppGraphService.cs | 2 +- Converge/Services/CachePlacesProviderService.cs | 1 - Converge/Services/UserGraphService.cs | 1 - Converge/appsettings.Development.json | 5 +---- Converge/appsettings.Staging.json | 5 +---- Converge/appsettings.Test.json | 2 +- Converge/appsettings.json | 5 ++++- 10 files changed, 18 insertions(+), 20 deletions(-) diff --git a/Converge/ClientApp/.env.example b/Converge/ClientApp/.env.example index 3e361c9..f754280 100644 --- a/Converge/ClientApp/.env.example +++ b/Converge/ClientApp/.env.example @@ -8,7 +8,3 @@ DOMAIN = localhost # Port used in API URL PORT = :3000 - -# Use this variable to create sub-versions of the -# add-on so multiple versions can be used in teams -NAME_SUFFIX = _local \ No newline at end of file diff --git a/Converge/ClientApp/buildManifest.js b/Converge/ClientApp/buildManifest.js index 3cc6ef6..becccc3 100644 --- a/Converge/ClientApp/buildManifest.js +++ b/Converge/ClientApp/buildManifest.js @@ -29,7 +29,7 @@ const cleanManifest = async () => { async function setDevEnvironment() { dotenv.config(); - process.env.NAME_SUFFIX = process.env.NAME_SUFFIX ?? "_local"; + process.env.NAME_SUFFIX = "_dev"; if (!process.env.DOMAIN) { throw new Error("Missing required environment variable: DOMAIN!"); @@ -55,6 +55,8 @@ async function setProdEnvironment() { } async function setStagingEnvironment() { + process.env.NAME_SUFFIX = "_staging"; + if (!process.env.WEBSITE) { throw new Error("Missing required environment variable: WEBSITE!"); } @@ -67,7 +69,7 @@ async function setStagingEnvironment() { } async function setTestingEnvironment() { - process.env.NAME_SUFFIX = process.env.NAME_SUFFIX ?? "_test"; + process.env.NAME_SUFFIX = "_test"; if (!process.env.DOMAIN) { throw new Error("Missing required environment variable: DOMAIN!"); diff --git a/Converge/Jobs/LocationPredictorJob.cs b/Converge/Jobs/LocationPredictorJob.cs index 691e5d3..964fad1 100644 --- a/Converge/Jobs/LocationPredictorJob.cs +++ b/Converge/Jobs/LocationPredictorJob.cs @@ -3,7 +3,6 @@ using Converge.Models; using Converge.Services; -using Microsoft.Extensions.Configuration; using Microsoft.Graph; using System; using System.Collections.Generic; @@ -52,6 +51,12 @@ namespace Converge.Jobs telemetryService.TrackException(ex, "Failed to get converge users."); return; } + if (users == null || users.Count == 0) + { + timePerRun.Stop(); + telemetryService.TrackEvent("Users not found", "Location Predictor Job found no Users.", timePerRun.ElapsedMilliseconds); + return; + } CheckAndUninstallUsers(users); users.RemoveAll(u => u.Extensions == null); diff --git a/Converge/Services/AppGraphService.cs b/Converge/Services/AppGraphService.cs index ca0de9c..567c026 100644 --- a/Converge/Services/AppGraphService.cs +++ b/Converge/Services/AppGraphService.cs @@ -304,7 +304,7 @@ namespace Converge.Services var installedApps = request.CurrentPage as List; foreach (UserScopeTeamsAppInstallation app in installedApps) { - if (app.TeamsAppDefinition.DisplayName == ConvergeDisplayName) + if (app.TeamsAppDefinition.DisplayName.SameAs(ConvergeDisplayName)) { return true; } diff --git a/Converge/Services/CachePlacesProviderService.cs b/Converge/Services/CachePlacesProviderService.cs index bb200f4..c73c56b 100644 --- a/Converge/Services/CachePlacesProviderService.cs +++ b/Converge/Services/CachePlacesProviderService.cs @@ -11,7 +11,6 @@ using System; using System.Collections.Generic; using System.Linq; using System.Text; -using System.Threading.Tasks; namespace Converge.Services { diff --git a/Converge/Services/UserGraphService.cs b/Converge/Services/UserGraphService.cs index 0ee9975..a810540 100644 --- a/Converge/Services/UserGraphService.cs +++ b/Converge/Services/UserGraphService.cs @@ -6,7 +6,6 @@ using Converge.Models; using Converge.Models.Enums; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.Logging; using Microsoft.Graph; using Microsoft.Identity.Web; using Newtonsoft.Json; diff --git a/Converge/appsettings.Development.json b/Converge/appsettings.Development.json index 80fbd1a..7cc1dd9 100644 --- a/Converge/appsettings.Development.json +++ b/Converge/appsettings.Development.json @@ -9,8 +9,5 @@ "Microsoft.Hosting.Lifetime": "Information" } }, - "AppEnvironment": "-Dev", - "CachingEnabled": { - "BuildingsService": true - } + "AppEnvironment": "_dev" } \ No newline at end of file diff --git a/Converge/appsettings.Staging.json b/Converge/appsettings.Staging.json index 4a5701a..a848a1c 100644 --- a/Converge/appsettings.Staging.json +++ b/Converge/appsettings.Staging.json @@ -10,8 +10,5 @@ } }, "AllowedHosts": "*", - "AppEnvironment": "-Staging", - "CachingEnabled": { - "BuildingsService": true - } + "AppEnvironment": "_staging" } diff --git a/Converge/appsettings.Test.json b/Converge/appsettings.Test.json index 90fbbea..835e418 100644 --- a/Converge/appsettings.Test.json +++ b/Converge/appsettings.Test.json @@ -17,5 +17,5 @@ } }, "MaxPredictionWindow": 14, - "AppEnvironment": "-Test" + "AppEnvironment": "_test" } diff --git a/Converge/appsettings.json b/Converge/appsettings.json index 9ea44e2..018e990 100644 --- a/Converge/appsettings.json +++ b/Converge/appsettings.json @@ -39,5 +39,8 @@ "FilterUsersByTitle": null, "AppEnvironment": "", "AppBannerMessage": null, - "NewPlacesAvailableByDefault": true + "NewPlacesAvailableByDefault": true, + "CachingEnabled": { + "BuildingsService": true + } } From 55f9fab773446cf67fc19e9fe91dd4c9cad954ba Mon Sep 17 00:00:00 2001 From: Muhammadhu Abubakkar Date: Fri, 21 Jan 2022 16:48:17 +0000 Subject: [PATCH 05/41] Merged PR 1243: 3825 - conf room availability start and end times requested are wrong - Fixed Before submitting this PR, please make ensure you've completed the following: - [ ] Your code builds clean without any errors **or warnings**. - [ ] All unused code has been deleted. - [ ] All TODOs in the code base have an associated ticket in ADO. - [ ] Your squash commit to main has a short descriptive commit message. Describe your PR: If submitting to the FrontEnd UI, include a screenshot of the change: ![image.png](https://dev.azure.com/mwcci/08fbf972-cba0-4622-a98f-aff4c4b4c567/_apis/git/repositories/5d547110-08b1-402c-91d8-153ba56a7d02/pullRequests/1243/attachments/image.png) 3825 - conf room availability start and end times requested are wrong - Fixed Related work items: #3825 --- .../workspace/components/CampusPlaceEventTitle.tsx | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/Converge/ClientApp/src/tabs/workspace/components/CampusPlaceEventTitle.tsx b/Converge/ClientApp/src/tabs/workspace/components/CampusPlaceEventTitle.tsx index a2f356a..6ea131c 100644 --- a/Converge/ClientApp/src/tabs/workspace/components/CampusPlaceEventTitle.tsx +++ b/Converge/ClientApp/src/tabs/workspace/components/CampusPlaceEventTitle.tsx @@ -29,8 +29,14 @@ const CampusPlaceEventTitle:React.FC = (props) => { const [isAvailable, setIsAvailable] = useState(true); useEffect(() => { - let startDay = dayjs(`${dayjs(date).format("MM-DD-YYYY")} ${start}`, "MM-DD-YYYY h:mm A"); - let endDay = dayjs(`${dayjs(date).format("MM-DD-YYYY")} ${end}`, "MM-DD-YYYY h:mm A"); + let startDay = dayjs(dayjs(date) + .hour(start?.hour() ?? 0) + .minute(start?.minute() ?? 0) + .format("MM-DD-YYYY h:mm A")); + let endDay = dayjs(dayjs(date) + .hour(end?.hour() ?? 0) + .minute(end?.minute() ?? 0) + .format("MM-DD-YYYY h:mm A")); if (isAllDay) { startDay = dayjs(dayjs(date).format("MM-DD-YYYY")); endDay = dayjs(startDay).add(1, "day"); From b7887027c5cff371023f0384501eaaec9bb56b28 Mon Sep 17 00:00:00 2001 From: Megan Slater Date: Fri, 21 Jan 2022 18:04:25 +0000 Subject: [PATCH 06/41] Remove the global error handler and always show errors in the UI --- Converge/ClientApp/src/App.tsx | 49 ++++++------ .../providers/ConvergeSettingsProvider.tsx | 77 +++++++++++++------ .../src/providers/ErrorAlertProvider.tsx | 77 ------------------- Converge/ClientApp/src/tabs/home/Welcome.tsx | 68 +++++++++------- Converge/ClientApp/src/tabs/home/index.tsx | 18 +---- Converge/ClientApp/src/types/LoggerTypes.ts | 3 +- .../src/utilities/EnterZipCodeDialog.tsx | 7 +- .../ClientApp/src/utilities/ErrorAlert.tsx | 50 ------------ 8 files changed, 124 insertions(+), 225 deletions(-) delete mode 100644 Converge/ClientApp/src/providers/ErrorAlertProvider.tsx delete mode 100644 Converge/ClientApp/src/utilities/ErrorAlert.tsx diff --git a/Converge/ClientApp/src/App.tsx b/Converge/ClientApp/src/App.tsx index 55b4a0b..ab497dc 100644 --- a/Converge/ClientApp/src/App.tsx +++ b/Converge/ClientApp/src/App.tsx @@ -14,8 +14,6 @@ import TeamsThemeProvider from "./providers/TeamsThemeProvider"; import Home from "./tabs/home"; import Collaborate from "./tabs/collaborate"; import Workspace from "./tabs/workspace"; -import { ErrorAlertProvider } from "./providers/ErrorAlertProvider"; -import ErrorAlert from "./utilities/ErrorAlert"; import ConvergeSettingsProvider from "./providers/ConvergeSettingsProvider"; import "./app.css"; import { SearchContextProvider } from "./providers/SearchProvider"; @@ -38,31 +36,28 @@ const App: React.FC = () => ( - - - - - - - - ( - - - - )} - /> - - - - - + + + + + + ( + + + + )} + /> + + + + diff --git a/Converge/ClientApp/src/providers/ConvergeSettingsProvider.tsx b/Converge/ClientApp/src/providers/ConvergeSettingsProvider.tsx index d5be626..c9f870b 100644 --- a/Converge/ClientApp/src/providers/ConvergeSettingsProvider.tsx +++ b/Converge/ClientApp/src/providers/ConvergeSettingsProvider.tsx @@ -1,7 +1,9 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -import { Loader } from "@fluentui/react-northstar"; +import { + Alert, Button, Flex, Loader, Text, +} from "@fluentui/react-northstar"; import { GeoCoordinates } from "@microsoft/microsoft-graph-types"; import React, { useContext, useEffect, useReducer, useState, @@ -15,8 +17,11 @@ import getSettings, { import BuildingBasicInfo from "../types/BuildingBasicInfo"; import ConvergeSettings from "../types/ConvergeSettings"; import UpcomingBuildingsResponse from "../types/UpcomingBuildingsResponse"; -import { useProvider as errorAlertProvider, helpers } from "./ErrorAlertProvider"; import ExchangePlace from "../types/ExchangePlace"; +import { + USER_INTERACTION, UI_SECTION, UISections, DESCRIPTION, +} from "../types/LoggerTypes"; +import { logEvent } from "../utilities/LogWrapper"; type IBuildingState = { buildingsList: BuildingBasicInfo[]; @@ -328,8 +333,6 @@ const ConvergeSettingsProvider: React.FC = ({ children }) => { favoriteCampusesReducer, [], ); - const { errorDispatch } = errorAlertProvider(); - const getConvergeSettings = (): Promise => { convergeSettingsDispatch({ type: GET_CONVERGE_SETTINGS_REQUEST }); return getSettings() @@ -337,12 +340,6 @@ const ConvergeSettingsProvider: React.FC = ({ children }) => { convergeSettingsDispatch( { type: GET_CONVERGE_SETTINGS_RESPONSE, convergeSettings: settings }, ); - }) - .catch((error) => { - errorDispatch({ - type: "SET_ERROR_ALERT", - payload: helpers.getDefaultToastObject(error.message, "getSettings"), - }); }); }; @@ -350,23 +347,11 @@ const ConvergeSettingsProvider: React.FC = ({ children }) => { .then(() => convergeSettingsDispatch({ type: SET_CONVERGE_SETTINGS_REQUEST, convergeSettings: settings, - })) - .catch((error) => { - errorDispatch({ - type: "SET_ERROR_ALERT", - payload: helpers.getDefaultToastObject(error.message, "setSettings"), - }); - }); + })); const setupNewUserWrapper = (settings: ConvergeSettings): Promise => setupNewUser(settings) .then(() => { convergeSettingsDispatch({ type: SETUP_NEW_USER_RESPONSE, convergeSettings: settings }); - }) - .catch((error) => { - errorDispatch({ - type: "SET_ERROR_ALERT", - payload: helpers.getDefaultToastObject(error.message, "setupNewUser"), - }); }); const getFavoriteCampusesWrapper = (): Promise => { @@ -474,9 +459,11 @@ const ConvergeSettingsProvider: React.FC = ({ children }) => { }, [state.buildingsByRadiusDistance]); const [loading, setLoading] = useState(true); + const [isError, setIsError] = useState(false); useEffect(() => { getConvergeSettings() + .catch(() => setIsError(true)) .finally(() => setLoading(false)); getRecentBuildingsBasicDetails().then((basicRecentBuildings) => { updateRecentBuildings(basicRecentBuildings); @@ -509,7 +496,49 @@ const ConvergeSettingsProvider: React.FC = ({ children }) => { updateRecentBuildings, }} > - {loading ? : children} + {loading && } + {!loading && isError && ( + + + - )} - headerAction={{ - icon: , - title: "Close", - onClick: () => { - logEvent(USER_INTERACTION, [ - { name: UI_SECTION, value: UISections.WorkspaceHome }, - { name: DESCRIPTION, value: "close_workspace_dialog" }, - ]); - clearEvent(); - setOpen(false); + onClick={() => { + refreshWorkSpace(); + logEvent(USER_INTERACTION, [ + { name: UI_SECTION, value: UISections.WorkspaceHome }, + { name: DESCRIPTION, value: "refreshWorkSpace" }, + ]); + }} + color="red" + className={classes.retryBtn} + /> + + )} + + {loading && } + {!loading && schedule && } + + + ({ + rootWidth: "795px", + headerFontSize: "18px", + rootBackground: colorScheme.default.background, + color: colorScheme.default.background3, + }), }, }} - className={classes.dialog} - /> - - - - + > + { + logEvent(USER_INTERACTION, [ + { name: UI_SECTION, value: UISections.WorkspaceHome }, + { name: DESCRIPTION, value: "open_workspace_dialog" }, + ]); + setOpen(true); + }} + onCancel={() => { + clearEvent(); + logEvent(USER_INTERACTION, [ + { name: UI_SECTION, value: UISections.WorkspaceHome }, + { name: DESCRIPTION, value: "cancel_workspace_dialog" }, + ]); + setOpen(false); + }} + onConfirm={() => { + logEvent(USER_INTERACTION, [ + { name: UI_SECTION, value: UISections.WorkspaceHome }, + { name: DESCRIPTION, value: "confirm_workspace_dialog" }, + ]); + setLoading(true); + let startDate = start.toDate(); + let endDate = end.toDate(); + if (isAllDay) { + startDate = dayjs(start.format("YYYY-MM-DD")).toDate(); + endDate = dayjs(end.add(1, "day").format("YYYY-MM-DD")).toDate(); + } + createEvent({ + isAllDay, + start: startDate, + end: endDate, + attendees: [{ + emailAddress: flexiblePlace?.identity, + type: "resource" as MicrosoftGraph.AttendeeType, + }], + location: { + displayName: flexiblePlace?.displayName, + locationEmailAddress: flexiblePlace?.identity, + locationType: "conferenceRoom", + }, + title: "Converge Workspace Booking", + showAs: "free" as MicrosoftGraph.FreeBusyStatus, + }) + .then(() => { + updateMyPredictedLocation({ + year: dayjs.utc(startDate).year(), + month: dayjs.utc(startDate).month() + 1, + day: dayjs.utc(startDate).date(), + userPredictedLocation: { + campusUpn: flexiblePlace?.locality, + }, + }); + if (flexiblePlace) { + const newSettings = { + ...convergeSettings, + recentBuildingUpns: AddRecentBuildings( + convergeSettings?.recentBuildingUpns, + flexiblePlace.locality, + ), + }; + setConvergeSettings(newSettings); + } + }) + .then(refreshRecommended) + .then(() => { + setOpen(false); + clearEvent(); + Notifications.show({ + duration: 5000, + title: "You reserved a workspace.", + content: `${flexiblePlace?.displayName} (${dayjs(startDate).format("ddd @ h:mm A")})`, + }); + }) + .catch(() => { + setErr("Something went wrong with your workspace reservation. Please try again."); + }) + .finally(() => { + setLoading(false); + }); + }} + confirmButton={{ + content: "Reserve", + loading, + }} + cancelButton="Cancel" + content={( + flexiblePlace && ( + + ) + )} + header={( + + )} + trigger={( + + )} + headerAction={{ + icon: , + title: "Close", + onClick: () => { + logEvent(USER_INTERACTION, [ + { name: UI_SECTION, value: UISections.WorkspaceHome }, + { name: DESCRIPTION, value: "close_workspace_dialog" }, + ]); + clearEvent(); + setOpen(false); + }, + }} + className={classes.dialog} + /> + + + + )} diff --git a/Converge/ClientApp/src/tabs/home/styles/BookWorkspaceStyles.ts b/Converge/ClientApp/src/tabs/home/styles/BookWorkspaceStyles.ts index fc67b37..10f203c 100644 --- a/Converge/ClientApp/src/tabs/home/styles/BookWorkspaceStyles.ts +++ b/Converge/ClientApp/src/tabs/home/styles/BookWorkspaceStyles.ts @@ -73,7 +73,6 @@ const BookWorkspaceStyles = makeStyles(() => ({ buildingContent: { overflowy: "auto", overflowX: "hidden !important", - maxHeight: "320px", MsOverflowStyle: "none", "@media (max-width: 1366px)": { height: "auto", @@ -82,7 +81,6 @@ const BookWorkspaceStyles = makeStyles(() => ({ WorkSpacebuildingContent: { overflowy: "auto", overflowX: "hidden !important", - maxHeight: "260px", MsOverflowStyle: "none", "@media (max-width: 1366px)": { height: "auto", diff --git a/Converge/ClientApp/src/tabs/workspace/components/LocationFilter.tsx b/Converge/ClientApp/src/tabs/workspace/components/LocationFilter.tsx index d9d52e8..6b9781f 100644 --- a/Converge/ClientApp/src/tabs/workspace/components/LocationFilter.tsx +++ b/Converge/ClientApp/src/tabs/workspace/components/LocationFilter.tsx @@ -30,7 +30,7 @@ const LocationFilter: React.FC = (props) => { const { buildings } = props; const classes = LocationFilterStyles(); - const handleDropdownChange = (bldg:string | undefined) => { + const handleDropdownChange = (bldg: string | undefined) => { logEvent(USER_INTERACTION, [ { name: UI_SECTION, value: UISections.WorkspaceHome }, { name: DESCRIPTION, value: `selected_building_change_${bldg}` }, @@ -54,6 +54,9 @@ const LocationFilter: React.FC = (props) => { value={buildings.find((b) => b.identity === state.location)?.displayName} placeholderTitle="Select a building" buttonTitle="Show more" + otherOptionsList={[]} + maxHeight="260px" + /> ); diff --git a/Converge/ClientApp/src/utilities/ChangeLocationModal.tsx b/Converge/ClientApp/src/utilities/ChangeLocationModal.tsx index c4692cc..bfaa2ce 100644 --- a/Converge/ClientApp/src/utilities/ChangeLocationModal.tsx +++ b/Converge/ClientApp/src/utilities/ChangeLocationModal.tsx @@ -2,19 +2,19 @@ // Licensed under the MIT License. import { - Dialog, Flex, Provider, Box, Text, Button, DropdownProps, + Dialog, Flex, Provider, Box, Text, Button, } from "@fluentui/react-northstar"; import { CloseIcon } from "@fluentui/react-icons-northstar"; import React, { useState } from "react"; import dayjs from "dayjs"; import { makeStyles } from "@fluentui/react-theme-provider"; -import PrimaryDropdown from "./PrimaryDropdown"; import BuildingBasicInfo from "../types/BuildingBasicInfo"; import { logEvent } from "../utilities/LogWrapper"; import { UI_SECTION, UISections, USER_INTERACTION, DESCRIPTION, IMPORTANT_ACTION, ImportantActions, } from "../types/LoggerTypes"; import { updateMyPredictedLocation } from "../api/meService"; +import PopupMenuWrapper from "./popupMenuWrapper"; const ChangeLocationModalStyles = makeStyles(() => ({ triggerBtn: { @@ -60,15 +60,51 @@ const ChangeLocationModal: React.FC = (props) => { const [loading, setLoading] = useState(false); const classes = ChangeLocationModalStyles(); - const handleLocationChange = ( - event: React.MouseEvent | React.KeyboardEvent | null, - data: DropdownProps, - ) => { + const handleLocationChange = (bldg: string | undefined) => { logEvent(USER_INTERACTION, [ { name: UI_SECTION, value: UISections.LocationChangeModalHome }, { name: DESCRIPTION, value: "change_converge_prediction" }, ]); - setLocation(data?.value?.toString() || ""); + setLocation(bldg || ""); + }; + + const onConfirmbutton = () => { + setLoading(true); + logEvent(USER_INTERACTION, [ + { name: UI_SECTION, value: UISections.LocationChangeModalHome }, + { name: DESCRIPTION, value: "confirm_converge_prediction" }, + ]); + const day = dayjs.utc(date); + let campusUpn; + let otherLocationOption: "Remote" | "Out of Office" | undefined; + if (location === "Remote") { + otherLocationOption = "Remote"; + } else if (location === "Out of Office") { + otherLocationOption = "Out of Office"; + } else { + const building = buildings.find((b) => b.displayName === location); + if (building) { + campusUpn = building.identity; + } + } + updateMyPredictedLocation({ + year: day.year(), + month: day.month() + 1, + day: day.date(), + userPredictedLocation: { + campusUpn, + otherLocationOption, + }, + }).then(() => { + logEvent(USER_INTERACTION, [ + { name: IMPORTANT_ACTION, value: ImportantActions.ChangeConvergePrediction }, + ]); + refreshRecommendation(); + }) + .then(() => { + setOpen(false); + }) + .finally(() => setLoading(false)); }; return ( @@ -98,44 +134,7 @@ const ChangeLocationModal: React.FC = (props) => { ]); setOpen(false); }} - onConfirm={() => { - setLoading(true); - logEvent(USER_INTERACTION, [ - { name: UI_SECTION, value: UISections.LocationChangeModalHome }, - { name: DESCRIPTION, value: "confirm_converge_prediction" }, - ]); - const day = dayjs.utc(date); - let campusUpn; - let otherLocationOption: "Remote" | "Out of Office" | undefined; - if (location === "Remote") { - otherLocationOption = "Remote"; - } else if (location === "Out of Office") { - otherLocationOption = "Out of Office"; - } else { - const building = buildings.find((b) => b.displayName === location); - if (building) { - campusUpn = building.identity; - } - } - updateMyPredictedLocation({ - year: day.year(), - month: day.month() + 1, - day: day.date(), - userPredictedLocation: { - campusUpn, - otherLocationOption, - }, - }).then(() => { - logEvent(USER_INTERACTION, [ - { name: IMPORTANT_ACTION, value: ImportantActions.ChangeConvergePrediction }, - ]); - refreshRecommendation(); - }) - .then(() => { - setOpen(false); - }) - .finally(() => setLoading(false)); - }} + onConfirm={() => { onConfirmbutton(); }} confirmButton={{ content: "Confirm", loading, @@ -177,16 +176,11 @@ const ChangeLocationModal: React.FC = (props) => { ? - b.displayName).concat(["Remote", "Out of Office"])} - value={location} - handleDropdownChange={handleLocationChange} - clearable - width="212px" - /> + + b.displayName)} locationBuildingName="" otherOptionsList={["Remote", "Out Of Office"]} width="320px" marginContent="3.8rem" value={location} placeholderTitle="Select One" buttonTitle="See more" maxHeight="90px" /> + - )} + )} /> ); diff --git a/Converge/ClientApp/src/utilities/popupMenuContent.tsx b/Converge/ClientApp/src/utilities/popupMenuContent.tsx index 048757f..8c9f531 100644 --- a/Converge/ClientApp/src/utilities/popupMenuContent.tsx +++ b/Converge/ClientApp/src/utilities/popupMenuContent.tsx @@ -15,15 +15,17 @@ import { interface Props { headerTitle: string, - locationBuildingName:string | undefined; - buildingList:ShorthandCollection>; - handleDropdownChange:(bldg:string| undefined)=>void; - buttonTitle:string; + locationBuildingName: string | undefined; + otherOptionsList: string[]; + buildingList: ShorthandCollection>; + handleDropdownChange: (bldg: string | undefined) => void; + buttonTitle: string; + maxHeight: string; } const PopupMenuContent: React.FunctionComponent = (props) => { const { - headerTitle, buildingList, locationBuildingName, buttonTitle, + headerTitle, buildingList, locationBuildingName, buttonTitle, otherOptionsList, maxHeight, } = props; const { state, setBuildingsByDistanceRadius, @@ -48,7 +50,7 @@ const PopupMenuContent: React.FunctionComponent = (props) => { {location.pathname !== "/workspace" && locationBuildingName !== "" - && ( + && ( <> - )} + )} {location.pathname === "/workspace" && state.recentBuildings.length > 0 - && ( - - {state.recentBuildings.map((item) => ( - <> - - - - - ))} - - )} + && ( + + {state.recentBuildings.map((item) => ( + <> + + + + + ))} + + )} + {location.pathname !== "/workspace" && otherOptionsList.length > 0 + && ( + + {otherOptionsList.map((item) => ( + <> + + + + + ))} + + )} - + {buildingList.length > 0 - && ( - - {buildingList.map((item) => ( - <> - - - - - ))} - - )} + && ( + + {buildingList.map((item) => ( + <> + + + + + ))} + + )} {state.buildingListLoading && } diff --git a/Converge/ClientApp/src/utilities/popupMenuWrapper.tsx b/Converge/ClientApp/src/utilities/popupMenuWrapper.tsx index 0975105..82c8098 100644 --- a/Converge/ClientApp/src/utilities/popupMenuWrapper.tsx +++ b/Converge/ClientApp/src/utilities/popupMenuWrapper.tsx @@ -16,20 +16,22 @@ import { logEvent } from "./LogWrapper"; interface Props { headerTitle: string, - locationBuildingName:string | undefined, - buildingList:ShorthandCollection>, - handleDropdownChange:(bldg:string | undefined)=>void; - marginContent:string, - width:string, - value:string|undefined, - placeholderTitle:string, - buttonTitle:string, + locationBuildingName: string | undefined, + otherOptionsList: string[]; + buildingList: ShorthandCollection>, + handleDropdownChange: (bldg: string | undefined) => void; + marginContent: string, + width: string, + value: string | undefined, + placeholderTitle: string, + buttonTitle: string, + maxHeight: string, } const PopupMenuWrapper: React.FunctionComponent = (props) => { const { headerTitle, buildingList, locationBuildingName, width, - marginContent, value, placeholderTitle, buttonTitle, + marginContent, value, placeholderTitle, buttonTitle, otherOptionsList, maxHeight, } = props; const { updateLocation } = PlaceProvider(); @@ -41,9 +43,9 @@ const PopupMenuWrapper: React.FunctionComponent = (props) => { const [popup, setPopup] = React.useState(false); const [selectedBuildingName, setSelectedBuildingName] = React.useState< - string|undefined>(value); + string | undefined>(value); - const handleDropdownChange = (bldg:string | undefined) => { + const handleDropdownChange = (bldg: string | undefined) => { setPopup(false); setSelectedBuildingName(bldg); const selectedBuilding = state.buildingsList.find((b) => b.displayName === bldg); @@ -60,7 +62,7 @@ const PopupMenuWrapper: React.FunctionComponent = (props) => { else setPopup(true); setSelectedBuildingName(""); }; - const handleTextboxChange = (searchText:string | undefined) => { + const handleTextboxChange = (searchText: string | undefined) => { setPopup(true); setSelectedBuildingName(searchText || ""); updateLocation(undefined); @@ -93,13 +95,13 @@ const PopupMenuWrapper: React.FunctionComponent = (props) => { ]); }} /> -)} + )} value={selectedBuildingName} clearable onChange={((event, data) => handleTextboxChange(data?.value))} placeholder={placeholderTitle} /> -)} + )} content={{ styles: { width, @@ -118,6 +120,8 @@ const PopupMenuWrapper: React.FunctionComponent = (props) => { handleDropdownChange={handleDropdownChange} locationBuildingName={locationBuildingName} buttonTitle={buttonTitle} + otherOptionsList={otherOptionsList} + maxHeight={maxHeight} /> ), }} From 41a29ba5049ef150d42872395a670b683d8b8ef9 Mon Sep 17 00:00:00 2001 From: Muhammadhu Abubakkar Date: Mon, 24 Jan 2022 03:43:24 +0000 Subject: [PATCH 08/41] Merged PR 1246: 3810_Reservation list updation after successful booking in workspace tab Before submitting this PR, please make ensure you've completed the following: - [ ] Your code builds clean without any errors **or warnings**. - [ ] All unused code has been deleted. - [ ] All TODOs in the code base have an associated ticket in ADO. - [ ] Your squash commit to main has a short descriptive commit message. Describe your PR: Bug 3810: Fixed reservation list not updating after successful booking in workspace tab - FIXED If submitting to the FrontEnd UI, include a screenshot of the change: Fixed reservation list not updating after successful booking in workspace tab Related work items: #3810 --- .../ClientApp/src/tabs/workspace/components/PlaceCard.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Converge/ClientApp/src/tabs/workspace/components/PlaceCard.tsx b/Converge/ClientApp/src/tabs/workspace/components/PlaceCard.tsx index 0c6d83a..4184acb 100644 --- a/Converge/ClientApp/src/tabs/workspace/components/PlaceCard.tsx +++ b/Converge/ClientApp/src/tabs/workspace/components/PlaceCard.tsx @@ -45,7 +45,7 @@ const PlaceCard: React.FC = (props) => { const [bookable, setBookable] = useState(true); const [loading, setLoading] = useState(false); const [err, setErr] = useState(undefined); - const { state } = PlaceProvider(); + const { state, loadUpcomingReservations } = PlaceProvider(); const [start, setStart] = useState(state.startDate); const [end, setEnd] = useState(state.endDate); const [isAllDay, setIsAllDay] = useState(false); @@ -257,6 +257,10 @@ const PlaceCard: React.FC = (props) => { clearPlaceCard(); setOpen(false); getAvailability(); + loadUpcomingReservations( + state.upcomingReservationsStartDate, + state.upcomingReservationsEndDate, + ); Notifications.show({ duration: 5000, title: "You reserved a workspace.", From f529edfc23aa769be4fca542464052ba4e3b18ce Mon Sep 17 00:00:00 2001 From: Muhammadhu Abubakkar Date: Mon, 24 Jan 2022 15:50:15 +0000 Subject: [PATCH 09/41] Merged PR 1251: New Conference Room event is now updated to refresh reservation list. Before submitting this PR, please make ensure you've completed the following: - [ ] Your code builds clean without any errors **or warnings**. - [ ] All unused code has been deleted. - [ ] All TODOs in the code base have an associated ticket in ADO. - [ ] Your squash commit to main has a short descriptive commit message. Describe your PR: If submitting to the FrontEnd UI, include a screenshot of the change: New Conference Room event is now updated to refresh reservation list. Related work items: #3858 --- .../src/tabs/workspace/components/NewConfRoomEvent.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Converge/ClientApp/src/tabs/workspace/components/NewConfRoomEvent.tsx b/Converge/ClientApp/src/tabs/workspace/components/NewConfRoomEvent.tsx index 5303b1a..a0a1f07 100644 --- a/Converge/ClientApp/src/tabs/workspace/components/NewConfRoomEvent.tsx +++ b/Converge/ClientApp/src/tabs/workspace/components/NewConfRoomEvent.tsx @@ -43,7 +43,7 @@ const NewConfRoomEvent:React.FC = (props) => { clearPlaceCard, getAvailability, } = props; - const { createReservation } = PlaceFilterProvider(); + const { createReservation, loadUpcomingReservations } = PlaceFilterProvider(); const [isAllDay, setIsAllDay] = useState(false); const [subject, setSubject] = useState(""); const [message, setMessage] = useState(""); @@ -143,6 +143,7 @@ const NewConfRoomEvent:React.FC = (props) => { }; setConvergeSettings(newSettings); createReservation(calendarEvent); + loadUpcomingReservations(start, end); getAvailability(); Notifications.show({ duration: 5000, From d17a51d422b5ca283a1902f5ed76da5b16c6badf Mon Sep 17 00:00:00 2001 From: Muhammadhu Abubakkar Date: Mon, 24 Jan 2022 15:51:29 +0000 Subject: [PATCH 10/41] Merged PR 1250: Updated plain date payload Before submitting this PR, please make ensure you've completed the following: - [ ] Your code builds clean without any errors **or warnings**. - [ ] All unused code has been deleted. - [ ] All TODOs in the code base have an associated ticket in ADO. - [ ] Your squash commit to main has a short descriptive commit message. Describe your PR: If submitting to the FrontEnd UI, include a screenshot of the change: Updated plain date payload Related work items: #3857 --- .../src/tabs/workspace/components/BookPlaceModal.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Converge/ClientApp/src/tabs/workspace/components/BookPlaceModal.tsx b/Converge/ClientApp/src/tabs/workspace/components/BookPlaceModal.tsx index 541bf91..ec63ba7 100644 --- a/Converge/ClientApp/src/tabs/workspace/components/BookPlaceModal.tsx +++ b/Converge/ClientApp/src/tabs/workspace/components/BookPlaceModal.tsx @@ -166,8 +166,8 @@ const BookPlaceModal: React.FC = (props) => { } else if (start.utc().toISOString() <= end.utc().toISOString()) { getRoomAvailability( place.identity, - dayjs(startDay).utc().toISOString(), - dayjs(endDay).utc().toISOString(), + dayjs(start).utc().toISOString(), + dayjs(end).utc().toISOString(), ).then(setIsAvailable); } }, [isAllDay, start, end]); From 6b350e6d30775353e22d7b4862d9777a3a7fa446 Mon Sep 17 00:00:00 2001 From: "Tej.Neelapala" Date: Mon, 24 Jan 2022 16:20:43 +0000 Subject: [PATCH 11/41] Merged PR 1248: Fix to Bug # 3822 :: Building capacity fails when trying to display in new test tenant. Before submitting this PR, please make ensure you've completed the following: - [ ] Your code builds clean without any errors **or warnings**. - [ ] All unused code has been deleted. - [ ] All TODOs in the code base have an associated ticket in ADO. - [ ] Your squash commit to main has a short descriptive commit message. Describe your PR: If submitting to the FrontEnd UI, include a screenshot of the change: Related work items: #3822 --- Converge/Controllers/UsersController.cs | 2 +- Converge/Services/BuildingsService.cs | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/Converge/Controllers/UsersController.cs b/Converge/Controllers/UsersController.cs index aae5fbd..fbfd370 100644 --- a/Converge/Controllers/UsersController.cs +++ b/Converge/Controllers/UsersController.cs @@ -177,7 +177,7 @@ namespace Converge.Controllers /// Search Users that match the provided search string /// /// The string to user for user search - /// query optionsquery options /// List of users whose DisplayName/UserPrincipalName starts with input string> [HttpGet] [Route("search")] diff --git a/Converge/Services/BuildingsService.cs b/Converge/Services/BuildingsService.cs index 2ce10a8..022f796 100644 --- a/Converge/Services/BuildingsService.cs +++ b/Converge/Services/BuildingsService.cs @@ -255,6 +255,10 @@ namespace Converge.Services return new ConvergeSchedule(); } var buildingCapacity = exchangePlacesResponse.ExchangePlacesList.Sum(ws => ws.Capacity); + if (buildingCapacity == 0) + { + return new ConvergeSchedule(); + } double reserved = 0; foreach (ExchangePlace workspace in exchangePlacesResponse.ExchangePlacesList) From adf05f43490272e5e8236fcf81b4a2604133ffc2 Mon Sep 17 00:00:00 2001 From: Megan Slater Date: Mon, 24 Jan 2022 20:52:43 +0000 Subject: [PATCH 12/41] Use axios cache for place photos, campus and venue details API calls --- Converge/ClientApp/package-lock.json | 247 ++++++++++-------- Converge/ClientApp/package.json | 3 +- Converge/ClientApp/src/App.tsx | 39 ++- .../src/api/AuthenticationService.ts | 22 +- .../ClientApp/src/api/buildingService.test.ts | 145 ---------- Converge/ClientApp/src/api/buildingService.ts | 74 ++---- Converge/ClientApp/src/api/searchService.ts | 26 +- .../src/providers/PlacePhotosProvider.tsx | 12 - .../components/CampusPlacePanel.tsx | 16 +- .../components/FavoritesToCollaborate.tsx | 12 +- .../workspace/components/BookPlaceModal.tsx | 31 +-- .../CollaborationCampusPlaceCard.tsx | 22 +- .../tabs/workspace/components/PlaceCard.tsx | 11 +- .../src/utilities/CachedServiceProvider.tsx | 78 ------ Converge/ClientApp/src/utilities/Constants.ts | 6 + Converge/ClientApp/yarn.lock | 100 ++++--- 16 files changed, 304 insertions(+), 540 deletions(-) delete mode 100644 Converge/ClientApp/src/api/buildingService.test.ts delete mode 100644 Converge/ClientApp/src/providers/PlacePhotosProvider.tsx delete mode 100644 Converge/ClientApp/src/utilities/CachedServiceProvider.tsx create mode 100644 Converge/ClientApp/src/utilities/Constants.ts diff --git a/Converge/ClientApp/package-lock.json b/Converge/ClientApp/package-lock.json index 162deab..ca6182d 100644 --- a/Converge/ClientApp/package-lock.json +++ b/Converge/ClientApp/package-lock.json @@ -28,7 +28,8 @@ "@types/react": "16.8.0", "@types/react-dom": "16.8.0", "@uifabric/icons": "7.5.23", - "axios": "0.24.0", + "axios": "0.21.4", + "axios-cache-adapter": "2.7.3", "bingmaps": "2.0.3", "dayjs": "1.10.5", "lodash": "4.17.21", @@ -5867,11 +5868,23 @@ } }, "node_modules/axios": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.24.0.tgz", - "integrity": "sha512-Q6cWsys88HoPgAaFAVUb0WpPk0O8iTeisR9IMqy9G8AbO4NlpVknrnQS03zzF9PGAWgO3cgletO3VjV/P7VztA==", + "version": "0.21.4", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz", + "integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==", "dependencies": { - "follow-redirects": "^1.14.4" + "follow-redirects": "^1.14.0" + } + }, + "node_modules/axios-cache-adapter": { + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/axios-cache-adapter/-/axios-cache-adapter-2.7.3.tgz", + "integrity": "sha512-A+ZKJ9lhpjthOEp4Z3QR/a9xC4du1ALaAsejgRGrH9ef6kSDxdFrhRpulqsh9khsEnwXxGfgpUuDp1YXMNMEiQ==", + "dependencies": { + "cache-control-esm": "1.0.0", + "md5": "^2.2.1" + }, + "peerDependencies": { + "axios": "~0.21.1" } }, "node_modules/axobject-query": { @@ -6401,6 +6414,11 @@ "node": ">= 0.8" } }, + "node_modules/cache-control-esm": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/cache-control-esm/-/cache-control-esm-1.0.0.tgz", + "integrity": "sha512-Fa3UV4+eIk4EOih8FTV6EEsVKO0W5XWtNs6FC3InTfVz+EjurjPfDXY5wZDo/lxjDxg5RjNcurLyxEJBcEUx9g==" + }, "node_modules/call-bind": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", @@ -6497,6 +6515,14 @@ "node": ">=10" } }, + "node_modules/charenc": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz", + "integrity": "sha1-wKHS86cJLgN3S/qD8UwPxXkKhmc=", + "engines": { + "node": "*" + } + }, "node_modules/check-types": { "version": "11.1.2", "resolved": "https://registry.npmjs.org/check-types/-/check-types-11.1.2.tgz", @@ -6971,6 +6997,14 @@ "node": ">= 8" } }, + "node_modules/crypt": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz", + "integrity": "sha1-iNf/fsDfuG9xPch7u0LQRNPmxBs=", + "engines": { + "node": "*" + } + }, "node_modules/crypto-random-string": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz", @@ -11052,6 +11086,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" + }, "node_modules/is-callable": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.4.tgz", @@ -11134,18 +11173,6 @@ "node": ">=0.10.0" } }, - "node_modules/is-ip": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/is-ip/-/is-ip-3.1.0.tgz", - "integrity": "sha512-35vd5necO7IitFPjd/YBeqwWnyDWbuLH9ZXQdMfDA8TEo7pv5X8yfrvVO3xbJbLUlERCMvf6X0hTUamQxCYJ9Q==", - "dev": true, - "dependencies": { - "ip-regex": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/is-module": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", @@ -13729,6 +13756,16 @@ "tmpl": "1.0.5" } }, + "node_modules/md5": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/md5/-/md5-2.3.0.tgz", + "integrity": "sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==", + "dependencies": { + "charenc": "0.0.2", + "crypt": "0.0.2", + "is-buffer": "~1.1.6" + } + }, "node_modules/mdn-data": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.4.tgz", @@ -13944,30 +13981,29 @@ "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" }, "node_modules/mkcert": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/mkcert/-/mkcert-1.4.0.tgz", - "integrity": "sha512-0cQXdsoOKq7EHS4Jkxnj16JA4eTt/noXUcaFr44aFAlqfgdCmIGqfGcGoosdXf46YzbaEfEQmrsHGYFV9XvpmA==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/mkcert/-/mkcert-1.5.0.tgz", + "integrity": "sha512-Jc5tW6XGpRZR/GXimztRtvv0Q46Qkv0/iqy06sBOldb1I5FfkeD7/LDPOMV9fnV6Nkx8YbNv+Z/8AmmQGfPtgg==", "dev": true, "dependencies": { - "commander": "^6.1.0", - "is-ip": "^3.1.0", - "node-forge": "^0.10.0", - "random-int": "^2.0.1" + "commander": "^8.3.0", + "ip-regex": "^4.3.0", + "node-forge": "^1.2.1" }, "bin": { "mkcert": "src/cli.js" }, "engines": { - "node": ">=8" + "node": ">=12" } }, "node_modules/mkcert/node_modules/commander": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz", - "integrity": "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==", + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", "dev": true, "engines": { - "node": ">= 6" + "node": ">= 12" } }, "node_modules/mkdirp": { @@ -14004,9 +14040,9 @@ "integrity": "sha1-iZ8R2WhuXgXLkbNdXw5jt3PPyQE=" }, "node_modules/nanoid": { - "version": "3.1.30", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.30.tgz", - "integrity": "sha512-zJpuPDwOv8D2zq2WRoMe1HsfZthVewpel9CAvTfc/2mBD1uUT/agc5f7GHGWXlYkFvi1mVxe4IjvP2HNrop7nQ==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.2.0.tgz", + "integrity": "sha512-fmsZYa9lpn69Ad5eDn7FMcnnSR+8R34W9qJEijxYhTbfOWzr22n1QxCMzXLK+ODyW2973V3Fux959iQoUxzUIA==", "bin": { "nanoid": "bin/nanoid.cjs" }, @@ -14042,15 +14078,23 @@ } }, "node_modules/node-fetch": { - "version": "2.6.6", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.6.tgz", - "integrity": "sha512-Z8/6vRlTUChSdIgMa51jxQ4lrw/Jy5SOW10ObaA47/RElsAN2c5Pn8bTgFGWn/ibwzXTE8qwr1Yzx28vsecXEA==", + "version": "2.6.7", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", + "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", "dev": true, "dependencies": { "whatwg-url": "^5.0.0" }, "engines": { "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } } }, "node_modules/node-fetch/node_modules/tr46": { @@ -14076,12 +14120,11 @@ } }, "node_modules/node-forge": { - "version": "0.10.0", - "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.10.0.tgz", - "integrity": "sha512-PPmu8eEeG9saEUvI97fm4OYxXVB6bFvyNTyiUOBichBpFG8A1Ljw3bY62+5oOjDEMHRnd0Y7HQ+x7uzxOzC6JA==", - "dev": true, + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.2.1.tgz", + "integrity": "sha512-Fcvtbb+zBcZXbTTVwqGA5W+MKBj56UjVRevvchv5XrcyXbmNdesfZL37nlcWOfpgHhgmxApw3tQbTr4CqNmX4w==", "engines": { - "node": ">= 6.0.0" + "node": ">= 6.13.0" } }, "node_modules/node-int64": { @@ -16190,15 +16233,6 @@ "performance-now": "^2.1.0" } }, - "node_modules/random-int": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/random-int/-/random-int-2.0.1.tgz", - "integrity": "sha512-YALjWK2Rt9EMIv9BF/3mvlzFWQathsvb5UZmN1QmhfIOfcQYXc/UcLzg0ablqesSBpBVLt2Tlwv/eTuBh4LXUQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, "node_modules/randombytes": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", @@ -17743,14 +17777,6 @@ "node": ">=10" } }, - "node_modules/selfsigned/node_modules/node-forge": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.2.1.tgz", - "integrity": "sha512-Fcvtbb+zBcZXbTTVwqGA5W+MKBj56UjVRevvchv5XrcyXbmNdesfZL37nlcWOfpgHhgmxApw3tQbTr4CqNmX4w==", - "engines": { - "node": ">= 6.13.0" - } - }, "node_modules/semver": { "version": "7.3.2", "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.2.tgz", @@ -24667,11 +24693,20 @@ "dev": true }, "axios": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.24.0.tgz", - "integrity": "sha512-Q6cWsys88HoPgAaFAVUb0WpPk0O8iTeisR9IMqy9G8AbO4NlpVknrnQS03zzF9PGAWgO3cgletO3VjV/P7VztA==", + "version": "0.21.4", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz", + "integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==", "requires": { - "follow-redirects": "^1.14.4" + "follow-redirects": "^1.14.0" + } + }, + "axios-cache-adapter": { + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/axios-cache-adapter/-/axios-cache-adapter-2.7.3.tgz", + "integrity": "sha512-A+ZKJ9lhpjthOEp4Z3QR/a9xC4du1ALaAsejgRGrH9ef6kSDxdFrhRpulqsh9khsEnwXxGfgpUuDp1YXMNMEiQ==", + "requires": { + "cache-control-esm": "1.0.0", + "md5": "^2.2.1" } }, "axobject-query": { @@ -25096,6 +25131,11 @@ "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=" }, + "cache-control-esm": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/cache-control-esm/-/cache-control-esm-1.0.0.tgz", + "integrity": "sha512-Fa3UV4+eIk4EOih8FTV6EEsVKO0W5XWtNs6FC3InTfVz+EjurjPfDXY5wZDo/lxjDxg5RjNcurLyxEJBcEUx9g==" + }, "call-bind": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", @@ -25164,6 +25204,11 @@ "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==" }, + "charenc": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz", + "integrity": "sha1-wKHS86cJLgN3S/qD8UwPxXkKhmc=" + }, "check-types": { "version": "11.1.2", "resolved": "https://registry.npmjs.org/check-types/-/check-types-11.1.2.tgz", @@ -25538,6 +25583,11 @@ } } }, + "crypt": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz", + "integrity": "sha1-iNf/fsDfuG9xPch7u0LQRNPmxBs=" + }, "crypto-random-string": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz", @@ -28544,6 +28594,11 @@ "call-bind": "^1.0.2" } }, + "is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" + }, "is-callable": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.4.tgz", @@ -28590,15 +28645,6 @@ "is-extglob": "^2.1.1" } }, - "is-ip": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/is-ip/-/is-ip-3.1.0.tgz", - "integrity": "sha512-35vd5necO7IitFPjd/YBeqwWnyDWbuLH9ZXQdMfDA8TEo7pv5X8yfrvVO3xbJbLUlERCMvf6X0hTUamQxCYJ9Q==", - "dev": true, - "requires": { - "ip-regex": "^4.0.0" - } - }, "is-module": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", @@ -30573,6 +30619,16 @@ "tmpl": "1.0.5" } }, + "md5": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/md5/-/md5-2.3.0.tgz", + "integrity": "sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==", + "requires": { + "charenc": "0.0.2", + "crypt": "0.0.2", + "is-buffer": "~1.1.6" + } + }, "mdn-data": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.4.tgz", @@ -30726,21 +30782,20 @@ "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" }, "mkcert": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/mkcert/-/mkcert-1.4.0.tgz", - "integrity": "sha512-0cQXdsoOKq7EHS4Jkxnj16JA4eTt/noXUcaFr44aFAlqfgdCmIGqfGcGoosdXf46YzbaEfEQmrsHGYFV9XvpmA==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/mkcert/-/mkcert-1.5.0.tgz", + "integrity": "sha512-Jc5tW6XGpRZR/GXimztRtvv0Q46Qkv0/iqy06sBOldb1I5FfkeD7/LDPOMV9fnV6Nkx8YbNv+Z/8AmmQGfPtgg==", "dev": true, "requires": { - "commander": "^6.1.0", - "is-ip": "^3.1.0", - "node-forge": "^0.10.0", - "random-int": "^2.0.1" + "commander": "^8.3.0", + "ip-regex": "^4.3.0", + "node-forge": "^1.2.1" }, "dependencies": { "commander": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz", - "integrity": "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==", + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", "dev": true } } @@ -30773,9 +30828,9 @@ "integrity": "sha1-iZ8R2WhuXgXLkbNdXw5jt3PPyQE=" }, "nanoid": { - "version": "3.1.30", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.30.tgz", - "integrity": "sha512-zJpuPDwOv8D2zq2WRoMe1HsfZthVewpel9CAvTfc/2mBD1uUT/agc5f7GHGWXlYkFvi1mVxe4IjvP2HNrop7nQ==" + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.2.0.tgz", + "integrity": "sha512-fmsZYa9lpn69Ad5eDn7FMcnnSR+8R34W9qJEijxYhTbfOWzr22n1QxCMzXLK+ODyW2973V3Fux959iQoUxzUIA==" }, "natural-compare": { "version": "1.4.0", @@ -30802,9 +30857,9 @@ } }, "node-fetch": { - "version": "2.6.6", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.6.tgz", - "integrity": "sha512-Z8/6vRlTUChSdIgMa51jxQ4lrw/Jy5SOW10ObaA47/RElsAN2c5Pn8bTgFGWn/ibwzXTE8qwr1Yzx28vsecXEA==", + "version": "2.6.7", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", + "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", "dev": true, "requires": { "whatwg-url": "^5.0.0" @@ -30835,10 +30890,9 @@ } }, "node-forge": { - "version": "0.10.0", - "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.10.0.tgz", - "integrity": "sha512-PPmu8eEeG9saEUvI97fm4OYxXVB6bFvyNTyiUOBichBpFG8A1Ljw3bY62+5oOjDEMHRnd0Y7HQ+x7uzxOzC6JA==", - "dev": true + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.2.1.tgz", + "integrity": "sha512-Fcvtbb+zBcZXbTTVwqGA5W+MKBj56UjVRevvchv5XrcyXbmNdesfZL37nlcWOfpgHhgmxApw3tQbTr4CqNmX4w==" }, "node-int64": { "version": "0.4.0", @@ -32263,12 +32317,6 @@ "performance-now": "^2.1.0" } }, - "random-int": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/random-int/-/random-int-2.0.1.tgz", - "integrity": "sha512-YALjWK2Rt9EMIv9BF/3mvlzFWQathsvb5UZmN1QmhfIOfcQYXc/UcLzg0ablqesSBpBVLt2Tlwv/eTuBh4LXUQ==", - "dev": true - }, "randombytes": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", @@ -33415,13 +33463,6 @@ "integrity": "sha512-cUdFiCbKoa1mZ6osuJs2uDHrs0k0oprsKveFiiaBKCNq3SYyb5gs2HxhQyDNLCmL51ZZThqi4YNDpCK6GOP1iQ==", "requires": { "node-forge": "^1.2.0" - }, - "dependencies": { - "node-forge": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.2.1.tgz", - "integrity": "sha512-Fcvtbb+zBcZXbTTVwqGA5W+MKBj56UjVRevvchv5XrcyXbmNdesfZL37nlcWOfpgHhgmxApw3tQbTr4CqNmX4w==" - } } }, "semver": { diff --git a/Converge/ClientApp/package.json b/Converge/ClientApp/package.json index 5489438..181ef30 100644 --- a/Converge/ClientApp/package.json +++ b/Converge/ClientApp/package.json @@ -23,7 +23,8 @@ "@types/react": "16.8.0", "@types/react-dom": "16.8.0", "@uifabric/icons": "7.5.23", - "axios": "0.24.0", + "axios": "0.21.4", + "axios-cache-adapter": "2.7.3", "bingmaps": "2.0.3", "dayjs": "1.10.5", "lodash": "4.17.21", diff --git a/Converge/ClientApp/src/App.tsx b/Converge/ClientApp/src/App.tsx index ab497dc..6dcc0bc 100644 --- a/Converge/ClientApp/src/App.tsx +++ b/Converge/ClientApp/src/App.tsx @@ -19,7 +19,6 @@ import "./app.css"; import { SearchContextProvider } from "./providers/SearchProvider"; import { AppSettingProvider } from "./providers/AppSettingsProvider"; import { TeamsContextProvider } from "./providers/TeamsContextProvider"; -import { PlacePhotosProvider } from "./providers/PlacePhotosProvider"; import ContextLoader from "./ContextLoader"; import AppBanner from "./utilities/AppBanner"; @@ -37,26 +36,24 @@ const App: React.FC = () => ( - - - - - ( - - - - )} - /> - - - + + + + ( + + + + )} + /> + + diff --git a/Converge/ClientApp/src/api/AuthenticationService.ts b/Converge/ClientApp/src/api/AuthenticationService.ts index 55c3265..6ac389f 100644 --- a/Converge/ClientApp/src/api/AuthenticationService.ts +++ b/Converge/ClientApp/src/api/AuthenticationService.ts @@ -2,7 +2,8 @@ // Licensed under the MIT License. import * as microsoftTeams from "@microsoft/teams-js"; -import axios, { AxiosStatic } from "axios"; +import { AxiosInstance } from "axios"; +import { setup } from "axios-cache-adapter"; async function getSSOToken(): Promise { return new Promise((resolve, reject) => { @@ -13,13 +14,22 @@ async function getSSOToken(): Promise { }); } -const getAxiosClient = async (): Promise => { +let api: AxiosInstance; + +const getAxiosClient = async (): Promise => { const accessToken = await getSSOToken(); + if (!api) { + // Allow callers to configure caching on a per-endpoint basis + api = setup({ + cache: { + maxAge: 0, + }, + }); + api.defaults.headers.common["Content-Type"] = "application/json"; + } + api.defaults.headers.common.Authorization = `Bearer ${accessToken}`; - axios.defaults.headers.common.Authorization = `Bearer ${accessToken}`; - axios.defaults.headers.common["Content-Type"] = "application/json"; - - return axios; + return api; }; export default getAxiosClient; diff --git a/Converge/ClientApp/src/api/buildingService.test.ts b/Converge/ClientApp/src/api/buildingService.test.ts deleted file mode 100644 index 4d88d5f..0000000 --- a/Converge/ClientApp/src/api/buildingService.test.ts +++ /dev/null @@ -1,145 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -import CampusToCollaborate from "../types/CampusToCollaborate"; -import { generatePlaceDetailsRetrievalKey, generatePlaceDetailsStoreKey } from "./buildingService"; - -const dummyData: CampusToCollaborate[] = [ - { - availableSlots: 12, - identity: "Campus1Building1", - displayName: "Campus 1 Building 1", - street: "Campus1Street", - city: "CampusTown", - state: "Campington", - postalCode: "98124", - countryOrRegion: "Campusopolis", - phone: "12345678901", - capacity: 24, - building: "Building 1", - label: "C1B1", - audioDeviceName: "Campus one Building one", - videoDeviceName: "Campus one Building one", - displayDeviceName: "Campus one Building one", - floor: "1", - tags: ["Campus 1", "Building 1", "Floor 1"], - locality: "UTC", - sharePointID: "Campus1Building1", - }, - { - availableSlots: 8, - identity: "Campus1Building2", - displayName: "Campus 1 Building 2", - street: "Campus1Street", - city: "CampusTown", - state: "Campington", - postalCode: "98124", - countryOrRegion: "Campusopolis", - phone: "12345678902", - capacity: 34, - building: "Building 2", - label: "C1B2", - audioDeviceName: "Campus one Building two", - videoDeviceName: "Campus one Building two", - displayDeviceName: "Campus one Building two", - floor: "1", - tags: ["Campus 1", "Building 2", "Floor 1"], - locality: "UTC", - sharePointID: "Campus1Building2", - }, - { - availableSlots: 16, - identity: "Campus2Building1", - displayName: "Campus 2 Building 1", - street: "Campus2Street", - city: "CampusTown", - state: "Campington", - postalCode: "98124", - countryOrRegion: "Campusopolis", - phone: "12345678903", - capacity: 24, - building: "Building 1", - label: "C2B1", - audioDeviceName: "Campus two Building one", - videoDeviceName: "Campus two Building one", - displayDeviceName: "Campus two Building one", - floor: "2", - tags: ["Campus 2", "Building 1", "Floor 2"], - locality: "UTC", - sharePointID: "Campus2Building1", - }, -]; - -describe("place details caching dependencies", () => { - const firstItem = dummyData[0]; - const secondItem = dummyData[1]; - - const originalDateRange = { - start: new Date("2021-12-17T03:15:00"), - end: new Date("2021-12-17T05:15:00"), - }; - const matchingDateRange = { - start: new Date("2021-12-17T03:50:00"), - end: new Date("2021-12-17T05:50:00"), - }; - const nonMatchingDateRange = { - start: new Date("2021-12-17T04:50:00"), - end: new Date("2021-12-17T06:50:00"), - }; - - const originalStoreKey = generatePlaceDetailsStoreKey(firstItem, originalDateRange); - const matchingStoreKey = generatePlaceDetailsStoreKey(firstItem, matchingDateRange); - const nonMatchingDateStoreKey = generatePlaceDetailsStoreKey(firstItem, nonMatchingDateRange); - const nonMatchingCampusStoreKey = generatePlaceDetailsStoreKey(secondItem, matchingDateRange); - - const originalRetrievalKey = generatePlaceDetailsRetrievalKey( - firstItem.identity, - originalDateRange, - ); - const matchRetrievalKey = generatePlaceDetailsRetrievalKey( - firstItem.identity, - matchingDateRange, - ); - const nonMatchDateRetrievalKey = generatePlaceDetailsRetrievalKey( - firstItem.identity, - nonMatchingDateRange, - ); - const nonMatchingCampusRetrievalKey = generatePlaceDetailsRetrievalKey( - secondItem.identity, - matchingDateRange, - ); - - test("Expect retrieval key with same params to match storeKey.", () => { - expect(originalRetrievalKey).toBe(originalStoreKey); - }); - - test("Buildings with same storekey and but different minute on date still return same key.", () => { - expect(matchingStoreKey).toBe(originalStoreKey); - expect(matchingStoreKey).toBe(originalRetrievalKey); - }); - - test("Expect store key with different hours to not match.", () => { - expect(nonMatchingDateStoreKey).not.toBe(originalStoreKey); - expect(nonMatchingDateStoreKey).not.toBe(originalRetrievalKey); - }); - - test("Expect store key with different campus to not match.", () => { - expect(nonMatchingCampusStoreKey).not.toBe(originalStoreKey); - expect(nonMatchingCampusStoreKey).not.toBe(originalRetrievalKey); - }); - - test("Expect retrieval key with same hour to match.", () => { - expect(matchRetrievalKey).toBe(originalStoreKey); - expect(matchRetrievalKey).toBe(originalRetrievalKey); - }); - - test("Expect retrieval key with different hour to not match.", () => { - expect(nonMatchDateRetrievalKey).not.toBe(originalStoreKey); - expect(nonMatchDateRetrievalKey).not.toBe(originalRetrievalKey); - }); - - test("Expect retrieval key with different campus to not match.", () => { - expect(nonMatchingCampusRetrievalKey).not.toBe(originalStoreKey); - expect(nonMatchingCampusRetrievalKey).not.toBe(originalRetrievalKey); - }); -}); diff --git a/Converge/ClientApp/src/api/buildingService.ts b/Converge/ClientApp/src/api/buildingService.ts index 56a24bf..8c2e2f1 100644 --- a/Converge/ClientApp/src/api/buildingService.ts +++ b/Converge/ClientApp/src/api/buildingService.ts @@ -8,8 +8,8 @@ import ExchangePlace, { PhotoType, PlaceType } from "../types/ExchangePlace"; import ExchangePlacePhoto from "../types/ExchangePlacePhoto"; import Schedule from "../types/Schedule"; import UpcomingBuildingsResponse from "../types/UpcomingBuildingsResponse"; -import createCachedQuery, { CachedQuery } from "../utilities/CachedQuery"; import getAxiosClient from "./AuthenticationService"; +import Constants from "../utilities/Constants"; interface IGetBuildingPlacesRequestParams { topCount?: number; @@ -95,7 +95,7 @@ interface PlaceDetailsQueryParams { start: Date, end: Date, } -const getPlaceDetails = async ( +export const getPlaceDetails = async ( id: string, { start, @@ -108,28 +108,14 @@ const getPlaceDetails = async ( start, end, }, + cache: { + maxAge: Constants.TWO_HOURS_IN_MILLISECONDS, + }, }); return request.data.result; }; -const createCacheTimestamp = (date: Date): string => `${date.getDate()}/${date.getMonth()}/${date.getFullYear()}T${date.getHours()}`; - -export const generatePlaceDetailsStoreKey = ( - campus: CampusToCollaborate, - { - start, - end, - }: PlaceDetailsQueryParams, -): string => `${campus.identity}-${createCacheTimestamp(start)}/${createCacheTimestamp(end)}`; - -export const generatePlaceDetailsRetrievalKey = (search: string, { - start, - end, -}: PlaceDetailsQueryParams): string => `${search}-${createCacheTimestamp(start)}/${createCacheTimestamp(end)}`; - -type CachedPlaceDetailsQuery = CachedQuery; - -interface PlacePhotosResult { +export interface PlacePhotosResult { sharePointId: string; photos: ExchangePlacePhoto[]; coverPhoto?: ExchangePlacePhoto; @@ -137,32 +123,6 @@ interface PlacePhotosResult { allOtherPhotos: ExchangePlacePhoto[]; } -// Function that returns a cached getUserCoordinates function. -export function createCachedPlaceDetailsQuery(): CachedPlaceDetailsQuery { - return createCachedQuery( - generatePlaceDetailsStoreKey, - generatePlaceDetailsRetrievalKey, - (entities: string[], params: PlaceDetailsQueryParams) => getPlaceDetails(entities[0], params) - .then((item) => [item]), - ); -} - -const getPlacePhotos = async ( - sharePointId: string, -): Promise => { - const axios = await getAxiosClient(); - const request = await axios.get>(`/api/places/${sharePointId}/photos`); - return request.data.result; -}; - -const generatePlacePhotosStoreKey = ( - photos: PlacePhotosResult, -): string => photos.sharePointId; - -const generatePlacePhotosRetrievalKey = (search: string): string => search; - -type CachedPlacePhotosQuery = CachedQuery; - function generateResultObject( sharePointId: string, photos: ExchangePlacePhoto[], @@ -182,14 +142,14 @@ function generateResultObject( }; } -// Function that returns a cached getUserCoordinates function. -export function createCachedPlacePhotosQuery(): CachedPlacePhotosQuery { - return createCachedQuery( - generatePlacePhotosStoreKey, - generatePlacePhotosRetrievalKey, - (entities: string[]) => getPlacePhotos(entities[0]) - // Add sharepointid to results for caching purposes. - .then((photos) => generateResultObject(entities[0], photos)) - .then((item) => [item]), - ); -} +export const getPlacePhotos = async ( + sharePointId: string, +): Promise => { + const axios = await getAxiosClient(); + const request = await axios.get>(`/api/places/${sharePointId}/photos`, { + cache: { + maxAge: Constants.TWO_HOURS_IN_MILLISECONDS, + }, + }); + return generateResultObject(sharePointId, request.data.result); +}; diff --git a/Converge/ClientApp/src/api/searchService.ts b/Converge/ClientApp/src/api/searchService.ts index 18a95e0..a97e380 100644 --- a/Converge/ClientApp/src/api/searchService.ts +++ b/Converge/ClientApp/src/api/searchService.ts @@ -9,7 +9,7 @@ import VenuesToCollaborateResponse from "../types/VenuesToCollaborateResponse"; import VenueReviewsResponse from "../types/VenueReviewsResponse"; import VenueDetails from "../types/VenueDetails"; import getAxiosClient from "./AuthenticationService"; -import createCachedQuery, { CachedQuery } from "../utilities/CachedQuery"; +import Constants from "../utilities/Constants"; export const searchCampusesToCollaborate = async ( campusesToCollaborateRequest: CampusesToCollaborateRequest, @@ -34,28 +34,14 @@ export const getVenueDetails = async ( venueId: string, ): Promise => { const axios = await getAxiosClient(); - const request = await axios.get>(`/api/search/venues/${venueId}/details`); + const request = await axios.get>(`/api/search/venues/${venueId}/details`, { + cache: { + maxAge: Constants.TWO_HOURS_IN_MILLISECONDS, + }, + }); return request.data.result; }; -const generateVenueDetailsStoreKey = ( - venue: VenueDetails, -) => venue.venueId; - -const generateVenueDetailsRetrievalKey = (search: string) => search; - -type CachedVenueDetailsQuery = CachedQuery; - -// Function that returns a cached getUserCoordinates function. -export function createCachedVenueDetailsQuery(): CachedVenueDetailsQuery { - return createCachedQuery( - generateVenueDetailsStoreKey, - generateVenueDetailsRetrievalKey, - (entities: string[]) => getVenueDetails(entities[0]) - .then((item) => [item]), - ); -} - export const getReviews = async ( venueId: string, ): Promise => { diff --git a/Converge/ClientApp/src/providers/PlacePhotosProvider.tsx b/Converge/ClientApp/src/providers/PlacePhotosProvider.tsx deleted file mode 100644 index 8119108..0000000 --- a/Converge/ClientApp/src/providers/PlacePhotosProvider.tsx +++ /dev/null @@ -1,12 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -import { createCachedPlacePhotosQuery } from "../api/buildingService"; -import createCachedServiceProvider from "../utilities/CachedServiceProvider"; - -const [ - PlacePhotosProvider, - usePlacePhotos, -] = createCachedServiceProvider(createCachedPlacePhotosQuery()); - -export { PlacePhotosProvider, usePlacePhotos }; diff --git a/Converge/ClientApp/src/tabs/collaborate/components/CampusPlacePanel.tsx b/Converge/ClientApp/src/tabs/collaborate/components/CampusPlacePanel.tsx index 848708e..ce6bb00 100644 --- a/Converge/ClientApp/src/tabs/collaborate/components/CampusPlacePanel.tsx +++ b/Converge/ClientApp/src/tabs/collaborate/components/CampusPlacePanel.tsx @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -import React, { useEffect, useMemo } from "react"; +import React, { useEffect, useMemo, useState } from "react"; import { Box, Image, Flex, Button, Divider, Text, } from "@fluentui/react-northstar"; @@ -19,7 +19,7 @@ import { import ImagePlaceholder from "../../../utilities/ImagePlaceholder"; import { useConvergeSettingsContextProvider } from "../../../providers/ConvergeSettingsProvider"; import CampusPlacePanelStyles from "../styles/CampusPlacePanelStyles"; -import { usePlacePhotos } from "../../../providers/PlacePhotosProvider"; +import { getPlacePhotos, PlacePhotosResult } from "../../../api/buildingService"; interface Props { setOpen: (open: boolean) => void; @@ -39,15 +39,12 @@ const CampusPlacePanel: React.FC = (props) => { } = props; const classes = CampusPlacePanelStyles(); - const [, - placePhotos = [],, - getPlacePhotos, - ] = usePlacePhotos(); + const [placePhotos, setPlacePhotos] = useState(undefined); const images = useMemo(() => { const img: string[] = []; - const cover = placePhotos?.[0]?.coverPhoto?.url; - const floorPlan = placePhotos?.[0]?.floorPlan?.url; + const cover = placePhotos?.coverPhoto?.url; + const floorPlan = placePhotos?.floorPlan?.url; if (cover) { img.push(cover); } @@ -59,7 +56,8 @@ const CampusPlacePanel: React.FC = (props) => { useEffect(() => { if (place.sharePointID) { - getPlacePhotos([place.sharePointID]); + getPlacePhotos(place.sharePointID) + .then(setPlacePhotos); } }, [place.sharePointID]); diff --git a/Converge/ClientApp/src/tabs/collaborate/components/FavoritesToCollaborate.tsx b/Converge/ClientApp/src/tabs/collaborate/components/FavoritesToCollaborate.tsx index 3077d18..c0213aa 100644 --- a/Converge/ClientApp/src/tabs/collaborate/components/FavoritesToCollaborate.tsx +++ b/Converge/ClientApp/src/tabs/collaborate/components/FavoritesToCollaborate.tsx @@ -11,11 +11,11 @@ import { useBoolean } from "@fluentui/react-hooks"; import dayjs from "dayjs"; import CampusToCollaborate from "../../../types/CampusToCollaborate"; import VenueToCollaborate from "../../../types/VenueToCollaborate"; -import { createCachedVenueDetailsQuery } from "../../../api/searchService"; +import { getVenueDetails } from "../../../api/searchService"; import VenueDetails from "../../../types/VenueDetails"; import CollaborationPlaceResults from "./CollaborationPlaceResults"; import CollaborationPlaceDetails from "./CollaborationPlaceDetails"; -import { createCachedPlaceDetailsQuery } from "../../../api/buildingService"; +import { getPlaceDetails } from "../../../api/buildingService"; import { useConvergeSettingsContextProvider } from "../../../providers/ConvergeSettingsProvider"; import FavoritesToCollaborateStyles from "../styles/FavoritesToCollaborateStyles"; import { @@ -50,8 +50,6 @@ function createVenueToCollaborate(v: VenueDetails): VenueToCollaborate { transactions: v.transactions, }; } -const { getItems: getPlaceDetails } = createCachedPlaceDetailsQuery(); -const { getItems: getVenueDetails } = createCachedVenueDetailsQuery(); const FavoritesToCollaborate: React.FC = (props) => { const classes = FavoritesToCollaborateStyles(); @@ -78,8 +76,7 @@ const FavoritesToCollaborate: React.FC = (props) => { if (convergeSettings?.favoriteCampusesToCollaborate) { const placeDetails = await Promise.all( convergeSettings.favoriteCampusesToCollaborate - .map((v) => getPlaceDetails([v], { start: new Date(), end: dayjs().utc().add(30, "minute").toDate() }) - .then((results) => results[0]) + .map((v) => getPlaceDetails(v, { start: new Date(), end: dayjs().utc().add(30, "minute").toDate() }) .catch(() => { setIsError(true); const isErrorPlace = 0 as unknown as CampusToCollaborate; @@ -90,8 +87,7 @@ const FavoritesToCollaborate: React.FC = (props) => { } if (convergeSettings?.favoriteVenuesToCollaborate) { const venueDetails = await Promise.all( - convergeSettings.favoriteVenuesToCollaborate.map((v) => getVenueDetails([v]) - .then((results) => results[0]) + convergeSettings.favoriteVenuesToCollaborate.map((v) => getVenueDetails(v) .catch(() => { setIsError(true); const isErrorVenue = 0 as unknown as VenueToCollaborate; diff --git a/Converge/ClientApp/src/tabs/workspace/components/BookPlaceModal.tsx b/Converge/ClientApp/src/tabs/workspace/components/BookPlaceModal.tsx index ec63ba7..683baa8 100644 --- a/Converge/ClientApp/src/tabs/workspace/components/BookPlaceModal.tsx +++ b/Converge/ClientApp/src/tabs/workspace/components/BookPlaceModal.tsx @@ -13,7 +13,7 @@ import dayjs, { Dayjs } from "dayjs"; import { IComboBox, IComboBoxOption } from "@fluentui/react"; import TimePicker from "./TimePicker"; import PlaceCarousel from "./PlaceCarousel"; -import ExchangePlace, { PhotoType, PlaceType } from "../../../types/ExchangePlace"; +import ExchangePlace, { PlaceType } from "../../../types/ExchangePlace"; import getPlaceMaxReserved, { getRoomAvailability } from "../../../api/placeService"; import { logEvent } from "../../../utilities/LogWrapper"; import { @@ -23,9 +23,9 @@ import { TimePickerChangeHandler, TimePickerContext, TimePickerProvider } from " import DatePickerPrimary from "../../../utilities/datePickerPrimary"; import PlaceAmmenities from "./PlaceAmmenities"; import BookPlaceModalStyles from "../styles/BookPlaceModalStyles"; -import { usePlacePhotos } from "../../../providers/PlacePhotosProvider"; import { useConvergeSettingsContextProvider } from "../../../providers/ConvergeSettingsProvider"; import { setSettings } from "../../../api/meService"; +import { getPlacePhotos, PlacePhotosResult } from "../../../api/buildingService"; type Props = { place: ExchangePlace, @@ -59,10 +59,7 @@ const BookPlaceModal: React.FC = (props) => { } = props; const [maxReserved, setMaxReserved] = useState(0); const [isAvailable, setIsAvailable] = useState(true); - const [, - placePhotos,, - getPlacePhotos, - ] = usePlacePhotos(); + const [placePhotos, setPlacePhotos] = useState(undefined); const [photoUrl, setPhotoUrl] = useState(undefined); const [floorPlanUrl, setFloorPlanUrl] = useState(undefined); const [otherPhotos, setOtherPhotos] = useState([]); @@ -173,28 +170,22 @@ const BookPlaceModal: React.FC = (props) => { }, [isAllDay, start, end]); useEffect(() => { - if (placePhotos && placePhotos.length === 1) { - const cover = placePhotos[0].photos.find((p) => p.photoType === PhotoType.Cover); - const floorPlan = placePhotos[0].photos.find((p) => p.photoType === PhotoType.FloorPlan); - const allOtherPhotos = placePhotos[0].photos.filter( - (p) => p.photoType !== PhotoType.FloorPlan && p.photoType !== PhotoType.Cover, - ).map((p) => p.url); - - if (cover) { - setPhotoUrl(cover.url); + if (placePhotos) { + if (placePhotos.coverPhoto?.url) { + setPhotoUrl(placePhotos.coverPhoto.url); } - if (floorPlan) { - setFloorPlanUrl(floorPlan.url); + if (placePhotos.floorPlan) { + setFloorPlanUrl(placePhotos.floorPlan.url); } - if (allOtherPhotos.length) { - setOtherPhotos(allOtherPhotos); + if (placePhotos.allOtherPhotos.length) { + setOtherPhotos(placePhotos.allOtherPhotos.map((photo) => photo.url)); } } }, [placePhotos]); useEffect(() => { if (place.sharePointID) { - getPlacePhotos([place.sharePointID]); + getPlacePhotos(place.sharePointID).then(setPlacePhotos); } }, [place.sharePointID]); diff --git a/Converge/ClientApp/src/tabs/workspace/components/CollaborationCampusPlaceCard.tsx b/Converge/ClientApp/src/tabs/workspace/components/CollaborationCampusPlaceCard.tsx index 5e90409..a264e8e 100644 --- a/Converge/ClientApp/src/tabs/workspace/components/CollaborationCampusPlaceCard.tsx +++ b/Converge/ClientApp/src/tabs/workspace/components/CollaborationCampusPlaceCard.tsx @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -import React, { useEffect } from "react"; +import React, { useEffect, useState } from "react"; import { Image, Text, Flex, FlexItem, StarIcon, Box, } from "@fluentui/react-northstar"; @@ -14,8 +14,8 @@ import { } from "../../../types/LoggerTypes"; import { useConvergeSettingsContextProvider } from "../../../providers/ConvergeSettingsProvider"; import CollaborationCampusPlaceCardStyles from "../styles/CollaborationCampusPlaceCardStyles"; -import { usePlacePhotos } from "../../../providers/PlacePhotosProvider"; import { logEvent } from "../../../utilities/LogWrapper"; +import { getPlacePhotos, PlacePhotosResult } from "../../../api/buildingService"; type Props = { placeToCollaborate: CampusToCollaborate, @@ -27,17 +27,19 @@ const CollaborationCampusPlaceCard:React.FC = (props) => { const { convergeSettings } = useConvergeSettingsContextProvider(); const classes = CollaborationCampusPlaceCardStyles(); const ammenities = getAmmenities(placeToCollaborate); - const [ - placePhotosLoading, - placePhotos, - placePhotosError, - getPlacePhotos, - ] = usePlacePhotos(); - const photoUrl = placePhotos?.[0].coverPhoto?.url; + const [placePhotos, setPlacePhotos] = useState(undefined); + const [placePhotosLoading, setPlacePhotosLoading] = useState(false); + const [placePhotosError, setPlacePhotosError] = useState(false); + + const photoUrl = placePhotos?.coverPhoto?.url; useEffect(() => { if (placeToCollaborate.sharePointID) { - getPlacePhotos([placeToCollaborate.sharePointID]); + setPlacePhotosLoading(true); + getPlacePhotos(placeToCollaborate.sharePointID) + .then(setPlacePhotos) + .catch(() => setPlacePhotosError(true)) + .finally(() => setPlacePhotosLoading(false)); } }, [placeToCollaborate.sharePointID]); diff --git a/Converge/ClientApp/src/tabs/workspace/components/PlaceCard.tsx b/Converge/ClientApp/src/tabs/workspace/components/PlaceCard.tsx index 4184acb..ac5ae1f 100644 --- a/Converge/ClientApp/src/tabs/workspace/components/PlaceCard.tsx +++ b/Converge/ClientApp/src/tabs/workspace/components/PlaceCard.tsx @@ -22,10 +22,10 @@ import Notifications from "../../../utilities/ToastManager"; import ImagePlaceholder from "../../../utilities/ImagePlaceholder"; import PlaceCardStyles from "../styles/PlaceCardStyles"; import { updateMyPredictedLocation } from "../../../api/meService"; -import { usePlacePhotos } from "../../../providers/PlacePhotosProvider"; import { useConvergeSettingsContextProvider } from "../../../providers/ConvergeSettingsProvider"; import { AddRecentBuildings } from "../../../utilities/RecentBuildingsManager"; import getPlaceMaxReserved, { getRoomAvailability } from "../../../api/placeService"; +import { getPlacePhotos, PlacePhotosResult } from "../../../api/buildingService"; type Props = { place: ExchangePlace, @@ -53,15 +53,12 @@ const PlaceCard: React.FC = (props) => { const [availability, setAvailability] = useState(0); const [isAvailable, setIsAvailable] = useState(false); const [availabilityLoading, setAvailabilityLoading] = useState(false); + const [placePhotos, setPlacePhotos] = useState(undefined); - const [, - placePhotos,, - getPlacePhotos, - ] = usePlacePhotos(); - const photoUrl = placePhotos?.[0].coverPhoto?.url; + const photoUrl = placePhotos?.coverPhoto?.url; useEffect(() => { if (place.sharePointID) { - getPlacePhotos([place.sharePointID]); + getPlacePhotos(place.sharePointID).then(setPlacePhotos); } }, [place.sharePointID]); diff --git a/Converge/ClientApp/src/utilities/CachedServiceProvider.tsx b/Converge/ClientApp/src/utilities/CachedServiceProvider.tsx deleted file mode 100644 index 69caab2..0000000 --- a/Converge/ClientApp/src/utilities/CachedServiceProvider.tsx +++ /dev/null @@ -1,78 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -import React, { useContext } from "react"; -import usePromise from "../hooks/usePromise"; -import { CachedQuery } from "./CachedQuery"; - -type CachedServiceProvider = React.FC; -type GetFunction

= (searchStrings: string[], params: P) => Promise; -type UpdateFunction

= (searchStrings: string[], params: P) => Promise; - -type UseCachedServiceHook = () => [ - boolean, - T[] | undefined | null, - unknown, - GetFunction

, - UpdateFunction

-]; -type CachedServiceProviderReturnType = [ - CachedServiceProvider, - UseCachedServiceHook -]; - -/** - * Function that generates context provider and hook for cached queries - * - * @param cachedService cached query service being modeled. - * @returns Returns provider and hook to access cached query state. - */ -const createCachedServiceProvider = ( - cachedService: CachedQuery, -): CachedServiceProviderReturnType => { - const Context = React.createContext>(cachedService); - - const CachedServiceProvider: React.FC = ({ children }) => { - const value = useContext(Context); - return ( - - {children} - - ); - }; - - const useCachedService: UseCachedServiceHook = () => { - const { getItems, forceUpdate } = useContext(Context); - const [ - loading, - result, - error, - waitFor, - ] = usePromise(); - - const get = ( - searchStrings: string[], - params: P, - ) => waitFor(getItems(searchStrings, params)); - - const update = ( - searchStrings: string[], - params: P, - ) => waitFor(forceUpdate(searchStrings, params)); - - return [ - loading, - result, - error, - get, - update, - ]; - }; - - return [ - CachedServiceProvider, - useCachedService, - ]; -}; - -export default createCachedServiceProvider; diff --git a/Converge/ClientApp/src/utilities/Constants.ts b/Converge/ClientApp/src/utilities/Constants.ts new file mode 100644 index 0000000..fe75ed7 --- /dev/null +++ b/Converge/ClientApp/src/utilities/Constants.ts @@ -0,0 +1,6 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +export default { + TWO_HOURS_IN_MILLISECONDS: 120 * 60 * 1000, +}; diff --git a/Converge/ClientApp/yarn.lock b/Converge/ClientApp/yarn.lock index da476d4..35f2dcf 100644 --- a/Converge/ClientApp/yarn.lock +++ b/Converge/ClientApp/yarn.lock @@ -3301,12 +3301,20 @@ "resolved" "https://registry.npmjs.org/axe-core/-/axe-core-4.3.5.tgz" "version" "4.3.5" -"axios@0.24.0": - "integrity" "sha512-Q6cWsys88HoPgAaFAVUb0WpPk0O8iTeisR9IMqy9G8AbO4NlpVknrnQS03zzF9PGAWgO3cgletO3VjV/P7VztA==" - "resolved" "https://registry.npmjs.org/axios/-/axios-0.24.0.tgz" - "version" "0.24.0" +"axios-cache-adapter@2.7.3": + "integrity" "sha512-A+ZKJ9lhpjthOEp4Z3QR/a9xC4du1ALaAsejgRGrH9ef6kSDxdFrhRpulqsh9khsEnwXxGfgpUuDp1YXMNMEiQ==" + "resolved" "https://registry.npmjs.org/axios-cache-adapter/-/axios-cache-adapter-2.7.3.tgz" + "version" "2.7.3" dependencies: - "follow-redirects" "^1.14.4" + "cache-control-esm" "1.0.0" + "md5" "^2.2.1" + +"axios@~0.21.1", "axios@0.21.4": + "integrity" "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==" + "resolved" "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz" + "version" "0.21.4" + dependencies: + "follow-redirects" "^1.14.0" "axobject-query@^2.2.0": "integrity" "sha512-Td525n+iPOOyUQIeBfcASuG6uJsDOITl7Mds5gFyerkWiX7qhUTdYUBlSgNMyVqtSJqwpt1kXGLdUt6SykLMRA==" @@ -3597,6 +3605,11 @@ "resolved" "https://registry.npmjs.org/bytes/-/bytes-3.1.1.tgz" "version" "3.1.1" +"cache-control-esm@1.0.0": + "integrity" "sha512-Fa3UV4+eIk4EOih8FTV6EEsVKO0W5XWtNs6FC3InTfVz+EjurjPfDXY5wZDo/lxjDxg5RjNcurLyxEJBcEUx9g==" + "resolved" "https://registry.npmjs.org/cache-control-esm/-/cache-control-esm-1.0.0.tgz" + "version" "1.0.0" + "call-bind@^1.0.0", "call-bind@^1.0.2": "integrity" "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==" "resolved" "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz" @@ -3722,6 +3735,11 @@ "resolved" "https://registry.npmjs.org/char-regex/-/char-regex-2.0.0.tgz" "version" "2.0.0" +"charenc@0.0.2": + "integrity" "sha1-wKHS86cJLgN3S/qD8UwPxXkKhmc=" + "resolved" "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz" + "version" "0.0.2" + "check-types@^11.1.1": "integrity" "sha512-tzWzvgePgLORb9/3a0YenggReLKAIb2owL03H2Xdoe5pKcUyWRSEQ8xfCar8t2SIAuEDwtmx2da1YB52YuHQMQ==" "resolved" "https://registry.npmjs.org/check-types/-/check-types-11.1.2.tgz" @@ -3862,11 +3880,6 @@ "resolved" "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz" "version" "2.20.3" -"commander@^6.1.0": - "integrity" "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==" - "resolved" "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz" - "version" "6.2.1" - "commander@^6.2.0": "integrity" "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==" "resolved" "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz" @@ -4038,6 +4051,11 @@ "shebang-command" "^2.0.0" "which" "^2.0.1" +"crypt@0.0.2": + "integrity" "sha1-iNf/fsDfuG9xPch7u0LQRNPmxBs=" + "resolved" "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz" + "version" "0.0.2" + "crypto-random-string@^2.0.0": "integrity" "sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==" "resolved" "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz" @@ -5674,7 +5692,7 @@ "resolved" "https://registry.npmjs.org/flatted/-/flatted-3.2.2.tgz" "version" "3.2.2" -"follow-redirects@^1.0.0", "follow-redirects@^1.14.4": +"follow-redirects@^1.0.0", "follow-redirects@^1.14.0": "integrity" "sha512-+hbxoLbFMbRKDwohX8GkTataGqO6Jb7jGwpAlwgy2bIz25XtRm7KEzJM76R1WiNT5SwZkX4Y75SwBolkpmE7iQ==" "resolved" "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.7.tgz" "version" "1.14.7" @@ -6245,7 +6263,7 @@ "has" "^1.0.3" "side-channel" "^1.0.4" -"ip-regex@^4.0.0": +"ip-regex@^4.3.0": "integrity" "sha512-B9ZWJxHHOHUhUjCPrMpLD4xEq35bUTClHM1S6CBU5ixQnkZmwipwgc96vAd7AAGM9TGHvJR+Uss+/Ak6UphK+Q==" "resolved" "https://registry.npmjs.org/ip-regex/-/ip-regex-4.3.0.tgz" "version" "4.3.0" @@ -6297,6 +6315,11 @@ dependencies: "call-bind" "^1.0.2" +"is-buffer@~1.1.6": + "integrity" "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" + "resolved" "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz" + "version" "1.1.6" + "is-callable@^1.1.4", "is-callable@^1.2.4": "integrity" "sha512-nsuwtxZfMX67Oryl9LCQ+upnC0Z0BgpwntpS89m1H/TLF0zNfzfLMV/9Wa/6MZsj0acpEjAO0KF1xT6ZdLl95w==" "resolved" "https://registry.npmjs.org/is-callable/-/is-callable-1.2.4.tgz" @@ -6355,13 +6378,6 @@ dependencies: "is-extglob" "^2.1.1" -"is-ip@^3.1.0": - "integrity" "sha512-35vd5necO7IitFPjd/YBeqwWnyDWbuLH9ZXQdMfDA8TEo7pv5X8yfrvVO3xbJbLUlERCMvf6X0hTUamQxCYJ9Q==" - "resolved" "https://registry.npmjs.org/is-ip/-/is-ip-3.1.0.tgz" - "version" "3.1.0" - dependencies: - "ip-regex" "^4.0.0" - "is-module@^1.0.0": "integrity" "sha1-Mlj7afeMFNW4FdZkM2tM/7ZEFZE=" "resolved" "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz" @@ -7343,6 +7359,15 @@ dependencies: "tmpl" "1.0.5" +"md5@^2.2.1": + "integrity" "sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==" + "resolved" "https://registry.npmjs.org/md5/-/md5-2.3.0.tgz" + "version" "2.3.0" + dependencies: + "charenc" "0.0.2" + "crypt" "0.0.2" + "is-buffer" "~1.1.6" + "mdn-data@2.0.14": "integrity" "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==" "resolved" "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz" @@ -7453,14 +7478,13 @@ "version" "1.2.5" "mkcert@^1.4.0": - "integrity" "sha512-0cQXdsoOKq7EHS4Jkxnj16JA4eTt/noXUcaFr44aFAlqfgdCmIGqfGcGoosdXf46YzbaEfEQmrsHGYFV9XvpmA==" - "resolved" "https://registry.npmjs.org/mkcert/-/mkcert-1.4.0.tgz" - "version" "1.4.0" + "integrity" "sha512-Jc5tW6XGpRZR/GXimztRtvv0Q46Qkv0/iqy06sBOldb1I5FfkeD7/LDPOMV9fnV6Nkx8YbNv+Z/8AmmQGfPtgg==" + "resolved" "https://registry.npmjs.org/mkcert/-/mkcert-1.5.0.tgz" + "version" "1.5.0" dependencies: - "commander" "^6.1.0" - "is-ip" "^3.1.0" - "node-forge" "^0.10.0" - "random-int" "^2.0.1" + "commander" "^8.3.0" + "ip-regex" "^4.3.0" + "node-forge" "^1.2.1" "mkdirp@^0.5.5", "mkdirp@~0.5.1": "integrity" "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==" @@ -7498,9 +7522,9 @@ "thunky" "^1.0.2" "nanoid@^3.1.30": - "integrity" "sha512-zJpuPDwOv8D2zq2WRoMe1HsfZthVewpel9CAvTfc/2mBD1uUT/agc5f7GHGWXlYkFvi1mVxe4IjvP2HNrop7nQ==" - "resolved" "https://registry.npmjs.org/nanoid/-/nanoid-3.1.30.tgz" - "version" "3.1.30" + "integrity" "sha512-fmsZYa9lpn69Ad5eDn7FMcnnSR+8R34W9qJEijxYhTbfOWzr22n1QxCMzXLK+ODyW2973V3Fux959iQoUxzUIA==" + "resolved" "https://registry.npmjs.org/nanoid/-/nanoid-3.2.0.tgz" + "version" "3.2.0" "natural-compare@^1.4.0": "integrity" "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=" @@ -7526,18 +7550,13 @@ "tslib" "^2.0.3" "node-fetch@^2.6.1": - "integrity" "sha512-Z8/6vRlTUChSdIgMa51jxQ4lrw/Jy5SOW10ObaA47/RElsAN2c5Pn8bTgFGWn/ibwzXTE8qwr1Yzx28vsecXEA==" - "resolved" "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.6.tgz" - "version" "2.6.6" + "integrity" "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==" + "resolved" "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz" + "version" "2.6.7" dependencies: "whatwg-url" "^5.0.0" -"node-forge@^0.10.0": - "integrity" "sha512-PPmu8eEeG9saEUvI97fm4OYxXVB6bFvyNTyiUOBichBpFG8A1Ljw3bY62+5oOjDEMHRnd0Y7HQ+x7uzxOzC6JA==" - "resolved" "https://registry.npmjs.org/node-forge/-/node-forge-0.10.0.tgz" - "version" "0.10.0" - -"node-forge@^1.2.0": +"node-forge@^1.2.0", "node-forge@^1.2.1": "integrity" "sha512-Fcvtbb+zBcZXbTTVwqGA5W+MKBj56UjVRevvchv5XrcyXbmNdesfZL37nlcWOfpgHhgmxApw3tQbTr4CqNmX4w==" "resolved" "https://registry.npmjs.org/node-forge/-/node-forge-1.2.1.tgz" "version" "1.2.1" @@ -8687,11 +8706,6 @@ dependencies: "performance-now" "^2.1.0" -"random-int@^2.0.1": - "integrity" "sha512-YALjWK2Rt9EMIv9BF/3mvlzFWQathsvb5UZmN1QmhfIOfcQYXc/UcLzg0ablqesSBpBVLt2Tlwv/eTuBh4LXUQ==" - "resolved" "https://registry.npmjs.org/random-int/-/random-int-2.0.1.tgz" - "version" "2.0.1" - "randombytes@^2.1.0": "integrity" "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==" "resolved" "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz" From d750172b6fd4a3d2709d83d13e3046ee1e733e15 Mon Sep 17 00:00:00 2001 From: "Tej.Neelapala" Date: Mon, 24 Jan 2022 21:11:24 +0000 Subject: [PATCH 13/41] Merged PR 1255: Fix to Bug # 3822 :: Building capacity fails when trying to display in new test tenant. Before submitting this PR, please make ensure you've completed the following: - [ ] Your code builds clean without any errors **or warnings**. - [ ] All unused code has been deleted. - [ ] All TODOs in the code base have an associated ticket in ADO. - [ ] Your squash commit to main has a short descriptive commit message. Describe your PR: If submitting to the FrontEnd UI, include a screenshot of the change: Related work items: #3822 --- Converge/Services/BuildingsService.cs | 4 ++++ Converge/Services/ScheduleService.cs | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/Converge/Services/BuildingsService.cs b/Converge/Services/BuildingsService.cs index 022f796..b6df3b6 100644 --- a/Converge/Services/BuildingsService.cs +++ b/Converge/Services/BuildingsService.cs @@ -263,6 +263,10 @@ namespace Converge.Services double reserved = 0; foreach (ExchangePlace workspace in exchangePlacesResponse.ExchangePlacesList) { + if (workspace.Capacity == 0) + { + continue; + } double workspaceReserved = await placesService.GetReserved(workspace, nonNullStartString, nonNullEndString); reserved += workspaceReserved * ((double) workspace.Capacity / (double) buildingCapacity); } diff --git a/Converge/Services/ScheduleService.cs b/Converge/Services/ScheduleService.cs index f65371c..6b04dbe 100644 --- a/Converge/Services/ScheduleService.cs +++ b/Converge/Services/ScheduleService.cs @@ -27,7 +27,7 @@ namespace Converge.Services end ); double reserved = TimeHelper.GetAverageReserved(DateTime.Parse(start), DateTime.Parse(end), events); - return reserved / workspace.Capacity * 100; + return (workspace.Capacity == 0) ? 0 : reserved / workspace.Capacity * 100; } public async Task GetMaxReserved(string start, string end, string id) From f4202e1e29d08fbcb747d6ef60b2a2a16592d795 Mon Sep 17 00:00:00 2001 From: Ganesh Kotagiri Date: Tue, 25 Jan 2022 03:55:45 +0000 Subject: [PATCH 14/41] Merged PR 1252: UI Bug's Fixed Before submitting this PR, please make ensure you've completed the following: - [ ] Your code builds clean without any errors **or warnings**. - [ ] All unused code has been deleted. - [ ] All TODOs in the code base have an associated ticket in ADO. - [ ] Your squash commit to main has a short descriptive commit message. Describe your PR: Please find below Bug's Id:- 1)BUG 3807 2)BUG 3808 3)BUG 3835 4)BUG 3827 ![image.png](https://dev.azure.com/mwcci/08fbf972-cba0-4622-a98f-aff4c4b4c567/_apis/git/repositories/5d547110-08b1-402c-91d8-153ba56a7d02/pullRequests/1252/attachments/image.png) ![image (2).png](https://dev.azure.com/mwcci/08fbf972-cba0-4622-a98f-aff4c4b4c567/_apis/git/repositories/5d547110-08b1-402c-91d8-153ba56a7d02/pullRequests/1252/attachments/image%20%282%29.png) ![image (3).png](https://dev.azure.com/mwcci/08fbf972-cba0-4622-a98f-aff4c4b4c567/_apis/git/repositories/5d547110-08b1-402c-91d8-153ba56a7d02/pullRequests/1252/attachments/image%20%283%29.png) If submitting to the FrontEnd UI, include a screenshot of the change: Related work items: #3807, #3808, #3827, #3835 --- Converge/ClientApp/src/api/buildingService.ts | 12 +++++++++ .../ClientApp/src/tabs/home/BookWorkspace.tsx | 27 +++++++++++++------ .../src/tabs/home/ConnectTeammates.tsx | 1 - .../src/tabs/home/Table/SelectableTable.tsx | 2 +- .../tabs/home/styles/SelectableTableStyles.ts | 6 +++++ .../ClientApp/src/tabs/workspace/Places.tsx | 4 --- .../workspace/components/BuildingPlaces.tsx | 7 ++--- .../CustomizedPlaceCollectionAccordian.tsx | 4 +++ .../workspace/components/RepeatingBox.tsx | 24 ++++++++++------- .../workspace/styles/BuildingPlacesStyles.ts | 3 +++ .../src/tabs/workspace/styles/PlacesStyles.ts | 6 +++++ .../src/utilities/popupMenuWrapper.tsx | 3 +++ Converge/Controllers/BuildingsController.cs | 21 +++++++++++++++ Converge/Services/AppGraphService.cs | 23 ++++++++++++++++ Converge/Services/BuildingsService.cs | 23 ++++++++++++++++ 15 files changed, 140 insertions(+), 26 deletions(-) diff --git a/Converge/ClientApp/src/api/buildingService.ts b/Converge/ClientApp/src/api/buildingService.ts index 8c2e2f1..95c84a6 100644 --- a/Converge/ClientApp/src/api/buildingService.ts +++ b/Converge/ClientApp/src/api/buildingService.ts @@ -2,6 +2,7 @@ // Licensed under the MIT License. import AutoWrapperResponse from "../types/AutoWrapperResponse"; +import BuildingBasicInfo from "../types/BuildingBasicInfo"; import BuildingSearchInfo from "../types/BuildingSearchInfo"; import CampusToCollaborate from "../types/CampusToCollaborate"; import ExchangePlace, { PhotoType, PlaceType } from "../types/ExchangePlace"; @@ -41,6 +42,17 @@ export const getBuildingPlaces = async ( return request.data.result; }; +export const getBuildingByDisplayName = async ( + buildingDisplayName?:string, +): Promise => { + const axios = await getAxiosClient(); + const request = await axios.get>( + `/api/buildings/buildingByName/${buildingDisplayName}`, { + }, + ); + return request.data.result; +}; + export const getBuildingsByDistance = async ( sourceGeoCoordinates?:string, distanceFromSource?:number, diff --git a/Converge/ClientApp/src/tabs/home/BookWorkspace.tsx b/Converge/ClientApp/src/tabs/home/BookWorkspace.tsx index e269989..9085383 100644 --- a/Converge/ClientApp/src/tabs/home/BookWorkspace.tsx +++ b/Converge/ClientApp/src/tabs/home/BookWorkspace.tsx @@ -17,7 +17,7 @@ import AvailabilityChart from "./components/AvailabilityChart"; import BuildingCapacity from "./components/BuildingCapacity"; import Schedule from "../../types/Schedule"; import ExchangePlace, { PlaceType } from "../../types/ExchangePlace"; -import { getBuildingSchedule, getBuildingPlaces } from "../../api/buildingService"; +import { getBuildingSchedule, getBuildingPlaces, getBuildingByDisplayName } from "../../api/buildingService"; import { getWorkingHours, createEvent } from "../../api/calendarService"; import { DESCRIPTION, @@ -142,14 +142,15 @@ const BookWorkspace: React.FC = () => { const day = dayjs.utc(start); setSelectedBuilding(undefined); await getMyRecommendation(day.year(), day.month() + 1, day.date()) - .then((recommendation) => { + .then(async (recommendation) => { setMyRecommendation(recommendation); if (recommendation !== "Remote" && recommendation !== "Out of Office") { - const building = buildingsList.find((b) => b.displayName === recommendation); - if (building) { - getSchedule(building); - setSelectedBuilding(building); - } + await getBuildingByDisplayName(recommendation).then((building) => { + if (building) { + getSchedule(building); + setSelectedBuilding(building); + } + }).catch(() => setIsError(true)); } }).catch(() => setIsError(true)); }; @@ -217,6 +218,16 @@ const BookWorkspace: React.FC = () => { } setLoading(false); }; + const onClearTextBox = async (isValid:boolean) => { + if (isValid === true) { + setMyRecommendation(myRecommendation); + const building = buildingsList.find((b) => b.displayName === myRecommendation); + if (building) { + getSchedule(building); + setSelectedBuilding(building); + } + } + }; const refreshRecommended = async () => { setLoading(true); @@ -259,7 +270,7 @@ const BookWorkspace: React.FC = () => { - x.displayName)} locationBuildingName={myRecommendation} width="320px" marginContent="5.1rem" value={selectedBuildingName === RECOMMENDED ? "" : selectedBuildingName} placeholderTitle={RECOMMENDED} buttonTitle="See more" otherOptionsList={[]} maxHeight="320px" /> + x.displayName)} locationBuildingName={myRecommendation} width="320px" marginContent="5.1rem" value={selectedBuildingName === RECOMMENDED ? "" : selectedBuildingName} placeholderTitle={RECOMMENDED} buttonTitle="See more" otherOptionsList={[]} maxHeight="320px" clearTextBox={onClearTextBox} /> { headerContent="Connect with teammates" descriptionContent="Improve team collaboration by spending time with people in your network" gridArea="ConnectTeammates" - height="95vh" showCallOut widgetActions={widget} handleCalloutItemClick={openEnterZipCodeDialog} diff --git a/Converge/ClientApp/src/tabs/home/Table/SelectableTable.tsx b/Converge/ClientApp/src/tabs/home/Table/SelectableTable.tsx index 1b8d557..bb15ad3 100644 --- a/Converge/ClientApp/src/tabs/home/Table/SelectableTable.tsx +++ b/Converge/ClientApp/src/tabs/home/Table/SelectableTable.tsx @@ -344,7 +344,7 @@ const SelectableTable: React.FC = (props) => { }} > - +
({ fontSize: "small", color: "red important", }, + tableheight: { + maxHeight: "67vh", + "@media (max-width: 1366px)": { + height: "auto", + }, + }, })); export default SelectableTableStyles; diff --git a/Converge/ClientApp/src/tabs/workspace/Places.tsx b/Converge/ClientApp/src/tabs/workspace/Places.tsx index b03e5b5..2a4b195 100644 --- a/Converge/ClientApp/src/tabs/workspace/Places.tsx +++ b/Converge/ClientApp/src/tabs/workspace/Places.tsx @@ -14,7 +14,6 @@ import PlaceTypeFilter from "./components/PlaceTypeFilter"; import FeatureFilter from "./components/FeatureFilter"; import { useProvider as PlaceProvider } from "../../providers/PlaceFilterProvider"; import { deserializeSubEntityId } from "../../utilities/deepLink"; -import IsThisHelpful from "../../utilities/IsThisHelpful"; import { DESCRIPTION, UISections, UI_SECTION, USER_INTERACTION, } from "../../types/LoggerTypes"; @@ -193,9 +192,6 @@ const Places: React.FC = (props) => { )} {convergeState.buildingListLoading && } - - - ); }; diff --git a/Converge/ClientApp/src/tabs/workspace/components/BuildingPlaces.tsx b/Converge/ClientApp/src/tabs/workspace/components/BuildingPlaces.tsx index e3d4e15..663ed1e 100644 --- a/Converge/ClientApp/src/tabs/workspace/components/BuildingPlaces.tsx +++ b/Converge/ClientApp/src/tabs/workspace/components/BuildingPlaces.tsx @@ -24,6 +24,7 @@ import BuildingPlacesStyles from "../styles/BuildingPlacesStyles"; import PlaceCard from "./PlaceCard"; import RepeatingBox from "./RepeatingBox"; import { useProvider as PlaceFilterProvider } from "../../../providers/PlaceFilterProvider"; +import IsThisHelpful from "../../../utilities/IsThisHelpful"; interface IPlaceResultSetProps { buildingUpn: string; @@ -70,9 +71,6 @@ const BuildingPlaces: React.FC = ({ buildingUpn, placeType return ( @@ -162,6 +160,9 @@ const BuildingPlaces: React.FC = ({ buildingUpn, placeType )} + + + ); }; diff --git a/Converge/ClientApp/src/tabs/workspace/components/CustomizedPlaceCollectionAccordian.tsx b/Converge/ClientApp/src/tabs/workspace/components/CustomizedPlaceCollectionAccordian.tsx index 4bfa011..8038428 100644 --- a/Converge/ClientApp/src/tabs/workspace/components/CustomizedPlaceCollectionAccordian.tsx +++ b/Converge/ClientApp/src/tabs/workspace/components/CustomizedPlaceCollectionAccordian.tsx @@ -16,6 +16,7 @@ import { import ExchangePlace, { PlaceType } from "../../../types/ExchangePlace"; import useBuildingPlaces from "../../../hooks/useBuildingPlaces"; import BuildingBasicInfo from "../../../types/BuildingBasicInfo"; +import IsThisHelpful from "../../../utilities/IsThisHelpful"; interface Props { closestBuilding: BuildingBasicInfo; @@ -214,6 +215,9 @@ const CustomizedPlaceCollectionAccordian: React.FC = (props) => { defaultActiveIndex={newCustomizedPanels.map((p, i) => i)} onActiveIndexChange={handleCustomizedAccordionChange} /> + + + ); }; diff --git a/Converge/ClientApp/src/tabs/workspace/components/RepeatingBox.tsx b/Converge/ClientApp/src/tabs/workspace/components/RepeatingBox.tsx index 1485b47..2d827f3 100644 --- a/Converge/ClientApp/src/tabs/workspace/components/RepeatingBox.tsx +++ b/Converge/ClientApp/src/tabs/workspace/components/RepeatingBox.tsx @@ -3,19 +3,25 @@ import { Box } from "@fluentui/react-northstar"; import React from "react"; +import { useProvider as PlaceProvider } from "../../../providers/PlaceFilterProvider"; +import PlacesStyles from "../styles/PlacesStyles"; const RepeatingBox:React.FC = (props) => { const { children } = props; + const { state } = PlaceProvider(); + const classes = PlacesStyles(); return ( - {children} diff --git a/Converge/ClientApp/src/tabs/workspace/styles/BuildingPlacesStyles.ts b/Converge/ClientApp/src/tabs/workspace/styles/BuildingPlacesStyles.ts index 19fa4ee..7ab2a90 100644 --- a/Converge/ClientApp/src/tabs/workspace/styles/BuildingPlacesStyles.ts +++ b/Converge/ClientApp/src/tabs/workspace/styles/BuildingPlacesStyles.ts @@ -16,6 +16,9 @@ const BuildingPlacesStyles = makeStyles(() => ({ overflowX: "hidden", }, }, + isThisHelpful: { + marginBottom: "2rem", + }, })); export default BuildingPlacesStyles; diff --git a/Converge/ClientApp/src/tabs/workspace/styles/PlacesStyles.ts b/Converge/ClientApp/src/tabs/workspace/styles/PlacesStyles.ts index 2b8033c..9007a79 100644 --- a/Converge/ClientApp/src/tabs/workspace/styles/PlacesStyles.ts +++ b/Converge/ClientApp/src/tabs/workspace/styles/PlacesStyles.ts @@ -12,6 +12,12 @@ const PlacesStyles = makeStyles(() => ({ isThisHelpful: { margin: "2.5em 0 0", }, + placeCardBox: { + height: "49vh", + overflowX: "auto", + }, + cardBox: { + }, })); export default PlacesStyles; diff --git a/Converge/ClientApp/src/utilities/popupMenuWrapper.tsx b/Converge/ClientApp/src/utilities/popupMenuWrapper.tsx index 82c8098..442ae68 100644 --- a/Converge/ClientApp/src/utilities/popupMenuWrapper.tsx +++ b/Converge/ClientApp/src/utilities/popupMenuWrapper.tsx @@ -26,6 +26,7 @@ interface Props { placeholderTitle: string, buttonTitle: string, maxHeight: string, + clearTextBox?: (isValid:boolean) => void; } const PopupMenuWrapper: React.FunctionComponent = (props) => { @@ -51,6 +52,7 @@ const PopupMenuWrapper: React.FunctionComponent = (props) => { const selectedBuilding = state.buildingsList.find((b) => b.displayName === bldg); updateLocation(selectedBuilding?.identity); props.handleDropdownChange(bldg); + props.clearTextBox?.(false); }; useEffect(() => { @@ -67,6 +69,7 @@ const PopupMenuWrapper: React.FunctionComponent = (props) => { setSelectedBuildingName(searchText || ""); updateLocation(undefined); updateSearchString(searchText); + props.clearTextBox?.(true); }; return ( diff --git a/Converge/Controllers/BuildingsController.cs b/Converge/Controllers/BuildingsController.cs index b393403..097d785 100644 --- a/Converge/Controllers/BuildingsController.cs +++ b/Converge/Controllers/BuildingsController.cs @@ -78,6 +78,27 @@ namespace Converge.Controllers } } + /// + /// Get Building details by given display-name of Building. + /// + /// Building-name + /// Building and its details + [HttpGet] + [Route("buildingByName/{buildingDisplayName}")] + public async Task> GetBuildingByDisplayName(string buildingDisplayName) + { + try + { + var result = await buildingsService.GetBuildingByDisplayName(buildingDisplayName); + return Ok(result); + } + catch (Exception ex) + { + logger.LogError(ex, $"Error occurred while getting the Building details for display-name: {buildingDisplayName}."); + throw; + } + } + /// /// Returns all the Conference-rooms belonging to the Building referenced using its UPN, supporting Pagination, /// defaulted to first 100 Conference rooms if there is no value on how many records to skip. diff --git a/Converge/Services/AppGraphService.cs b/Converge/Services/AppGraphService.cs index 567c026..a6f7418 100644 --- a/Converge/Services/AppGraphService.cs +++ b/Converge/Services/AppGraphService.cs @@ -478,6 +478,29 @@ namespace Converge.Services return new GraphRoomsListResponse(placeList, skipToken); } + public async Task> GetRoomListsByDisplayName(List roomListsDisplayNames) + { + StringBuilder predicateBuilder = new StringBuilder(); + for (int index = 0; index < roomListsDisplayNames.Count; ++index) + { + predicateBuilder.Append($"displayName eq '{roomListsDisplayNames[index]}'"); + if (index + 1 < roomListsDisplayNames.Count) + { + predicateBuilder.Append(" or "); + } + } + + var placesUrl = appGraphServiceClient.Places.AppendSegmentToRequestUrl("microsoft.graph.roomList"); + var placesCollPage = await new GraphServicePlacesCollectionRequestBuilder(placesUrl, appGraphServiceClient) + .Request() + .Filter(predicateBuilder.ToString()) + .OrderBy("displayName") + .GetAsync(); + + List placesList = placesCollPage.CurrentPage as List; + return placesList; + } + public async Task GetRoomListById(string roomListIdentity) { List places = await GetRoomListsCollectionByIds(new List { roomListIdentity }); diff --git a/Converge/Services/BuildingsService.cs b/Converge/Services/BuildingsService.cs index b6df3b6..c839b24 100644 --- a/Converge/Services/BuildingsService.cs +++ b/Converge/Services/BuildingsService.cs @@ -142,6 +142,29 @@ namespace Converge.Services return new BasicBuildingsResponse(buildingsList.Select(b => new BuildingBasicInfo(b.Identity, b.DisplayName)).ToList(), roomsListResponse.SkipToken); } + public async Task GetBuildingByDisplayName(string buildingDisplayName) + { + BuildingBasicInfo buildingBasicInfo = null; + + var buildingsDisplayNamesList = new List() { buildingDisplayName }; + List roomsList = await appGraphService.GetRoomListsByDisplayName(buildingsDisplayNamesList); + if (roomsList == null || roomsList.Count != 1) + { + return buildingBasicInfo; + } + + roomsList[0].AdditionalData.TryGetValue("emailAddress", out object buildingObject); + string buildingEmailAddress = Convert.ToString(buildingObject); + if (string.IsNullOrWhiteSpace(buildingEmailAddress)) + { + return buildingBasicInfo; + } + buildingEmailAddress = buildingEmailAddress.Trim(); + + + return new BuildingBasicInfo(buildingEmailAddress, buildingDisplayName); ; + } + public async Task> GetBuildingsBasicInfo(List buildingsUpnList) { List buildingsBasicInfoList = new List(); From f2f1f67fe519142a096078102b3e3a36ed965c3d Mon Sep 17 00:00:00 2001 From: Muhammadhu Abubakkar Date: Tue, 25 Jan 2022 07:06:02 +0000 Subject: [PATCH 15/41] Merged PR 1249: Fixes for bugs 3795 and 3796 Before submitting this PR, please make ensure you've completed the following: - [ ] Your code builds clean without any errors **or warnings**. - [ ] All unused code has been deleted. - [ ] All TODOs in the code base have an associated ticket in ADO. - [ ] Your squash commit to main has a short descriptive commit message. Describe your PR: If submitting to the FrontEnd UI, include a screenshot of the change: Fixes: 1. Bug 3795 - "My List" is set at top of the teammates filter dropdown. 2. Bug 3796 - Default view will change from "My List" and load "Suggested", when "My List" is empty. Related work items: #3795, #3796 --- .../src/providers/TeammateFilterProvider.tsx | 96 ++++++++++++++++--- .../src/tabs/home/ConnectTeammates.tsx | 6 +- 2 files changed, 83 insertions(+), 19 deletions(-) diff --git a/Converge/ClientApp/src/providers/TeammateFilterProvider.tsx b/Converge/ClientApp/src/providers/TeammateFilterProvider.tsx index 02c4a18..a13815f 100644 --- a/Converge/ClientApp/src/providers/TeammateFilterProvider.tsx +++ b/Converge/ClientApp/src/providers/TeammateFilterProvider.tsx @@ -13,11 +13,12 @@ import { DESCRIPTION, OVERLAP_PERCENTAGE, USER_INTERACTION, ViralityMeasures, VIRALITY_MEASURE, } from "../types/LoggerTypes"; import QueryOption from "../types/QueryOption"; +import { useConvergeSettingsContextProvider } from "./ConvergeSettingsProvider"; export enum TeammateList { + MyList = "My List", Suggested = "Suggested", MyOrganization = "My Organization", - MyList = "My List", All = "All", } @@ -27,6 +28,25 @@ export interface Teammate { availableTimes?: TimeLimit[], } +export interface TeammateListSettings { + optionSelected: TeammateList, + optionsOrdered: TeammateList[], +} + +export const teammateFilterListFirst = [ + TeammateList.MyList, + TeammateList.Suggested, + TeammateList.MyOrganization, + TeammateList.All, +]; + +export const teammateFilterSuggestedFirst = [ + TeammateList.Suggested, + TeammateList.MyList, + TeammateList.MyOrganization, + TeammateList.All, +]; + const UPDATE_LOCATION = "UPDATE_LOCATION"; const TEAMMATES_REQUEST = "TEAMMATES_REQUEST"; const TEAMMATES_RESPONSE = "TEAMMATES_RESPONSE"; @@ -37,6 +57,7 @@ const UPDATE_SEARCH_STRING = "UPDATE_SEARCH_STRING"; const UPDATE_SEARCH_QUERY_OPTIONS = "UPDATE_SEARCH_QUERY_OPTIONS"; const SET_TEAMMATE_LOCATION = "SET_TEAMMATE_LOCATION"; const SET_MORE_TEAMMATES_LOADING = "SET_TEAMMATE_LOADING"; +const SET_TEAMMATES_DROPDOWN = "SET_TEAMMATES_DROPDOWN"; interface UpdateTeammateLocationAction { type: typeof UPDATE_LOCATION, @@ -86,6 +107,11 @@ interface SetMoreTeammateLoadingAction { payload: boolean, } +interface SetTeammatesDropdownAction { + type: typeof SET_TEAMMATES_DROPDOWN, + payload: TeammateList[], +} + type ITeammateAction = UpdateTeammateLocationAction | GetTeammatesRequestAction | GetTeammatesResponseAction @@ -95,7 +121,8 @@ type ITeammateAction = UpdateTeammateLocationAction | UpdateSearchString | UpdateSearchQueryOptions | SetTeammateLocationAction - | SetMoreTeammateLoadingAction; + | SetMoreTeammateLoadingAction + | SetTeammatesDropdownAction; type ITeammateState = { list: TeammateList; @@ -108,12 +135,13 @@ type ITeammateState = { searchQueryOptions?: QueryOption[]; teammatesLoading: boolean; moreTeammatesLoading: boolean; + teammatesDropdown: TeammateList[]; }; type ITeammateFilterModel = { state: ITeammateState; updateLocations: (locations: string[]) => void; - updateList: (list: TeammateList) => void; + updateList: (list: TeammateList, force?: boolean) => void; updateDate: (date: Date) => void; getTeammates: (list: TeammateList, date: Date, searchString?: string) => void; updateSearchString: (searchString?: string) => void; @@ -125,17 +153,7 @@ type ITeammateFilterModel = { ) => void; setTeammateLocation: (id: string, location: string) => void; setMoreTeammatesLoading: (buttonLoading: boolean) => void; -}; - -const initialState: ITeammateState = { - teammates: [], - locations: [], - teammatesLoading: false, - list: TeammateList.MyList, - date: new Date(), - getFilteredTeammates: (teammates: Teammate[]) => teammates, - searchQueryOptions: [], - moreTeammatesLoading: false, + setTeammatesDropdown: (listOptions: TeammateList[]) => void; }; const getFilterMethod = (state: ITeammateState) => { @@ -260,12 +278,56 @@ const reducer = (state: ITeammateState, action: ITeammateAction): ITeammateState return newState; } + + case SET_TEAMMATES_DROPDOWN: { + const newState = { + ...state, + teammatesDropdown: action.payload, + }; + return { + ...newState, + getFilteredTeammates: getFilterMethod(newState), + }; + } + default: return state; } }; const TeammateFilterProvider: React.FC = ({ children }) => { + const { convergeSettings } = useConvergeSettingsContextProvider(); + + const getInitialTeammatesListSettings = (): TeammateListSettings => { + if (convergeSettings !== undefined) { + const userMyList = convergeSettings?.myList ?? []; + if (userMyList.length === 0) { + return { + optionSelected: TeammateList.Suggested, + optionsOrdered: teammateFilterSuggestedFirst, + }; + } + } + return { + optionSelected: TeammateList.MyList, + optionsOrdered: teammateFilterListFirst, + }; + }; + + const teammateListPerSessionSetup = getInitialTeammatesListSettings(); + + const initialState: ITeammateState = { + teammates: [], + locations: [], + teammatesLoading: false, + list: teammateListPerSessionSetup.optionSelected, + date: new Date(), + getFilteredTeammates: (teammates: Teammate[]) => teammates, + searchQueryOptions: [], + moreTeammatesLoading: false, + teammatesDropdown: teammateListPerSessionSetup.optionsOrdered, + }; + const [state, dispatch] = useReducer( reducer, initialState, @@ -273,6 +335,7 @@ const TeammateFilterProvider: React.FC = ({ children }) => { const updateLocations = (location: string[]) => { dispatch({ type: UPDATE_LOCATION, payload: location }); + dispatch({ type: UPDATE_LIST, payload: TeammateList.Suggested }); }; const getTeammates = (list: TeammateList, date: Date, searchString?: string) => { @@ -371,6 +434,10 @@ const TeammateFilterProvider: React.FC = ({ children }) => { dispatch({ type: SET_TEAMMATE_LOCATION, payload: { id, location } }); }; + const setTeammatesDropdown = (listOptions: TeammateList[]) => { + dispatch({ type: SET_TEAMMATES_DROPDOWN, payload: listOptions }); + }; + return ( { searchMoreTeammates, setTeammateLocation, setMoreTeammatesLoading, + setTeammatesDropdown, }} > {children} diff --git a/Converge/ClientApp/src/tabs/home/ConnectTeammates.tsx b/Converge/ClientApp/src/tabs/home/ConnectTeammates.tsx index f8fe378..268f0c1 100644 --- a/Converge/ClientApp/src/tabs/home/ConnectTeammates.tsx +++ b/Converge/ClientApp/src/tabs/home/ConnectTeammates.tsx @@ -276,11 +276,7 @@ const ConnectTeammates: React.FC = () => { className={classes.header} > Date: Tue, 25 Jan 2022 16:06:56 +0000 Subject: [PATCH 16/41] Reconfigure the services to all use the same Axios instance --- Converge/ClientApp/src/App.tsx | 59 ++-- .../src/api/AuthenticationService.ts | 29 +- Converge/ClientApp/src/api/buildingService.ts | 263 +++++++-------- Converge/ClientApp/src/api/calendarService.ts | 80 ++--- Converge/ClientApp/src/api/meService.ts | 170 +++++----- Converge/ClientApp/src/api/placeService.ts | 58 ++-- Converge/ClientApp/src/api/routeService.ts | 26 +- Converge/ClientApp/src/api/searchService.ts | 80 ++--- Converge/ClientApp/src/api/settingsService.ts | 20 +- Converge/ClientApp/src/api/userService.ts | 305 +++++++++--------- .../ClientApp/src/hooks/useBuildingPlaces.tsx | 6 +- .../ClientApp/src/providers/ApiProvider.tsx | 60 ++++ .../src/providers/AppSettingsProvider.tsx | 5 +- .../providers/ConvergeSettingsProvider.tsx | 33 +- .../ClientApp/src/providers/MapProvider.tsx | 11 +- .../src/providers/PlaceFilterProvider.tsx | 7 +- .../src/providers/SearchProvider.tsx | 7 +- .../src/providers/TeammateFilterProvider.tsx | 16 +- .../components/CampusPlacePanel.tsx | 9 +- .../components/CollaborationPlaceDetails.tsx | 8 +- .../components/FavoritesToCollaborate.tsx | 13 +- .../src/tabs/collaborate/components/Map.tsx | 7 +- .../collaborate/components/NewEventModal.tsx | 5 +- .../components/RecommendedToCollaborate.tsx | 7 +- .../components/UserSearchDropdown.tsx | 5 +- .../components/VenuePlacePanel.tsx | 5 +- .../collaborate/components/VenueReviews.tsx | 5 +- .../ClientApp/src/tabs/collaborate/index.tsx | 5 +- .../ClientApp/src/tabs/home/BookWorkspace.tsx | 31 +- .../src/tabs/home/ConnectTeammates.tsx | 8 +- .../src/tabs/home/Table/SelectableTable.tsx | 5 +- .../src/tabs/home/Table/UserLocationCell.tsx | 5 +- .../src/tabs/home/components/TravelTimes.tsx | 5 +- .../tabs/home/components/WorkgroupAvatar.tsx | 5 +- .../workspace/components/BookPlaceModal.tsx | 18 +- .../workspace/components/BuildingPlaces.tsx | 4 +- .../components/CampusPlaceEventTitle.tsx | 5 +- .../CollaborationCampusPlaceCard.tsx | 6 +- .../CustomizedPlaceCollectionAccordian.tsx | 4 +- .../workspace/components/NewConfRoomEvent.tsx | 5 +- .../tabs/workspace/components/PlaceCard.tsx | 24 +- .../tabs/workspace/components/Reservation.tsx | 5 +- .../src/types/IExchangePlacesResponse.ts | 9 + .../src/utilities/ChangeLocationModal.tsx | 9 +- .../ClientApp/src/utilities/IsThisHelpful.tsx | 7 +- 45 files changed, 817 insertions(+), 642 deletions(-) create mode 100644 Converge/ClientApp/src/providers/ApiProvider.tsx create mode 100644 Converge/ClientApp/src/types/IExchangePlacesResponse.ts diff --git a/Converge/ClientApp/src/App.tsx b/Converge/ClientApp/src/App.tsx index 6dcc0bc..15a90d8 100644 --- a/Converge/ClientApp/src/App.tsx +++ b/Converge/ClientApp/src/App.tsx @@ -21,6 +21,7 @@ import { AppSettingProvider } from "./providers/AppSettingsProvider"; import { TeamsContextProvider } from "./providers/TeamsContextProvider"; import ContextLoader from "./ContextLoader"; import AppBanner from "./utilities/AppBanner"; +import { ApiProvider } from "./providers/ApiProvider"; initializeIcons(); dayjs.extend(duration); @@ -31,34 +32,36 @@ dayjs.extend(timezone); const App: React.FC = () => (
- - - - - - - - - ( - - - - )} - /> - - - - - - - + + + + + + + + + + ( + + + + )} + /> + + + + + + + +
); diff --git a/Converge/ClientApp/src/api/AuthenticationService.ts b/Converge/ClientApp/src/api/AuthenticationService.ts index 6ac389f..50e0f65 100644 --- a/Converge/ClientApp/src/api/AuthenticationService.ts +++ b/Converge/ClientApp/src/api/AuthenticationService.ts @@ -5,31 +5,28 @@ import * as microsoftTeams from "@microsoft/teams-js"; import { AxiosInstance } from "axios"; import { setup } from "axios-cache-adapter"; -async function getSSOToken(): Promise { - return new Promise((resolve, reject) => { +export default class AuthenticationService { + private getSSOToken = (): Promise => new Promise((resolve, reject) => { microsoftTeams.authentication.getAuthToken({ successCallback: resolve, failureCallback: reject, }); - }); -} + }) -let api: AxiosInstance; + private api: AxiosInstance; -const getAxiosClient = async (): Promise => { - const accessToken = await getSSOToken(); - if (!api) { - // Allow callers to configure caching on a per-endpoint basis - api = setup({ + constructor() { + this.api = setup({ cache: { maxAge: 0, }, }); - api.defaults.headers.common["Content-Type"] = "application/json"; + this.api.defaults.headers.common["Content-Type"] = "application/json"; } - api.defaults.headers.common.Authorization = `Bearer ${accessToken}`; - return api; -}; - -export default getAxiosClient; + getAxiosClient = async (): Promise => { + const accessToken = await this.getSSOToken(); + this.api.defaults.headers.common.Authorization = `Bearer ${accessToken}`; + return this.api; + } +} diff --git a/Converge/ClientApp/src/api/buildingService.ts b/Converge/ClientApp/src/api/buildingService.ts index 95c84a6..84bd8b3 100644 --- a/Converge/ClientApp/src/api/buildingService.ts +++ b/Converge/ClientApp/src/api/buildingService.ts @@ -5,11 +5,12 @@ import AutoWrapperResponse from "../types/AutoWrapperResponse"; import BuildingBasicInfo from "../types/BuildingBasicInfo"; import BuildingSearchInfo from "../types/BuildingSearchInfo"; import CampusToCollaborate from "../types/CampusToCollaborate"; -import ExchangePlace, { PhotoType, PlaceType } from "../types/ExchangePlace"; +import { PhotoType, PlaceType } from "../types/ExchangePlace"; import ExchangePlacePhoto from "../types/ExchangePlacePhoto"; +import { IExchangePlacesResponse } from "../types/IExchangePlacesResponse"; import Schedule from "../types/Schedule"; import UpcomingBuildingsResponse from "../types/UpcomingBuildingsResponse"; -import getAxiosClient from "./AuthenticationService"; +import AuthenticationService from "./AuthenticationService"; import Constants from "../utilities/Constants"; interface IGetBuildingPlacesRequestParams { @@ -22,110 +23,10 @@ interface IGetBuildingPlacesRequestParams { displayNameSearchString?: string; } -export interface IExchangePlacesResponse { - exchangePlacesList: ExchangePlace[]; - skipToken: string | null; -} - -export const getBuildingPlaces = async ( - buildingUpn: string, - placeType: PlaceType, - params?: IGetBuildingPlacesRequestParams, -): Promise => { - const axios = await getAxiosClient(); - const type = placeType === PlaceType.Room ? "room" : "space"; - const request = await axios.get>( - `/api/buildings/${buildingUpn}/${type}s`, { - params, - }, - ); - return request.data.result; -}; - -export const getBuildingByDisplayName = async ( - buildingDisplayName?:string, -): Promise => { - const axios = await getAxiosClient(); - const request = await axios.get>( - `/api/buildings/buildingByName/${buildingDisplayName}`, { - }, - ); - return request.data.result; -}; - -export const getBuildingsByDistance = async ( - sourceGeoCoordinates?:string, - distanceFromSource?:number, -): Promise => { - const axios = await getAxiosClient(); - const request = await axios.get>( - "/api/buildings/sortByDistance", { - params: { - sourceGeoCoordinates, - distanceFromSource, - }, - }, - ); - return request.data.result; -}; - -export const getBuildingsByName = async (): Promise => { - const axios = await getAxiosClient(); - const request = await axios.get>( - "/api/buildings/sortByName", - ); - return request.data.result; -}; - -export const getSearchForBuildings = async (searchString:string|undefined) -: Promise => { - const axios = await getAxiosClient(); - const request = await axios.get>( - `/api/buildings/searchForBuildings/${searchString}`, { - params: { - searchString, - }, - }, - ); - return request.data.result; -}; - -export const getBuildingSchedule = async ( - id: string, start: string, end: string, -): Promise => { - const axios = await getAxiosClient(); - const request = await axios.get>(`/api/buildings/${id}/schedule`, { - params: { - start, - end, - }, - }); - return request.data.result; -}; - interface PlaceDetailsQueryParams { start: Date, end: Date, } -export const getPlaceDetails = async ( - id: string, - { - start, - end, - }: PlaceDetailsQueryParams, -): Promise => { - const axios = await getAxiosClient(); - const request = await axios.get>(`/api/places/${id}/details`, { - params: { - start, - end, - }, - cache: { - maxAge: Constants.TWO_HOURS_IN_MILLISECONDS, - }, - }); - return request.data.result; -}; export interface PlacePhotosResult { sharePointId: string; @@ -135,33 +36,137 @@ export interface PlacePhotosResult { allOtherPhotos: ExchangePlacePhoto[]; } -function generateResultObject( - sharePointId: string, photos: - ExchangePlacePhoto[], -): PlacePhotosResult { - const coverPhoto = photos.find((p) => p.photoType === PhotoType.Cover); - const floorPlan = photos.find((p) => p.photoType === PhotoType.FloorPlan); - const allOtherPhotos = photos.filter( - (p) => p.photoType !== PhotoType.FloorPlan && p.photoType !== PhotoType.Cover, - ); +export default class BuildingService { + private authenticationService: AuthenticationService - return { - sharePointId, - photos, - coverPhoto, - floorPlan, - allOtherPhotos, + private generatePlacePhotosResult = ( + sharePointId: string, photos: + ExchangePlacePhoto[], + ): PlacePhotosResult => { + const coverPhoto = photos.find((p) => p.photoType === PhotoType.Cover); + const floorPlan = photos.find((p) => p.photoType === PhotoType.FloorPlan); + const allOtherPhotos = photos.filter( + (p) => p.photoType !== PhotoType.FloorPlan && p.photoType !== PhotoType.Cover, + ); + + return { + sharePointId, + photos, + coverPhoto, + floorPlan, + allOtherPhotos, + }; + } + + constructor(authenticationService: AuthenticationService) { + this.authenticationService = authenticationService; + } + + getBuildingPlaces = async ( + buildingUpn: string, + placeType: PlaceType, + params?: IGetBuildingPlacesRequestParams, + ): Promise => { + const axios = await this.authenticationService.getAxiosClient(); + const type = placeType === PlaceType.Room ? "room" : "space"; + const request = await axios.get>( + `/api/buildings/${buildingUpn}/${type}s`, { + params, + }, + ); + return request.data.result; }; -} -export const getPlacePhotos = async ( - sharePointId: string, -): Promise => { - const axios = await getAxiosClient(); - const request = await axios.get>(`/api/places/${sharePointId}/photos`, { - cache: { - maxAge: Constants.TWO_HOURS_IN_MILLISECONDS, - }, - }); - return generateResultObject(sharePointId, request.data.result); -}; + getBuildingsByDistance = async ( + sourceGeoCoordinates?:string, + distanceFromSource?:number, + ): Promise => { + const axios = await this.authenticationService.getAxiosClient(); + const request = await axios.get>( + "/api/buildings/sortByDistance", { + params: { + sourceGeoCoordinates, + distanceFromSource, + }, + }, + ); + return request.data.result; + }; + + getBuildingByDisplayName = async ( + buildingDisplayName?:string, + ): Promise => { + const axios = await this.authenticationService.getAxiosClient(); + const request = await axios.get>( + `/api/buildings/buildingByName/${buildingDisplayName}`, { + }, + ); + return request.data.result; + }; + + getBuildingsByName = async (): Promise => { + const axios = await this.authenticationService.getAxiosClient(); + const request = await axios.get>( + "/api/buildings/sortByName", + ); + return request.data.result; + }; + + getSearchForBuildings = async (searchString:string|undefined) + : Promise => { + const axios = await this.authenticationService.getAxiosClient(); + const request = await axios.get>( + `/api/buildings/searchForBuildings/${searchString}`, { + params: { + searchString, + }, + }, + ); + return request.data.result; + }; + + getBuildingSchedule = async ( + id: string, start: string, end: string, + ): Promise => { + const axios = await this.authenticationService.getAxiosClient(); + const request = await axios.get>(`/api/buildings/${id}/schedule`, { + params: { + start, + end, + }, + }); + return request.data.result; + }; + + getPlaceDetails = async ( + id: string, + { + start, + end, + }: PlaceDetailsQueryParams, + ): Promise => { + const axios = await this.authenticationService.getAxiosClient(); + const request = await axios.get>(`/api/places/${id}/details`, { + params: { + start, + end, + }, + cache: { + maxAge: Constants.TWO_HOURS_IN_MILLISECONDS, + }, + }); + return request.data.result; + }; + + getPlacePhotos = async ( + sharePointId: string, + ): Promise => { + const axios = await this.authenticationService.getAxiosClient(); + const request = await axios.get>(`/api/places/${sharePointId}/photos`, { + cache: { + maxAge: Constants.TWO_HOURS_IN_MILLISECONDS, + }, + }); + return this.generatePlacePhotosResult(sharePointId, request.data.result); + } +} diff --git a/Converge/ClientApp/src/api/calendarService.ts b/Converge/ClientApp/src/api/calendarService.ts index 25194d9..5f920cb 100644 --- a/Converge/ClientApp/src/api/calendarService.ts +++ b/Converge/ClientApp/src/api/calendarService.ts @@ -10,43 +10,51 @@ import { import UpcomingReservationsResponse from "../types/UpcomingReservationsResponse"; import WorkingStartEnd from "../types/WorkingStartEnd"; import { logEvent } from "../utilities/LogWrapper"; -import getAxiosClient from "./AuthenticationService"; +import AuthenticationService from "./AuthenticationService"; -export const getWorkingHours = async (): Promise => { - const axios = await getAxiosClient(); - const response = await axios.get>("/api/calendar/mailboxSettings/workingHours"); - return response.data.result; -}; +export default class CalendarService { + private authenticationService: AuthenticationService -export const getUpcomingReservations = async ( - startDateTime: string, endDateTime: string, top?:number, skip?:number, -): Promise => { - const axios = await getAxiosClient(); - const response = await axios.get>("/api/calendar/upcomingReservations", { - params: { - startDateTime, endDateTime, top, skip, - }, - }); - return response.data.result; -}; + constructor(authenticationService: AuthenticationService) { + this.authenticationService = authenticationService; + } -export const deleteEvent = async (eventId: string, messageComment: string): Promise => { - const axios = await getAxiosClient(); - await axios.get(`/api/calendar/events/${eventId}/deleteEvent`, { - params: { - messageComment, - }, - }); - logEvent(USER_INTERACTION, [ - { name: IMPORTANT_ACTION, value: ImportantActions.EventCancelled }, - ]); -}; + getWorkingHours = async (): Promise => { + const axios = await this.authenticationService.getAxiosClient(); + const response = await axios.get>("/api/calendar/mailboxSettings/workingHours"); + return response.data.result; + }; -export const createEvent = async (newEvent: CalendarEventRequest) : Promise => { - const axios = await getAxiosClient(); - const response = await axios.post>("/api/calendar/event", newEvent); - logEvent(USER_INTERACTION, [ - { name: IMPORTANT_ACTION, value: ImportantActions.EventCreated }, - ]); - return response.data.result; -}; + getUpcomingReservations = async ( + startDateTime: string, endDateTime: string, top?:number, skip?:number, + ): Promise => { + const axios = await this.authenticationService.getAxiosClient(); + const response = await axios.get>("/api/calendar/upcomingReservations", { + params: { + startDateTime, endDateTime, top, skip, + }, + }); + return response.data.result; + }; + + deleteEvent = async (eventId: string, messageComment: string): Promise => { + const axios = await this.authenticationService.getAxiosClient(); + await axios.get(`/api/calendar/events/${eventId}/deleteEvent`, { + params: { + messageComment, + }, + }); + logEvent(USER_INTERACTION, [ + { name: IMPORTANT_ACTION, value: ImportantActions.EventCancelled }, + ]); + }; + + createEvent = async (newEvent: CalendarEventRequest) : Promise => { + const axios = await this.authenticationService.getAxiosClient(); + const response = await axios.post>("/api/calendar/event", newEvent); + logEvent(USER_INTERACTION, [ + { name: IMPORTANT_ACTION, value: ImportantActions.EventCreated }, + ]); + return response.data.result; + }; +} diff --git a/Converge/ClientApp/src/api/meService.ts b/Converge/ClientApp/src/api/meService.ts index 6d05dcb..a7d6a24 100644 --- a/Converge/ClientApp/src/api/meService.ts +++ b/Converge/ClientApp/src/api/meService.ts @@ -5,100 +5,106 @@ import * as MicrosoftGraph from "@microsoft/microsoft-graph-types"; import ConvergeSettings from "../types/ConvergeSettings"; import AutoWrapperResponse from "../types/AutoWrapperResponse"; import UserPredictedLocationRequest from "../types/UserPredictedLocationRequest"; -import getAxiosClient from "./AuthenticationService"; import BuildingBasicInfo from "../types/BuildingBasicInfo"; import ExchangePlace from "../types/ExchangePlace"; +import AuthenticationService from "./AuthenticationService"; -const getSettings = async (): Promise => { - const axios = await getAxiosClient(); - const request = await axios.get>( - "/api/me/convergeSettings", - ); - if (request.status === 204) { - return null; +export default class MeService { + private authenticationService: AuthenticationService; + + constructor(authenticationService: AuthenticationService) { + this.authenticationService = authenticationService; } - return request.data.result; -}; -export const setSettings = async (settings: ConvergeSettings): Promise => { - const axios = await getAxiosClient(); - const request = axios.post("/api/me/convergeSettings", settings); - return (await request).data.result; -}; + getSettings = async (): Promise => { + const axios = await this.authenticationService.getAxiosClient(); + const request = await axios.get>( + "/api/me/convergeSettings", + ); + if (request.status === 204) { + return null; + } + return request.data.result; + }; -export const setupNewUser = async (settings: ConvergeSettings): Promise => { - const axios = await getAxiosClient(); - const request = axios.post("/api/me/setup", settings); - return (await request).data.result; -}; + setSettings = async (settings: ConvergeSettings): Promise => { + const axios = await this.authenticationService.getAxiosClient(); + const request = axios.post("/api/me/convergeSettings", settings); + return (await request).data.result; + }; -export const getWorkgroup = async (): Promise => { - const axios = await getAxiosClient(); - const request = await axios.get>( - "/api/me/workgroup", - ); - return request.data.result; -}; + setupNewUser = async (settings: ConvergeSettings): Promise => { + const axios = await this.authenticationService.getAxiosClient(); + const request = axios.post("/api/me/setup", settings); + return (await request).data.result; + }; -export const getPeople = async (): Promise => { - const axios = await getAxiosClient(); - const request = await axios.get>( - "/api/me/people", - ); - return request.data.result; -}; + getWorkgroup = async (): Promise => { + const axios = await this.authenticationService.getAxiosClient(); + const request = await axios.get>( + "/api/me/workgroup", + ); + return request.data.result; + }; -export const getMyList = async (): Promise => { - const axios = await getAxiosClient(); - const request = await axios.get>( - "/api/me/list", - ); - return request.data.result; -}; + getPeople = async (): Promise => { + const axios = await this.authenticationService.getAxiosClient(); + const request = await axios.get>( + "/api/me/people", + ); + return request.data.result; + }; -export const updateMyPredictedLocation = async ( - request: UserPredictedLocationRequest, -): Promise => { - const axios = await getAxiosClient(); - const response = await axios.put("/api/me/updatePredictedLocation", request); - return response.data.result; -}; + getMyList = async (): Promise => { + const axios = await this.authenticationService.getAxiosClient(); + const request = await axios.get>( + "/api/me/list", + ); + return request.data.result; + }; -export const getMyRecommendation = async ( - year: number, - month: number, - day: number, -): Promise => { - const axios = await getAxiosClient(); - const request = await axios.get>( - "/api/me/recommendation", - { params: { year, month, day } }, - ); - return request.data.result; -}; + updateMyPredictedLocation = async ( + request: UserPredictedLocationRequest, + ): Promise => { + const axios = await this.authenticationService.getAxiosClient(); + const response = await axios.put("/api/me/updatePredictedLocation", request); + return response.data.result; + }; -export const getConvergeCalendar = async ( -): Promise => { - const axios = await getAxiosClient(); - const request = await axios.get>("/api/me/convergeCalendar"); - if (request.status === 204) { - return null; - } - return request.data.result; -}; + getMyRecommendation = async ( + year: number, + month: number, + day: number, + ): Promise => { + const axios = await this.authenticationService.getAxiosClient(); + const request = await axios.get>( + "/api/me/recommendation", + { params: { year, month, day } }, + ); + return request.data.result; + }; -export const getRecentBuildingsBasicDetails = async ( -): Promise => { - const axios = await getAxiosClient(); - const request = await axios.get>("/api/me/recentBuildings"); - return request.data.result; -}; + getConvergeCalendar = async ( + ): Promise => { + const axios = await this.authenticationService.getAxiosClient(); + const request = await axios.get>("/api/me/convergeCalendar"); + if (request.status === 204) { + return null; + } + return request.data.result; + }; -export const getFavoritePlaces = async ( -): Promise => { - const axios = await getAxiosClient(); - const request = await axios.get>("/api/me/favoriteCampusesDetails"); - return request.data.result; -}; + getRecentBuildingsBasicDetails = async ( + ): Promise => { + const axios = await this.authenticationService.getAxiosClient(); + const request = await axios.get>("/api/me/recentBuildings"); + return request.data.result; + }; -export default getSettings; + getFavoritePlaces = async ( + ): Promise => { + const axios = await this.authenticationService.getAxiosClient(); + const request = await axios.get>("/api/me/favoriteCampusesDetails"); + return request.data.result; + }; +} diff --git a/Converge/ClientApp/src/api/placeService.ts b/Converge/ClientApp/src/api/placeService.ts index 77b839c..5d7d6fa 100644 --- a/Converge/ClientApp/src/api/placeService.ts +++ b/Converge/ClientApp/src/api/placeService.ts @@ -2,32 +2,38 @@ // Licensed under the MIT License. import AutoWrapperResponse from "../types/AutoWrapperResponse"; -import getAxiosClient from "./AuthenticationService"; +import AuthenticationService from "./AuthenticationService"; -const getPlaceMaxReserved = async ( - id: string, start: string, end: string, -): Promise => { - const axios = await getAxiosClient(); - const request = await axios.get>(`/api/places/${id}/maxReserved`, { - params: { - start, - end, - }, - }); - return request.data.result; -}; +export default class PlaceService { + private authenticationService: AuthenticationService; -export const getRoomAvailability = async ( - id: string, start: string, end: string, -): Promise => { - const axios = await getAxiosClient(); - const request = await axios.get>(`/api/places/${id}/availability`, { - params: { - start, - end, - }, - }); - return request.data.result; -}; + constructor(authenticationService: AuthenticationService) { + this.authenticationService = authenticationService; + } -export default getPlaceMaxReserved; + getPlaceMaxReserved = async ( + id: string, start: string, end: string, + ): Promise => { + const axios = await this.authenticationService.getAxiosClient(); + const request = await axios.get>(`/api/places/${id}/maxReserved`, { + params: { + start, + end, + }, + }); + return request.data.result; + }; + + getRoomAvailability = async ( + id: string, start: string, end: string, + ): Promise => { + const axios = await this.authenticationService.getAxiosClient(); + const request = await axios.get>(`/api/places/${id}/availability`, { + params: { + start, + end, + }, + }); + return request.data.result; + }; +} diff --git a/Converge/ClientApp/src/api/routeService.ts b/Converge/ClientApp/src/api/routeService.ts index 55978a1..5b005a6 100644 --- a/Converge/ClientApp/src/api/routeService.ts +++ b/Converge/ClientApp/src/api/routeService.ts @@ -3,15 +3,21 @@ import AutoWrapperResponse from "../types/AutoWrapperResponse"; import RouteResponse from "../types/RouteResponse"; -import getAxiosClient from "./AuthenticationService"; +import AuthenticationService from "./AuthenticationService"; -const getRoute = async (start: string, end: string): Promise => { - const axios = await getAxiosClient(); - const request = await axios.get>( - "/api/route/travelTime", - { params: { start, end } }, - ); - return request.data.result; -}; +export default class RouteService { + private authenticationService: AuthenticationService; -export default getRoute; + constructor(authenticationService: AuthenticationService) { + this.authenticationService = authenticationService; + } + + getRoute = async (start: string, end: string): Promise => { + const axios = await this.authenticationService.getAxiosClient(); + const request = await axios.get>( + "/api/route/travelTime", + { params: { start, end } }, + ); + return request.data.result; + }; +} diff --git a/Converge/ClientApp/src/api/searchService.ts b/Converge/ClientApp/src/api/searchService.ts index a97e380..c023435 100644 --- a/Converge/ClientApp/src/api/searchService.ts +++ b/Converge/ClientApp/src/api/searchService.ts @@ -8,44 +8,52 @@ import VenuesToCollaborateRequest from "../types/VenuesToCollaborateRequest"; import VenuesToCollaborateResponse from "../types/VenuesToCollaborateResponse"; import VenueReviewsResponse from "../types/VenueReviewsResponse"; import VenueDetails from "../types/VenueDetails"; -import getAxiosClient from "./AuthenticationService"; +import AuthenticationService from "./AuthenticationService"; import Constants from "../utilities/Constants"; -export const searchCampusesToCollaborate = async ( - campusesToCollaborateRequest: CampusesToCollaborateRequest, -): Promise => { - const axios = await getAxiosClient(); - const request = await axios.post>( - "/api/search/campusesToCollaborate", - campusesToCollaborateRequest, { params: {} }, - ); - return request.data.result; -}; +export default class SearchService { + private authenticationService: AuthenticationService; -export const searchVenuesToCollaborate = async ( - venuesToCollaborateRequest: VenuesToCollaborateRequest, -): Promise => { - const axios = await getAxiosClient(); - const request = await axios.post>("/api/search/venuesToCollaborate", venuesToCollaborateRequest); - return request.data.result; -}; + constructor(authenticationService: AuthenticationService) { + this.authenticationService = authenticationService; + } -export const getVenueDetails = async ( - venueId: string, -): Promise => { - const axios = await getAxiosClient(); - const request = await axios.get>(`/api/search/venues/${venueId}/details`, { - cache: { - maxAge: Constants.TWO_HOURS_IN_MILLISECONDS, - }, - }); - return request.data.result; -}; + searchCampusesToCollaborate = async ( + campusesToCollaborateRequest: CampusesToCollaborateRequest, + ): Promise => { + const axios = await this.authenticationService.getAxiosClient(); + const request = await axios.post>( + "/api/search/campusesToCollaborate", + campusesToCollaborateRequest, { params: {} }, + ); + return request.data.result; + }; -export const getReviews = async ( - venueId: string, -): Promise => { - const axios = await getAxiosClient(); - const request = await axios.get>(`/api/search/venues/${venueId}/reviews`); - return request.data.result; -}; + searchVenuesToCollaborate = async ( + venuesToCollaborateRequest: VenuesToCollaborateRequest, + ): Promise => { + const axios = await this.authenticationService.getAxiosClient(); + const request = await axios.post>("/api/search/venuesToCollaborate", venuesToCollaborateRequest); + return request.data.result; + }; + + getVenueDetails = async ( + venueId: string, + ): Promise => { + const axios = await this.authenticationService.getAxiosClient(); + const request = await axios.get>(`/api/search/venues/${venueId}/details`, { + cache: { + maxAge: Constants.TWO_HOURS_IN_MILLISECONDS, + }, + }); + return request.data.result; + }; + + getReviews = async ( + venueId: string, + ): Promise => { + const axios = await this.authenticationService.getAxiosClient(); + const request = await axios.get>(`/api/search/venues/${venueId}/reviews`); + return request.data.result; + }; +} diff --git a/Converge/ClientApp/src/api/settingsService.ts b/Converge/ClientApp/src/api/settingsService.ts index 4ee3749..8888624 100644 --- a/Converge/ClientApp/src/api/settingsService.ts +++ b/Converge/ClientApp/src/api/settingsService.ts @@ -3,12 +3,18 @@ import AutoWrapperResponse from "../types/AutoWrapperResponse"; import AppSettings from "../types/Settings"; -import getAxiosClient from "./AuthenticationService"; +import AuthenticationService from "./AuthenticationService"; -const getAppSettings = async (): Promise => { - const axios = await getAxiosClient(); - const request = await axios.get>("/api/settings/appSettings"); - return request.data.result; -}; +export default class SettingsService { + private authenticationService: AuthenticationService; -export default getAppSettings; + constructor(authenticationService: AuthenticationService) { + this.authenticationService = authenticationService; + } + + getAppSettings = async (): Promise => { + const axios = await this.authenticationService.getAxiosClient(); + const request = await axios.get>("/api/settings/appSettings"); + return request.data.result; + }; +} diff --git a/Converge/ClientApp/src/api/userService.ts b/Converge/ClientApp/src/api/userService.ts index a55a050..943e2a3 100644 --- a/Converge/ClientApp/src/api/userService.ts +++ b/Converge/ClientApp/src/api/userService.ts @@ -6,172 +6,181 @@ import AutoWrapperResponse from "../types/AutoWrapperResponse"; import UserCoordinatesResponse from "../types/UserCoordinatesResponse"; import MultiUserAvailableTimesResponse from "../types/MultiUserAvailableTimesResponse"; import UserProfile from "../types/UserProfile"; -import getAxiosClient from "./AuthenticationService"; +import AuthenticationService from "./AuthenticationService"; import UserCoordianates from "../types/UserCoordinates"; import createCachedQuery, { CachedQuery } from "../utilities/CachedQuery"; import QueryOption from "../types/QueryOption"; import UserSearchPagedResponse from "../types/UserSearchPagedResponse"; -const getCollaborator = async (userPrincipalName: string): Promise => { - const axios = await getAxiosClient(); - const response = await axios.get>(`/api/users/${userPrincipalName}`); - return response.data.result; -}; - -export default getCollaborator; - interface UserPhotoResult { id: string; userPhoto: string | null; } -const getUserPhoto = async (id: string): Promise => { - const axios = await getAxiosClient(); - const request = await fetch(`/api/users/${id}/photo`, { - headers: { Authorization: axios.defaults.headers.common.Authorization }, - }); - if (request.status === 200) { - const photoBlob = await request.blob(); - if (photoBlob.size !== 0) { - // Create URL to to allow image to be used. - const photoUrl = URL.createObjectURL(photoBlob); - return { - id, - userPhoto: photoUrl, - }; - } - return { - id, - userPhoto: null, - }; - } - return request.json(); -}; - -const generateUserPhotoStoreKey = ( - result: UserPhotoResult, -): string => result.id; - -const generateUserPhotoRetrievalKey = (search: string): string => search; - type CachedUserPhotoQuery = CachedQuery; -// Function that returns a cached getUserCoordinates function. -export function createUserPhotoService(): CachedUserPhotoQuery { - return createCachedQuery( - generateUserPhotoStoreKey, - generateUserPhotoRetrievalKey, - (ids: string[]) => getUserPhoto(ids[0]) - .then((result) => [result]), - ); -} - -export const getUserProfile = async (id?: string): Promise => { - const axios = await getAxiosClient(); - const request = await axios.get>(`/api/users/${id}/userProfile`, { - headers: { Authorization: axios.defaults.headers.common.Authorization }, - }); - return request.data.result; -}; - -export const getPresence = async ( - id: string, -): Promise => { - const axios = await getAxiosClient(); - const request = await axios.get>( - `/api/users/${id}/presence`, - ); - return request.data.result; -}; - -export const getLocation = async ( - id: string, - year: number, - month: number, - day: number, -): Promise => { - const axios = await getAxiosClient(); - const request = await axios.get>(`/api/users/${id}/location`, { - params: { year, month, day }, - }); - return request.data.result; -}; - -export const getMultiUserAvailabilityTimes = async ( - userPrincipalNames: string[], - year: number, - month: number, - day: number, - scheduleFrom?: Date, - scheduleTo?: Date, -): Promise => { - const axios = await getAxiosClient(); - const request = await axios.post>("/api/users/multi/availableTimes", { - year, month, day, usersUpnList: userPrincipalNames, scheduleFrom, scheduleTo, - }); - return request.data.result; -}; - -export const searchUsers = async ( - searchQuery?: string, - options?: QueryOption[], -): Promise => { - const axios = await getAxiosClient(); - if (!searchQuery) { - return { users: [], queryOptions: [] }; - } - const request = await axios.get>("/api/users/search", { - params: { - searchString: searchQuery, - QueryOptions: JSON.stringify(options), - }, - }); - return request.data.result; -}; - interface UserCoordinateQueryParams { year: number, month: number, day: number } -const getUserCoordinates = async ( - users: string[], - { - year, - month, - day, - }: UserCoordinateQueryParams, -): Promise => { - const axios = await getAxiosClient(); - const request = await axios.post>("/api/users/coordinates", { - year, month, day, usersUpnList: users, - }); - return request.data.result.userCoordinatesList; -}; - -const generateUserCoordinateStoreKey = ( - user: UserCoordianates, - { - day, - month, - year, - }: UserCoordinateQueryParams, -) => `${user.userPrincipalName}-${year}/${month}/${day}`; - -const generateUserCoordinateRetrievalKey = (search: string, { - day, - month, - year, -}: UserCoordinateQueryParams) => `${search}-${year}/${month}/${day}`; - type CachedUserCoordinateQuery = CachedQuery; -// Function that returns a cached getUserCoordinates function. -export function createUserCoordinateService(): CachedUserCoordinateQuery { - return createCachedQuery( - generateUserCoordinateStoreKey, - generateUserCoordinateRetrievalKey, - getUserCoordinates, - ); +export default class UserService { + private authenticationService: AuthenticationService; + + private generateUserPhotoStoreKey = ( + result: UserPhotoResult, + ): string => result.id; + + private generateUserPhotoRetrievalKey = (search: string): string => search; + + private generateUserCoordinateStoreKey = ( + user: UserCoordianates, + { + day, + month, + year, + }: UserCoordinateQueryParams, + ) => `${user.userPrincipalName}-${year}/${month}/${day}`; + + private generateUserCoordinateRetrievalKey = (search: string, { + day, + month, + year, + }: UserCoordinateQueryParams) => `${search}-${year}/${month}/${day}`; + + constructor(authenticationService: AuthenticationService) { + this.authenticationService = authenticationService; + } + + getCollaborator = async (userPrincipalName: string): Promise => { + const axios = await this.authenticationService.getAxiosClient(); + const response = await axios.get>(`/api/users/${userPrincipalName}`); + return response.data.result; + }; + + getUserPhoto = async (id: string): Promise => { + const axios = await this.authenticationService.getAxiosClient(); + const request = await fetch(`/api/users/${id}/photo`, { + headers: { Authorization: axios.defaults.headers.common.Authorization }, + }); + if (request.status === 200) { + const photoBlob = await request.blob(); + if (photoBlob.size !== 0) { + // Create URL to to allow image to be used. + const photoUrl = URL.createObjectURL(photoBlob); + return { + id, + userPhoto: photoUrl, + }; + } + return { + id, + userPhoto: null, + }; + } + return request.json(); + }; + + /** + * Function that returns a cached getUserCoordinates function. + * @returns A cached user photo query + */ + createUserPhotoService = (): CachedUserPhotoQuery => createCachedQuery( + this.generateUserPhotoStoreKey, + this.generateUserPhotoRetrievalKey, + (ids: string[]) => this.getUserPhoto(ids[0]) + .then((result) => [result]), + ) + + getUserProfile = async (id?: string): Promise => { + const axios = await this.authenticationService.getAxiosClient(); + const request = await axios.get>(`/api/users/${id}/userProfile`, { + headers: { Authorization: axios.defaults.headers.common.Authorization }, + }); + return request.data.result; + }; + + getPresence = async ( + id: string, + ): Promise => { + const axios = await this.authenticationService.getAxiosClient(); + const request = await axios.get>( + `/api/users/${id}/presence`, + ); + return request.data.result; + }; + + getLocation = async ( + id: string, + year: number, + month: number, + day: number, + ): Promise => { + const axios = await this.authenticationService.getAxiosClient(); + const request = await axios.get>(`/api/users/${id}/location`, { + params: { year, month, day }, + }); + return request.data.result; + }; + + getMultiUserAvailabilityTimes = async ( + userPrincipalNames: string[], + year: number, + month: number, + day: number, + scheduleFrom?: Date, + scheduleTo?: Date, + ): Promise => { + const axios = await this.authenticationService.getAxiosClient(); + const request = await axios.post>("/api/users/multi/availableTimes", { + year, month, day, usersUpnList: userPrincipalNames, scheduleFrom, scheduleTo, + }); + return request.data.result; + }; + + searchUsers = async ( + searchQuery?: string, + options?: QueryOption[], + ): Promise => { + const axios = await this.authenticationService.getAxiosClient(); + if (!searchQuery) { + return { users: [], queryOptions: [] }; + } + const request = await axios.get>("/api/users/search", { + params: { + searchString: searchQuery, + QueryOptions: JSON.stringify(options), + }, + }); + return request.data.result; + }; + + getUserCoordinates = async ( + users: string[], + { + year, + month, + day, + }: UserCoordinateQueryParams, + ): Promise => { + const axios = await this.authenticationService.getAxiosClient(); + const request = await axios.post>("/api/users/coordinates", { + year, month, day, usersUpnList: users, + }); + return request.data.result.userCoordinatesList; + }; + + /** + * Function that returns a cached getUserCoordinates function. + * @returns A cached user coordinate query. + */ + createUserCoordinateService = (): + CachedUserCoordinateQuery => createCachedQuery( + this.generateUserCoordinateStoreKey, + this.generateUserCoordinateRetrievalKey, + this.getUserCoordinates, + ) } diff --git a/Converge/ClientApp/src/hooks/useBuildingPlaces.tsx b/Converge/ClientApp/src/hooks/useBuildingPlaces.tsx index a6a6848..cb601e5 100644 --- a/Converge/ClientApp/src/hooks/useBuildingPlaces.tsx +++ b/Converge/ClientApp/src/hooks/useBuildingPlaces.tsx @@ -2,8 +2,9 @@ // Licensed under the MIT License. import { useEffect, useState } from "react"; -import { getBuildingPlaces, IExchangePlacesResponse } from "../api/buildingService"; +import BuildingService from "../api/buildingService"; import ExchangePlace, { PlaceType } from "../types/ExchangePlace"; +import { IExchangePlacesResponse } from "../types/IExchangePlacesResponse"; import usePromise, { ILoadingState, IPromiseError } from "./usePromise"; interface BuildingPlacesFilterOptions { @@ -28,6 +29,7 @@ interface IUseBuildingWorkspacesHookReturnType { } function useBuildingPlaces( + buildingService: BuildingService, buildingUpn?: string, ): IUseBuildingWorkspacesHookReturnType { const [ @@ -49,7 +51,7 @@ function useBuildingPlaces( setPlaces([]); } if (buildingUpn && placeType !== undefined) { - const result = getBuildingPlaces( + const result = buildingService.getBuildingPlaces( buildingUpn, placeType, { diff --git a/Converge/ClientApp/src/providers/ApiProvider.tsx b/Converge/ClientApp/src/providers/ApiProvider.tsx new file mode 100644 index 0000000..28f20c8 --- /dev/null +++ b/Converge/ClientApp/src/providers/ApiProvider.tsx @@ -0,0 +1,60 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +import React, { + createContext, useContext, useState, +} from "react"; +import AuthenticationService from "../api/AuthenticationService"; +import BuildingService from "../api/buildingService"; +import CalendarService from "../api/calendarService"; +import MeService from "../api/meService"; +import PlaceService from "../api/placeService"; +import RouteService from "../api/routeService"; +import SearchService from "../api/searchService"; +import SettingsService from "../api/settingsService"; +import UserService from "../api/userService"; + +interface ApiModel { + buildingService: BuildingService; + calendarService: CalendarService; + meService: MeService; + placeService: PlaceService; + routeService: RouteService; + searchService: SearchService; + settingsService: SettingsService; + userService: UserService; +} + +const Context = createContext({} as ApiModel); + +const ApiProvider: React.FC = (props) => { + const { children } = props; + const authenticationService = new AuthenticationService(); + const [buildingService] = useState(new BuildingService(authenticationService)); + const [calendarService] = useState(new CalendarService(authenticationService)); + const [meService] = useState(new MeService(authenticationService)); + const [placeService] = useState(new PlaceService(authenticationService)); + const [routeService] = useState(new RouteService(authenticationService)); + const [searchService] = useState(new SearchService(authenticationService)); + const [settingsService] = useState(new SettingsService(authenticationService)); + const [userService] = useState(new UserService(authenticationService)); + + return ( + + {children} + + ); +}; + +const useApiProvider = (): ApiModel => useContext(Context); +export { ApiProvider, useApiProvider }; diff --git a/Converge/ClientApp/src/providers/AppSettingsProvider.tsx b/Converge/ClientApp/src/providers/AppSettingsProvider.tsx index 44daa35..2aaed72 100644 --- a/Converge/ClientApp/src/providers/AppSettingsProvider.tsx +++ b/Converge/ClientApp/src/providers/AppSettingsProvider.tsx @@ -3,8 +3,8 @@ import React, { createContext, useContext, useEffect } from "react"; import Settings from "../types/Settings"; -import getAppSettings from "../api/settingsService"; import usePromise from "../hooks/usePromise"; +import { useApiProvider } from "./ApiProvider"; interface SettingModel { appSettingsLoading: boolean; @@ -15,6 +15,7 @@ interface SettingModel { const Context = createContext({} as SettingModel); const AppSettingProvider: React.FC = ({ children }) => { + const { settingsService } = useApiProvider(); const [ appSettingsLoading, appSettings, @@ -22,7 +23,7 @@ const AppSettingProvider: React.FC = ({ children }) => { waitFor, ] = usePromise(undefined, true); - const getSettings = () => waitFor(getAppSettings()); + const getSettings = () => waitFor(settingsService.getAppSettings()); useEffect(() => { getSettings(); diff --git a/Converge/ClientApp/src/providers/ConvergeSettingsProvider.tsx b/Converge/ClientApp/src/providers/ConvergeSettingsProvider.tsx index c9f870b..9049240 100644 --- a/Converge/ClientApp/src/providers/ConvergeSettingsProvider.tsx +++ b/Converge/ClientApp/src/providers/ConvergeSettingsProvider.tsx @@ -8,12 +8,6 @@ import { GeoCoordinates } from "@microsoft/microsoft-graph-types"; import React, { useContext, useEffect, useReducer, useState, } from "react"; -import { - getBuildingsByDistance, getSearchForBuildings, getBuildingsByName, -} from "../api/buildingService"; -import getSettings, { - getFavoritePlaces, getRecentBuildingsBasicDetails, setSettings, setupNewUser, -} from "../api/meService"; import BuildingBasicInfo from "../types/BuildingBasicInfo"; import ConvergeSettings from "../types/ConvergeSettings"; import UpcomingBuildingsResponse from "../types/UpcomingBuildingsResponse"; @@ -22,6 +16,7 @@ import { USER_INTERACTION, UI_SECTION, UISections, DESCRIPTION, } from "../types/LoggerTypes"; import { logEvent } from "../utilities/LogWrapper"; +import { useApiProvider } from "./ApiProvider"; type IBuildingState = { buildingsList: BuildingBasicInfo[]; @@ -318,6 +313,10 @@ function favoriteCampusesReducer(state: ExchangePlace[], action: IPlaceAction): } const ConvergeSettingsProvider: React.FC = ({ children }) => { + const { + buildingService, + meService, + } = useApiProvider(); const [convergeSettings, convergeSettingsDispatch] = useReducer< ConvergeSettings | null, ConvergeSettingsAction>( convergeSettingsReducer, @@ -335,7 +334,7 @@ const ConvergeSettingsProvider: React.FC = ({ children }) => { const getConvergeSettings = (): Promise => { convergeSettingsDispatch({ type: GET_CONVERGE_SETTINGS_REQUEST }); - return getSettings() + return meService.getSettings() .then((settings) => { convergeSettingsDispatch( { type: GET_CONVERGE_SETTINGS_RESPONSE, convergeSettings: settings }, @@ -343,20 +342,22 @@ const ConvergeSettingsProvider: React.FC = ({ children }) => { }); }; - const setConvergeSettings = (settings: ConvergeSettings): Promise => setSettings(settings) + const setConvergeSettings = (settings: ConvergeSettings): Promise => meService + .setSettings(settings) .then(() => convergeSettingsDispatch({ type: SET_CONVERGE_SETTINGS_REQUEST, convergeSettings: settings, })); - const setupNewUserWrapper = (settings: ConvergeSettings): Promise => setupNewUser(settings) + const setupNewUserWrapper = (settings: ConvergeSettings): Promise => meService + .setupNewUser(settings) .then(() => { convergeSettingsDispatch({ type: SETUP_NEW_USER_RESPONSE, convergeSettings: settings }); }); const getFavoriteCampusesWrapper = (): Promise => { favoriteCampusesDispatch({ type: GET_FAVORITE_CAMPUSES_REQUEST }); - return getFavoritePlaces() + return meService.getFavoritePlaces() .then((favs) => { favoriteCampusesDispatch({ type: GET_FAVORITE_CAMPUSES_RESPONSE, favoriteCampuses: favs }); }); @@ -387,7 +388,7 @@ const ConvergeSettingsProvider: React.FC = ({ children }) => { const loadBuildingsByDistance = (geoCoordinates: GeoCoordinates) => { setBuildingListLoading(true); setBuildingsLoadingMessage("No nearby results, expanding search."); - getBuildingsByDistance(`${geoCoordinates.latitude},${geoCoordinates.longitude}`, 10) + buildingService.getBuildingsByDistance(`${geoCoordinates.latitude},${geoCoordinates.longitude}`, 10) .then((response) => { if (response.buildingsList.length === 0 && state.buildingsByRadiusDistance < 1000) { setBuildingsByDistanceRadius(state.buildingsByRadiusDistance * 10); @@ -405,7 +406,7 @@ const ConvergeSettingsProvider: React.FC = ({ children }) => { const loadBuildingsByName = () => { setBuildingListLoading(true); - getBuildingsByName() + buildingService.getBuildingsByName() .then((response) => { dispatch({ type: UPDATE_BUILDINGS_LIST, payload: response }); }) @@ -415,7 +416,7 @@ const ConvergeSettingsProvider: React.FC = ({ children }) => { const loadMoreBuildingsByDistance = (geoCoordinates: GeoCoordinates, distance: number) => { setBuildingListLoading(true); - getBuildingsByDistance(`${geoCoordinates.latitude},${geoCoordinates.longitude}`, distance) + buildingService.getBuildingsByDistance(`${geoCoordinates.latitude},${geoCoordinates.longitude}`, distance) .then((response) => { dispatch({ type: UPDATE_BUILDINGS_LIST, payload: response }); }) @@ -427,7 +428,7 @@ const ConvergeSettingsProvider: React.FC = ({ children }) => { searchString?: string, ) => { setBuildingListLoading(true); - getSearchForBuildings(searchString).then((data) => { + buildingService.getSearchForBuildings(searchString).then((data) => { dispatch({ type: UPDATE_SEARCH_BUILDINGS_LIST, payload: data.buildingInfoList, @@ -465,13 +466,13 @@ const ConvergeSettingsProvider: React.FC = ({ children }) => { getConvergeSettings() .catch(() => setIsError(true)) .finally(() => setLoading(false)); - getRecentBuildingsBasicDetails().then((basicRecentBuildings) => { + meService.getRecentBuildingsBasicDetails().then((basicRecentBuildings) => { updateRecentBuildings(basicRecentBuildings); }); }, []); useEffect(() => { - getRecentBuildingsBasicDetails().then((basicRecentBuildings) => { + meService.getRecentBuildingsBasicDetails().then((basicRecentBuildings) => { updateRecentBuildings(basicRecentBuildings); }); }, [convergeSettings?.recentBuildingUpns]); diff --git a/Converge/ClientApp/src/providers/MapProvider.tsx b/Converge/ClientApp/src/providers/MapProvider.tsx index 272f7d5..c9bdefc 100644 --- a/Converge/ClientApp/src/providers/MapProvider.tsx +++ b/Converge/ClientApp/src/providers/MapProvider.tsx @@ -5,8 +5,7 @@ import React, { createContext, useContext, useMemo, useReducer, } from "react"; import * as MicrosoftGraph from "@microsoft/microsoft-graph-types"; - -import getCollaborator, { createUserPhotoService } from "../api/userService"; +import { useApiProvider } from "./ApiProvider"; const GET_USER_RESPONSE = "GET_USER_RESPONSE"; const GET_PHOTO_RESPONSE = "GET_PHOTO_RESPONSE"; @@ -67,7 +66,11 @@ const reducer = (state: MapState, action: MapProviderAction): MapState => { }; const MapProvider: React.FC = ({ children }) => { - const photoService = useMemo(() => createUserPhotoService(), [createUserPhotoService]); + const { userService } = useApiProvider(); + const photoService = useMemo( + () => userService.createUserPhotoService(), + [userService.createUserPhotoService], + ); const [state, dispatch] = useReducer( reducer, initialState, @@ -95,7 +98,7 @@ const MapProvider: React.FC = ({ children }) => { if (state.users[userPrincipalName]) { return Promise.resolve(state.users[userPrincipalName]); } - return getCollaborator(userPrincipalName) + return userService.getCollaborator(userPrincipalName) .then((user) => { dispatch({ type: GET_USER_RESPONSE, diff --git a/Converge/ClientApp/src/providers/PlaceFilterProvider.tsx b/Converge/ClientApp/src/providers/PlaceFilterProvider.tsx index 2cdc6ce..7abc92d 100644 --- a/Converge/ClientApp/src/providers/PlaceFilterProvider.tsx +++ b/Converge/ClientApp/src/providers/PlaceFilterProvider.tsx @@ -8,8 +8,8 @@ import React, { import ExchangePlace, { PlaceType } from "../types/ExchangePlace"; import SortOptions from "../types/SortOptions"; import CalendarEvent from "../types/CalendarEvent"; -import { getUpcomingReservations } from "../api/calendarService"; import UpcomingReservationsResponse from "../types/UpcomingReservationsResponse"; +import { useApiProvider } from "./ApiProvider"; const UPDATE_LOCATION = "UPDATE_LOCATION"; const UPDATE_START_DATE = "UPDATE_START_DATE"; @@ -399,6 +399,7 @@ const iState: IPlaceState = { }; const PlaceContextProvider: React.FC = ({ children }) => { + const { calendarService } = useApiProvider(); const [state, dispatch] = useReducer( reducer, { @@ -425,7 +426,7 @@ const PlaceContextProvider: React.FC = ({ children }) => { setReservationsListLoading(true); const resStartRange = dayjs.utc(start).toISOString(); const resEndRange = dayjs.utc(end).toISOString(); - getUpcomingReservations(resStartRange, resEndRange, 10, 0) + calendarService.getUpcomingReservations(resStartRange, resEndRange, 10, 0) .then((response) => { dispatch({ type: UPDATE_UPCOMING_RESERVATIONS_LIST, @@ -440,7 +441,7 @@ const PlaceContextProvider: React.FC = ({ children }) => { setReservationsListLoading(true); const resStartRange = dayjs.utc(state.upcomingReservationsStartDate).toISOString(); const resEndRange = dayjs.utc(state.upcomingReservationsEndDate).toISOString(); - getUpcomingReservations(resStartRange, resEndRange, 10, skip) + calendarService.getUpcomingReservations(resStartRange, resEndRange, 10, skip) .then((response) => { dispatch({ type: LOAD_MORE_UPCOMING_RESERVATIONS, diff --git a/Converge/ClientApp/src/providers/SearchProvider.tsx b/Converge/ClientApp/src/providers/SearchProvider.tsx index 98b07fe..09adbae 100644 --- a/Converge/ClientApp/src/providers/SearchProvider.tsx +++ b/Converge/ClientApp/src/providers/SearchProvider.tsx @@ -4,7 +4,6 @@ import { User } from "@microsoft/microsoft-graph-types"; import { Dayjs } from "dayjs"; import React, { createContext, useContext, useEffect } from "react"; -import { searchCampusesToCollaborate, searchVenuesToCollaborate } from "../api/searchService"; import CampusesToCollaborateRequest from "../types/CampusesToCollaborateRequest"; import CampusesToCollaborateResponse from "../types/CampusesToCollaborateResponse"; import CampusToCollaborate from "../types/CampusToCollaborate"; @@ -12,6 +11,7 @@ import { CollaborationVenueType, getCollaborationVenueTypeString } from "../type import VenuesToCollaborateResponse from "../types/VenuesToCollaborateResponse"; import VenueToCollaborate from "../types/VenueToCollaborate"; import useEnhancedReducer from "../utilities/enhancedReducer"; +import { useApiProvider } from "./ApiProvider"; import { getDefaultTime } from "./PlaceFilterProvider"; const SET_START_TIME = "SET_START_TIME"; @@ -302,6 +302,7 @@ const reducer = (state: ISearchState, action: ISearchAction): ISearchState => { }; const SearchContextProvider: React.FC = ({ children }) => { + const { searchService } = useApiProvider(); const [state, dispatch, getState] = useEnhancedReducer( reducer, { ...iState }, @@ -331,9 +332,9 @@ const SearchContextProvider: React.FC = ({ children }) => { closeToUser: searchState.meetUsers.length > 1 ? "" : searchState.meetUsers[0], distanceFromSource: searchState.campusSearchRangeInMiles, }; - return searchCampusesToCollaborate(request); + return searchService.searchCampusesToCollaborate(request); } - return searchVenuesToCollaborate({ + return searchService.searchVenuesToCollaborate({ teamMembers: userList, venueType: getCollaborationVenueTypeString(searchState.venueType as CollaborationVenueType), endTime: searchState.endTime.utc().toDate(), diff --git a/Converge/ClientApp/src/providers/TeammateFilterProvider.tsx b/Converge/ClientApp/src/providers/TeammateFilterProvider.tsx index a13815f..e9db26d 100644 --- a/Converge/ClientApp/src/providers/TeammateFilterProvider.tsx +++ b/Converge/ClientApp/src/providers/TeammateFilterProvider.tsx @@ -3,16 +3,13 @@ import React, { createContext, useContext, useReducer } from "react"; import * as MicrosoftGraph from "@microsoft/microsoft-graph-types"; -import { - getMyList, getPeople, getWorkgroup, -} from "../api/meService"; -import { searchUsers } from "../api/userService"; import TimeLimit from "../types/TimeLimit"; import { logEvent } from "../utilities/LogWrapper"; import { DESCRIPTION, OVERLAP_PERCENTAGE, USER_INTERACTION, ViralityMeasures, VIRALITY_MEASURE, } from "../types/LoggerTypes"; import QueryOption from "../types/QueryOption"; +import { useApiProvider } from "./ApiProvider"; import { useConvergeSettingsContextProvider } from "./ConvergeSettingsProvider"; export enum TeammateList { @@ -296,6 +293,7 @@ const reducer = (state: ITeammateState, action: ITeammateAction): ITeammateState }; const TeammateFilterProvider: React.FC = ({ children }) => { + const { meService, userService } = useApiProvider(); const { convergeSettings } = useConvergeSettingsContextProvider(); const getInitialTeammatesListSettings = (): TeammateListSettings => { @@ -343,13 +341,13 @@ const TeammateFilterProvider: React.FC = ({ children }) => { let requestMethod; switch (list) { case TeammateList.Suggested: - requestMethod = getPeople; + requestMethod = meService.getPeople; break; case TeammateList.MyList: - requestMethod = getMyList; + requestMethod = meService.getMyList; break; case TeammateList.MyOrganization: - requestMethod = getWorkgroup; + requestMethod = meService.getWorkgroup; break; default: throw new Error("Invalid list type requested."); @@ -363,7 +361,7 @@ const TeammateFilterProvider: React.FC = ({ children }) => { }) .catch(() => dispatch({ type: TEAMMATES_ERROR })); } else { - searchUsers(searchString) + userService.searchUsers(searchString) .then((response) => { const payload = response.users.map((teammate) => ({ user: teammate, @@ -384,7 +382,7 @@ const TeammateFilterProvider: React.FC = ({ children }) => { teammatesPreset?: Teammate[], ) => { setMoreTeammatesLoading(true); - searchUsers(searchString, qOptions) + userService.searchUsers(searchString, qOptions) .then((data) => { const payload = data.users.map((teammate) => ({ user: teammate, diff --git a/Converge/ClientApp/src/tabs/collaborate/components/CampusPlacePanel.tsx b/Converge/ClientApp/src/tabs/collaborate/components/CampusPlacePanel.tsx index ce6bb00..b646e48 100644 --- a/Converge/ClientApp/src/tabs/collaborate/components/CampusPlacePanel.tsx +++ b/Converge/ClientApp/src/tabs/collaborate/components/CampusPlacePanel.tsx @@ -12,14 +12,14 @@ import { Icon } from "office-ui-fabric-react"; import { logEvent } from "../../../utilities/LogWrapper"; import CampusToCollaborate from "../../../types/CampusToCollaborate"; import PlaceAmmenities from "../../workspace/components/PlaceAmmenities"; -import { setSettings } from "../../../api/meService"; import { ImportantActions, IMPORTANT_ACTION, UISections, UI_SECTION, USER_INTERACTION, } from "../../../types/LoggerTypes"; import ImagePlaceholder from "../../../utilities/ImagePlaceholder"; import { useConvergeSettingsContextProvider } from "../../../providers/ConvergeSettingsProvider"; import CampusPlacePanelStyles from "../styles/CampusPlacePanelStyles"; -import { getPlacePhotos, PlacePhotosResult } from "../../../api/buildingService"; +import { useApiProvider } from "../../../providers/ApiProvider"; +import { PlacePhotosResult } from "../../../api/buildingService"; interface Props { setOpen: (open: boolean) => void; @@ -28,6 +28,7 @@ interface Props { } const CampusPlacePanel: React.FC = (props) => { + const { meService, buildingService } = useApiProvider(); const { convergeSettings, setConvergeSettings, @@ -56,7 +57,7 @@ const CampusPlacePanel: React.FC = (props) => { useEffect(() => { if (place.sharePointID) { - getPlacePhotos(place.sharePointID) + buildingService.getPlacePhotos(place.sharePointID) .then(setPlacePhotos); } }, [place.sharePointID]); @@ -124,7 +125,7 @@ const CampusPlacePanel: React.FC = (props) => { favoriteCampusesToCollaborate, }; setConvergeSettings(newSettings); - setSettings(newSettings) + meService.setSettings(newSettings) .then(() => { if (!isFavorite) { logEvent(USER_INTERACTION, [ diff --git a/Converge/ClientApp/src/tabs/collaborate/components/CollaborationPlaceDetails.tsx b/Converge/ClientApp/src/tabs/collaborate/components/CollaborationPlaceDetails.tsx index 7b6b038..65420e2 100644 --- a/Converge/ClientApp/src/tabs/collaborate/components/CollaborationPlaceDetails.tsx +++ b/Converge/ClientApp/src/tabs/collaborate/components/CollaborationPlaceDetails.tsx @@ -14,7 +14,6 @@ import VenueToCollaborate from "../../../types/VenueToCollaborate"; import CampusPlacePanel from "./CampusPlacePanel"; import NewEventModal from "./NewEventModal"; import Notifications from "../../../utilities/ToastManager"; -import { createEvent } from "../../../api/calendarService"; import CalendarEventRequest from "../../../types/CalendarEventRequest"; import CampusPlaceEventTitle from "../../workspace/components/CampusPlaceEventTitle"; import VenueEventTitle from "./VenueEventTitle"; @@ -24,9 +23,9 @@ import { } from "../../../types/LoggerTypes"; import CollaborationPlaceDetailsStyles from "../styles/CollaborationPlaceDetailsStyles"; import { PlaceType } from "../../../types/ExchangePlace"; -import { updateMyPredictedLocation } from "../../../api/meService"; import { AddRecentBuildings } from "../../../utilities/RecentBuildingsManager"; import { useConvergeSettingsContextProvider } from "../../../providers/ConvergeSettingsProvider"; +import { useApiProvider } from "../../../providers/ApiProvider"; interface Props { isOpen: boolean; @@ -37,6 +36,7 @@ interface Props { } const CollaborationPlaceDetails: React.FC = (props) => { + const { calendarService, meService } = useApiProvider(); const classes = CollaborationPlaceDetailsStyles(); const { isOpen, @@ -199,7 +199,7 @@ const CollaborationPlaceDetails: React.FC = (props) => { { name: VIRALITY_MEASURE, value: ViralityMeasures.CollaboratorCount }, { name: COLLABORATE_COUNT, value: attendees.length.toString() }, ]); - createEvent(newEvent) + calendarService.createEvent(newEvent) .then(() => { const newSettings = { ...convergeSettings, @@ -210,7 +210,7 @@ const CollaborationPlaceDetails: React.FC = (props) => { }; setConvergeSettings(newSettings); if ((place as CampusToCollaborate).type === PlaceType.Space) { - return updateMyPredictedLocation({ + return meService.updateMyPredictedLocation({ year: dayjs.utc(startDate).year(), month: dayjs.utc(startDate).month() + 1, day: dayjs.utc(startDate).date(), diff --git a/Converge/ClientApp/src/tabs/collaborate/components/FavoritesToCollaborate.tsx b/Converge/ClientApp/src/tabs/collaborate/components/FavoritesToCollaborate.tsx index c0213aa..1f21ad4 100644 --- a/Converge/ClientApp/src/tabs/collaborate/components/FavoritesToCollaborate.tsx +++ b/Converge/ClientApp/src/tabs/collaborate/components/FavoritesToCollaborate.tsx @@ -11,17 +11,16 @@ import { useBoolean } from "@fluentui/react-hooks"; import dayjs from "dayjs"; import CampusToCollaborate from "../../../types/CampusToCollaborate"; import VenueToCollaborate from "../../../types/VenueToCollaborate"; -import { getVenueDetails } from "../../../api/searchService"; -import VenueDetails from "../../../types/VenueDetails"; import CollaborationPlaceResults from "./CollaborationPlaceResults"; import CollaborationPlaceDetails from "./CollaborationPlaceDetails"; -import { getPlaceDetails } from "../../../api/buildingService"; +import VenueDetails from "../../../types/VenueDetails"; import { useConvergeSettingsContextProvider } from "../../../providers/ConvergeSettingsProvider"; import FavoritesToCollaborateStyles from "../styles/FavoritesToCollaborateStyles"; import { DESCRIPTION, UISections, UI_SECTION, USER_INTERACTION, } from "../../../types/LoggerTypes"; import { logEvent } from "../../../utilities/LogWrapper"; +import { useApiProvider } from "../../../providers/ApiProvider"; interface Props { setMapPlaces: (places: (CampusToCollaborate | VenueToCollaborate)[]) => void; @@ -52,6 +51,10 @@ function createVenueToCollaborate(v: VenueDetails): VenueToCollaborate { } const FavoritesToCollaborate: React.FC = (props) => { + const { + searchService, + buildingService, + } = useApiProvider(); const classes = FavoritesToCollaborateStyles(); const [open, setOpen] = useState(false); const [isError, setIsError] = useState(false); @@ -76,7 +79,7 @@ const FavoritesToCollaborate: React.FC = (props) => { if (convergeSettings?.favoriteCampusesToCollaborate) { const placeDetails = await Promise.all( convergeSettings.favoriteCampusesToCollaborate - .map((v) => getPlaceDetails(v, { start: new Date(), end: dayjs().utc().add(30, "minute").toDate() }) + .map((v) => buildingService.getPlaceDetails(v, { start: new Date(), end: dayjs().utc().add(30, "minute").toDate() }) .catch(() => { setIsError(true); const isErrorPlace = 0 as unknown as CampusToCollaborate; @@ -87,7 +90,7 @@ const FavoritesToCollaborate: React.FC = (props) => { } if (convergeSettings?.favoriteVenuesToCollaborate) { const venueDetails = await Promise.all( - convergeSettings.favoriteVenuesToCollaborate.map((v) => getVenueDetails(v) + convergeSettings.favoriteVenuesToCollaborate.map((v) => searchService.getVenueDetails(v) .catch(() => { setIsError(true); const isErrorVenue = 0 as unknown as VenueToCollaborate; diff --git a/Converge/ClientApp/src/tabs/collaborate/components/Map.tsx b/Converge/ClientApp/src/tabs/collaborate/components/Map.tsx index f00ba58..39fe137 100644 --- a/Converge/ClientApp/src/tabs/collaborate/components/Map.tsx +++ b/Converge/ClientApp/src/tabs/collaborate/components/Map.tsx @@ -12,7 +12,6 @@ import { useBoolean } from "@fluentui/react-hooks"; import { User } from "@microsoft/microsoft-graph-types"; import BingMaps from "../../../utilities/BingMaps"; import { loadBingApi, Microsoft } from "../../../utilities/BingMapLoader"; -import { createUserCoordinateService } from "../../../api/userService"; import { useSearchContextProvider } from "../../../providers/SearchProvider"; import CampusToCollaborate from "../../../types/CampusToCollaborate"; import VenueToCollaborate from "../../../types/VenueToCollaborate"; @@ -27,6 +26,7 @@ import useAsyncRecord from "../../../hooks/useAsyncRecord"; import { ItemRecord } from "../../../hooks/useRecord"; import ErrorBoundary from "../../../utilities/ErrorBoundary"; import { createUserPushpin } from "../../../utilities/Pushpins"; +import { useApiProvider } from "../../../providers/ApiProvider"; interface Props { userRecord: ItemRecord, @@ -39,11 +39,14 @@ const Map: React.FC = ({ updateUserRecord, setUsersMissingCoordinates, }) => { + const { + userService, + } = useApiProvider(); const { convergeSettings, } = useConvergeSettingsContextProvider(); const { teamsContext } = useTeamsContext(); - const userCoordinateService = useMemo(createUserCoordinateService, []); + const userCoordinateService = useMemo(userService.createUserCoordinateService, []); const { state, } = useSearchContextProvider(); diff --git a/Converge/ClientApp/src/tabs/collaborate/components/NewEventModal.tsx b/Converge/ClientApp/src/tabs/collaborate/components/NewEventModal.tsx index 007ae9d..3f03988 100644 --- a/Converge/ClientApp/src/tabs/collaborate/components/NewEventModal.tsx +++ b/Converge/ClientApp/src/tabs/collaborate/components/NewEventModal.tsx @@ -31,9 +31,9 @@ import { logEvent } from "../../../utilities/LogWrapper"; import { DESCRIPTION, UISections, UI_SECTION, USER_INTERACTION, } from "../../../types/LoggerTypes"; -import { searchUsers } from "../../../api/userService"; import DatePickerPrimary from "../../../utilities/datePickerPrimary"; import NewEventModalStyles from "../styles/NewEventModalStyles"; +import { useApiProvider } from "../../../providers/ApiProvider"; type Props = { attendees: MicrosoftGraph.User[]; @@ -74,6 +74,7 @@ const NewEventModal: React.FC = (props) => { setMessage, err, } = props; + const { userService } = useApiProvider(); const classes = NewEventModalStyles(); const [attendeesLoading, setAttendeesLoading] = useState(false); const [attendeeItems, setAttendeeItems] = useState([]); @@ -169,7 +170,7 @@ const NewEventModal: React.FC = (props) => { ) => { if (data?.searchQuery) { setAttendeesLoading(true); - searchUsers(data.searchQuery.toString()) + userService.searchUsers(data.searchQuery.toString()) .then((response) => { setAttendeeItems( response.users diff --git a/Converge/ClientApp/src/tabs/collaborate/components/RecommendedToCollaborate.tsx b/Converge/ClientApp/src/tabs/collaborate/components/RecommendedToCollaborate.tsx index 00d0e59..e987573 100644 --- a/Converge/ClientApp/src/tabs/collaborate/components/RecommendedToCollaborate.tsx +++ b/Converge/ClientApp/src/tabs/collaborate/components/RecommendedToCollaborate.tsx @@ -9,7 +9,6 @@ import { Button, ErrorIcon, Loader, Text, } from "@fluentui/react-northstar"; -import { searchCampusesToCollaborate } from "../../../api/searchService"; import CampusToCollaborate from "../../../types/CampusToCollaborate"; import CollaborationPlaceDetails from "./CollaborationPlaceDetails"; import CollaborationPlaceResults from "./CollaborationPlaceResults"; @@ -20,6 +19,7 @@ import { DESCRIPTION, UISections, UI_SECTION, USER_INTERACTION, } from "../../../types/LoggerTypes"; import { logEvent } from "../../../utilities/LogWrapper"; +import { useApiProvider } from "../../../providers/ApiProvider"; interface Props { setMapPlaces: (places: (CampusToCollaborate | VenueToCollaborate)[]) => void; @@ -33,6 +33,7 @@ const RecommendedToCollaborate: React.FC = (props) => { placesLoading, setPlacesLoading, } = props; + const { searchService } = useApiProvider(); const { teamsContext } = useTeamsContext(); const classes = RecommendedToCollaborateStyles(); const [open, setOpen] = useState(false); @@ -47,7 +48,7 @@ const RecommendedToCollaborate: React.FC = (props) => { if (teamsContext?.userPrincipalName) { setUserPrincipalName(teamsContext.userPrincipalName); setPlacesLoading(true); - searchCampusesToCollaborate({ + searchService.searchCampusesToCollaborate({ teamMembers: [teamsContext.userPrincipalName], startTime: dayjs().utc().add(5, "minutes").toDate(), endTime: dayjs().utc().add(35, "minutes").toDate(), @@ -65,7 +66,7 @@ const RecommendedToCollaborate: React.FC = (props) => { const getRecommendations = () => { setIsError(false); setPlacesLoading(true); - searchCampusesToCollaborate({ + searchService.searchCampusesToCollaborate({ teamMembers: [upn], startTime: dayjs().utc().add(5, "minutes").toDate(), endTime: dayjs().utc().add(35, "minutes").toDate(), diff --git a/Converge/ClientApp/src/tabs/collaborate/components/UserSearchDropdown.tsx b/Converge/ClientApp/src/tabs/collaborate/components/UserSearchDropdown.tsx index 0f7bf07..083b50d 100644 --- a/Converge/ClientApp/src/tabs/collaborate/components/UserSearchDropdown.tsx +++ b/Converge/ClientApp/src/tabs/collaborate/components/UserSearchDropdown.tsx @@ -9,12 +9,12 @@ import { User } from "@microsoft/microsoft-graph-types"; import React, { useEffect, useState } from "react"; import debounce from "lodash/debounce"; import { logEvent } from "../../../utilities/LogWrapper"; -import { searchUsers } from "../../../api/userService"; import PrimaryDropdown from "../../../utilities/PrimaryDropdown"; import { DESCRIPTION, UISections, UI_SECTION, USER_INTERACTION, } from "../../../types/LoggerTypes"; import UserSearchDropdownStyles from "../styles/UserSearchDropdownStyles"; +import { useApiProvider } from "../../../providers/ApiProvider"; interface Props { maxSelected?: number, @@ -30,6 +30,7 @@ const getA11ySelectionMessage = { }; const UserSearchDropdown:React.FC = (props) => { + const { userService } = useApiProvider(); const { selectedUsers, onSelectedUsersChange, @@ -84,7 +85,7 @@ const UserSearchDropdown:React.FC = (props) => { ) => { if (data?.searchQuery) { setLoading(true); - searchUsers(data.searchQuery.toString()) + userService.searchUsers(data.searchQuery.toString()) .then((response) => { setInputItems(response.users .filter((u) => !!u.displayName) diff --git a/Converge/ClientApp/src/tabs/collaborate/components/VenuePlacePanel.tsx b/Converge/ClientApp/src/tabs/collaborate/components/VenuePlacePanel.tsx index f2c0a5a..d3402e8 100644 --- a/Converge/ClientApp/src/tabs/collaborate/components/VenuePlacePanel.tsx +++ b/Converge/ClientApp/src/tabs/collaborate/components/VenuePlacePanel.tsx @@ -16,11 +16,11 @@ import { USER_INTERACTION, UI_SECTION, UISections, DESCRIPTION, IMPORTANT_ACTION, ImportantActions, } from "../../../types/LoggerTypes"; import VenueReviews from "./VenueReviews"; -import { getVenueDetails } from "../../../api/searchService"; import VenueDetails from "../../../types/VenueDetails"; import VenueDetailsDisplay from "./VenueDetailsDisplay"; import { useConvergeSettingsContextProvider } from "../../../providers/ConvergeSettingsProvider"; import VenuePlacePanelStyles from "../styles/VenuePlacePanelStyles"; +import { useApiProvider } from "../../../providers/ApiProvider"; interface Props { setOpen: (open: boolean) => void; @@ -34,6 +34,7 @@ enum VenueDetailsTabs { } const PlacePanel: React.FC = (props) => { + const { searchService } = useApiProvider(); const { convergeSettings, setConvergeSettings, @@ -50,7 +51,7 @@ const PlacePanel: React.FC = (props) => { useEffect(() => { setLoading(true); - getVenueDetails(place.venueId) + searchService.getVenueDetails(place.venueId) .then( (response) => setVenueDetails( response, diff --git a/Converge/ClientApp/src/tabs/collaborate/components/VenueReviews.tsx b/Converge/ClientApp/src/tabs/collaborate/components/VenueReviews.tsx index 873957b..8a153f4 100644 --- a/Converge/ClientApp/src/tabs/collaborate/components/VenueReviews.tsx +++ b/Converge/ClientApp/src/tabs/collaborate/components/VenueReviews.tsx @@ -3,12 +3,12 @@ import { Flex, Divider } from "@fluentui/react-northstar"; import React, { useEffect, useState } from "react"; -import { getReviews } from "../../../api/searchService"; import YelpReview from "../../../types/Review"; import VenueToCollaborate from "../../../types/VenueToCollaborate"; import Review from "./Review"; import VenueReviewsStyles from "../styles/VenueReviewsStyles"; +import { useApiProvider } from "../../../providers/ApiProvider"; interface Props { place: VenueToCollaborate; @@ -16,10 +16,11 @@ interface Props { const VenueReviews:React.FC = (props) => { const { place } = props; + const { searchService } = useApiProvider(); const classes = VenueReviewsStyles(); const [reviews, setReviews] = useState([]); useEffect(() => { - getReviews(place.venueId).then((yelpReviews) => { + searchService.getReviews(place.venueId).then((yelpReviews) => { setReviews(yelpReviews.response.reviews); }); }, [place.venueId]); diff --git a/Converge/ClientApp/src/tabs/collaborate/index.tsx b/Converge/ClientApp/src/tabs/collaborate/index.tsx index 5e4ce1d..98c44d6 100644 --- a/Converge/ClientApp/src/tabs/collaborate/index.tsx +++ b/Converge/ClientApp/src/tabs/collaborate/index.tsx @@ -9,13 +9,14 @@ import CollaborateFurther from "./components/CollaborateFurther"; import CollaborateHeader from "./CollaborateHeader"; import Map from "./components/Map"; import { useSearchContextProvider } from "../../providers/SearchProvider"; -import getCollaborator from "../../api/userService"; import { deserializeSubEntityId } from "../../utilities/deepLink"; import { MapProvider } from "../../providers/MapProvider"; import useRecord from "../../hooks/useRecord"; import { useTeamsContext } from "../../providers/TeamsContextProvider"; +import { useApiProvider } from "../../providers/ApiProvider"; const Collaborate: React.FC = () => { + const { userService } = useApiProvider(); const [loading, setLoading] = useState(true); const { setStartTime, @@ -33,7 +34,7 @@ const Collaborate: React.FC = () => { if (!inCache) cacheMisses.push(name); return inCache; }).map((name) => userRecord[name]); - const su = await Promise.all(cacheMisses.map(getCollaborator)); + const su = await Promise.all(cacheMisses.map(userService.getCollaborator)); // Add new users to cache su.filter((user) => !!user.userPrincipalName) diff --git a/Converge/ClientApp/src/tabs/home/BookWorkspace.tsx b/Converge/ClientApp/src/tabs/home/BookWorkspace.tsx index 9085383..f7de751 100644 --- a/Converge/ClientApp/src/tabs/home/BookWorkspace.tsx +++ b/Converge/ClientApp/src/tabs/home/BookWorkspace.tsx @@ -17,15 +17,12 @@ import AvailabilityChart from "./components/AvailabilityChart"; import BuildingCapacity from "./components/BuildingCapacity"; import Schedule from "../../types/Schedule"; import ExchangePlace, { PlaceType } from "../../types/ExchangePlace"; -import { getBuildingSchedule, getBuildingPlaces, getBuildingByDisplayName } from "../../api/buildingService"; -import { getWorkingHours, createEvent } from "../../api/calendarService"; import { DESCRIPTION, UISections, UI_SECTION, USER_INTERACTION, } from "../../types/LoggerTypes"; import { logEvent } from "../../utilities/LogWrapper"; import createDeepLink from "../../utilities/deepLink"; -import { getMyRecommendation, updateMyPredictedLocation } from "../../api/meService"; import TravelTimes from "./components/TravelTimes"; import WorkingStartEnd from "../../types/WorkingStartEnd"; import IsThisHelpful from "../../utilities/IsThisHelpful"; @@ -41,11 +38,17 @@ import { useAppSettingsProvider } from "../../providers/AppSettingsProvider"; import AddRecentBuildings from "../../utilities/RecentBuildingsManager"; import PopupMenuWrapper from "../../utilities/popupMenuWrapper"; import BuildingBasicInfo from "../../types/BuildingBasicInfo"; +import { useApiProvider } from "../../providers/ApiProvider"; const RECOMMENDED = "My Location"; const BookWorkspace: React.FC = () => { const classes = BookWorkspaceStyles(); + const { + calendarService, + meService, + buildingService, + } = useApiProvider(); const { state, convergeSettings, @@ -118,7 +121,7 @@ const BookWorkspace: React.FC = () => { setIsError(false); let hours = workingHours; if (!hours) { - hours = await getWorkingHours(); + hours = await calendarService.getWorkingHours(); setWorkingHours(hours); } const startHours = dayjs(hours.start); @@ -128,7 +131,7 @@ const BookWorkspace: React.FC = () => { if (endBuilding.isBefore(startBuilding)) { endBuilding = endBuilding.add(1, "day"); } - return getBuildingSchedule( + return buildingService.getBuildingSchedule( building.identity, startBuilding.toISOString(), endBuilding.toISOString(), @@ -137,15 +140,15 @@ const BookWorkspace: React.FC = () => { .finally(() => setLoading(false)); }; - const refreshRecommendation = async () => { + const refreshRecommendation = () => { setIsError(false); const day = dayjs.utc(start); setSelectedBuilding(undefined); - await getMyRecommendation(day.year(), day.month() + 1, day.date()) - .then(async (recommendation) => { + meService.getMyRecommendation(day.year(), day.month() + 1, day.date()) + .then((recommendation) => { setMyRecommendation(recommendation); if (recommendation !== "Remote" && recommendation !== "Out of Office") { - await getBuildingByDisplayName(recommendation).then((building) => { + buildingService.getBuildingByDisplayName(recommendation).then((building) => { if (building) { getSchedule(building); setSelectedBuilding(building); @@ -170,7 +173,7 @@ const BookWorkspace: React.FC = () => { useEffect(() => { if (selectedBuilding) { - getBuildingPlaces( + buildingService.getBuildingPlaces( selectedBuilding.identity, PlaceType.Space, { @@ -232,7 +235,7 @@ const BookWorkspace: React.FC = () => { const refreshRecommended = async () => { setLoading(true); const day = dayjs.utc(start); - getMyRecommendation(day.year(), day.month() + 1, day.date()) + meService.getMyRecommendation(day.year(), day.month() + 1, day.date()) .then((recommendation) => { setMyRecommendation(recommendation); if (recommendation !== "Remote" && recommendation !== "Out of Office") { @@ -248,7 +251,7 @@ const BookWorkspace: React.FC = () => { useEffect(() => { if (selectedBuilding?.identity) { - getBuildingPlaces(selectedBuilding?.identity, PlaceType.Room) + buildingService.getBuildingPlaces(selectedBuilding?.identity, PlaceType.Room) .then((response) => { const place = response.exchangePlacesList.find((ep) => ( ep.street || ep.city || ep.postalCode || ep.countryOrRegion @@ -399,7 +402,7 @@ const BookWorkspace: React.FC = () => { startDate = dayjs(start.format("YYYY-MM-DD")).toDate(); endDate = dayjs(end.add(1, "day").format("YYYY-MM-DD")).toDate(); } - createEvent({ + calendarService.createEvent({ isAllDay, start: startDate, end: endDate, @@ -416,7 +419,7 @@ const BookWorkspace: React.FC = () => { showAs: "free" as MicrosoftGraph.FreeBusyStatus, }) .then(() => { - updateMyPredictedLocation({ + meService.updateMyPredictedLocation({ year: dayjs.utc(startDate).year(), month: dayjs.utc(startDate).month() + 1, day: dayjs.utc(startDate).date(), diff --git a/Converge/ClientApp/src/tabs/home/ConnectTeammates.tsx b/Converge/ClientApp/src/tabs/home/ConnectTeammates.tsx index 268f0c1..2e05ce6 100644 --- a/Converge/ClientApp/src/tabs/home/ConnectTeammates.tsx +++ b/Converge/ClientApp/src/tabs/home/ConnectTeammates.tsx @@ -27,10 +27,10 @@ import IsThisHelpful from "../../utilities/IsThisHelpful"; import PrimaryDropdown from "../../utilities/PrimaryDropdown"; import DatePickerPrimary from "../../utilities/datePickerPrimary"; import EnterZipcode from "../../utilities/EnterZipCodeDialog"; -import { getConvergeCalendar, setupNewUser } from "../../api/meService"; import ConvergeSettings from "../../types/ConvergeSettings"; import { useConvergeSettingsContextProvider } from "../../providers/ConvergeSettingsProvider"; import ConnectTeammatesStyles from "./styles/ConnectTeammatesStyles"; +import { useApiProvider } from "../../providers/ApiProvider"; type IWidget = { id: string; @@ -38,6 +38,7 @@ type IWidget = { } const ConnectTeammates: React.FC = () => { + const { meService } = useApiProvider(); const { convergeSettings } = useConvergeSettingsContextProvider(); const classes = ConnectTeammatesStyles(); const [isError, setIsError] = React.useState(false); @@ -99,7 +100,7 @@ const ConnectTeammates: React.FC = () => { }; const getMyConvergeCalendar = async () => { - getConvergeCalendar() + meService.getConvergeCalendar() .then((ConvergeCalendar) => { if (ConvergeCalendar === null || ConvergeCalendar === undefined) { setConvergeCalendar(true); @@ -113,7 +114,8 @@ const ConnectTeammates: React.FC = () => { getMyConvergeCalendar(); }, []); - const setupNewUserWrapper = (settings: ConvergeSettings): Promise => setupNewUser(settings) + const setupNewUserWrapper = (settings: ConvergeSettings): + Promise => meService.setupNewUser(settings) .then(() => { setConvergeCalendar(false); }) diff --git a/Converge/ClientApp/src/tabs/home/Table/SelectableTable.tsx b/Converge/ClientApp/src/tabs/home/Table/SelectableTable.tsx index bb15ad3..aabe073 100644 --- a/Converge/ClientApp/src/tabs/home/Table/SelectableTable.tsx +++ b/Converge/ClientApp/src/tabs/home/Table/SelectableTable.tsx @@ -39,10 +39,10 @@ import { Teammate, TeammateList, useTeammateProvider } from "../../../providers/ import WorkgroupAvatar from "../components/WorkgroupAvatar"; import AvailableTimesCell from "./AvailableTimesCell"; import UserLocationCell from "./UserLocationCell"; -import { getMultiUserAvailabilityTimes } from "../../../api/userService"; import TimeLimit from "../../../types/TimeLimit"; import { useConvergeSettingsContextProvider } from "../../../providers/ConvergeSettingsProvider"; import SelectableTableStyles from "../styles/SelectableTableStyles"; +import { useApiProvider } from "../../../providers/ApiProvider"; const USER_AVAILABILITY_REQUEST = "USER_AVAILABILITY_REQUEST"; const USER_AVAILABILITY_RESPONSE = "USER_AVAILABILITY_RESPONSE"; @@ -163,6 +163,7 @@ interface Props { } const SelectableTable: React.FC = (props) => { + const { userService } = useApiProvider(); const { teammates } = props; const { convergeSettings, @@ -275,7 +276,7 @@ const SelectableTable: React.FC = (props) => { .second(0); const scheduleStart = scheduleDate.toDate(); const scheduleEnd = dayjs(scheduleStart).add(1, "day").toDate(); - getMultiUserAvailabilityTimes( + userService.getMultiUserAvailabilityTimes( users.map((teammate) => teammate.user.userPrincipalName as string), day.year(), day.month() + 1, diff --git a/Converge/ClientApp/src/tabs/home/Table/UserLocationCell.tsx b/Converge/ClientApp/src/tabs/home/Table/UserLocationCell.tsx index cbed221..2313d1e 100644 --- a/Converge/ClientApp/src/tabs/home/Table/UserLocationCell.tsx +++ b/Converge/ClientApp/src/tabs/home/Table/UserLocationCell.tsx @@ -4,7 +4,7 @@ import { Loader } from "@fluentui/react-northstar"; import dayjs from "dayjs"; import React, { useEffect, useState } from "react"; -import { getLocation } from "../../../api/userService"; +import { useApiProvider } from "../../../providers/ApiProvider"; import { Teammate, useTeammateProvider } from "../../../providers/TeammateFilterProvider"; interface Props { @@ -13,13 +13,14 @@ interface Props { const UserLocationCell: React.FC = (props) => { const { teammate } = props; + const { userService } = useApiProvider(); const [loading, setLoading] = useState(true); const { state, setTeammateLocation } = useTeammateProvider(); const [isError, setIsError] = React.useState(false); useEffect(() => { if (teammate.user.id) { const day = dayjs.utc(state.date); - getLocation(teammate.user.id, day.year(), day.month() + 1, day.date()) + userService.getLocation(teammate.user.id, day.year(), day.month() + 1, day.date()) .then((loc) => { if (teammate.user.id) { setTeammateLocation(teammate.user.id, loc); diff --git a/Converge/ClientApp/src/tabs/home/components/TravelTimes.tsx b/Converge/ClientApp/src/tabs/home/components/TravelTimes.tsx index da7b801..f1d82d1 100644 --- a/Converge/ClientApp/src/tabs/home/components/TravelTimes.tsx +++ b/Converge/ClientApp/src/tabs/home/components/TravelTimes.tsx @@ -5,7 +5,7 @@ import { Flex } from "@fluentui/react-northstar"; import dayjs from "dayjs"; import { Icon } from "office-ui-fabric-react"; import React, { useEffect, useState } from "react"; -import getRoute from "../../../api/routeService"; +import { useApiProvider } from "../../../providers/ApiProvider"; interface Props { start: string, @@ -14,11 +14,12 @@ interface Props { const TravelTimes:React.FC = (props) => { const { start, end } = props; + const { routeService } = useApiProvider(); const [driveTime, setDriveTime] = useState(""); const [transitTime, setTransitTime] = useState(""); const [isError, setIsError] = useState(false); useEffect(() => { - getRoute(start, end) + routeService.getRoute(start, end) .then((routeResponse) => { setDriveTime(dayjs.duration(routeResponse.driveTravelTimeInSeconds, "seconds").humanize()); setTransitTime(dayjs.duration(routeResponse.transitTravelTimeInSeconds, "seconds").humanize()); diff --git a/Converge/ClientApp/src/tabs/home/components/WorkgroupAvatar.tsx b/Converge/ClientApp/src/tabs/home/components/WorkgroupAvatar.tsx index 2c45815..966871c 100644 --- a/Converge/ClientApp/src/tabs/home/components/WorkgroupAvatar.tsx +++ b/Converge/ClientApp/src/tabs/home/components/WorkgroupAvatar.tsx @@ -7,10 +7,10 @@ import { Avatar, AcceptIcon, WindowMinimizeIcon, CloseIcon, CircleIcon, ShiftActivityIcon, ArrowLeftIcon, Flex, } from "@fluentui/react-northstar"; -import { getUserProfile } from "../../../api/userService"; import PresenceAvailability from "../../../types/PresenceAvailability"; import WorkgroupAvatarStyles from "../styles/WorkgroupAvatarStyles"; import ApiPresence from "../../../types/ApiPresence"; +import { useApiProvider } from "../../../providers/ApiProvider"; interface Props { user: MicrosoftGraph.User @@ -92,6 +92,7 @@ const getAvailabilityBorderColor = ( }; const WorkgroupAvatar: React.FC = (props) => { + const { userService } = useApiProvider(); const { user, } = props; @@ -102,7 +103,7 @@ const WorkgroupAvatar: React.FC = (props) => { useEffect(() => { if (user.userPrincipalName) { - const response = getUserProfile(user.userPrincipalName); + const response = userService.getUserProfile(user.userPrincipalName); response.then((photo) => { const blob = new Blob(photo.userPhoto); if (blob.size !== 0) { diff --git a/Converge/ClientApp/src/tabs/workspace/components/BookPlaceModal.tsx b/Converge/ClientApp/src/tabs/workspace/components/BookPlaceModal.tsx index 683baa8..67e0eb8 100644 --- a/Converge/ClientApp/src/tabs/workspace/components/BookPlaceModal.tsx +++ b/Converge/ClientApp/src/tabs/workspace/components/BookPlaceModal.tsx @@ -14,7 +14,6 @@ import { IComboBox, IComboBoxOption } from "@fluentui/react"; import TimePicker from "./TimePicker"; import PlaceCarousel from "./PlaceCarousel"; import ExchangePlace, { PlaceType } from "../../../types/ExchangePlace"; -import getPlaceMaxReserved, { getRoomAvailability } from "../../../api/placeService"; import { logEvent } from "../../../utilities/LogWrapper"; import { USER_INTERACTION, UISections, UI_SECTION, DESCRIPTION, IMPORTANT_ACTION, ImportantActions, @@ -24,8 +23,8 @@ import DatePickerPrimary from "../../../utilities/datePickerPrimary"; import PlaceAmmenities from "./PlaceAmmenities"; import BookPlaceModalStyles from "../styles/BookPlaceModalStyles"; import { useConvergeSettingsContextProvider } from "../../../providers/ConvergeSettingsProvider"; -import { setSettings } from "../../../api/meService"; -import { getPlacePhotos, PlacePhotosResult } from "../../../api/buildingService"; +import { useApiProvider } from "../../../providers/ApiProvider"; +import { PlacePhotosResult } from "../../../api/buildingService"; type Props = { place: ExchangePlace, @@ -57,6 +56,11 @@ const BookPlaceModal: React.FC = (props) => { setIsAllDay, isFlexible, } = props; + const { + meService, + placeService, + buildingService, + } = useApiProvider(); const [maxReserved, setMaxReserved] = useState(0); const [isAvailable, setIsAvailable] = useState(true); const [placePhotos, setPlacePhotos] = useState(undefined); @@ -154,14 +158,14 @@ const BookPlaceModal: React.FC = (props) => { } if (place.type === PlaceType.Space) { if (start.utc().toISOString() <= end.utc().toISOString()) { - getPlaceMaxReserved( + placeService.getPlaceMaxReserved( place.identity, dayjs(startDay).utc().toISOString(), dayjs(endDay).utc().toISOString(), ).then(setMaxReserved); } } else if (start.utc().toISOString() <= end.utc().toISOString()) { - getRoomAvailability( + placeService.getRoomAvailability( place.identity, dayjs(start).utc().toISOString(), dayjs(end).utc().toISOString(), @@ -185,7 +189,7 @@ const BookPlaceModal: React.FC = (props) => { useEffect(() => { if (place.sharePointID) { - getPlacePhotos(place.sharePointID).then(setPlacePhotos); + buildingService.getPlacePhotos(place.sharePointID).then(setPlacePhotos); } }, [place.sharePointID]); @@ -388,7 +392,7 @@ const BookPlaceModal: React.FC = (props) => { favoriteCampusesToCollaborate, }; setConvergeSettings(newSettings); - setSettings(newSettings) + meService.setSettings(newSettings) .then(() => { if (!isFavorite) { logEvent(USER_INTERACTION, [ diff --git a/Converge/ClientApp/src/tabs/workspace/components/BuildingPlaces.tsx b/Converge/ClientApp/src/tabs/workspace/components/BuildingPlaces.tsx index 663ed1e..31b1718 100644 --- a/Converge/ClientApp/src/tabs/workspace/components/BuildingPlaces.tsx +++ b/Converge/ClientApp/src/tabs/workspace/components/BuildingPlaces.tsx @@ -24,6 +24,7 @@ import BuildingPlacesStyles from "../styles/BuildingPlacesStyles"; import PlaceCard from "./PlaceCard"; import RepeatingBox from "./RepeatingBox"; import { useProvider as PlaceFilterProvider } from "../../../providers/PlaceFilterProvider"; +import { useApiProvider } from "../../../providers/ApiProvider"; import IsThisHelpful from "../../../utilities/IsThisHelpful"; interface IPlaceResultSetProps { @@ -35,13 +36,14 @@ const BuildingPlaces: React.FC = ({ buildingUpn, placeType const { theme } = useFluentContext(); const { state } = PlaceFilterProvider(); const classes = BuildingPlacesStyles(); + const { buildingService } = useApiProvider(); const { placesLoading, places, placesError, requestBuildingPlaces, hasMore, - } = useBuildingPlaces(buildingUpn); + } = useBuildingPlaces(buildingService, buildingUpn); const pageSizeOptions = [ 10, 15, 25, 50, diff --git a/Converge/ClientApp/src/tabs/workspace/components/CampusPlaceEventTitle.tsx b/Converge/ClientApp/src/tabs/workspace/components/CampusPlaceEventTitle.tsx index 6ea131c..146cd74 100644 --- a/Converge/ClientApp/src/tabs/workspace/components/CampusPlaceEventTitle.tsx +++ b/Converge/ClientApp/src/tabs/workspace/components/CampusPlaceEventTitle.tsx @@ -6,8 +6,8 @@ import React, { useEffect, useState } from "react"; import dayjs, { Dayjs } from "dayjs"; import { Icon } from "office-ui-fabric-react"; import ExchangePlace from "../../../types/ExchangePlace"; -import { getRoomAvailability } from "../../../api/placeService"; import CampusPlaceEventTitleStyles from "../styles/CampusPlaceEventTitleStyles"; +import { useApiProvider } from "../../../providers/ApiProvider"; interface Props { place: ExchangePlace, @@ -18,6 +18,7 @@ interface Props { } const CampusPlaceEventTitle:React.FC = (props) => { + const { placeService } = useApiProvider(); const classes = CampusPlaceEventTitleStyles(); const { place, @@ -41,7 +42,7 @@ const CampusPlaceEventTitle:React.FC = (props) => { startDay = dayjs(dayjs(date).format("MM-DD-YYYY")); endDay = dayjs(startDay).add(1, "day"); } - getRoomAvailability( + placeService.getRoomAvailability( place.identity, startDay.utc().toISOString(), endDay.utc().toISOString(), diff --git a/Converge/ClientApp/src/tabs/workspace/components/CollaborationCampusPlaceCard.tsx b/Converge/ClientApp/src/tabs/workspace/components/CollaborationCampusPlaceCard.tsx index a264e8e..f9328f6 100644 --- a/Converge/ClientApp/src/tabs/workspace/components/CollaborationCampusPlaceCard.tsx +++ b/Converge/ClientApp/src/tabs/workspace/components/CollaborationCampusPlaceCard.tsx @@ -15,7 +15,8 @@ import { import { useConvergeSettingsContextProvider } from "../../../providers/ConvergeSettingsProvider"; import CollaborationCampusPlaceCardStyles from "../styles/CollaborationCampusPlaceCardStyles"; import { logEvent } from "../../../utilities/LogWrapper"; -import { getPlacePhotos, PlacePhotosResult } from "../../../api/buildingService"; +import { PlacePhotosResult } from "../../../api/buildingService"; +import { useApiProvider } from "../../../providers/ApiProvider"; type Props = { placeToCollaborate: CampusToCollaborate, @@ -24,6 +25,7 @@ type Props = { const CollaborationCampusPlaceCard:React.FC = (props) => { const { placeToCollaborate, onPlaceClick } = props; + const { buildingService } = useApiProvider(); const { convergeSettings } = useConvergeSettingsContextProvider(); const classes = CollaborationCampusPlaceCardStyles(); const ammenities = getAmmenities(placeToCollaborate); @@ -36,7 +38,7 @@ const CollaborationCampusPlaceCard:React.FC = (props) => { useEffect(() => { if (placeToCollaborate.sharePointID) { setPlacePhotosLoading(true); - getPlacePhotos(placeToCollaborate.sharePointID) + buildingService.getPlacePhotos(placeToCollaborate.sharePointID) .then(setPlacePhotos) .catch(() => setPlacePhotosError(true)) .finally(() => setPlacePhotosLoading(false)); diff --git a/Converge/ClientApp/src/tabs/workspace/components/CustomizedPlaceCollectionAccordian.tsx b/Converge/ClientApp/src/tabs/workspace/components/CustomizedPlaceCollectionAccordian.tsx index 8038428..af48ad5 100644 --- a/Converge/ClientApp/src/tabs/workspace/components/CustomizedPlaceCollectionAccordian.tsx +++ b/Converge/ClientApp/src/tabs/workspace/components/CustomizedPlaceCollectionAccordian.tsx @@ -16,6 +16,7 @@ import { import ExchangePlace, { PlaceType } from "../../../types/ExchangePlace"; import useBuildingPlaces from "../../../hooks/useBuildingPlaces"; import BuildingBasicInfo from "../../../types/BuildingBasicInfo"; +import { useApiProvider } from "../../../providers/ApiProvider"; import IsThisHelpful from "../../../utilities/IsThisHelpful"; interface Props { @@ -26,13 +27,14 @@ interface Props { const CustomizedPlaceCollectionAccordian: React.FC = (props) => { const { closestBuilding, favoriteCampuses } = props; const { state, updateLocation } = PlaceFilterProvider(); + const { buildingService } = useApiProvider(); const { placesLoading, places, placesError, requestBuildingPlaces, hasMore, - } = useBuildingPlaces(closestBuilding.identity); + } = useBuildingPlaces(buildingService, closestBuilding.identity); useEffect(() => { requestBuildingPlaces( diff --git a/Converge/ClientApp/src/tabs/workspace/components/NewConfRoomEvent.tsx b/Converge/ClientApp/src/tabs/workspace/components/NewConfRoomEvent.tsx index a0a1f07..9667c28 100644 --- a/Converge/ClientApp/src/tabs/workspace/components/NewConfRoomEvent.tsx +++ b/Converge/ClientApp/src/tabs/workspace/components/NewConfRoomEvent.tsx @@ -10,7 +10,6 @@ import { logEvent } from "../../../utilities/LogWrapper"; import NewEventModal from "../../collaborate/components/NewEventModal"; import ExchangePlace from "../../../types/ExchangePlace"; import { useProvider as PlaceFilterProvider } from "../../../providers/PlaceFilterProvider"; -import { createEvent } from "../../../api/calendarService"; import Notifications from "../../../utilities/ToastManager"; import CampusPlaceEventTitle from "./CampusPlaceEventTitle"; import { @@ -18,6 +17,7 @@ import { } from "../../../types/LoggerTypes"; import AddRecentBuildings from "../../../utilities/RecentBuildingsManager"; import { useConvergeSettingsContextProvider } from "../../../providers/ConvergeSettingsProvider"; +import { useApiProvider } from "../../../providers/ApiProvider"; interface Props { open: boolean; @@ -43,6 +43,7 @@ const NewConfRoomEvent:React.FC = (props) => { clearPlaceCard, getAvailability, } = props; + const { calendarService } = useApiProvider(); const { createReservation, loadUpcomingReservations } = PlaceFilterProvider(); const [isAllDay, setIsAllDay] = useState(false); const [subject, setSubject] = useState(""); @@ -103,7 +104,7 @@ const NewConfRoomEvent:React.FC = (props) => { endDate = dayjs(end.add(1, "day").format("YYYY-MM-DD")).toDate(); } - createEvent({ + calendarService.createEvent({ isAllDay, start: startDate, end: endDate, diff --git a/Converge/ClientApp/src/tabs/workspace/components/PlaceCard.tsx b/Converge/ClientApp/src/tabs/workspace/components/PlaceCard.tsx index ac5ae1f..b7e76ac 100644 --- a/Converge/ClientApp/src/tabs/workspace/components/PlaceCard.tsx +++ b/Converge/ClientApp/src/tabs/workspace/components/PlaceCard.tsx @@ -17,15 +17,13 @@ import { UI_SECTION, UISections, USER_INTERACTION, DESCRIPTION, } from "../../../types/LoggerTypes"; import NewConfRoomEvent from "./NewConfRoomEvent"; -import { createEvent } from "../../../api/calendarService"; import Notifications from "../../../utilities/ToastManager"; import ImagePlaceholder from "../../../utilities/ImagePlaceholder"; import PlaceCardStyles from "../styles/PlaceCardStyles"; -import { updateMyPredictedLocation } from "../../../api/meService"; +import { useApiProvider } from "../../../providers/ApiProvider"; import { useConvergeSettingsContextProvider } from "../../../providers/ConvergeSettingsProvider"; -import { AddRecentBuildings } from "../../../utilities/RecentBuildingsManager"; -import getPlaceMaxReserved, { getRoomAvailability } from "../../../api/placeService"; -import { getPlacePhotos, PlacePhotosResult } from "../../../api/buildingService"; +import { PlacePhotosResult } from "../../../api/buildingService"; +import AddRecentBuildings from "../../../utilities/RecentBuildingsManager"; type Props = { place: ExchangePlace, @@ -34,6 +32,12 @@ type Props = { const PlaceCard: React.FC = (props) => { const { place, buildingName } = props; + const { + calendarService, + meService, + placeService, + buildingService, + } = useApiProvider(); const classes = PlaceCardStyles(); const { convergeSettings, @@ -58,7 +62,7 @@ const PlaceCard: React.FC = (props) => { const photoUrl = placePhotos?.coverPhoto?.url; useEffect(() => { if (place.sharePointID) { - getPlacePhotos(place.sharePointID).then(setPlacePhotos); + buildingService.getPlacePhotos(place.sharePointID).then(setPlacePhotos); } }, [place.sharePointID]); @@ -81,7 +85,7 @@ const PlaceCard: React.FC = (props) => { setAvailabilityLoading(true); if (state.startDate.utc().toISOString() <= state.endDate.utc().toISOString()) { if (place.type === PlaceType.Space) { - getPlaceMaxReserved( + placeService.getPlaceMaxReserved( place.identity, (state.startDate).utc().toISOString(), (state.endDate).utc().toISOString(), @@ -90,7 +94,7 @@ const PlaceCard: React.FC = (props) => { }) .finally(() => setAvailabilityLoading(false)); } else { - getRoomAvailability( + placeService.getRoomAvailability( place.identity, (state.startDate).utc().toISOString(), (state.endDate).utc().toISOString(), @@ -215,7 +219,7 @@ const PlaceCard: React.FC = (props) => { startDate = dayjs(start.format("YYYY-MM-DD")).toDate(); endDate = dayjs(end.add(1, "day").format("YYYY-MM-DD")).toDate(); } - createEvent({ + calendarService.createEvent({ isAllDay, start: startDate, end: endDate, @@ -241,7 +245,7 @@ const PlaceCard: React.FC = (props) => { }; setConvergeSettings(newSettings); createReservation(calendarEvent); - return updateMyPredictedLocation({ + return meService.updateMyPredictedLocation({ year: dayjs.utc(startDate).year(), month: dayjs.utc(startDate).month() + 1, day: dayjs.utc(startDate).date(), diff --git a/Converge/ClientApp/src/tabs/workspace/components/Reservation.tsx b/Converge/ClientApp/src/tabs/workspace/components/Reservation.tsx index 1edde44..eb446fa 100644 --- a/Converge/ClientApp/src/tabs/workspace/components/Reservation.tsx +++ b/Converge/ClientApp/src/tabs/workspace/components/Reservation.tsx @@ -12,13 +12,13 @@ import CalendarEvent from "../../../types/CalendarEvent"; import { DESCRIPTION, UISections, UI_SECTION, USER_INTERACTION, } from "../../../types/LoggerTypes"; -import { deleteEvent } from "../../../api/calendarService"; import Notifications from "../../../utilities/ToastManager"; import ReservationStyles from "../styles/ReservationStyles"; import { useProvider as PlaceFilterProvider, } from "../../../providers/PlaceFilterProvider"; import { logEvent } from "../../../utilities/LogWrapper"; +import { useApiProvider } from "../../../providers/ApiProvider"; interface Props { reservation: CalendarEvent, @@ -26,6 +26,7 @@ interface Props { const Reservation: React.FC = (props) => { const { reservation } = props; + const { calendarService } = useApiProvider(); const { cancelReservation } = PlaceFilterProvider(); const [openCancelDialog, setOpenCancelDialog] = React.useState(false); const [canceling, setCanceling] = React.useState(false); @@ -54,7 +55,7 @@ const Reservation: React.FC = (props) => { value: "cancel_reservation", }, ]); - deleteEvent(reservation.id, message).then(() => { + calendarService.deleteEvent(reservation.id, message).then(() => { Notifications.show({ duration: 5000, title: "You cancelled a reservation.", diff --git a/Converge/ClientApp/src/types/IExchangePlacesResponse.ts b/Converge/ClientApp/src/types/IExchangePlacesResponse.ts new file mode 100644 index 0000000..4d4f268 --- /dev/null +++ b/Converge/ClientApp/src/types/IExchangePlacesResponse.ts @@ -0,0 +1,9 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import ExchangePlace from "./ExchangePlace"; + +export interface IExchangePlacesResponse { + exchangePlacesList: ExchangePlace[]; + skipToken: string | null; +} diff --git a/Converge/ClientApp/src/utilities/ChangeLocationModal.tsx b/Converge/ClientApp/src/utilities/ChangeLocationModal.tsx index bfaa2ce..c0986f8 100644 --- a/Converge/ClientApp/src/utilities/ChangeLocationModal.tsx +++ b/Converge/ClientApp/src/utilities/ChangeLocationModal.tsx @@ -13,7 +13,7 @@ import { logEvent } from "../utilities/LogWrapper"; import { UI_SECTION, UISections, USER_INTERACTION, DESCRIPTION, IMPORTANT_ACTION, ImportantActions, } from "../types/LoggerTypes"; -import { updateMyPredictedLocation } from "../api/meService"; +import { useApiProvider } from "../providers/ApiProvider"; import PopupMenuWrapper from "./popupMenuWrapper"; const ChangeLocationModalStyles = makeStyles(() => ({ @@ -55,6 +55,9 @@ const ChangeLocationModal: React.FC = (props) => { date, refreshRecommendation, } = props; + const { + meService, + } = useApiProvider(); const [open, setOpen] = useState(false); const [location, setLocation] = useState(recommendation || ""); const [loading, setLoading] = useState(false); @@ -87,7 +90,7 @@ const ChangeLocationModal: React.FC = (props) => { campusUpn = building.identity; } } - updateMyPredictedLocation({ + meService.updateMyPredictedLocation({ year: day.year(), month: day.month() + 1, day: day.date(), @@ -134,7 +137,7 @@ const ChangeLocationModal: React.FC = (props) => { ]); setOpen(false); }} - onConfirm={() => { onConfirmbutton(); }} + onConfirm={onConfirmbutton} confirmButton={{ content: "Confirm", loading, diff --git a/Converge/ClientApp/src/utilities/IsThisHelpful.tsx b/Converge/ClientApp/src/utilities/IsThisHelpful.tsx index 072214a..f099570 100644 --- a/Converge/ClientApp/src/utilities/IsThisHelpful.tsx +++ b/Converge/ClientApp/src/utilities/IsThisHelpful.tsx @@ -12,9 +12,9 @@ import { UI_SECTION, USER_INTERACTION, } from "../types/LoggerTypes"; -import { setSettings } from "../api/meService"; import { useConvergeSettingsContextProvider } from "../providers/ConvergeSettingsProvider"; import { logEvent } from "./LogWrapper"; +import { useApiProvider } from "../providers/ApiProvider"; const useIsThisHelpfulStyles = makeStyles(() => ({ root: { @@ -37,6 +37,9 @@ const IsThisHelpful: React.FC = (props) => { convergeSettings, setConvergeSettings, } = useConvergeSettingsContextProvider(); + const { + meService, + } = useApiProvider(); const likeSection = () => { const didLike = convergeSettings?.likedSections?.includes(sectionName) || false; @@ -75,7 +78,7 @@ const IsThisHelpful: React.FC = (props) => { likedSections: newLikedSections, dislikedSections: newDislikedSections, }; - setSettings(newSettings); + meService.setSettings(newSettings); setConvergeSettings(newSettings); }; const likeIsTrue = convergeSettings?.likedSections?.includes(sectionName) || false; From 7f7a41510bf6c5a39be1921736334ea1ca786ca4 Mon Sep 17 00:00:00 2001 From: Megan Slater Date: Tue, 25 Jan 2022 17:31:30 +0000 Subject: [PATCH 17/41] Reduce number of calls to recent buildings and favorite buildings --- .../src/providers/ConvergeSettingsProvider.tsx | 17 ++++++----------- Converge/ClientApp/src/tabs/workspace/index.tsx | 5 +---- .../src/utilities/popupMenuContent.tsx | 17 +++++++++++++++-- 3 files changed, 22 insertions(+), 17 deletions(-) diff --git a/Converge/ClientApp/src/providers/ConvergeSettingsProvider.tsx b/Converge/ClientApp/src/providers/ConvergeSettingsProvider.tsx index 9049240..4d4ba95 100644 --- a/Converge/ClientApp/src/providers/ConvergeSettingsProvider.tsx +++ b/Converge/ClientApp/src/providers/ConvergeSettingsProvider.tsx @@ -48,7 +48,7 @@ interface IConvergeContext { searchString?: string, presetBuildings?: string[], ) => void; - updateRecentBuildings: (recentBuildings: BuildingBasicInfo[]) => void; + getRecentBuildings: () => Promise; } const ConvergeSettingsContext = React.createContext( @@ -453,6 +453,10 @@ const ConvergeSettingsProvider: React.FC = ({ children }) => { dispatch({ type: UPDATE_RECENT_BUILDINGS, payload: recentBuildings }); }; + const getRecentBuildings = () => meService + .getRecentBuildingsBasicDetails() + .then(updateRecentBuildings); + useEffect(() => { if (state.buildingsByRadiusDistance !== 10 && convergeSettings?.geoCoordinates) { loadMoreBuildingsByDistance(convergeSettings.geoCoordinates, state.buildingsByRadiusDistance); @@ -466,17 +470,8 @@ const ConvergeSettingsProvider: React.FC = ({ children }) => { getConvergeSettings() .catch(() => setIsError(true)) .finally(() => setLoading(false)); - meService.getRecentBuildingsBasicDetails().then((basicRecentBuildings) => { - updateRecentBuildings(basicRecentBuildings); - }); }, []); - useEffect(() => { - meService.getRecentBuildingsBasicDetails().then((basicRecentBuildings) => { - updateRecentBuildings(basicRecentBuildings); - }); - }, [convergeSettings?.recentBuildingUpns]); - return ( { setBuildingsListError, searchMoreBuildings, updateSearchString, - updateRecentBuildings, + getRecentBuildings, }} > {loading && } diff --git a/Converge/ClientApp/src/tabs/workspace/index.tsx b/Converge/ClientApp/src/tabs/workspace/index.tsx index fcc677a..2a48bfb 100644 --- a/Converge/ClientApp/src/tabs/workspace/index.tsx +++ b/Converge/ClientApp/src/tabs/workspace/index.tsx @@ -16,15 +16,12 @@ const Workspace: React.FC = () => { } = useConvergeSettingsContextProvider(); const [loading, setLoading] = useState(true); const [isError, setIsError] = useState(false); + useEffect(() => { setLoading(true); getFavoriteCampuses() .catch(() => setIsError(true)) .finally(() => setLoading(false)); - }, []); - - useEffect(() => { - getFavoriteCampuses(); }, [convergeSettings?.favoriteCampusesToCollaborate]); if (loading) { diff --git a/Converge/ClientApp/src/utilities/popupMenuContent.tsx b/Converge/ClientApp/src/utilities/popupMenuContent.tsx index 8c9f531..9ee99d2 100644 --- a/Converge/ClientApp/src/utilities/popupMenuContent.tsx +++ b/Converge/ClientApp/src/utilities/popupMenuContent.tsx @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -import * as React from "react"; +import React, { useState, useEffect } from "react"; import { Flex, Divider, Text, DropdownItemProps, ShorthandCollection, Provider, Button, Loader, Box, } from "@fluentui/react-northstar"; @@ -28,11 +28,15 @@ const PopupMenuContent: React.FunctionComponent = (props) => { headerTitle, buildingList, locationBuildingName, buttonTitle, otherOptionsList, maxHeight, } = props; const { - state, setBuildingsByDistanceRadius, + state, + setBuildingsByDistanceRadius, + getRecentBuildings, + convergeSettings, } = useConvergeSettingsContextProvider(); const classes = BookWorkspaceStyles(); const location = useLocation(); + const [recentBuildingsLoading, setRecentBuildingsLoading] = useState(false); const onClickSeeMore = () => { if (state.buildingsByRadiusDistance < 1000) { @@ -42,6 +46,14 @@ const PopupMenuContent: React.FunctionComponent = (props) => { } }; + useEffect(() => { + if (!state.recentBuildings.length) { + setRecentBuildingsLoading(true); + } + getRecentBuildings() + .finally(() => setRecentBuildingsLoading(false)); + }, [convergeSettings?.recentBuildingUpns]); + return ( @@ -82,6 +94,7 @@ const PopupMenuContent: React.FunctionComponent = (props) => { )} + {location.pathname === "/workspace" && recentBuildingsLoading && } {location.pathname === "/workspace" && state.recentBuildings.length > 0 && ( From d446328351f4ab9309a7e7f9cc5d03bc5a7e9b37 Mon Sep 17 00:00:00 2001 From: Megan Slater Date: Tue, 25 Jan 2022 17:53:05 +0000 Subject: [PATCH 18/41] Split out finding building by distance and by name --- Converge/Services/BuildingsService.cs | 63 ++++++++++++--------------- Converge/Services/PlacesService.cs | 8 +--- 2 files changed, 31 insertions(+), 40 deletions(-) diff --git a/Converge/Services/BuildingsService.cs b/Converge/Services/BuildingsService.cs index c839b24..fa6975f 100644 --- a/Converge/Services/BuildingsService.cs +++ b/Converge/Services/BuildingsService.cs @@ -94,52 +94,47 @@ namespace Converge.Services return sourceGpsCoords; } - private async Task GetBuildingsBySortRequest(CampusSortRequest buildingSortRequest) + private async Task GetBuildingsByDistance(CampusSortRequest campusSortRequest) { List buildingsList = new List(); - GraphRoomsListResponse roomsListResponse = await appGraphService.GetRoomListsConstrained(buildingSortRequest); + GraphExchangePlacesResponse exchangePlacesResponse = await placesService.GetPlacesBySortRequest(campusSortRequest); + List buildingUpnList = exchangePlacesResponse.ExchangePlacesList.Select(ep => ep.Locality).Distinct().ToList(); + + + foreach (string buildingUpn in buildingUpnList) + { + ExchangePlace exchangePlaceModel = exchangePlacesResponse.ExchangePlacesList.FirstOrDefault(ep => ep.Locality == buildingUpn); + buildingsList.Add(Building.Instantiate(exchangePlaceModel)); + } + + //Employed Haversine formula. + buildingsList = campusSortRequest.SortBuildingsByDistance(buildingsList); + + return new BasicBuildingsResponse(buildingsList.Select(b => new BuildingBasicInfo(b.Identity, b.DisplayName)).ToList(), null); + } + + private async Task GetBuildingsByName(CampusSortRequest campusSortRequest) + { + List buildingsList = new List(); + + GraphRoomsListResponse roomsListResponse = await appGraphService.GetRoomListsConstrained(campusSortRequest); var roomsList = roomsListResponse.RoomsList.Where(r => r.AdditionalData != null && r.AdditionalData["emailAddress"] != null).ToList(); if (roomsList.Count == 0) { return new BasicBuildingsResponse(new List()); } - GraphExchangePlacesResponse exchangePlacesResponse = await placesService.GetPlacesBySortRequest(buildingSortRequest); + return new BasicBuildingsResponse(roomsList.Select(r => new BuildingBasicInfo(r.AdditionalData["emailAddress"].ToString(), r.DisplayName)).ToList(), roomsListResponse.SkipToken); + } - foreach (Place room in roomsList) + private async Task GetBuildingsBySortRequest(CampusSortRequest campusSortRequest) + { + if (campusSortRequest.SortByType == CampusSortByType.Distance) { - room.AdditionalData.TryGetValue("emailAddress", out object buildingObject); - string buildingEmailAddress = Convert.ToString(buildingObject); - if (string.IsNullOrWhiteSpace(buildingEmailAddress)) - { - continue; - } - buildingEmailAddress = buildingEmailAddress.Trim(); - - //We need the Places list. - var exchangePlacesList = exchangePlacesResponse.ExchangePlacesList - .Where(p => p.Locality.SameAs(buildingEmailAddress)).ToList(); - //We just need one of the records to consume & set Building info. - var exchangePlaceModel = exchangePlacesList.FirstOrDefault(); - if (exchangePlaceModel != null) - { - buildingsList.Add(Building.Instantiate(exchangePlaceModel)); - } + return await GetBuildingsByDistance(campusSortRequest); } - - //Now get the buildings sorted by the distance, when requested. - if (buildingSortRequest.SortByType == CampusSortByType.Distance) - { - //Employed Haversine formula. - buildingsList = buildingSortRequest.SortBuildingsByDistance(buildingsList); - } - else - { - buildingsList = buildingSortRequest.SortByName(buildingsList); - } - - return new BasicBuildingsResponse(buildingsList.Select(b => new BuildingBasicInfo(b.Identity, b.DisplayName)).ToList(), roomsListResponse.SkipToken); + return await GetBuildingsByName(campusSortRequest); } public async Task GetBuildingByDisplayName(string buildingDisplayName) diff --git a/Converge/Services/PlacesService.cs b/Converge/Services/PlacesService.cs index 33cdc05..32fbc88 100644 --- a/Converge/Services/PlacesService.cs +++ b/Converge/Services/PlacesService.cs @@ -74,12 +74,8 @@ namespace Converge.Services { ExchangePlace place = DeserializeHelper.DeserializeExchangePlace(gli.ListItem.Fields.AdditionalData, logger); place.SharePointID = gli.ListItem.Fields.Id; - - if (string.IsNullOrWhiteSpace(place.Building)) - { - Place targetRoom = appGraphService.GetRoomListById(place.Locality).Result; - place.Building = targetRoom?.DisplayName; - } + Place targetRoom = appGraphService.GetRoomListById(place.Locality).Result; + place.Building = targetRoom?.DisplayName; lock (p) { From c3cfca72bfe461f30845c6c76c22020777f1ad1c Mon Sep 17 00:00:00 2001 From: Megan Slater Date: Tue, 25 Jan 2022 20:57:46 +0000 Subject: [PATCH 19/41] Support top and skip for room list query from Graph --- Converge/Controllers/BuildingsController.cs | 14 +-- Converge/Models/BuildingSearchInfo.cs | 7 +- Converge/Models/GraphRoomsListResponse.cs | 5 +- Converge/Services/AppGraphService.cs | 100 +++--------------- Converge/Services/BuildingsService.cs | 55 ++++------ .../Services/CachePlacesProviderService.cs | 18 ++-- .../Services/HttpClientProviderService.cs | 24 ----- Converge/Services/SyncService.cs | 7 +- Converge/Services/UserGraphService.cs | 61 +++++++++++ ConvergeUnitTests/Services/SyncServiceTest.cs | 6 +- 10 files changed, 118 insertions(+), 179 deletions(-) diff --git a/Converge/Controllers/BuildingsController.cs b/Converge/Controllers/BuildingsController.cs index 097d785..26082d3 100644 --- a/Converge/Controllers/BuildingsController.cs +++ b/Converge/Controllers/BuildingsController.cs @@ -35,16 +35,16 @@ namespace Converge.Controllers /// defaulted to first 100 buildings if there is no value on how many records to skip. ///
/// Number of records after the skipCount used to skip the number of records. - /// Skip-token option as string to get next set of records. + /// The number of records to skip. /// BuildingsResponse: Containing the list of Buildings records and the count of those. [HttpGet] [Route("sortByName")] - public async Task> GetBuildings(int? topCount = null, string skipTokenString = null) + public async Task> GetBuildingsByName(int? topCount = 10, int? skip = 0) { try { buildingsService.SetPrincipalUserIdentity(User.Identity); - var result = await buildingsService.GetBuildings(topCount, skipTokenString); + var result = await buildingsService.GetBuildingsByName(topCount, skip); return Ok(result); } catch (Exception ex) @@ -63,12 +63,12 @@ namespace Converge.Controllers /// BuildingsResponse: Containing the list of Buildings records and the count of those. [HttpGet] [Route("sortByDistance")] - public async Task> GetBuildings(string sourceGeoCoordinates, double? distanceFromSource) + public async Task> GetBuildingsByDistance(string sourceGeoCoordinates, double? distanceFromSource) { try { buildingsService.SetPrincipalUserIdentity(User.Identity); - var result = await buildingsService.GetBuildings(sourceGeoCoordinates, distanceFromSource); + var result = await buildingsService.GetBuildingsByDistance(sourceGeoCoordinates, distanceFromSource); return Ok(result); } catch (Exception ex) @@ -254,11 +254,11 @@ namespace Converge.Controllers [HttpGet] [Route("searchForBuildings/{searchString}")] - public async Task> SearchForBuildings(string searchString, int? topCount = null, string skipToken = null) + public async Task> SearchForBuildings(string searchString, int? topCount = 10, int? skip = 0) { try { - return await buildingsService.SearchForBuildings(searchString, topCount, skipToken); + return await buildingsService.SearchForBuildings(searchString, topCount, skip); } catch (Exception ex) { diff --git a/Converge/Models/BuildingSearchInfo.cs b/Converge/Models/BuildingSearchInfo.cs index 194bbf8..b052dfa 100644 --- a/Converge/Models/BuildingSearchInfo.cs +++ b/Converge/Models/BuildingSearchInfo.cs @@ -2,22 +2,17 @@ // Licensed under the MIT License. using Microsoft.Graph; -using Newtonsoft.Json; -using System; using System.Collections.Generic; -using System.Linq; namespace Converge.Models { public class BuildingSearchInfo { public List BuildingInfoList { get; set; } - public QueryOption SkipToken { get; set; } - public BuildingSearchInfo(List buildingInfoList, QueryOption skipToken) + public BuildingSearchInfo(List buildingInfoList) { BuildingInfoList = buildingInfoList; - SkipToken = skipToken; } } } diff --git a/Converge/Models/GraphRoomsListResponse.cs b/Converge/Models/GraphRoomsListResponse.cs index bd9d96c..9f13fa4 100644 --- a/Converge/Models/GraphRoomsListResponse.cs +++ b/Converge/Models/GraphRoomsListResponse.cs @@ -10,12 +10,9 @@ namespace Converge.Models { public List RoomsList { get; set; } - public QueryOption SkipToken { get; set; } - - public GraphRoomsListResponse(List roomsList, QueryOption skipToken) + public GraphRoomsListResponse(List roomsList) { RoomsList = roomsList; - SkipToken = skipToken; } } } diff --git a/Converge/Services/AppGraphService.cs b/Converge/Services/AppGraphService.cs index a6f7418..d55a621 100644 --- a/Converge/Services/AppGraphService.cs +++ b/Converge/Services/AppGraphService.cs @@ -441,41 +441,28 @@ namespace Converge.Services } } - public virtual async Task> GetRoomLists() + public virtual async Task> GetAllRoomLists() { - var placesUrl = appGraphServiceClient.Places.AppendSegmentToRequestUrl("microsoft.graph.roomList"); - var places = await new GraphServicePlacesCollectionRequestBuilder(placesUrl, appGraphServiceClient) + var url = appGraphServiceClient.Places.AppendSegmentToRequestUrl("microsoft.graph.roomList"); + var roomLists = await new GraphServicePlacesCollectionRequestBuilder(url, appGraphServiceClient) .Request() .GetAsync(); - List placeList = places.CurrentPage as List; - return placeList; - } + List result = new List(); - public async Task GetRoomListsConstrained(CampusSortRequest buildingSortRequest) - { - var queryOptions = new List - { - new QueryOption("$count", "true") - }; + var pageIterator = PageIterator + .CreatePageIterator( + appGraphServiceClient, + roomLists, + (room) => + { + result.Add(room); + return true; + } + ); - var placesUrl = appGraphServiceClient.Places.AppendSegmentToRequestUrl("microsoft.graph.roomList"); - var placesCollRequest = new GraphServicePlacesCollectionRequestBuilder(placesUrl, appGraphServiceClient) - .Request(queryOptions) - .Header("ConsistencyLevel", "Eventual") - .Filter($"emailAddress ne ''"); - if (buildingSortRequest.SortByType == CampusSortByType.DisplayName) - { - placesCollRequest = placesCollRequest.OrderBy("displayName") - .Top(buildingSortRequest.TopCount.Value); - } - var placesCollPage = await placesCollRequest.GetAsync(); - - List placeList = placesCollPage.CurrentPage as List; - QueryOption skipToken = placesCollPage.NextPageRequest? - .QueryOptions.FirstOrDefault(x => x.Name.SameAs("$skiptoken")); - - return new GraphRoomsListResponse(placeList, skipToken); + await pageIterator.IterateAsync(); + return result; } public async Task> GetRoomListsByDisplayName(List roomListsDisplayNames) @@ -767,60 +754,5 @@ namespace Converge.Services return null; } } - - public async Task SearchRoomLists(string searchString, int? topCount = null, QueryOption skipToken = null) - { - topCount ??= 10; - var placesUrl = appGraphServiceClient.Places.AppendSegmentToRequestUrl("microsoft.graph.roomList"); - - //Extending on the search by transforming the search-string to all-lower-case, ALL-UPPER-CASE and also to Proper/Pascal-case. - //Eg. "San FRANcisco" transformed to "san francisco", "SAN FRANCISCO" and "San Francisco", besides search for the actual string. - string allLowerCase = searchString.ToLower(); - string allUpperCase = searchString.ToUpper(); - string properCase = searchString; - string[] multiWords = searchString.Split(' ', StringSplitOptions.RemoveEmptyEntries); - foreach(var word in multiWords) - { - var pascalCase = new StringBuilder(word.Substring(0, 1).ToUpper() + word.Substring(1).ToLower()); - properCase = properCase.Replace(word, pascalCase.ToString(), StringComparison.InvariantCultureIgnoreCase); - } - - //Search for actual-string, all-lower-case, ALL-UPPER-CASE and Proper/Pascal-case. - StringBuilder predicateBuilder = new StringBuilder($"emailAddress ne ''"); - predicateBuilder.Append($" and ("); - predicateBuilder.Append($"contains(displayName, '{searchString}')"); - predicateBuilder.Append($" or contains(displayName, '{properCase}')"); - predicateBuilder.Append($" or contains(displayName, '{allLowerCase}')"); - predicateBuilder.Append($" or contains(displayName, '{allUpperCase}')"); - predicateBuilder.Append($" or contains(address/city, '{searchString}')"); - predicateBuilder.Append($" or contains(address/city, '{properCase}')"); - predicateBuilder.Append($" or contains(address/city, '{allLowerCase}')"); - predicateBuilder.Append($" or contains(address/city, '{allUpperCase}')"); - predicateBuilder.Append($")"); - - - //Now lets build the request. - var placesCollRequestBuilder = new GraphServicePlacesCollectionRequestBuilder(placesUrl, appGraphServiceClient); - IGraphServicePlacesCollectionRequest placesCollRequest; - if (skipToken == null) - { - placesCollRequest = placesCollRequestBuilder.Request(); - } - else - { - placesCollRequest = placesCollRequestBuilder.Request(new List { skipToken }); - } - placesCollRequest = placesCollRequest.Header("ConsistencyLevel", "Eventual") - .Filter(predicateBuilder.ToString()) - .OrderBy("displayName") - .Top(topCount.Value); - var placesCollPage = await placesCollRequest.GetAsync(); - - List placeList = placesCollPage.CurrentPage as List; - placeList = placeList.Where(r => r.AdditionalData != null && r.AdditionalData["emailAddress"] != null).ToList(); - skipToken = placesCollPage.NextPageRequest?.QueryOptions.FirstOrDefault(x => x.Name.SameAs("$skiptoken")); - - return new GraphRoomsListResponse(placeList, skipToken); - } } } diff --git a/Converge/Services/BuildingsService.cs b/Converge/Services/BuildingsService.cs index fa6975f..2492142 100644 --- a/Converge/Services/BuildingsService.cs +++ b/Converge/Services/BuildingsService.cs @@ -37,25 +37,30 @@ namespace Converge.Services principalUserIdentity = userIdentity; } - public async Task GetBuildings(int? topCount = null, string skipTokenString = null) + public async Task GetBuildingsByName(int? topCount = 10, int? skip = 0) { - topCount ??= 100; - - BasicBuildingsResponse buildingsResponse = cachePlacesProviderService.GetBuildings(topCount, skipTokenString); + BasicBuildingsResponse buildingsResponse = cachePlacesProviderService.GetBuildings(topCount, skip); if (buildingsResponse == null) { - CampusSortRequest buildingSortRequest = new CampusSortRequest(CampusSortByType.DisplayName, topCount.Value, skipTokenString); + List buildingsList = new List(); - buildingsResponse = await GetBuildingsBySortRequest(buildingSortRequest); + GraphRoomsListResponse roomsListResponse = await userGraphService.GetRoomListsByName(topCount, skip); + var roomsList = roomsListResponse.RoomsList.Where(r => r.AdditionalData != null && r.AdditionalData["emailAddress"] != null).ToList(); + if (roomsList.Count == 0) + { + buildingsResponse = new BasicBuildingsResponse(new List()); + } + + buildingsResponse = new BasicBuildingsResponse(roomsList.Select(r => new BuildingBasicInfo(r.AdditionalData["emailAddress"].ToString(), r.DisplayName)).ToList()); //Add to Cache. - cachePlacesProviderService.AddBuildings(buildingsResponse, topCount, skipTokenString); + cachePlacesProviderService.AddBuildings(buildingsResponse, topCount, skip); } return buildingsResponse; } - public async Task GetBuildings(string sourceGeoCoordinates, double? distanceFromSource) + public async Task GetBuildingsByDistance(string sourceGeoCoordinates, double? distanceFromSource) { GPSCoordinates sourceGpsCoords = await DetermineSourceGpsCoordinates(sourceGeoCoordinates); if (sourceGpsCoords == null) @@ -67,7 +72,7 @@ namespace Converge.Services sourceGpsCoords, distanceFromSource); - return await GetBuildingsBySortRequest(buildingSortRequest); + return await GetBuildingsByDistance(buildingSortRequest); } private async Task DetermineSourceGpsCoordinates(string sourceGeoCoordinates) @@ -114,29 +119,6 @@ namespace Converge.Services return new BasicBuildingsResponse(buildingsList.Select(b => new BuildingBasicInfo(b.Identity, b.DisplayName)).ToList(), null); } - private async Task GetBuildingsByName(CampusSortRequest campusSortRequest) - { - List buildingsList = new List(); - - GraphRoomsListResponse roomsListResponse = await appGraphService.GetRoomListsConstrained(campusSortRequest); - var roomsList = roomsListResponse.RoomsList.Where(r => r.AdditionalData != null && r.AdditionalData["emailAddress"] != null).ToList(); - if (roomsList.Count == 0) - { - return new BasicBuildingsResponse(new List()); - } - - return new BasicBuildingsResponse(roomsList.Select(r => new BuildingBasicInfo(r.AdditionalData["emailAddress"].ToString(), r.DisplayName)).ToList(), roomsListResponse.SkipToken); - } - - private async Task GetBuildingsBySortRequest(CampusSortRequest campusSortRequest) - { - if (campusSortRequest.SortByType == CampusSortByType.Distance) - { - return await GetBuildingsByDistance(campusSortRequest); - } - return await GetBuildingsByName(campusSortRequest); - } - public async Task GetBuildingByDisplayName(string buildingDisplayName) { BuildingBasicInfo buildingBasicInfo = null; @@ -296,17 +278,16 @@ namespace Converge.Services }; } - public async Task SearchForBuildings(string searchString, int? topCount = null, string skipTokenString = null) + public async Task SearchForBuildings(string searchString, int? topCount = 10, int? skip = 0) { List buildingsBasicInfoList = new List(); if (string.IsNullOrWhiteSpace(searchString)) { - return new BuildingSearchInfo(buildingsBasicInfoList, null); + return new BuildingSearchInfo(buildingsBasicInfoList); } - var skipToken = DeserializeHelper.QueryOption(skipTokenString); - GraphRoomsListResponse roomListsResponse = await appGraphService.SearchRoomLists(searchString, topCount, skipToken); + GraphRoomsListResponse roomListsResponse = await userGraphService.SearchRoomLists(searchString, topCount, skip); foreach (Place room in roomListsResponse.RoomsList) { room.AdditionalData.TryGetValue("emailAddress", out object buildingObject); @@ -320,7 +301,7 @@ namespace Converge.Services buildingsBasicInfoList.Add(buildingBasicInfo); } - return new BuildingSearchInfo(buildingsBasicInfoList, roomListsResponse.SkipToken); + return new BuildingSearchInfo(buildingsBasicInfoList); } } } diff --git a/Converge/Services/CachePlacesProviderService.cs b/Converge/Services/CachePlacesProviderService.cs index c73c56b..8e5261d 100644 --- a/Converge/Services/CachePlacesProviderService.cs +++ b/Converge/Services/CachePlacesProviderService.cs @@ -146,7 +146,7 @@ namespace Converge.Services return partCacheKey.ToString(); } - public BasicBuildingsResponse GetBuildings(int? topCount = null, string skipTokenString = null) + public BasicBuildingsResponse GetBuildings(int? topCount = 10, int? skip = 0) { BasicBuildingsResponse buildingsResponse = null; if (!CacheConfigEnabled) @@ -157,7 +157,7 @@ namespace Converge.Services try { string cacheKey = new StringBuilder(BuildPartCacheKey(keyBuildings) - + BuildPartCacheKey(topCount, skipTokenString)).ToString(); + + BuildPartCacheKey(topCount, skip.ToString())).ToString(); //Get Buildings-Upns-List from the Cache buildingsPlacesCache.TryGetValue(keyBuildingsUpnInfo, out List buildingsUpnInfoList); //Get Buildings-list from the Cache @@ -172,13 +172,7 @@ namespace Converge.Services var buildingsList = new List(cachedBuildingsList.Select(b => new BuildingBasicInfo(b.Identity, b.DisplayName))); buildingsList = SortBuildingsAsCached(buildingsUpnInfoList, buildingsList, cacheKey); - //Get Skip-Token-Upns-List from the Cache - buildingsPlacesCache.TryGetValue(keySkipTokens, out List skipTokensList); - //Get Skip-token from the Cache. - var skipTokenData = skipTokensList?.FirstOrDefault(x => x.CachedPageKey.SameAs(cacheKey)); - var skipToken = skipTokenData?.SkipToken; - - return new BasicBuildingsResponse(buildingsList, skipToken); + return new BasicBuildingsResponse(buildingsList); } catch (Exception ex) { @@ -187,7 +181,7 @@ namespace Converge.Services } } - public void AddBuildings(BasicBuildingsResponse buildingsResponse, int? topCount = null, string skipTokenString = null) + public void AddBuildings(BasicBuildingsResponse buildingsResponse, int? topCount = null, int? skip = null) { if (!CacheConfigEnabled) { @@ -197,7 +191,7 @@ namespace Converge.Services try { string cacheKey = new StringBuilder(BuildPartCacheKey(keyBuildings) - + BuildPartCacheKey(topCount, skipTokenString)).ToString(); + + BuildPartCacheKey(topCount, skip.ToString())).ToString(); //Get Buildings-list from the Cache buildingsPlacesCache.TryGetValue(keyBuildings, out List buildingsList); @@ -210,7 +204,7 @@ namespace Converge.Services AddBuildingUpnInfoToCache(buildingsList, cacheKey); //Add Skip-token to the Cache. - AddSkipTokenToCache(skipTokenString, cacheKey); + AddSkipTokenToCache(skip.ToString(), cacheKey); } catch (Exception ex) { diff --git a/Converge/Services/HttpClientProviderService.cs b/Converge/Services/HttpClientProviderService.cs index a57d55d..ab9d88e 100644 --- a/Converge/Services/HttpClientProviderService.cs +++ b/Converge/Services/HttpClientProviderService.cs @@ -11,7 +11,6 @@ namespace Converge.Services { public interface IHttpClientProviderService { - public HttpClient Set(ITokenAcquisition tokenAcquisition, TimeSpan? timeSpan = null, string mediaType = null); HttpClient Set(string authnToken, TimeSpan? timeSpan = null, string mediaType = null); HttpClient GetObject(); } @@ -62,29 +61,6 @@ namespace Converge.Services return httpClient; } - /// - /// This method will set HttpClient object's properties - /// - /// - /// - /// - /// - public HttpClient Set(ITokenAcquisition tokenAcquisition, TimeSpan? timeSpan = null, string mediaType = null) - { - string authnToken; - - try - { - authnToken = tokenAcquisition.GetAccessTokenForUserAsync(Constant.ScopesToAccessGraphApi).GetAwaiter().GetResult(); - } - catch - { - throw; - } - - return Set(authnToken, timeSpan, mediaType); - } - /// /// This method should be consumed only after Set() is consumed atleast once.. Meant for multiple service calls if the need be. /// diff --git a/Converge/Services/SyncService.cs b/Converge/Services/SyncService.cs index 70b86a8..09cbe13 100644 --- a/Converge/Services/SyncService.cs +++ b/Converge/Services/SyncService.cs @@ -15,7 +15,10 @@ namespace Converge.Services private readonly TelemetryService telemetryService; private readonly AppGraphService appGraphService; private readonly SearchBingMapsService searchBingMapsService; - public SyncService(TelemetryService telemetryService, AppGraphService appGraphService, SearchBingMapsService searchBingMapsSvc) + public SyncService( + TelemetryService telemetryService, + AppGraphService appGraphService, + SearchBingMapsService searchBingMapsSvc) { this.telemetryService = telemetryService; this.appGraphService = appGraphService; @@ -30,7 +33,7 @@ namespace Converge.Services { try { - List roomLists = await appGraphService.GetRoomLists(); + List roomLists = await appGraphService.GetAllRoomLists(); List allGraphPlaces = new List(); foreach (Place roomList in roomLists) { diff --git a/Converge/Services/UserGraphService.cs b/Converge/Services/UserGraphService.cs index a810540..db5723a 100644 --- a/Converge/Services/UserGraphService.cs +++ b/Converge/Services/UserGraphService.cs @@ -82,6 +82,67 @@ namespace Converge.Services .GetAsync(); } + public async Task GetRoomListsByName(int? top = 10, int? skip = 0) + { + var placesUrl = graphServiceClient.Places.AppendSegmentToRequestUrl($"microsoft.graph.roomList"); + var placesCollPage = await new GraphServicePlacesCollectionRequestBuilder(placesUrl, graphServiceClient) + .Request() + .OrderBy("displayName") + .Header("ConsistencyLevel", "Eventual") + .Top(top ?? 10) + .Skip(skip ?? 0) + .GetAsync(); + + List placeList = placesCollPage.CurrentPage as List; + + return new GraphRoomsListResponse(placeList); + } + + public async Task SearchRoomLists(string searchString, int? topCount = 10, int? skip = 0) + { + var placesUrl = graphServiceClient.Places.AppendSegmentToRequestUrl("microsoft.graph.roomList"); + + //Extending on the search by transforming the search-string to all-lower-case, ALL-UPPER-CASE and also to Proper/Pascal-case. + //Eg. "San FRANcisco" transformed to "san francisco", "SAN FRANCISCO" and "San Francisco", besides search for the actual string. + string allLowerCase = searchString.ToLower(); + string allUpperCase = searchString.ToUpper(); + string properCase = searchString; + string[] multiWords = searchString.Split(' ', StringSplitOptions.RemoveEmptyEntries); + foreach (var word in multiWords) + { + var pascalCase = new StringBuilder(word.Substring(0, 1).ToUpper() + word.Substring(1).ToLower()); + properCase = properCase.Replace(word, pascalCase.ToString(), StringComparison.InvariantCultureIgnoreCase); + } + + //Search for actual-string, all-lower-case, ALL-UPPER-CASE and Proper/Pascal-case. + StringBuilder predicateBuilder = new StringBuilder($"emailAddress ne ''"); + predicateBuilder.Append($" and ("); + predicateBuilder.Append($"contains(displayName, '{searchString}')"); + predicateBuilder.Append($" or contains(displayName, '{properCase}')"); + predicateBuilder.Append($" or contains(displayName, '{allLowerCase}')"); + predicateBuilder.Append($" or contains(displayName, '{allUpperCase}')"); + predicateBuilder.Append($" or contains(address/city, '{searchString}')"); + predicateBuilder.Append($" or contains(address/city, '{properCase}')"); + predicateBuilder.Append($" or contains(address/city, '{allLowerCase}')"); + predicateBuilder.Append($" or contains(address/city, '{allUpperCase}')"); + predicateBuilder.Append($")"); + + + //Now lets build the request. + var placesCollRequestBuilder = new GraphServicePlacesCollectionRequestBuilder(placesUrl, graphServiceClient); + var placesCollPage = await placesCollRequestBuilder.Request().Header("ConsistencyLevel", "Eventual") + .Filter(predicateBuilder.ToString()) + .OrderBy("displayName") + .Top(topCount.Value) + .Skip(skip.Value) + .GetAsync(); + + List placeList = placesCollPage.CurrentPage as List; + placeList = placeList.Where(r => r.AdditionalData != null && r.AdditionalData["emailAddress"] != null).ToList(); + + return new GraphRoomsListResponse(placeList); + } + public async Task GetMyWorkingHours() { string mailboxSettingsUrl = graphServiceClient.Me.AppendSegmentToRequestUrl("mailboxSettings/workingHours"); diff --git a/ConvergeUnitTests/Services/SyncServiceTest.cs b/ConvergeUnitTests/Services/SyncServiceTest.cs index 2095ba3..dfc66bc 100644 --- a/ConvergeUnitTests/Services/SyncServiceTest.cs +++ b/ConvergeUnitTests/Services/SyncServiceTest.cs @@ -23,7 +23,7 @@ namespace ConvergeUnitTests.Services var telemetryService = new Mock(); var appGraphService = new Mock(); var bingMapService = new Mock(); - appGraphService.Setup(x => x.GetRoomLists()).Returns(() => Task.FromResult(new List())); + appGraphService.Setup(x => x.GetAllRoomLists()).Returns(() => Task.FromResult(new List())); SyncService syncService = new SyncService(telemetryService.Object, appGraphService.Object, bingMapService.Object); // Act @@ -40,7 +40,7 @@ namespace ConvergeUnitTests.Services var telemetryService = new Mock(); var appGraphService = new Mock(); var bingMapService = new Mock(); - appGraphService.Setup(x => x.GetRoomLists()).Returns(() => Task.FromResult(new List + appGraphService.Setup(x => x.GetAllRoomLists()).Returns(() => Task.FromResult(new List { It.IsAny() })); @@ -68,7 +68,7 @@ namespace ConvergeUnitTests.Services Place room1 = JsonConvert.DeserializeObject($"{{ \"emailAddress\": \"{testRoomListEmail1}\" }}"); Place room2 = JsonConvert.DeserializeObject($"{{ \"emailAddress\": \"{testRoomListEmail2}\" }}"); - appGraphService.Setup(x => x.GetRoomLists()).Returns(() => Task.FromResult(new List + appGraphService.Setup(x => x.GetAllRoomLists()).Returns(() => Task.FromResult(new List { room1, room2 From 43ee76ea6ecc650bb6fa59b81fae62380d17e4fa Mon Sep 17 00:00:00 2001 From: Megan Slater Date: Wed, 26 Jan 2022 15:51:56 +0000 Subject: [PATCH 20/41] Cache and retrieve building name --- Converge/Models/BasicBuildingsResponse.cs | 5 +-- Converge/Services/AppGraphService.cs | 15 +------- Converge/Services/BuildingsMonoService.cs | 5 ++- Converge/Services/BuildingsService.cs | 38 ++++++++----------- .../Services/CachePlacesProviderService.cs | 21 ++++++++-- Converge/Services/PlacesService.cs | 21 +++++++--- Converge/Services/SyncService.cs | 8 +++- ConvergeUnitTests/Services/SyncServiceTest.cs | 26 +++++++++---- 8 files changed, 81 insertions(+), 58 deletions(-) diff --git a/Converge/Models/BasicBuildingsResponse.cs b/Converge/Models/BasicBuildingsResponse.cs index 5755946..bfe6905 100644 --- a/Converge/Models/BasicBuildingsResponse.cs +++ b/Converge/Models/BasicBuildingsResponse.cs @@ -9,13 +9,10 @@ namespace Converge.Models public class BasicBuildingsResponse { public List BuildingsList { get; set; } - - public QueryOption SkipToken { get; set; } - public BasicBuildingsResponse(List buildingsList, QueryOption skipToken = null) + public BasicBuildingsResponse(List buildingsList) { BuildingsList = buildingsList; - SkipToken = skipToken; } } } diff --git a/Converge/Services/AppGraphService.cs b/Converge/Services/AppGraphService.cs index d55a621..5b3ad0b 100644 --- a/Converge/Services/AppGraphService.cs +++ b/Converge/Services/AppGraphService.cs @@ -516,7 +516,7 @@ namespace Converge.Services return (places == null || places.Count == 0) ? null : places.CurrentPage as List; } - public async Task GetListItemsBySortRequest(CampusSortRequest buildingSortRequest, PlaceType? placeType) + public async Task GetListItemsByGPSRange(GPSCoordinates[] gpsCoordinatesRange, PlaceType? placeType) { string siteId = configuration["SharePointSiteId"]; string listId = configuration["SharePointListId"]; @@ -527,13 +527,6 @@ namespace Converge.Services return null; } - GPSCoordinates[] gpsCoordinatesRange = null; - if (buildingSortRequest.SortByType == CampusSortByType.Distance - && buildingSortRequest.DistanceFromSource > 0.0 && buildingSortRequest.SourceGpsCoordinates != null) - { - gpsCoordinatesRange = buildingSortRequest.GetCoordinatesRange(); - } - StringBuilder predicateBuilder = new StringBuilder($"(fields/EmailAddress ne '')"); predicateBuilder.Append($" and (fields/IsAvailable eq 1)"); predicateBuilder.Append($" and (fields/BookingType ne '{BookingType.Reserved}')"); @@ -548,11 +541,7 @@ namespace Converge.Services predicateBuilder.Append($" and fields/Latitude le {gpsCoordinatesRange[1].Latitude} and fields/Longitude le {gpsCoordinatesRange[1].Longitude})"); } - IListItemsCollectionRequest listItemsCollRequest = BuildListItemsCollectionRequest(siteId, placesList.Id, predicateBuilder.ToString(), skipToken: buildingSortRequest.SkipToken); - if (buildingSortRequest.SortByType == CampusSortByType.DisplayName) - { - listItemsCollRequest = listItemsCollRequest.OrderBy("fields/Locality"); - } + IListItemsCollectionRequest listItemsCollRequest = BuildListItemsCollectionRequest(siteId, placesList.Id, predicateBuilder.ToString()); try { diff --git a/Converge/Services/BuildingsMonoService.cs b/Converge/Services/BuildingsMonoService.cs index f7b77ed..2ab1e06 100644 --- a/Converge/Services/BuildingsMonoService.cs +++ b/Converge/Services/BuildingsMonoService.cs @@ -27,12 +27,13 @@ namespace Converge.Services ILogger placesSvcLogger, AppGraphService appGraphSvc, CachePlacesProviderService cacheProviderService, - CacheSharePointContentService cacheSharePointContentService) + CacheSharePointContentService cacheSharePointContentService, + CachePlacesProviderService cachePlacesProviderService) { appGraphService = appGraphSvc; //Had to manually instantiate for addressing Singleton-classes-instantiation-issues during Dependency-injection. - placesService = new PlacesService(placesSvcLogger, configuration, appGraphService, new ScheduleService(appGraphService), cacheSharePointContentService); + placesService = new PlacesService(placesSvcLogger, configuration, appGraphService, new ScheduleService(appGraphService), cacheSharePointContentService, cachePlacesProviderService); buildingsService = new BuildingsService(appGraphService, cacheProviderService, placesService, null); } diff --git a/Converge/Services/BuildingsService.cs b/Converge/Services/BuildingsService.cs index 2492142..15b468a 100644 --- a/Converge/Services/BuildingsService.cs +++ b/Converge/Services/BuildingsService.cs @@ -68,11 +68,25 @@ namespace Converge.Services return new BasicBuildingsResponse(new List()); } - CampusSortRequest buildingSortRequest = new CampusSortRequest(CampusSortByType.Distance, + CampusSortRequest campusSortRequest = new CampusSortRequest(CampusSortByType.Distance, sourceGpsCoords, distanceFromSource); - return await GetBuildingsByDistance(buildingSortRequest); + List buildingsList = new List(); + + GraphExchangePlacesResponse exchangePlacesResponse = await placesService.GetPlacesBySortRequest(campusSortRequest); + List buildingUpnList = exchangePlacesResponse.ExchangePlacesList.Select(ep => ep.Locality).Distinct().ToList(); + + foreach (string buildingUpn in buildingUpnList) + { + ExchangePlace exchangePlaceModel = exchangePlacesResponse.ExchangePlacesList.FirstOrDefault(ep => ep.Locality == buildingUpn); + buildingsList.Add(Building.Instantiate(exchangePlaceModel)); + } + + //Employed Haversine formula. + buildingsList = campusSortRequest.SortBuildingsByDistance(buildingsList); + + return new BasicBuildingsResponse(buildingsList.Select(b => new BuildingBasicInfo(b.Identity, b.DisplayName)).ToList()); } private async Task DetermineSourceGpsCoordinates(string sourceGeoCoordinates) @@ -99,26 +113,6 @@ namespace Converge.Services return sourceGpsCoords; } - private async Task GetBuildingsByDistance(CampusSortRequest campusSortRequest) - { - List buildingsList = new List(); - - GraphExchangePlacesResponse exchangePlacesResponse = await placesService.GetPlacesBySortRequest(campusSortRequest); - List buildingUpnList = exchangePlacesResponse.ExchangePlacesList.Select(ep => ep.Locality).Distinct().ToList(); - - - foreach (string buildingUpn in buildingUpnList) - { - ExchangePlace exchangePlaceModel = exchangePlacesResponse.ExchangePlacesList.FirstOrDefault(ep => ep.Locality == buildingUpn); - buildingsList.Add(Building.Instantiate(exchangePlaceModel)); - } - - //Employed Haversine formula. - buildingsList = campusSortRequest.SortBuildingsByDistance(buildingsList); - - return new BasicBuildingsResponse(buildingsList.Select(b => new BuildingBasicInfo(b.Identity, b.DisplayName)).ToList(), null); - } - public async Task GetBuildingByDisplayName(string buildingDisplayName) { BuildingBasicInfo buildingBasicInfo = null; diff --git a/Converge/Services/CachePlacesProviderService.cs b/Converge/Services/CachePlacesProviderService.cs index 8e5261d..362e201 100644 --- a/Converge/Services/CachePlacesProviderService.cs +++ b/Converge/Services/CachePlacesProviderService.cs @@ -27,6 +27,11 @@ namespace Converge.Services private const string keyPlacesUpnInfo = "PlacesUpnInfo"; private const string keySkipTokens = "SkipTokens"; + /// + /// Left empty for testing purposes. + /// + public CachePlacesProviderService() { } + public CachePlacesProviderService(IConfiguration paramConfiguration, ILogger paramLogger, IMemoryCache paramBuildingsPlacesCache) @@ -51,7 +56,7 @@ namespace Converge.Services } } - private bool CacheConfigEnabled => bool.Parse(configuration["CachingEnabled:BuildingsService"] ?? "false"); + private bool CacheConfigEnabled => configuration != null ? bool.Parse(configuration["CachingEnabled:BuildingsService"] ?? "false") : false; private void AddBuildingUpnInfoToCache(List buildingsList, string cacheKey) { @@ -181,6 +186,17 @@ namespace Converge.Services } } + public BuildingBasicInfo GetBuildingFromCache(string buildingUpn) + { + buildingsPlacesCache.TryGetValue(keyBuildings, out List cachedBuildingsList); + var building = cachedBuildingsList.FirstOrDefault(b => b.Identity.Equals(buildingUpn)); + if (building == null) + { + return null; + } + return new BuildingBasicInfo(building.Identity, building.DisplayName); + } + public void AddBuildings(BasicBuildingsResponse buildingsResponse, int? topCount = null, int? skip = null) { if (!CacheConfigEnabled) @@ -202,9 +218,6 @@ namespace Converge.Services buildingsPlacesCache.Set(keyBuildings, buildingsList, memoryCacheEntryOptions); //Add Buildings-Places-Upn-list from the Cache AddBuildingUpnInfoToCache(buildingsList, cacheKey); - - //Add Skip-token to the Cache. - AddSkipTokenToCache(skip.ToString(), cacheKey); } catch (Exception ex) { diff --git a/Converge/Services/PlacesService.cs b/Converge/Services/PlacesService.cs index 32fbc88..8f62b1d 100644 --- a/Converge/Services/PlacesService.cs +++ b/Converge/Services/PlacesService.cs @@ -23,18 +23,21 @@ namespace Converge.Services private readonly AppGraphService appGraphService; private readonly ScheduleService scheduleService; private readonly CacheSharePointContentService cacheSharePointContentService; + private readonly CachePlacesProviderService cachePlacesProviderService; public PlacesService(ILogger logger, IConfiguration configuration, AppGraphService appGraphService, ScheduleService scheduleService, - CacheSharePointContentService cacheSharePointContentService) + CacheSharePointContentService cacheSharePointContentService, + CachePlacesProviderService cachePlacesProviderService) { this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); this.configuration = configuration; this.appGraphService = appGraphService; this.scheduleService = scheduleService; this.cacheSharePointContentService = cacheSharePointContentService; + this.cachePlacesProviderService = cachePlacesProviderService; } public async Task GetMaxReserved(string upn, string start, string end) @@ -54,7 +57,7 @@ namespace Converge.Services public async Task GetPlacesBySortRequest(CampusSortRequest buildingSortRequest, PlaceType? placeType = null) { - GraphListItemsResponse graphListItemsResponse = await appGraphService.GetListItemsBySortRequest(buildingSortRequest, placeType); + GraphListItemsResponse graphListItemsResponse = await appGraphService.GetListItemsByGPSRange(buildingSortRequest.GetCoordinatesRange(), placeType); GraphExchangePlacesResponse exchangePlacesResponse = CollectExchangePlaces(graphListItemsResponse); return exchangePlacesResponse; @@ -62,7 +65,7 @@ namespace Converge.Services private GraphExchangePlacesResponse CollectExchangePlaces(GraphListItemsResponse graphListItemsResponse) { - if (graphListItemsResponse == null || graphListItemsResponse.GraphListItems == null || graphListItemsResponse.GraphListItems.Count == 0) + if (graphListItemsResponse == null || graphListItemsResponse.GraphListItems == null || graphListItemsResponse.GraphListItems.Count() == 0) { return new GraphExchangePlacesResponse(new List(), null); } @@ -74,8 +77,16 @@ namespace Converge.Services { ExchangePlace place = DeserializeHelper.DeserializeExchangePlace(gli.ListItem.Fields.AdditionalData, logger); place.SharePointID = gli.ListItem.Fields.Id; - Place targetRoom = appGraphService.GetRoomListById(place.Locality).Result; - place.Building = targetRoom?.DisplayName; + // try the cache first + BuildingBasicInfo building = cachePlacesProviderService.GetBuildingFromCache(place.Locality); + string buildingName = building?.DisplayName; + if (building == null) + { + Place targetRoom = appGraphService.GetRoomListById(place.Locality).Result; + buildingName = targetRoom?.DisplayName; + } + + place.Building = buildingName; lock (p) { diff --git a/Converge/Services/SyncService.cs b/Converge/Services/SyncService.cs index 09cbe13..cb6146f 100644 --- a/Converge/Services/SyncService.cs +++ b/Converge/Services/SyncService.cs @@ -6,6 +6,7 @@ using Converge.Models; using Microsoft.Graph; using System; using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; namespace Converge.Services @@ -15,14 +16,18 @@ namespace Converge.Services private readonly TelemetryService telemetryService; private readonly AppGraphService appGraphService; private readonly SearchBingMapsService searchBingMapsService; + private readonly CachePlacesProviderService cachePlacesProviderService; + public SyncService( TelemetryService telemetryService, AppGraphService appGraphService, - SearchBingMapsService searchBingMapsSvc) + SearchBingMapsService searchBingMapsSvc, + CachePlacesProviderService cachePlacesProviderService) { this.telemetryService = telemetryService; this.appGraphService = appGraphService; this.searchBingMapsService = searchBingMapsSvc; + this.cachePlacesProviderService = cachePlacesProviderService; } /// @@ -34,6 +39,7 @@ namespace Converge.Services try { List roomLists = await appGraphService.GetAllRoomLists(); + cachePlacesProviderService.AddBuildings(new BasicBuildingsResponse(roomLists.Select(rl => new BuildingBasicInfo(rl.AdditionalData["emailAddress"].ToString(),rl.DisplayName)).ToList())); List allGraphPlaces = new List(); foreach (Place roomList in roomLists) { diff --git a/ConvergeUnitTests/Services/SyncServiceTest.cs b/ConvergeUnitTests/Services/SyncServiceTest.cs index dfc66bc..2bd7f1e 100644 --- a/ConvergeUnitTests/Services/SyncServiceTest.cs +++ b/ConvergeUnitTests/Services/SyncServiceTest.cs @@ -23,8 +23,14 @@ namespace ConvergeUnitTests.Services var telemetryService = new Mock(); var appGraphService = new Mock(); var bingMapService = new Mock(); + var cachePlacesProviderService = new Mock(); appGraphService.Setup(x => x.GetAllRoomLists()).Returns(() => Task.FromResult(new List())); - SyncService syncService = new SyncService(telemetryService.Object, appGraphService.Object, bingMapService.Object); + SyncService syncService = new SyncService( + telemetryService.Object, + appGraphService.Object, + bingMapService.Object, + cachePlacesProviderService.Object + ); // Act List places = syncService.GetAllGraphPlaces().GetAwaiter().GetResult(); @@ -40,13 +46,14 @@ namespace ConvergeUnitTests.Services var telemetryService = new Mock(); var appGraphService = new Mock(); var bingMapService = new Mock(); + var cachePlacesProviderService = new Mock(); appGraphService.Setup(x => x.GetAllRoomLists()).Returns(() => Task.FromResult(new List { It.IsAny() })); appGraphService.Setup(x => x.GetAllWorkspaces(It.IsAny())).Returns(() => Task.FromResult(new List())); appGraphService.Setup(x => x.GetAllConferenceRooms(It.IsAny())).Returns(() => Task.FromResult(new List())); - SyncService syncService = new SyncService(telemetryService.Object, appGraphService.Object, bingMapService.Object); + SyncService syncService = new SyncService(telemetryService.Object, appGraphService.Object, bingMapService.Object, cachePlacesProviderService.Object); // Act List places = syncService.GetAllGraphPlaces().GetAwaiter().GetResult(); @@ -62,6 +69,7 @@ namespace ConvergeUnitTests.Services var telemetryService = new Mock(); var appGraphService = new Mock(); var bingMapService = new Mock(); + var cachePlacesProviderService = new Mock(); string testRoomListEmail1 = "roomList1@example.com"; string testRoomListEmail2 = "roomList2example.com"; @@ -81,7 +89,7 @@ namespace ConvergeUnitTests.Services appGraphService.Setup(x => x.GetAllWorkspaces(testRoomListEmail1)).Returns(() => Task.FromResult(new List { workspace.Object })); appGraphService.Setup(x => x.GetAllConferenceRooms(testRoomListEmail2)).Returns(() => Task.FromResult(new List { conferenceRoom.Object })); - SyncService syncService = new SyncService(telemetryService.Object, appGraphService.Object, bingMapService.Object); + SyncService syncService = new SyncService(telemetryService.Object, appGraphService.Object, bingMapService.Object, cachePlacesProviderService.Object); // Act List places = syncService.GetAllGraphPlaces().GetAwaiter().GetResult(); @@ -99,7 +107,8 @@ namespace ConvergeUnitTests.Services var telemetryService = new Mock(); var appGraphService = new Mock(); var bingMapService = new Mock(); - var syncService = new SyncService(telemetryService.Object, appGraphService.Object, bingMapService.Object); + var cachePlacesProviderService = new Mock(); + var syncService = new SyncService(telemetryService.Object, appGraphService.Object, bingMapService.Object, cachePlacesProviderService.Object); // Act List placesToUpdate = syncService.GetSharePointPlacesToUpdate(graphPlaces, exchangePlaces); @@ -117,7 +126,8 @@ namespace ConvergeUnitTests.Services var telemetryService = new Mock(); var appGraphService = new Mock(); var bingMapService = new Mock(); - var syncService = new SyncService(telemetryService.Object, appGraphService.Object, bingMapService.Object); + var cachePlacesProviderService = new Mock(); + var syncService = new SyncService(telemetryService.Object, appGraphService.Object, bingMapService.Object, cachePlacesProviderService.Object); // Act List placesToUpdate = syncService.GetSharePointPlacesToUpdate(graphPlaces, exchangePlaces); @@ -141,7 +151,8 @@ namespace ConvergeUnitTests.Services var telemetryService = new Mock(); var appGraphService = new Mock(); var bingMapService = new Mock(); - var syncService = new SyncService(telemetryService.Object, appGraphService.Object, bingMapService.Object); + var cachePlacesProviderService = new Mock(); + var syncService = new SyncService(telemetryService.Object, appGraphService.Object, bingMapService.Object, cachePlacesProviderService.Object); // Act List placesToUpdate = syncService.GetSharePointPlacesToUpdate(graphPlaces, exchangePlaces); @@ -174,7 +185,8 @@ namespace ConvergeUnitTests.Services var telemetryService = new Mock(); var appGraphService = new Mock(); var bingMapService = new Mock(); - var syncService = new SyncService(telemetryService.Object, appGraphService.Object, bingMapService.Object); + var cachePlacesProviderService = new Mock(); + var syncService = new SyncService(telemetryService.Object, appGraphService.Object, bingMapService.Object, cachePlacesProviderService.Object); // Act // fix this From 89ed2e23d6e5494794696b4e51ed970e57d33da5 Mon Sep 17 00:00:00 2001 From: Megan Slater Date: Wed, 26 Jan 2022 17:12:59 +0000 Subject: [PATCH 21/41] Update front end build to support multiple environments based on env variables --- Converge/ClientApp/.env.example | 2 + .../appPackage/manifest.template.json | 2 +- Converge/ClientApp/buildManifest.js | 40 ++----------------- Converge/ClientApp/package.json | 4 +- Converge/Converge.csproj | 34 +--------------- azure-pipelines.yml | 3 +- 6 files changed, 11 insertions(+), 74 deletions(-) diff --git a/Converge/ClientApp/.env.example b/Converge/ClientApp/.env.example index f754280..7c4b57e 100644 --- a/Converge/ClientApp/.env.example +++ b/Converge/ClientApp/.env.example @@ -8,3 +8,5 @@ DOMAIN = localhost # Port used in API URL PORT = :3000 + +AppEnvironment = _dev diff --git a/Converge/ClientApp/appPackage/manifest.template.json b/Converge/ClientApp/appPackage/manifest.template.json index c9e2b27..bd8522a 100644 --- a/Converge/ClientApp/appPackage/manifest.template.json +++ b/Converge/ClientApp/appPackage/manifest.template.json @@ -15,7 +15,7 @@ "outline": "outline.png" }, "name": { - "short": "Converge%NAME_SUFFIX%", + "short": "Converge%AppEnvironment%", "full": "Go further together." }, "description": { diff --git a/Converge/ClientApp/buildManifest.js b/Converge/ClientApp/buildManifest.js index becccc3..7b092db 100644 --- a/Converge/ClientApp/buildManifest.js +++ b/Converge/ClientApp/buildManifest.js @@ -29,7 +29,7 @@ const cleanManifest = async () => { async function setDevEnvironment() { dotenv.config(); - process.env.NAME_SUFFIX = "_dev"; + process.env.AppEnvironment = "_dev"; if (!process.env.DOMAIN) { throw new Error("Missing required environment variable: DOMAIN!"); @@ -42,7 +42,7 @@ async function setDevEnvironment() { } } -async function setProdEnvironment() { +function checkEnvironmentVariables() { if (!process.env.WEBSITE) { throw new Error("Missing required environment variable: WEBSITE!"); } @@ -54,31 +54,6 @@ async function setProdEnvironment() { } } -async function setStagingEnvironment() { - process.env.NAME_SUFFIX = "_staging"; - - if (!process.env.WEBSITE) { - throw new Error("Missing required environment variable: WEBSITE!"); - } - if (!process.env.DOMAIN) { - throw new Error("Missing required environment variable: DOMAIN!"); - } - if (!process.env.APP_ID) { - throw new Error("Missing required environment variable: APP_ID!"); - } -} - -async function setTestingEnvironment() { - process.env.NAME_SUFFIX = "_test"; - - if (!process.env.DOMAIN) { - throw new Error("Missing required environment variable: DOMAIN!"); - } - if (!process.env.APP_ID) { - throw new Error("Missing required environment variable: APP_ID!"); - } -} - function getValueFromEnv(key) { return process.env[key] || ""; @@ -90,15 +65,8 @@ const buildManifest = async () => { case "dev": setDevEnvironment(); break; - case "prod": - setProdEnvironment(); - break; - case "stage": - setStagingEnvironment(); - break; - case "test": - setTestingEnvironment(); - break; + default: + checkEnvironmentVariables() } const manifestTemplateString = await new Promise((resolve, reject) => { diff --git a/Converge/ClientApp/package.json b/Converge/ClientApp/package.json index 181ef30..8d91ced 100644 --- a/Converge/ClientApp/package.json +++ b/Converge/ClientApp/package.json @@ -59,9 +59,7 @@ }, "scripts": { "start": "set HTTPS=true&&node ./buildManifest.js --environment dev&&npx office-addin-dev-certs install&&react-scripts start", - "build": "node ./buildManifest.js --environment prod && react-scripts build", - "build-stage": "node ./buildManifest.js --environment stage && react-scripts build", - "build-test": "node ./buildManifest.js --environment test && react-scripts build", + "build": "node ./buildManifest.js && react-scripts build", "test": "set CI=true && react-scripts test", "eject": "react-scripts eject" }, diff --git a/Converge/Converge.csproj b/Converge/Converge.csproj index bf2d670..c8dbaec 100644 --- a/Converge/Converge.csproj +++ b/Converge/Converge.csproj @@ -77,40 +77,8 @@ - - - - - - - - - - - %(DistFiles.Identity) - PreserveNewest - true - - - - - - - - - - - - - - %(DistFiles.Identity) - PreserveNewest - true - - - - + diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 406ccd9..bddf864 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -2,6 +2,7 @@ # AzureAd.TenantId*: Id of Tenant that app belongs to # AzureAd.ClientId*: ClientId of converge azure app service # AzureAd.Audience*: Azure AD Audience of token being authenticated in the format api://{hostname}/{clientId} +# AzureAd.Instance*: Use https://login.microsoftonline.com/ # SharePointSiteId*: Graph Id of the SharePoint site # SharePointListId*: Name or ID of SharePoint list where Exchange Places are being stored # SharePointPhotoListId*: Name or ID of SharePoint list where Place photos are stored @@ -11,7 +12,7 @@ # JobSchedules.PredictorSchedule*: Crontab expression for how often to check users' calendars and predict where they will be in the in the next number of days specified by the MaxPredictionWindow # JobSchedules.SyncPlacesSchedule*: Crontab expression for how often to sync SharePoint with the workspace and conference room data from Exchange # FilterUsersByTitle: Adding this string will ensure that users with the given title are filtered out of result sets -# AppEnvironment: Environment the app is running in +# AppEnvironment: Environment the app is running in - this is a required setting for all builds other than the production instance. # AppBannerMessage: Message displayed at the top of the site on all pages # NewPlacesAvailableByDefault: true/false field representing whether New Places are available by default # BuildParameters.ConnectedServiceName: Id of the service connection created with the subscription you're using in Azure. From f01f61f5b16b9094a4539a95a3f1b2cc7ec12746 Mon Sep 17 00:00:00 2001 From: "Tej.Neelapala" Date: Wed, 26 Jan 2022 20:59:06 +0000 Subject: [PATCH 22/41] Merged PR 1271: For Task # 2179 :: Understand Graph-Throttling and identify the scope. Before submitting this PR, please make ensure you've completed the following: - [ ] Your code builds clean without any errors **or warnings**. - [ ] All unused code has been deleted. - [ ] All TODOs in the code base have an associated ticket in ADO. - [ ] Your squash commit to main has a short descriptive commit message. Describe your PR: If submitting to the FrontEnd UI, include a screenshot of the change: Related work items: #2179 --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index ecb7800..467e406 100644 --- a/README.md +++ b/README.md @@ -44,4 +44,5 @@ Background Jobs for Location prediction - A good configured server (with configu around minimum of 50K-60K users (who have calendar events) within 24 hours' job-execution-window (considering 1.5 seconds on an average per user). And it may go maximum upto 100K users. +Graph Throttling Limitation - Based on single user statistics, and pro-rata based calculation, the number of users that can be supported = 5859 users simultaneously. From 09722e162378ff8258752857ab32bc04065d6f3c Mon Sep 17 00:00:00 2001 From: Ganesh Kotagiri Date: Thu, 27 Jan 2022 15:50:37 +0000 Subject: [PATCH 23/41] Merged PR 1273: When paging workspace results, using the skip token returns the same values as no skip token Before submitting this PR, please make ensure you've completed the following: - [ ] Your code builds clean without any errors **or warnings**. - [ ] All unused code has been deleted. - [ ] All TODOs in the code base have an associated ticket in ADO. - [ ] Your squash commit to main has a short descriptive commit message. Describe your PR: Please find below Bug Id:- 1)BUG 3805 If submitting to the FrontEnd UI, include a screenshot of the change: Related work items: #3805 --- .../ClientApp/src/hooks/useBuildingPlaces.tsx | 10 ++++++---- .../ClientApp/src/tabs/workspace/Places.tsx | 7 +++++++ .../workspace/components/BuildingPlaces.tsx | 18 +++++++++++++++--- .../CustomizedPlaceCollectionAccordian.tsx | 17 ++++++++++++----- .../src/types/IExchangePlacesResponse.ts | 2 +- Converge/Controllers/BuildingsController.cs | 4 ++-- Converge/Services/BuildingsService.cs | 8 ++++---- 7 files changed, 47 insertions(+), 19 deletions(-) diff --git a/Converge/ClientApp/src/hooks/useBuildingPlaces.tsx b/Converge/ClientApp/src/hooks/useBuildingPlaces.tsx index cb601e5..1b97fe1 100644 --- a/Converge/ClientApp/src/hooks/useBuildingPlaces.tsx +++ b/Converge/ClientApp/src/hooks/useBuildingPlaces.tsx @@ -22,7 +22,8 @@ interface IUseBuildingWorkspacesHookReturnType { placeType: PlaceType, itemsPerPage: number, filterOptions: BuildingPlacesFilterOptions, - clearList?: boolean + skipTokens?:string|undefined|null, + clearList?: boolean, ) => Promise, clearList: () => void, hasMore: boolean @@ -45,6 +46,7 @@ function useBuildingPlaces( placeType: PlaceType, itemsPerPage: number, filterOptions?: BuildingPlacesFilterOptions, + skipTokens?:string|undefined|null, clearList?: boolean, ) => { if (clearList) { @@ -56,7 +58,7 @@ function useBuildingPlaces( placeType, { topCount: itemsPerPage, - skipToken: placesResult?.skipToken, + skipToken: skipTokens, ...filterOptions, }, ); @@ -64,7 +66,7 @@ function useBuildingPlaces( return result; } setPlaces([]); - const result = { exchangePlacesList: [], skipToken: null }; + const result = { exchangePlacesList: [], skipToken: "" }; waitFor(Promise.resolve(result)); return Promise.resolve(result); }; @@ -73,7 +75,7 @@ function useBuildingPlaces( setPlaces([]); waitFor(Promise.resolve({ exchangePlacesList: [], - skipToken: null, + skipToken: "", })); }; diff --git a/Converge/ClientApp/src/tabs/workspace/Places.tsx b/Converge/ClientApp/src/tabs/workspace/Places.tsx index 2a4b195..cee034b 100644 --- a/Converge/ClientApp/src/tabs/workspace/Places.tsx +++ b/Converge/ClientApp/src/tabs/workspace/Places.tsx @@ -48,6 +48,7 @@ const Places: React.FC = (props) => { const [err, setErr] = useState(isError); const classes = PlacesStyles(); const { teamsContext } = useTeamsContext(); + const [skipTokenString, setSkipTokenString] = useState(""); const convertDateToTimeRange = (inputDate: Date) => ({ start: dayjs(`${dayjs(inputDate).format("MM-DD-YYYY")} ${state.startDate?.format("h:mm A")}`, "MM-DD-YYYY h:mm A"), end: dayjs(`${dayjs(inputDate).format("MM-DD-YYYY")} ${state.endDate?.format("h:mm A")}`, "MM-DD-YYYY h:mm A"), @@ -85,6 +86,10 @@ const Places: React.FC = (props) => { window.location.reload(); }; + const onSkipToken = (skipToken:string) => { + setSkipTokenString(skipToken); + }; + return ( = (props) => { buildingUpn={state.location} placeType={placeType} key={state.location + placeType} + skipToken={skipTokenString} /> )} {!!state.location && state.location === "Favorites" @@ -188,6 +194,7 @@ const Places: React.FC = (props) => { )} {convergeState.buildingListLoading && } diff --git a/Converge/ClientApp/src/tabs/workspace/components/BuildingPlaces.tsx b/Converge/ClientApp/src/tabs/workspace/components/BuildingPlaces.tsx index 31b1718..cf8cfc3 100644 --- a/Converge/ClientApp/src/tabs/workspace/components/BuildingPlaces.tsx +++ b/Converge/ClientApp/src/tabs/workspace/components/BuildingPlaces.tsx @@ -29,10 +29,14 @@ import IsThisHelpful from "../../../utilities/IsThisHelpful"; interface IPlaceResultSetProps { buildingUpn: string; + skipToken:string; placeType?: PlaceType } -const BuildingPlaces: React.FC = ({ buildingUpn, placeType }) => { +const BuildingPlaces: React.FC = ({ + buildingUpn, placeType, + skipToken, +}) => { const { theme } = useFluentContext(); const { state } = PlaceFilterProvider(); const classes = BuildingPlacesStyles(); @@ -49,6 +53,7 @@ const BuildingPlaces: React.FC = ({ buildingUpn, placeType 10, 15, 25, 50, ]; const [itemsPerPage, setItemsPerPage] = useState(pageSizeOptions[0]); + const [skipTokenString, setSkipTokenString] = useState(skipToken); const handleItemCountChange = ( event: React.MouseEvent | React.KeyboardEvent | null, data: DropdownProps, @@ -66,8 +71,11 @@ const BuildingPlaces: React.FC = ({ buildingUpn, placeType hasDisplay: state.attributeFilter.indexOf("displayDeviceName") > -1, hasVideo: state.attributeFilter.indexOf("videoDeviceName") > -1, }, + skipTokenString, true, - ); + ).then((s) => { + setSkipTokenString(s.skipToken); + }); }, [buildingUpn, state.attributeFilter]); return ( @@ -145,7 +153,11 @@ const BuildingPlaces: React.FC = ({ buildingUpn, placeType hasDisplay: state.attributeFilter.indexOf("displayDeviceName") > -1, hasVideo: state.attributeFilter.indexOf("videoDeviceName") > -1, }, - ); + skipTokenString, + false, + ).then((s) => { + setSkipTokenString(s.skipToken); + }); logEvent(USER_INTERACTION, [ { name: UI_SECTION, value: UISections.BookPlaceModal }, { name: DESCRIPTION, value: "requestWorkspaces" }, diff --git a/Converge/ClientApp/src/tabs/workspace/components/CustomizedPlaceCollectionAccordian.tsx b/Converge/ClientApp/src/tabs/workspace/components/CustomizedPlaceCollectionAccordian.tsx index af48ad5..dea0c16 100644 --- a/Converge/ClientApp/src/tabs/workspace/components/CustomizedPlaceCollectionAccordian.tsx +++ b/Converge/ClientApp/src/tabs/workspace/components/CustomizedPlaceCollectionAccordian.tsx @@ -22,10 +22,11 @@ import IsThisHelpful from "../../../utilities/IsThisHelpful"; interface Props { closestBuilding: BuildingBasicInfo; favoriteCampuses: ExchangePlace[]; + getSkipToken:(skipToken:string)=>void; } const CustomizedPlaceCollectionAccordian: React.FC = (props) => { - const { closestBuilding, favoriteCampuses } = props; + const { closestBuilding, favoriteCampuses, getSkipToken } = props; const { state, updateLocation } = PlaceFilterProvider(); const { buildingService } = useApiProvider(); const { @@ -46,8 +47,11 @@ const CustomizedPlaceCollectionAccordian: React.FC = (props) => { hasDisplay: state.attributeFilter.indexOf("displayDeviceName") > -1, hasVideo: state.attributeFilter.indexOf("videoDeviceName") > -1, }, + null, true, - ); + ).then((s) => { + getSkipToken(s.skipToken); + }); }, [ closestBuilding.identity, state.place, @@ -61,12 +65,12 @@ const CustomizedPlaceCollectionAccordian: React.FC = (props) => { ]); }; - const onLoadPlacesAgain = () => { + const onLoadPlacesAgain = async () => { logEvent(USER_INTERACTION, [ { name: UI_SECTION, value: UISections.PlaceResults }, { name: DESCRIPTION, value: "load_places_again" }, ]); - requestBuildingPlaces( + await requestBuildingPlaces( state.place, 4, { @@ -75,8 +79,11 @@ const CustomizedPlaceCollectionAccordian: React.FC = (props) => { hasDisplay: state.attributeFilter.indexOf("displayDeviceName") > -1, hasVideo: state.attributeFilter.indexOf("videoDeviceName") > -1, }, + null, true, - ); + ).then((s) => { + getSkipToken(s.skipToken); + }); }; const determineFavoritePanel = () => { diff --git a/Converge/ClientApp/src/types/IExchangePlacesResponse.ts b/Converge/ClientApp/src/types/IExchangePlacesResponse.ts index 4d4f268..333df05 100644 --- a/Converge/ClientApp/src/types/IExchangePlacesResponse.ts +++ b/Converge/ClientApp/src/types/IExchangePlacesResponse.ts @@ -5,5 +5,5 @@ import ExchangePlace from "./ExchangePlace"; export interface IExchangePlacesResponse { exchangePlacesList: ExchangePlace[]; - skipToken: string | null; + skipToken: string; } diff --git a/Converge/Controllers/BuildingsController.cs b/Converge/Controllers/BuildingsController.cs index 26082d3..e607034 100644 --- a/Converge/Controllers/BuildingsController.cs +++ b/Converge/Controllers/BuildingsController.cs @@ -160,7 +160,7 @@ namespace Converge.Controllers public async Task> GetBuildingWorkspaces( string buildingUpn, int? topCount = null, - string skipTokenString = null, + string skipToken = null, bool hasVideo = false, bool hasAudio = false, bool hasDisplay = false, @@ -178,7 +178,7 @@ namespace Converge.Controllers IsWheelChairAccessible = isWheelchairAccessible, DisplayNameSearchString = displayNameSearchString, }; - var result = await buildingsService.GetPlacesOfBuilding(buildingUpn, PlaceType.Space, topCount, skipTokenString, listItemFilterOptions); + var result = await buildingsService.GetPlacesOfBuilding(buildingUpn, PlaceType.Space, topCount, skipToken, listItemFilterOptions); return Ok(result); } catch (Exception ex) diff --git a/Converge/Services/BuildingsService.cs b/Converge/Services/BuildingsService.cs index 15b468a..a5b20f4 100644 --- a/Converge/Services/BuildingsService.cs +++ b/Converge/Services/BuildingsService.cs @@ -171,7 +171,7 @@ namespace Converge.Services public async Task GetPlacesOfBuilding(string buildingUpn, PlaceType? placeType = null, int? topCount = null, - string skipTokenString = null, + string skipToken = null, ListItemFilterOptions listItemFilterOptions = null) { if (string.IsNullOrWhiteSpace(buildingUpn)) @@ -184,19 +184,19 @@ namespace Converge.Services //Data when list-item-filter-options is defined, are not cached. if (listItemFilterOptions == null) { - exchangePlacesResponse = cachePlacesProviderService.GetPlacesOfBuilding(buildingUpn, placeType, topCount, skipTokenString); + exchangePlacesResponse = cachePlacesProviderService.GetPlacesOfBuilding(buildingUpn, placeType, topCount, skipToken); } if (exchangePlacesResponse == null) { var buildingsUpnList = new List() { buildingUpn }; - exchangePlacesResponse = await placesService.GetPlacesByBuildingUpns(buildingsUpnList, placeType, topCount, skipTokenString, listItemFilterOptions); + exchangePlacesResponse = await placesService.GetPlacesByBuildingUpns(buildingsUpnList, placeType, topCount, skipToken, listItemFilterOptions); if (exchangePlacesResponse.ExchangePlacesList == null || exchangePlacesResponse.ExchangePlacesList.Count() == 0) { return new GraphExchangePlacesResponse(new List(), null); } //Add to Cache. - cachePlacesProviderService.AddPlacesOfBuilding(exchangePlacesResponse.ExchangePlacesList, placeType, topCount, skipTokenString); + cachePlacesProviderService.AddPlacesOfBuilding(exchangePlacesResponse.ExchangePlacesList, placeType, topCount, skipToken); } return exchangePlacesResponse; From 7988e05b404d5596c58b615077978badd72397db Mon Sep 17 00:00:00 2001 From: Muhammadhu Abubakkar Date: Thu, 27 Jan 2022 17:51:09 +0000 Subject: [PATCH 24/41] Merged PR 1275: 3833 - Search Filter campusSearchRangeInMiles dispatch is now calculated on a... Before submitting this PR, please make ensure you've completed the following: - [ ] Your code builds clean without any errors **or warnings**. - [ ] All unused code has been deleted. - [ ] All TODOs in the code base have an associated ticket in ADO. - [ ] Your squash commit to main has a short descriptive commit message. Describe your PR: If submitting to the FrontEnd UI, include a screenshot of the change: 3833 - Search Filter campusSearchRangeInMiles dispatch is now calculated on a separate const before requesting for more campuses. This eliminates the issue of directly reading the campusSearchRangeInMiles from the data store and only to find that it is not yet set to the required increased range. Related work items: #3833 --- .../src/providers/SearchProvider.tsx | 66 ++++++++++--------- .../CollaborationPlaceResultsPaged.tsx | 9 +-- .../Services/CachePlacesProviderService.cs | 5 +- 3 files changed, 44 insertions(+), 36 deletions(-) diff --git a/Converge/ClientApp/src/providers/SearchProvider.tsx b/Converge/ClientApp/src/providers/SearchProvider.tsx index 09adbae..5535b99 100644 --- a/Converge/ClientApp/src/providers/SearchProvider.tsx +++ b/Converge/ClientApp/src/providers/SearchProvider.tsx @@ -164,14 +164,14 @@ interface ISearchProviderModel { setVenueType: (venueType: CollaborationVenueType) => void; setSelectedUsers: (users: User[]) => void; setMeetUsers: (meetUsers: string[]) => void; - searchPlacesToCollaborate: (force?: boolean) => void; + searchPlacesToCollaborate: (force?: boolean, specificRange?: number) => void; setLoginUser:(loginUser:string) => void; setMapPlaces: (mapPlaces: (CampusToCollaborate|VenueToCollaborate)[]) => void; setPlacesLoading: (placesLoading: boolean) => void; clearPlaceSearch: () => void; setStartAndEndTime: (startTime: Dayjs, endTime: Dayjs) => void; setVenueSkip: (skip: number) => void; - setCampusSearchNextRange: (reset?: boolean) => boolean; + setCampusSearchRange: (range: number) => void; setCampusSearchWaiting: (waitState: boolean) => void; } @@ -268,6 +268,7 @@ const reducer = (state: ISearchState, action: ISearchAction): ISearchState => { placesToCollaborate: [], venueType: CollaborationVenueType.Workspace, placesLoading: false, + campusSearchRangeInMiles: 10, }; case SET_VENUE_SKIP: return { @@ -301,6 +302,24 @@ const reducer = (state: ISearchState, action: ISearchAction): ISearchState => { } }; +export const getCampusSearchNextRange = (currentRange: number, reset?: boolean) : number => { + let newRange = 0; + if (reset) { + newRange = 10; + } + if (currentRange <= 4000) { + if (currentRange < 1000) { + newRange = currentRange * 10; + } else { + newRange = currentRange + 1000; + } + } + if (newRange !== 0) { + return newRange; + } + return currentRange; +}; + const SearchContextProvider: React.FC = ({ children }) => { const { searchService } = useApiProvider(); const [state, dispatch, getState] = useEnhancedReducer( @@ -310,6 +329,7 @@ const SearchContextProvider: React.FC = ({ children }) => { const searchPlaces = ( searchState: ISearchState, + specificRange?: number, ): Promise => { const userList: string[] = []; userList.push(searchState.loginUser); @@ -330,7 +350,7 @@ const SearchContextProvider: React.FC = ({ children }) => { placeType: searchState.venueType === CollaborationVenueType.Workspace ? "space" : "room", // If multiple meetUsers, it will use location in between. closeToUser: searchState.meetUsers.length > 1 ? "" : searchState.meetUsers[0], - distanceFromSource: searchState.campusSearchRangeInMiles, + distanceFromSource: specificRange ?? searchState.campusSearchRangeInMiles, }; return searchService.searchCampusesToCollaborate(request); } @@ -353,46 +373,30 @@ const SearchContextProvider: React.FC = ({ children }) => { }); }; - const setCampusSearchNextRange = (reset?: boolean) : boolean => { - if (reset) { - dispatch({ - type: SET_CAMPUS_SEARCH_RANGE, - payload: 10, - }); - return true; - } - if (state.campusSearchRangeInMiles <= 4000) { - if (state.campusSearchRangeInMiles < 1000) { - dispatch({ - type: SET_CAMPUS_SEARCH_RANGE, - payload: state.campusSearchRangeInMiles * 10, - }); - } else { - dispatch({ - type: SET_CAMPUS_SEARCH_RANGE, - payload: state.campusSearchRangeInMiles + 1000, - }); - } - return true; - } - return false; + const setCampusSearchRange = (range: number) => { + dispatch({ + type: SET_CAMPUS_SEARCH_RANGE, + payload: range, + }); }; - const searchPlacesToCollaborate = (force?: boolean) => { + const searchPlacesToCollaborate = (force?: boolean, specificRange?: number) => { const newState = getState(); const shouldSearch = !(newState.venueType === undefined) || force; if (shouldSearch) { dispatch({ type: SEARCH_PLACES_REQUEST }); - searchPlaces(newState) + searchPlaces(newState, specificRange) .then((response) => { if ( (response as CampusesToCollaborateResponse).campusesToCollaborateList !== undefined && (response as CampusesToCollaborateResponse).campusesToCollaborateList.length === 0 ) { - if (setCampusSearchNextRange()) { - searchPlacesToCollaborate(); + if (state.campusSearchRangeInMiles < 4000) { + const nextRange = getCampusSearchNextRange(state.campusSearchRangeInMiles); + setCampusSearchRange(nextRange); + searchPlacesToCollaborate(true, nextRange); } else { const payload = (response as CampusesToCollaborateResponse).campusesToCollaborateList || (response as VenuesToCollaborateResponse).venuesToCollaborateList; @@ -500,7 +504,7 @@ const SearchContextProvider: React.FC = ({ children }) => { clearPlaceSearch, setStartAndEndTime, setVenueSkip, - setCampusSearchNextRange, + setCampusSearchRange, setCampusSearchWaiting, }} > diff --git a/Converge/ClientApp/src/tabs/collaborate/components/CollaborationPlaceResultsPaged.tsx b/Converge/ClientApp/src/tabs/collaborate/components/CollaborationPlaceResultsPaged.tsx index d3cbeda..dc6bc28 100644 --- a/Converge/ClientApp/src/tabs/collaborate/components/CollaborationPlaceResultsPaged.tsx +++ b/Converge/ClientApp/src/tabs/collaborate/components/CollaborationPlaceResultsPaged.tsx @@ -15,7 +15,7 @@ import { DESCRIPTION, UISections, UI_SECTION, USER_INTERACTION, } from "../../../types/LoggerTypes"; import CollaborationPlaceResultsPagedStyles from "../styles/CollaborationPlaceResultsPagedStyles"; -import { useSearchContextProvider } from "../../../providers/SearchProvider"; +import { useSearchContextProvider, getCampusSearchNextRange } from "../../../providers/SearchProvider"; import { CollaborationVenueType } from "../../../types/ExchangePlace"; interface Props { @@ -32,7 +32,7 @@ const CollaborationPlaceResultsPaged: React.FC = (props) => { const { state, setVenueSkip, - setCampusSearchNextRange, + setCampusSearchRange, setCampusSearchWaiting, searchPlacesToCollaborate, } = useSearchContextProvider(); @@ -43,8 +43,9 @@ const CollaborationPlaceResultsPaged: React.FC = (props) => { const loadFartherPlaces = () => { setCampusSearchWaiting(true); - setCampusSearchNextRange(); - searchPlacesToCollaborate(); + const increasedSearchRange = getCampusSearchNextRange(state.campusSearchRangeInMiles); + setCampusSearchRange(increasedSearchRange); + searchPlacesToCollaborate(true, increasedSearchRange); }; return ( diff --git a/Converge/Services/CachePlacesProviderService.cs b/Converge/Services/CachePlacesProviderService.cs index 362e201..dc3d1d4 100644 --- a/Converge/Services/CachePlacesProviderService.cs +++ b/Converge/Services/CachePlacesProviderService.cs @@ -188,7 +188,10 @@ namespace Converge.Services public BuildingBasicInfo GetBuildingFromCache(string buildingUpn) { - buildingsPlacesCache.TryGetValue(keyBuildings, out List cachedBuildingsList); + if (!buildingsPlacesCache.TryGetValue(keyBuildings, out List cachedBuildingsList)) + { + return null; + } var building = cachedBuildingsList.FirstOrDefault(b => b.Identity.Equals(buildingUpn)); if (building == null) { From 35344789efa37fc9fe860667bcce77dd0d67a16c Mon Sep 17 00:00:00 2001 From: Muhammadhu Abubakkar Date: Thu, 27 Jan 2022 18:07:47 +0000 Subject: [PATCH 25/41] Merged PR 1276: 3789 - Map will load after userCoords data is generated. Before submitting this PR, please make ensure you've completed the following: - [ ] Your code builds clean without any errors **or warnings**. - [ ] All unused code has been deleted. - [ ] All TODOs in the code base have an associated ticket in ADO. - [ ] Your squash commit to main has a short descriptive commit message. Describe your PR: If submitting to the FrontEnd UI, include a screenshot of the change: 3789 - Map will load after userCoords data is generated. Related work items: #3789 --- .../src/tabs/collaborate/components/Map.tsx | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/Converge/ClientApp/src/tabs/collaborate/components/Map.tsx b/Converge/ClientApp/src/tabs/collaborate/components/Map.tsx index 39fe137..4aaf0f3 100644 --- a/Converge/ClientApp/src/tabs/collaborate/components/Map.tsx +++ b/Converge/ClientApp/src/tabs/collaborate/components/Map.tsx @@ -70,6 +70,7 @@ const Map: React.FC = ({ >(undefined); const [isOpen, { setTrue: openPanel, setFalse: dismissPanel }] = useBoolean(false); const { appSettings } = useAppSettingsProvider(); + const [userCoordsFetchCompleted, setCoordsFetchCompleted] = useState(false); useEffect(() => { if (appSettings?.bingAPIKey && appSettings?.bingAPIKey !== "") { @@ -89,11 +90,14 @@ const Map: React.FC = ({ day: day.date(), }, ); - setUserCoords({ - latitude: newCoordinates.latitude, - longitude: newCoordinates.longitude, - userPrincipalName: teamsContext.userPrincipalName, - }); + if (newCoordinates !== undefined) { + setUserCoords({ + latitude: newCoordinates.latitude, + longitude: newCoordinates.longitude, + userPrincipalName: teamsContext.userPrincipalName, + }); + } + setCoordsFetchCompleted(true); } }; @@ -212,7 +216,7 @@ const Map: React.FC = ({ return ( - {finishedLoading ? ( + {finishedLoading && userCoordsFetchCompleted ? ( Date: Thu, 27 Jan 2022 18:56:40 +0000 Subject: [PATCH 26/41] Merged PR 1277: User profile photos do not render on home tab Before submitting this PR, please make ensure you've completed the following: - [ ] Your code builds clean without any errors **or warnings**. - [ ] All unused code has been deleted. - [ ] All TODOs in the code base have an associated ticket in ADO. - [ ] Your squash commit to main has a short descriptive commit message. Describe your PR: Please find below bug Id's 1)BUG 3788 ![image.png](https://dev.azure.com/mwcci/08fbf972-cba0-4622-a98f-aff4c4b4c567/_apis/git/repositories/5d547110-08b1-402c-91d8-153ba56a7d02/pullRequests/1277/attachments/image.png) If submitting to the FrontEnd UI, include a screenshot of the change: Related work items: #3788 --- .../src/tabs/home/components/WorkgroupAvatar.tsx | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/Converge/ClientApp/src/tabs/home/components/WorkgroupAvatar.tsx b/Converge/ClientApp/src/tabs/home/components/WorkgroupAvatar.tsx index 966871c..7741ac6 100644 --- a/Converge/ClientApp/src/tabs/home/components/WorkgroupAvatar.tsx +++ b/Converge/ClientApp/src/tabs/home/components/WorkgroupAvatar.tsx @@ -96,20 +96,16 @@ const WorkgroupAvatar: React.FC = (props) => { const { user, } = props; - const [image, setImage] = useState(undefined); + const [image, setImage] = useState(undefined); const [IsDisplayName, setDisplayName] = useState(""); const classes = WorkgroupAvatarStyles(); const [presence, setPresence] = useState({} as ApiPresence); useEffect(() => { if (user.userPrincipalName) { - const response = userService.getUserProfile(user.userPrincipalName); + const response = userService.getUserPhoto(user.userPrincipalName); response.then((photo) => { - const blob = new Blob(photo.userPhoto); - if (blob.size !== 0) { - setImage(URL.createObjectURL(blob)); - } - setPresence(photo.presence); + setImage(photo.userPhoto); }).catch(() => { if (user.userPrincipalName?.split(" ")[1] !== undefined) { setDisplayName(`${user.userPrincipalName.split(" ")[0][0]}${user.userPrincipalName.split(" ")[1][0]}`); @@ -118,6 +114,10 @@ const WorkgroupAvatar: React.FC = (props) => { setDisplayName(`${user.userPrincipalName.split(" ")[0]}${user.userPrincipalName.split(" ")[1]}`); } }); + const responsePresence = userService.getUserProfile(user.userPrincipalName); + responsePresence.then((userPresence) => { + setPresence(userPresence.presence); + }); } return () => { From 5eb176d11154bba65559e318d23504128124674c Mon Sep 17 00:00:00 2001 From: Megan Slater Date: Thu, 27 Jan 2022 20:07:44 +0000 Subject: [PATCH 27/41] Use universal time correctly in schedule API --- .../ClientApp/src/tabs/home/BookWorkspace.tsx | 7 ++---- Converge/Helpers/TimeHelper.cs | 9 +++---- Converge/Services/ScheduleService.cs | 2 +- ConvergeUnitTests/Helpers/TimeHelperTest.cs | 24 +++++++++++++++++-- 4 files changed, 30 insertions(+), 12 deletions(-) diff --git a/Converge/ClientApp/src/tabs/home/BookWorkspace.tsx b/Converge/ClientApp/src/tabs/home/BookWorkspace.tsx index f7de751..266c9b3 100644 --- a/Converge/ClientApp/src/tabs/home/BookWorkspace.tsx +++ b/Converge/ClientApp/src/tabs/home/BookWorkspace.tsx @@ -126,11 +126,8 @@ const BookWorkspace: React.FC = () => { } const startHours = dayjs(hours.start); const endHours = dayjs(hours.end); - const startBuilding = dayjs.utc(start).set("hour", startHours.hour()).set("minute", startHours.minute()); - let endBuilding = dayjs.utc(start).set("hour", endHours.hour()).set("minute", endHours.minute()); - if (endBuilding.isBefore(startBuilding)) { - endBuilding = endBuilding.add(1, "day"); - } + const startBuilding = dayjs(start).set("hour", startHours.hour()).set("minute", startHours.minute()).utc(); + const endBuilding = dayjs(start).set("hour", endHours.hour()).set("minute", endHours.minute()).utc(); return buildingService.getBuildingSchedule( building.identity, startBuilding.toISOString(), diff --git a/Converge/Helpers/TimeHelper.cs b/Converge/Helpers/TimeHelper.cs index 1cb526c..2c05721 100644 --- a/Converge/Helpers/TimeHelper.cs +++ b/Converge/Helpers/TimeHelper.cs @@ -46,15 +46,16 @@ namespace Converge.Helpers foreach (TimeFrame timeFrame in timeFrames) { if ( - DateTime.Parse(e.Start.DateTime) < timeFrame.End && + (DateTime.Parse(e.Start.DateTime) < timeFrame.End && DateTime.Parse(e.End.DateTime) > timeFrame.Start && e.Attendees != null && - e.Attendees.Count() > 0 + e.Attendees.Count() > 0) || e.IsAllDay == true ) { + // Always include one for the organizer timeFrame.Reserved += e.Attendees - .Where(a => a.Status?.Response == ResponseType.Accepted && a.Type != AttendeeType.Resource) - .Count(); + .Where(a => a.Status?.Response == ResponseType.Accepted && a.Type != AttendeeType.Resource && a.EmailAddress.Address != e.Organizer.EmailAddress.Address) + .Count() + 1; } } } diff --git a/Converge/Services/ScheduleService.cs b/Converge/Services/ScheduleService.cs index 6b04dbe..ab8735d 100644 --- a/Converge/Services/ScheduleService.cs +++ b/Converge/Services/ScheduleService.cs @@ -26,7 +26,7 @@ namespace Converge.Services start, end ); - double reserved = TimeHelper.GetAverageReserved(DateTime.Parse(start), DateTime.Parse(end), events); + double reserved = TimeHelper.GetAverageReserved(DateTime.Parse(start).ToUniversalTime(), DateTime.Parse(end).ToUniversalTime(), events); return (workspace.Capacity == 0) ? 0 : reserved / workspace.Capacity * 100; } diff --git a/ConvergeUnitTests/Helpers/TimeHelperTest.cs b/ConvergeUnitTests/Helpers/TimeHelperTest.cs index ae5700b..9b35497 100644 --- a/ConvergeUnitTests/Helpers/TimeHelperTest.cs +++ b/ConvergeUnitTests/Helpers/TimeHelperTest.cs @@ -27,12 +27,22 @@ namespace ConvergeUnitTests.Helpers { new Attendee { - EmailAddress = new EmailAddress(), + EmailAddress = new EmailAddress + { + Address="organizer@example.com" + }, Status = new ResponseStatus() { Response = ResponseType.Accepted, }, } + }, + Organizer = new Recipient + { + EmailAddress = new EmailAddress + { + Address = "organizer@example.com" + } } } }; @@ -54,12 +64,22 @@ namespace ConvergeUnitTests.Helpers { new Attendee { - EmailAddress = new EmailAddress(), + EmailAddress = new EmailAddress + { + Address="organizer@example.com" + }, Status = new ResponseStatus() { Response = ResponseType.Accepted, }, } + }, + Organizer= new Recipient + { + EmailAddress= new EmailAddress + { + Address = "organizer@example.com" + } } } }; From a3f0e1574a2b0efd1173c162e6ec03b791bbc2ed Mon Sep 17 00:00:00 2001 From: Ganesh Kotagiri Date: Fri, 28 Jan 2022 16:49:25 +0000 Subject: [PATCH 28/41] Merged PR 1284: Fixed Bug's Id BUG 3895 and BUG 3830 Before submitting this PR, please make ensure you've completed the following: - [ ] Your code builds clean without any errors **or warnings**. - [ ] All unused code has been deleted. - [ ] All TODOs in the code base have an associated ticket in ADO. - [ ] Your squash commit to main has a short descriptive commit message. Describe your PR: Please find below Bug Id's 1)BUG 3830 2)BUG 3895 If submitting to the FrontEnd UI, include a screenshot of the change: Related work items: #3830, #3895 --- .../components/CollaborationPlaceResults.tsx | 34 ++++++++++++------- .../CollaborationPlaceResultsPaged.tsx | 23 +++++++++---- Converge/Controllers/BuildingsController.cs | 8 ++--- 3 files changed, 43 insertions(+), 22 deletions(-) diff --git a/Converge/ClientApp/src/tabs/collaborate/components/CollaborationPlaceResults.tsx b/Converge/ClientApp/src/tabs/collaborate/components/CollaborationPlaceResults.tsx index a8d1dc8..7ee27cb 100644 --- a/Converge/ClientApp/src/tabs/collaborate/components/CollaborationPlaceResults.tsx +++ b/Converge/ClientApp/src/tabs/collaborate/components/CollaborationPlaceResults.tsx @@ -1,9 +1,12 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + import React from "react"; import { - Box, Button, Flex, Loader, + Box, Button, Flex, Loader, Text, } from "@fluentui/react-northstar"; import { logEvent } from "../../../utilities/LogWrapper"; import CollaborationCampusPlaceCard from "../../workspace/components/CollaborationCampusPlaceCard"; @@ -85,18 +88,25 @@ const CollaborationPlaceResults: React.FC = (props) => { ) && ( {state.loadMorePlacesLoading - ? () + && ()} + {state.venueSkip < 1000 && state.loadMorePlacesLoading === false ? ( + /// UPN of Building. /// Number of records after the skipCount used to skip the number of records. - /// Skip-token option as string to get next set of records. + /// Skip-token option as string to get next set of records. /// Whether to return places with a video display device. /// Whether to return places with an audio device. /// Whether to return places with a display device. @@ -116,7 +116,7 @@ namespace Converge.Controllers public async Task> GetBuildingConferenceRooms( string buildingUpn, int? topCount = null, - string skipTokenString = null, + string skipToken = null, bool hasVideo = false, bool hasAudio = false, bool hasDisplay = false, @@ -132,7 +132,7 @@ namespace Converge.Controllers HasDisplay = hasDisplay, IsWheelChairAccessible = isWheelchairAccessible, }; - var result = await buildingsService.GetPlacesOfBuilding(buildingUpn, PlaceType.Room, topCount, skipTokenString, listItemFilterOptions); + var result = await buildingsService.GetPlacesOfBuilding(buildingUpn, PlaceType.Room, topCount, skipToken, listItemFilterOptions); return Ok(result); } catch (Exception ex) @@ -148,7 +148,7 @@ namespace Converge.Controllers /// /// UPN of Building. /// Number of records after the skipCount used to skip the number of records. - /// Skip-token option as string to get next set of records. + /// Skip-token option as string to get next set of records. /// Whether to return places with a video display device. /// Whether to return places with an audio device. /// Whether to return places with a display device. From fa927770481ab98bd43ed8039d3cc6ba0e8efd87 Mon Sep 17 00:00:00 2001 From: Muhammadhu Abubakkar Date: Mon, 31 Jan 2022 17:07:14 +0000 Subject: [PATCH 29/41] Merged PR 1282: 3823 - Recommendations tab pagination Before submitting this PR, please make ensure you've completed the following: - [ ] Your code builds clean without any errors **or warnings**. - [ ] All unused code has been deleted. - [ ] All TODOs in the code base have an associated ticket in ADO. - [ ] Your squash commit to main has a short descriptive commit message. Describe your PR: If submitting to the FrontEnd UI, include a screenshot of the change: 3823 - Recommendations tab pagination ![image.png](https://dev.azure.com/mwcci/08fbf972-cba0-4622-a98f-aff4c4b4c567/_apis/git/repositories/5d547110-08b1-402c-91d8-153ba56a7d02/pullRequests/1282/attachments/image.png) Related work items: #3823 --- .../src/providers/SearchProvider.tsx | 5 +- .../CollaborationPlaceResultsPaged.tsx | 23 ++++-- .../components/RecommendedToCollaborate.tsx | 74 +++++++++++-------- 3 files changed, 62 insertions(+), 40 deletions(-) diff --git a/Converge/ClientApp/src/providers/SearchProvider.tsx b/Converge/ClientApp/src/providers/SearchProvider.tsx index 5535b99..004483f 100644 --- a/Converge/ClientApp/src/providers/SearchProvider.tsx +++ b/Converge/ClientApp/src/providers/SearchProvider.tsx @@ -389,10 +389,7 @@ const SearchContextProvider: React.FC = ({ children }) => { dispatch({ type: SEARCH_PLACES_REQUEST }); searchPlaces(newState, specificRange) .then((response) => { - if ( - (response as CampusesToCollaborateResponse).campusesToCollaborateList !== undefined - && (response as CampusesToCollaborateResponse).campusesToCollaborateList.length === 0 - ) { + if (!((response as CampusesToCollaborateResponse)?.campusesToCollaborateList?.length)) { if (state.campusSearchRangeInMiles < 4000) { const nextRange = getCampusSearchNextRange(state.campusSearchRangeInMiles); setCampusSearchRange(nextRange); diff --git a/Converge/ClientApp/src/tabs/collaborate/components/CollaborationPlaceResultsPaged.tsx b/Converge/ClientApp/src/tabs/collaborate/components/CollaborationPlaceResultsPaged.tsx index c45fcde..390e75f 100644 --- a/Converge/ClientApp/src/tabs/collaborate/components/CollaborationPlaceResultsPaged.tsx +++ b/Converge/ClientApp/src/tabs/collaborate/components/CollaborationPlaceResultsPaged.tsx @@ -22,11 +22,17 @@ interface Props { places: (CampusToCollaborate | VenueToCollaborate)[]; openPanel: () => void; setSelectedPlace: (selectedPlace: CampusToCollaborate | VenueToCollaborate) => void; + forceVenueShowMore?: boolean; + recommendationSearchRadius?: number; + moreRecommendationsfetcher?: () => void; } const CollaborationPlaceResultsPaged: React.FC = (props) => { const { places, openPanel, setSelectedPlace, + forceVenueShowMore, + recommendationSearchRadius, + moreRecommendationsfetcher, } = props; const classes = CollaborationPlaceResultsPagedStyles(); const { @@ -42,10 +48,14 @@ const CollaborationPlaceResultsPaged: React.FC = (props) => { }; const loadFartherPlaces = () => { - setCampusSearchWaiting(true); - const increasedSearchRange = getCampusSearchNextRange(state.campusSearchRangeInMiles); - setCampusSearchRange(increasedSearchRange); - searchPlacesToCollaborate(true, increasedSearchRange); + if (moreRecommendationsfetcher === undefined) { + setCampusSearchWaiting(true); + const increasedSearchRange = getCampusSearchNextRange(state.campusSearchRangeInMiles); + setCampusSearchRange(increasedSearchRange); + searchPlacesToCollaborate(true, increasedSearchRange); + } else { + moreRecommendationsfetcher(); + } }; return ( @@ -124,10 +134,11 @@ const CollaborationPlaceResultsPaged: React.FC = (props) => { {( - (state.venueType === CollaborationVenueType.Workspace + (forceVenueShowMore + || state.venueType === CollaborationVenueType.Workspace || state.venueType === CollaborationVenueType.ConferenceRoom) ) && ( - state.campusSearchRangeInMiles < 4000 ? ( + (recommendationSearchRadius ?? state.campusSearchRangeInMiles) < 4000 ? (