Merge pull request #1 from microsoft/pagination-fixes

Pagination fixes
This commit is contained in:
Megan Slater 2022-02-08 10:14:42 -08:00 коммит произвёл GitHub
Родитель 74dcd0b23a aabb5422be
Коммит 0fe01a69b9
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
122 изменённых файлов: 3633 добавлений и 3531 удалений

Просмотреть файл

@ -9,6 +9,7 @@ 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
AppEnvironment = _dev
# Application description
DESCRIPTION = Go further together.

Просмотреть файл

@ -0,0 +1,7 @@
# FEATURE FLAGS SETUP
1. Install it from npm (npm i flagged).
2. Import the FlagsProvider in your code and wrap your application around it.(import { FlagsProvider } from 'flagged)
3. The features prop you pass to FlagsProvider could be an array of strings or an object. If you decide to use an object you could also pass nested objects to group feature flags together(<FlagsProvider features={flags}>).
4. Import the Feature in your code and wrap your application around it(import { Feature } from 'flagged').
5. Pass the name of the feature you want to check for and a children value and it will not render the children if the feature is enabled..

Просмотреть файл

@ -15,12 +15,12 @@
"outline": "outline.png"
},
"name": {
"short": "Converge%NAME_SUFFIX%",
"full": "Go further together."
"short": "Converge%AppEnvironment%",
"full": "Converge%AppEnvironment%"
},
"description": {
"short": "Go further together.",
"full": "Go further together."
"short": "%DESCRIPTION%",
"full": "%DESCRIPTION%"
},
"accentColor": "#FFFFFF",
"bots": [],

Просмотреть файл

@ -29,7 +29,7 @@ const cleanManifest = async () => {
async function setDevEnvironment() {
dotenv.config();
process.env.NAME_SUFFIX = process.env.NAME_SUFFIX ?? "_local";
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,29 +54,6 @@ async function setProdEnvironment() {
}
}
async function setStagingEnvironment() {
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 = 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] || "";
@ -88,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) => {

262
Converge/ClientApp/package-lock.json сгенерированный
Просмотреть файл

@ -28,9 +28,11 @@
"@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",
"flagged": "^2.0.1",
"lodash": "4.17.21",
"office-ui-fabric-react": "7.170.3",
"react": "16.10",
@ -5867,11 +5869,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 +6415,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 +6516,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 +6998,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",
@ -10103,6 +10138,14 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/flagged": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/flagged/-/flagged-2.0.1.tgz",
"integrity": "sha512-cKkJdbHruBbi4CAufayqDs1X6PYMKE+MZmG/3mYBPZw1M0dY+tA14tDTkg5+TISQwZ7tTIzwubFZNv4lm7XENw==",
"peerDependencies": {
"react": ">=16"
}
},
"node_modules/flat-cache": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz",
@ -11052,6 +11095,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 +11182,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 +13765,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 +13990,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 +14049,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 +14087,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 +14129,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 +16242,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 +17786,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 +24702,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 +25140,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 +25213,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 +25592,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",
@ -27875,6 +27934,12 @@
"path-exists": "^4.0.0"
}
},
"flagged": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/flagged/-/flagged-2.0.1.tgz",
"integrity": "sha512-cKkJdbHruBbi4CAufayqDs1X6PYMKE+MZmG/3mYBPZw1M0dY+tA14tDTkg5+TISQwZ7tTIzwubFZNv4lm7XENw==",
"requires": {}
},
"flat-cache": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz",
@ -28544,6 +28609,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 +28660,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 +30634,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 +30797,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 +30843,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 +30872,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 +30905,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 +32332,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 +33478,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": {

Просмотреть файл

@ -23,9 +23,11 @@
"@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",
"flagged": "^2.0.1",
"lodash": "4.17.21",
"office-ui-fabric-react": "7.170.3",
"react": "16.10",
@ -58,9 +60,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"
},

Просмотреть файл

@ -14,16 +14,14 @@ 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";
import { AppSettingProvider } from "./providers/AppSettingsProvider";
import { TeamsContextProvider } from "./providers/TeamsContextProvider";
import { PlacePhotosProvider } from "./providers/PlacePhotosProvider";
import ContextLoader from "./ContextLoader";
import AppBanner from "./utilities/AppBanner";
import { ApiProvider } from "./providers/ApiProvider";
initializeIcons();
dayjs.extend(duration);
@ -34,39 +32,36 @@ dayjs.extend(timezone);
const App: React.FC = () => (
<div style={{ background: "#f5f5f5" }}>
<AppSettingProvider>
<TeamsContextProvider>
<TeamsThemeProvider>
<ContextLoader>
<ErrorAlertProvider>
<ApiProvider>
<AppSettingProvider>
<TeamsContextProvider>
<TeamsThemeProvider>
<ContextLoader>
<ConvergeSettingsProvider>
<PlacePhotosProvider>
<ErrorAlert />
<AppBanner />
<Router>
<Route
exact
path="/tab"
component={Home}
/>
<Route
exact
path="/collaborate"
render={() => (
<SearchContextProvider>
<Collaborate />
</SearchContextProvider>
)}
/>
<Route exact path="/workspace" component={Workspace} />
</Router>
</PlacePhotosProvider>
<AppBanner />
<Router>
<Route
exact
path="/tab"
component={Home}
/>
<Route
exact
path="/collaborate"
render={() => (
<SearchContextProvider>
<Collaborate />
</SearchContextProvider>
)}
/>
<Route exact path="/workspace" component={Workspace} />
</Router>
</ConvergeSettingsProvider>
</ErrorAlertProvider>
</ContextLoader>
</TeamsThemeProvider>
</TeamsContextProvider>
</AppSettingProvider>
</ContextLoader>
</TeamsThemeProvider>
</TeamsContextProvider>
</AppSettingProvider>
</ApiProvider>
</div>
);

Просмотреть файл

@ -2,24 +2,31 @@
// 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<string> {
return new Promise<string>((resolve, reject) => {
export default class AuthenticationService {
private getSSOToken = (): Promise<string> => new Promise<string>((resolve, reject) => {
microsoftTeams.authentication.getAuthToken({
successCallback: resolve,
failureCallback: reject,
});
});
})
private api: AxiosInstance;
constructor() {
this.api = setup({
cache: {
maxAge: 0,
},
});
this.api.defaults.headers.common["Content-Type"] = "application/json";
}
getAxiosClient = async (): Promise<AxiosInstance> => {
const accessToken = await this.getSSOToken();
this.api.defaults.headers.common.Authorization = `Bearer ${accessToken}`;
return this.api;
}
}
const getAxiosClient = async (): Promise<AxiosStatic> => {
const accessToken = await getSSOToken();
axios.defaults.headers.common.Authorization = `Bearer ${accessToken}`;
axios.defaults.headers.common["Content-Type"] = "application/json";
return axios;
};
export default getAxiosClient;

Просмотреть файл

@ -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);
});
});

Просмотреть файл

@ -2,14 +2,16 @@
// 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";
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 createCachedQuery, { CachedQuery } from "../utilities/CachedQuery";
import getAxiosClient from "./AuthenticationService";
import AuthenticationService from "./AuthenticationService";
import Constants from "../utilities/Constants";
interface IGetBuildingPlacesRequestParams {
topCount?: number;
@ -21,115 +23,12 @@ interface IGetBuildingPlacesRequestParams {
displayNameSearchString?: string;
}
export interface IExchangePlacesResponse {
exchangePlacesList: ExchangePlace[];
skipToken: string | null;
}
export const getBuildingPlaces = async (
buildingUpn: string,
placeType: PlaceType,
params?: IGetBuildingPlacesRequestParams,
): Promise<IExchangePlacesResponse> => {
const axios = await getAxiosClient();
const type = placeType === PlaceType.Room ? "room" : "space";
const request = await axios.get<AutoWrapperResponse<IExchangePlacesResponse>>(
`/api/buildings/${buildingUpn}/${type}s`, {
params,
},
);
return request.data.result;
};
export const getBuildingsByDistance = async (
sourceGeoCoordinates?:string,
distanceFromSource?:number,
): Promise<UpcomingBuildingsResponse> => {
const axios = await getAxiosClient();
const request = await axios.get<AutoWrapperResponse<UpcomingBuildingsResponse>>(
"/api/buildings/sortByDistance", {
params: {
sourceGeoCoordinates,
distanceFromSource,
},
},
);
return request.data.result;
};
export const getBuildingsByName = async (): Promise<UpcomingBuildingsResponse> => {
const axios = await getAxiosClient();
const request = await axios.get<AutoWrapperResponse<UpcomingBuildingsResponse>>(
"/api/buildings/sortByName",
);
return request.data.result;
};
export const getSearchForBuildings = async (searchString:string|undefined)
: Promise<BuildingSearchInfo> => {
const axios = await getAxiosClient();
const request = await axios.get<AutoWrapperResponse<BuildingSearchInfo>>(
`/api/buildings/searchForBuildings/${searchString}`, {
params: {
searchString,
},
},
);
return request.data.result;
};
export const getBuildingSchedule = async (
id: string, start: string, end: string,
): Promise<Schedule> => {
const axios = await getAxiosClient();
const request = await axios.get<AutoWrapperResponse<Schedule>>(`/api/buildings/${id}/schedule`, {
params: {
start,
end,
},
});
return request.data.result;
};
interface PlaceDetailsQueryParams {
start: Date,
end: Date,
}
const getPlaceDetails = async (
id: string,
{
start,
end,
}: PlaceDetailsQueryParams,
): Promise<CampusToCollaborate> => {
const axios = await getAxiosClient();
const request = await axios.get<AutoWrapperResponse<CampusToCollaborate>>(`/api/places/${id}/details`, {
params: {
start,
end,
},
});
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<CampusToCollaborate, PlaceDetailsQueryParams>;
interface PlacePhotosResult {
export interface PlacePhotosResult {
sharePointId: string;
photos: ExchangePlacePhoto[];
coverPhoto?: ExchangePlacePhoto;
@ -137,59 +36,137 @@ interface PlacePhotosResult {
allOtherPhotos: ExchangePlacePhoto[];
}
// Function that returns a cached getUserCoordinates function.
export function createCachedPlaceDetailsQuery(): CachedPlaceDetailsQuery {
return createCachedQuery<CampusToCollaborate, PlaceDetailsQueryParams>(
generatePlaceDetailsStoreKey,
generatePlaceDetailsRetrievalKey,
(entities: string[], params: PlaceDetailsQueryParams) => getPlaceDetails(entities[0], params)
.then((item) => [item]),
);
}
export default class BuildingService {
private authenticationService: AuthenticationService
const getPlacePhotos = async (
sharePointId: string,
): Promise<ExchangePlacePhoto[]> => {
const axios = await getAxiosClient();
const request = await axios.get<AutoWrapperResponse<ExchangePlacePhoto[]>>(`/api/places/${sharePointId}/photos`);
return request.data.result;
};
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,
);
const generatePlacePhotosStoreKey = (
photos: PlacePhotosResult,
): string => photos.sharePointId;
return {
sharePointId,
photos,
coverPhoto,
floorPlan,
allOtherPhotos,
};
}
const generatePlacePhotosRetrievalKey = (search: string): string => search;
constructor(authenticationService: AuthenticationService) {
this.authenticationService = authenticationService;
}
type CachedPlacePhotosQuery = CachedQuery<PlacePhotosResult>;
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,
);
return {
sharePointId,
photos,
coverPhoto,
floorPlan,
allOtherPhotos,
getBuildingPlaces = async (
buildingUpn: string,
placeType: PlaceType,
params?: IGetBuildingPlacesRequestParams,
): Promise<IExchangePlacesResponse> => {
const axios = await this.authenticationService.getAxiosClient();
const type = placeType === PlaceType.Room ? "room" : "space";
const request = await axios.get<AutoWrapperResponse<IExchangePlacesResponse>>(
`/api/v1.0/buildings/${buildingUpn}/${type}s`, {
params,
},
);
return request.data.result;
};
}
// Function that returns a cached getUserCoordinates function.
export function createCachedPlacePhotosQuery(): CachedPlacePhotosQuery {
return createCachedQuery<PlacePhotosResult>(
generatePlacePhotosStoreKey,
generatePlacePhotosRetrievalKey,
(entities: string[]) => getPlacePhotos(entities[0])
// Add sharepointid to results for caching purposes.
.then((photos) => generateResultObject(entities[0], photos))
.then((item) => [item]),
);
getBuildingsByDistance = async (
sourceGeoCoordinates?:string,
distanceFromSource?:number,
): Promise<UpcomingBuildingsResponse> => {
const axios = await this.authenticationService.getAxiosClient();
const request = await axios.get<AutoWrapperResponse<UpcomingBuildingsResponse>>(
"/api/v1.0/buildings/sortByDistance", {
params: {
sourceGeoCoordinates,
distanceFromSource,
},
},
);
return request.data.result;
};
getBuildingByDisplayName = async (
buildingDisplayName?:string,
): Promise<BuildingBasicInfo> => {
const axios = await this.authenticationService.getAxiosClient();
const request = await axios.get<AutoWrapperResponse<BuildingBasicInfo>>(
`/api/v1.0/buildings/buildingByName/${buildingDisplayName}`, {
},
);
return request.data.result;
};
getBuildingsByName = async (): Promise<UpcomingBuildingsResponse> => {
const axios = await this.authenticationService.getAxiosClient();
const request = await axios.get<AutoWrapperResponse<UpcomingBuildingsResponse>>(
"/api/v1.0/buildings/sortByName",
);
return request.data.result;
};
getSearchForBuildings = async (searchString:string|undefined)
: Promise<BuildingSearchInfo> => {
const axios = await this.authenticationService.getAxiosClient();
const request = await axios.get<AutoWrapperResponse<BuildingSearchInfo>>(
`/api/v1.0/buildings/searchForBuildings/${searchString}`, {
params: {
searchString,
},
},
);
return request.data.result;
};
getBuildingSchedule = async (
id: string, start: string, end: string,
): Promise<Schedule> => {
const axios = await this.authenticationService.getAxiosClient();
const request = await axios.get<AutoWrapperResponse<Schedule>>(`/api/v1.0/buildings/${id}/schedule`, {
params: {
start,
end,
},
});
return request.data.result;
};
getPlaceDetails = async (
id: string,
{
start,
end,
}: PlaceDetailsQueryParams,
): Promise<CampusToCollaborate> => {
const axios = await this.authenticationService.getAxiosClient();
const request = await axios.get<AutoWrapperResponse<CampusToCollaborate>>(`/api/v1.0/places/${id}/details`, {
params: {
start,
end,
},
cache: {
maxAge: Constants.TWO_HOURS_IN_MILLISECONDS,
},
});
return request.data.result;
};
getPlacePhotos = async (
sharePointId: string,
): Promise<PlacePhotosResult> => {
const axios = await this.authenticationService.getAxiosClient();
const request = await axios.get<AutoWrapperResponse<ExchangePlacePhoto[]>>(`/api/v1.0/places/${sharePointId}/photos`, {
cache: {
maxAge: Constants.TWO_HOURS_IN_MILLISECONDS,
},
});
return this.generatePlacePhotosResult(sharePointId, request.data.result);
}
}

Просмотреть файл

@ -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<WorkingStartEnd> => {
const axios = await getAxiosClient();
const response = await axios.get<AutoWrapperResponse<WorkingStartEnd>>("/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<UpcomingReservationsResponse> => {
const axios = await getAxiosClient();
const response = await axios.get<AutoWrapperResponse<UpcomingReservationsResponse>>("/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<void> => {
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<WorkingStartEnd> => {
const axios = await this.authenticationService.getAxiosClient();
const response = await axios.get<AutoWrapperResponse<WorkingStartEnd>>("/api/v1.0/calendar/mailboxSettings/workingHours");
return response.data.result;
};
export const createEvent = async (newEvent: CalendarEventRequest) : Promise<CalendarEvent> => {
const axios = await getAxiosClient();
const response = await axios.post<AutoWrapperResponse<CalendarEvent>>("/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<UpcomingReservationsResponse> => {
const axios = await this.authenticationService.getAxiosClient();
const response = await axios.get<AutoWrapperResponse<UpcomingReservationsResponse>>("/api/v1.0/calendar/upcomingReservations", {
params: {
startDateTime, endDateTime, top, skip,
},
});
return response.data.result;
};
deleteEvent = async (eventId: string, messageComment: string): Promise<void> => {
const axios = await this.authenticationService.getAxiosClient();
await axios.get(`/api/v1.0/calendar/events/${eventId}/deleteEvent`, {
params: {
messageComment,
},
});
logEvent(USER_INTERACTION, [
{ name: IMPORTANT_ACTION, value: ImportantActions.EventCancelled },
]);
};
createEvent = async (newEvent: CalendarEventRequest) : Promise<CalendarEvent> => {
const axios = await this.authenticationService.getAxiosClient();
const response = await axios.post<AutoWrapperResponse<CalendarEvent>>("/api/v1.0/calendar/event", newEvent);
logEvent(USER_INTERACTION, [
{ name: IMPORTANT_ACTION, value: ImportantActions.EventCreated },
]);
return response.data.result;
};
}

Просмотреть файл

@ -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<ConvergeSettings | null> => {
const axios = await getAxiosClient();
const request = await axios.get<AutoWrapperResponse<ConvergeSettings>>(
"/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<void> => {
const axios = await getAxiosClient();
const request = axios.post("/api/me/convergeSettings", settings);
return (await request).data.result;
};
getSettings = async (): Promise<ConvergeSettings | null> => {
const axios = await this.authenticationService.getAxiosClient();
const request = await axios.get<AutoWrapperResponse<ConvergeSettings>>(
"/api/v1.0/me/convergeSettings",
);
if (request.status === 204) {
return null;
}
return request.data.result;
};
export const setupNewUser = async (settings: ConvergeSettings): Promise<void> => {
const axios = await getAxiosClient();
const request = axios.post("/api/me/setup", settings);
return (await request).data.result;
};
setSettings = async (settings: ConvergeSettings): Promise<void> => {
const axios = await this.authenticationService.getAxiosClient();
const request = axios.post("/api/v1.0/me/convergeSettings", settings);
return (await request).data.result;
};
export const getWorkgroup = async (): Promise<MicrosoftGraph.User[]> => {
const axios = await getAxiosClient();
const request = await axios.get<AutoWrapperResponse<MicrosoftGraph.User[]>>(
"/api/me/workgroup",
);
return request.data.result;
};
setupNewUser = async (settings: ConvergeSettings): Promise<void> => {
const axios = await this.authenticationService.getAxiosClient();
const request = axios.post("/api/v1.0/me/setup", settings);
return (await request).data.result;
};
export const getPeople = async (): Promise<MicrosoftGraph.User[]> => {
const axios = await getAxiosClient();
const request = await axios.get<AutoWrapperResponse<MicrosoftGraph.User[]>>(
"/api/me/people",
);
return request.data.result;
};
getWorkgroup = async (): Promise<MicrosoftGraph.User[]> => {
const axios = await this.authenticationService.getAxiosClient();
const request = await axios.get<AutoWrapperResponse<MicrosoftGraph.User[]>>(
"/api/v1.0/me/workgroup",
);
return request.data.result;
};
export const getMyList = async (): Promise<MicrosoftGraph.User[]> => {
const axios = await getAxiosClient();
const request = await axios.get<AutoWrapperResponse<MicrosoftGraph.User[]>>(
"/api/me/list",
);
return request.data.result;
};
getPeople = async (): Promise<MicrosoftGraph.User[]> => {
const axios = await this.authenticationService.getAxiosClient();
const request = await axios.get<AutoWrapperResponse<MicrosoftGraph.User[]>>(
"/api/v1.0/me/people",
);
return request.data.result;
};
export const updateMyPredictedLocation = async (
request: UserPredictedLocationRequest,
): Promise<void> => {
const axios = await getAxiosClient();
const response = await axios.put("/api/me/updatePredictedLocation", request);
return response.data.result;
};
getMyList = async (): Promise<MicrosoftGraph.User[]> => {
const axios = await this.authenticationService.getAxiosClient();
const request = await axios.get<AutoWrapperResponse<MicrosoftGraph.User[]>>(
"/api/v1.0/me/list",
);
return request.data.result;
};
export const getMyRecommendation = async (
year: number,
month: number,
day: number,
): Promise<string> => {
const axios = await getAxiosClient();
const request = await axios.get<AutoWrapperResponse<string>>(
"/api/me/recommendation",
{ params: { year, month, day } },
);
return request.data.result;
};
updateMyPredictedLocation = async (
request: UserPredictedLocationRequest,
): Promise<void> => {
const axios = await this.authenticationService.getAxiosClient();
const response = await axios.put("/api/v1.0/me/updatePredictedLocation", request);
return response.data.result;
};
export const getConvergeCalendar = async (
): Promise<MicrosoftGraph.Calendar | null> => {
const axios = await getAxiosClient();
const request = await axios.get<AutoWrapperResponse<MicrosoftGraph.Calendar>>("/api/me/convergeCalendar");
if (request.status === 204) {
return null;
}
return request.data.result;
};
getMyRecommendation = async (
year: number,
month: number,
day: number,
): Promise<string> => {
const axios = await this.authenticationService.getAxiosClient();
const request = await axios.get<AutoWrapperResponse<string>>(
"/api/v1.0/me/recommendation",
{ params: { year, month, day } },
);
return request.data.result;
};
export const getRecentBuildingsBasicDetails = async (
): Promise<BuildingBasicInfo[]> => {
const axios = await getAxiosClient();
const request = await axios.get<AutoWrapperResponse<BuildingBasicInfo[]>>("/api/me/recentBuildings");
return request.data.result;
};
getConvergeCalendar = async (
): Promise<MicrosoftGraph.Calendar | null> => {
const axios = await this.authenticationService.getAxiosClient();
const request = await axios.get<AutoWrapperResponse<MicrosoftGraph.Calendar>>("/api/v1.0/me/convergeCalendar");
if (request.status === 204) {
return null;
}
return request.data.result;
};
export const getFavoritePlaces = async (
): Promise<ExchangePlace[]> => {
const axios = await getAxiosClient();
const request = await axios.get<AutoWrapperResponse<ExchangePlace[]>>("/api/me/favoriteCampusesDetails");
return request.data.result;
};
getRecentBuildingsBasicDetails = async (
): Promise<BuildingBasicInfo[]> => {
const axios = await this.authenticationService.getAxiosClient();
const request = await axios.get<AutoWrapperResponse<BuildingBasicInfo[]>>("/api/v1.0/me/recentBuildings");
return request.data.result;
};
export default getSettings;
getFavoritePlaces = async (
): Promise<ExchangePlace[]> => {
const axios = await this.authenticationService.getAxiosClient();
const request = await axios.get<AutoWrapperResponse<ExchangePlace[]>>("/api/v1.0/me/favoriteCampusesDetails");
return request.data.result;
};
}

Просмотреть файл

@ -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<number> => {
const axios = await getAxiosClient();
const request = await axios.get<AutoWrapperResponse<number>>(`/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<boolean> => {
const axios = await getAxiosClient();
const request = await axios.get<AutoWrapperResponse<boolean>>(`/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<number> => {
const axios = await this.authenticationService.getAxiosClient();
const request = await axios.get<AutoWrapperResponse<number>>(`/api/v1.0/places/${id}/maxReserved`, {
params: {
start,
end,
},
});
return request.data.result;
};
getRoomAvailability = async (
id: string, start: string, end: string,
): Promise<boolean> => {
const axios = await this.authenticationService.getAxiosClient();
const request = await axios.get<AutoWrapperResponse<boolean>>(`/api/v1.0/places/${id}/availability`, {
params: {
start,
end,
},
});
return request.data.result;
};
}

Просмотреть файл

@ -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<RouteResponse> => {
const axios = await getAxiosClient();
const request = await axios.get<AutoWrapperResponse<RouteResponse>>(
"/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<RouteResponse> => {
const axios = await this.authenticationService.getAxiosClient();
const request = await axios.get<AutoWrapperResponse<RouteResponse>>(
"/api/v1.0/route/travelTime",
{ params: { start, end } },
);
return request.data.result;
};
}

Просмотреть файл

@ -8,58 +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 createCachedQuery, { CachedQuery } from "../utilities/CachedQuery";
import AuthenticationService from "./AuthenticationService";
import Constants from "../utilities/Constants";
export const searchCampusesToCollaborate = async (
campusesToCollaborateRequest: CampusesToCollaborateRequest,
): Promise<CampusesToCollaborateResponse> => {
const axios = await getAxiosClient();
const request = await axios.post<AutoWrapperResponse<CampusesToCollaborateResponse>>(
"/api/search/campusesToCollaborate",
campusesToCollaborateRequest, { params: {} },
);
return request.data.result;
};
export default class SearchService {
private authenticationService: AuthenticationService;
export const searchVenuesToCollaborate = async (
venuesToCollaborateRequest: VenuesToCollaborateRequest,
): Promise<VenuesToCollaborateResponse> => {
const axios = await getAxiosClient();
const request = await axios.post<AutoWrapperResponse<VenuesToCollaborateResponse>>("/api/search/venuesToCollaborate", venuesToCollaborateRequest);
return request.data.result;
};
constructor(authenticationService: AuthenticationService) {
this.authenticationService = authenticationService;
}
export const getVenueDetails = async (
venueId: string,
): Promise<VenueDetails> => {
const axios = await getAxiosClient();
const request = await axios.get<AutoWrapperResponse<VenueDetails>>(`/api/search/venues/${venueId}/details`);
return request.data.result;
};
searchCampusesToCollaborate = async (
campusesToCollaborateRequest: CampusesToCollaborateRequest,
): Promise<CampusesToCollaborateResponse> => {
const axios = await this.authenticationService.getAxiosClient();
const request = await axios.post<AutoWrapperResponse<CampusesToCollaborateResponse>>(
"/api/v1.0/search/campusesToCollaborate",
campusesToCollaborateRequest, { params: {} },
);
return request.data.result;
};
const generateVenueDetailsStoreKey = (
venue: VenueDetails,
) => venue.venueId;
searchVenuesToCollaborate = async (
venuesToCollaborateRequest: VenuesToCollaborateRequest,
): Promise<VenuesToCollaborateResponse> => {
const axios = await this.authenticationService.getAxiosClient();
const request = await axios.post<AutoWrapperResponse<VenuesToCollaborateResponse>>("/api/v1.0/search/venuesToCollaborate", venuesToCollaborateRequest);
return request.data.result;
};
const generateVenueDetailsRetrievalKey = (search: string) => search;
getVenueDetails = async (
venueId: string,
): Promise<VenueDetails> => {
const axios = await this.authenticationService.getAxiosClient();
const request = await axios.get<AutoWrapperResponse<VenueDetails>>(`/api/v1.0/search/venues/${venueId}/details`, {
cache: {
maxAge: Constants.TWO_HOURS_IN_MILLISECONDS,
},
});
return request.data.result;
};
type CachedVenueDetailsQuery = CachedQuery<VenueDetails, void>;
// Function that returns a cached getUserCoordinates function.
export function createCachedVenueDetailsQuery(): CachedVenueDetailsQuery {
return createCachedQuery<VenueDetails>(
generateVenueDetailsStoreKey,
generateVenueDetailsRetrievalKey,
(entities: string[]) => getVenueDetails(entities[0])
.then((item) => [item]),
);
getReviews = async (
venueId: string,
): Promise<VenueReviewsResponse> => {
const axios = await this.authenticationService.getAxiosClient();
const request = await axios.get<AutoWrapperResponse<VenueReviewsResponse>>(`/api/v1.0/search/venues/${venueId}/reviews`);
return request.data.result;
};
}
export const getReviews = async (
venueId: string,
): Promise<VenueReviewsResponse> => {
const axios = await getAxiosClient();
const request = await axios.get<AutoWrapperResponse<VenueReviewsResponse>>(`/api/search/venues/${venueId}/reviews`);
return request.data.result;
};

Просмотреть файл

@ -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<AppSettings> => {
const axios = await getAxiosClient();
const request = await axios.get<AutoWrapperResponse<AppSettings>>("/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<AppSettings> => {
const axios = await this.authenticationService.getAxiosClient();
const request = await axios.get<AutoWrapperResponse<AppSettings>>("/api/v1.0/settings/appSettings");
return request.data.result;
};
}

Просмотреть файл

@ -6,183 +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<MicrosoftGraph.User> => {
const axios = await getAxiosClient();
const response = await axios.get<AutoWrapperResponse<MicrosoftGraph.User>>(`/api/users/${userPrincipalName}`);
return response.data.result;
};
export default getCollaborator;
interface UserPhotoResult {
id: string;
userPhoto: string | null;
}
const getUserPhoto = async (id: string): Promise<UserPhotoResult> => {
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<UserPhotoResult>;
// Function that returns a cached getUserCoordinates function.
export function createUserPhotoService(): CachedUserPhotoQuery {
return createCachedQuery<UserPhotoResult>(
generateUserPhotoStoreKey,
generateUserPhotoRetrievalKey,
(ids: string[]) => getUserPhoto(ids[0])
.then((result) => [result]),
);
}
export const getUserProfile = async (id?: string): Promise<UserProfile> => {
const axios = await getAxiosClient();
const request = await axios.get<AutoWrapperResponse<UserProfile>>(`/api/users/${id}/userProfile`, {
headers: { Authorization: axios.defaults.headers.common.Authorization },
});
return request.data.result;
};
export const getPresence = async (
id: string,
): Promise<MicrosoftGraph.Presence> => {
const axios = await getAxiosClient();
const request = await axios.get<AutoWrapperResponse<MicrosoftGraph.Presence>>(
`/api/users/${id}/presence`,
);
return request.data.result;
};
export const getLocation = async (
id: string,
year: number,
month: number,
day: number,
): Promise<string> => {
const axios = await getAxiosClient();
const request = await axios.get<AutoWrapperResponse<string>>(`/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<MultiUserAvailableTimesResponse> => {
const axios = await getAxiosClient();
const request = await axios.post<AutoWrapperResponse<MultiUserAvailableTimesResponse>>("/api/users/multi/availableTimes", {
year, month, day, usersUpnList: userPrincipalNames, scheduleFrom, scheduleTo,
});
return request.data.result;
};
export const searchUsers = async (
searchString?: string,
): Promise<MicrosoftGraph.User[]> => {
const axios = await getAxiosClient();
if (!searchString) {
return [];
}
const request = await axios.get<AutoWrapperResponse<MicrosoftGraph.User[]>>(`/api/users/search/${searchString}`);
return request.data.result;
};
export const searchUsersByPage = async (
searchQuery?: string,
options?: QueryOption[],
): Promise<UserSearchPagedResponse> => {
const axios = await getAxiosClient();
if (!searchQuery) {
return { users: [], queryOptions: [] };
}
const request = await axios.get<AutoWrapperResponse<UserSearchPagedResponse>>("/api/users/searchAndPage", {
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<UserCoordianates[]> => {
const axios = await getAxiosClient();
const request = await axios.post<AutoWrapperResponse<UserCoordinatesResponse>>("/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<UserCoordianates, UserCoordinateQueryParams>;
// Function that returns a cached getUserCoordinates function.
export function createUserCoordinateService(): CachedUserCoordinateQuery {
return createCachedQuery<UserCoordianates, UserCoordinateQueryParams>(
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<MicrosoftGraph.User> => {
const axios = await this.authenticationService.getAxiosClient();
const response = await axios.get<AutoWrapperResponse<MicrosoftGraph.User>>(`/api/v1.0/users/${userPrincipalName}`);
return response.data.result;
};
getUserPhoto = async (id: string): Promise<UserPhotoResult> => {
const axios = await this.authenticationService.getAxiosClient();
const request = await fetch(`/api/v1.0/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<UserPhotoResult>(
this.generateUserPhotoStoreKey,
this.generateUserPhotoRetrievalKey,
(ids: string[]) => this.getUserPhoto(ids[0])
.then((result) => [result]),
)
getUserProfile = async (id?: string): Promise<UserProfile> => {
const axios = await this.authenticationService.getAxiosClient();
const request = await axios.get<AutoWrapperResponse<UserProfile>>(`/api/v1.0/users/${id}/userProfile`, {
headers: { Authorization: axios.defaults.headers.common.Authorization },
});
return request.data.result;
};
getPresence = async (
id: string,
): Promise<MicrosoftGraph.Presence> => {
const axios = await this.authenticationService.getAxiosClient();
const request = await axios.get<AutoWrapperResponse<MicrosoftGraph.Presence>>(
`/api/v1.0/users/${id}/presence`,
);
return request.data.result;
};
getLocation = async (
id: string,
year: number,
month: number,
day: number,
): Promise<string> => {
const axios = await this.authenticationService.getAxiosClient();
const request = await axios.get<AutoWrapperResponse<string>>(`/api/v1.0/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<MultiUserAvailableTimesResponse> => {
const axios = await this.authenticationService.getAxiosClient();
const request = await axios.post<AutoWrapperResponse<MultiUserAvailableTimesResponse>>("/api/v1.0/users/multi/availableTimes", {
year, month, day, usersUpnList: userPrincipalNames, scheduleFrom, scheduleTo,
});
return request.data.result;
};
searchUsers = async (
searchQuery?: string,
options?: QueryOption[],
): Promise<UserSearchPagedResponse> => {
const axios = await this.authenticationService.getAxiosClient();
if (!searchQuery) {
return { users: [], queryOptions: [] };
}
const request = await axios.get<AutoWrapperResponse<UserSearchPagedResponse>>("/api/v1.0/users/search", {
params: {
searchString: searchQuery,
QueryOptions: JSON.stringify(options),
},
});
return request.data.result;
};
getUserCoordinates = async (
users: string[],
{
year,
month,
day,
}: UserCoordinateQueryParams,
): Promise<UserCoordianates[]> => {
const axios = await this.authenticationService.getAxiosClient();
const request = await axios.post<AutoWrapperResponse<UserCoordinatesResponse>>("/api/v1.0/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<UserCoordianates, UserCoordinateQueryParams>(
this.generateUserCoordinateStoreKey,
this.generateUserCoordinateRetrievalKey,
this.getUserCoordinates,
)
}

Просмотреть файл

@ -0,0 +1,8 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
const flags = {
vipOnly: true,
adminOnly: true,
};
export default flags;

Просмотреть файл

@ -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 {
@ -21,13 +22,15 @@ interface IUseBuildingWorkspacesHookReturnType {
placeType: PlaceType,
itemsPerPage: number,
filterOptions: BuildingPlacesFilterOptions,
clearList?: boolean
skipTokens?:string|undefined|null,
clearList?: boolean,
) => Promise<IExchangePlacesResponse>,
clearList: () => void,
hasMore: boolean
}
function useBuildingPlaces(
buildingService: BuildingService,
buildingUpn?: string,
): IUseBuildingWorkspacesHookReturnType {
const [
@ -43,18 +46,19 @@ function useBuildingPlaces(
placeType: PlaceType,
itemsPerPage: number,
filterOptions?: BuildingPlacesFilterOptions,
skipTokens?:string|undefined|null,
clearList?: boolean,
) => {
if (clearList) {
setPlaces([]);
}
if (buildingUpn && placeType !== undefined) {
const result = getBuildingPlaces(
const result = buildingService.getBuildingPlaces(
buildingUpn,
placeType,
{
topCount: itemsPerPage,
skipToken: placesResult?.skipToken,
skipToken: skipTokens,
...filterOptions,
},
);
@ -62,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);
};
@ -71,7 +75,7 @@ function useBuildingPlaces(
setPlaces([]);
waitFor(Promise.resolve({
exchangePlacesList: [],
skipToken: null,
skipToken: "",
}));
};

Просмотреть файл

@ -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<BuildingService>(new BuildingService(authenticationService));
const [calendarService] = useState<CalendarService>(new CalendarService(authenticationService));
const [meService] = useState<MeService>(new MeService(authenticationService));
const [placeService] = useState<PlaceService>(new PlaceService(authenticationService));
const [routeService] = useState<RouteService>(new RouteService(authenticationService));
const [searchService] = useState<SearchService>(new SearchService(authenticationService));
const [settingsService] = useState<SettingsService>(new SettingsService(authenticationService));
const [userService] = useState<UserService>(new UserService(authenticationService));
return (
<Context.Provider
value={{
buildingService,
calendarService,
meService,
placeService,
routeService,
searchService,
settingsService,
userService,
}}
>
{children}
</Context.Provider>
);
};
const useApiProvider = (): ApiModel => useContext(Context);
export { ApiProvider, useApiProvider };

Просмотреть файл

@ -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<Settings>(undefined, true);
const getSettings = () => waitFor(getAppSettings());
const getSettings = () => waitFor(settingsService.getAppSettings());
useEffect(() => {
getSettings();

Просмотреть файл

@ -1,26 +1,27 @@
// 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,
} 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";
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";
import { useApiProvider } from "./ApiProvider";
type IBuildingState = {
buildingsList: BuildingBasicInfo[];
buildingListLoading: boolean;
clickBuildingListLoading:boolean;
buildingsByRadiusDistance: number,
loadMoreBuildingsByDistance: boolean,
buildingsListError: boolean;
@ -41,6 +42,7 @@ interface IConvergeContext {
loadBuildingsByDistance: (geoCoordinates: GeoCoordinates) => void;
loadBuildingsByName: () => void;
setBuildingListLoading: (currentState: boolean) => void;
setClickBuildingListLoading: (currentState: boolean) => void;
setBuildingsByDistanceRadius: (upcomingReservationDistance: number) => void;
setBuildingsListError: (currentState: boolean) => void;
updateSearchString: (searchString?: string) => void;
@ -48,7 +50,7 @@ interface IConvergeContext {
searchString?: string,
presetBuildings?: string[],
) => void;
updateRecentBuildings: (recentBuildings: BuildingBasicInfo[]) => void;
getRecentBuildings: () => Promise<void>;
}
const ConvergeSettingsContext = React.createContext<IConvergeContext>(
@ -114,6 +116,7 @@ function convergeSettingsReducer(
const UPDATE_BUILDINGS_LIST = "UPDATE_BUILDINGS_LIST";
const UPDATE_BUILDINGS_LIST_LOADING = "UPDATE_BUILDINGS_LIST_LOADING";
const CLICK_UPDATE_BUILDINGS_LIST_LOADING = "CLICK_UPDATE_BUILDINGS_LIST_LOADING";
const SET_BUILDINGS_DISTANCE = "SET_BUILDINGS_DISTANCE";
const LOAD_MORE_BUILDINGS = "LOAD_MORE_BUILDINGS";
const UPDATE_BUILDINGS_LIST_ERROR = "UPDATE_BUILDINGS_LIST_ERROR";
@ -132,6 +135,11 @@ interface UpdateBuildingsListLoadingAction {
payload: boolean;
}
interface ClickUpdateBuildingsListLoadingAction {
type: typeof CLICK_UPDATE_BUILDINGS_LIST_LOADING,
payload: boolean;
}
interface SetUpcomingBuildingDistanceAction {
type: typeof SET_BUILDINGS_DISTANCE,
payload: number,
@ -169,6 +177,7 @@ interface UpdateRecentBuildingsAction {
type IBuildingAction = loadBuildingsByDistanceAction
| UpdateBuildingsListLoadingAction
| ClickUpdateBuildingsListLoadingAction
| SetUpcomingBuildingDistanceAction
| LoadMoreBuildingsAction
| UpdateBuildingsListErrorAction
@ -194,6 +203,7 @@ type IPlaceAction = GetFavoriteCampusesRequestAction | GetFavoriteCampusesRespon
const iState: IBuildingState = {
buildingsList: [],
buildingListLoading: false,
clickBuildingListLoading: false,
buildingsByRadiusDistance: 10,
loadMoreBuildingsByDistance: true,
buildingsListError: false,
@ -227,6 +237,16 @@ const reducer = (state: IBuildingState, action: IBuildingAction): IBuildingState
};
}
case CLICK_UPDATE_BUILDINGS_LIST_LOADING: {
const newState = {
...state,
clickBuildingListLoading: action.payload,
};
return {
...newState,
};
}
case SET_BUILDINGS_DISTANCE: {
const newState = {
...state,
@ -313,6 +333,10 @@ function favoriteCampusesReducer(state: ExchangePlace[], action: IPlaceAction):
}
const ConvergeSettingsProvider: React.FC = ({ children }) => {
const {
buildingService,
meService,
} = useApiProvider();
const [convergeSettings, convergeSettingsDispatch] = useReducer<
ConvergeSettings | null, ConvergeSettingsAction>(
convergeSettingsReducer,
@ -328,50 +352,32 @@ const ConvergeSettingsProvider: React.FC = ({ children }) => {
favoriteCampusesReducer, [],
);
const { errorDispatch } = errorAlertProvider();
const getConvergeSettings = (): Promise<void> => {
convergeSettingsDispatch({ type: GET_CONVERGE_SETTINGS_REQUEST });
return getSettings()
return meService.getSettings()
.then((settings) => {
convergeSettingsDispatch(
{ type: GET_CONVERGE_SETTINGS_RESPONSE, convergeSettings: settings },
);
})
.catch((error) => {
errorDispatch({
type: "SET_ERROR_ALERT",
payload: helpers.getDefaultToastObject(error.message, "getSettings"),
});
});
};
const setConvergeSettings = (settings: ConvergeSettings): Promise<void> => setSettings(settings)
const setConvergeSettings = (settings: ConvergeSettings): Promise<void> => meService
.setSettings(settings)
.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<void> => setupNewUser(settings)
const setupNewUserWrapper = (settings: ConvergeSettings): Promise<void> => meService
.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<void> => {
favoriteCampusesDispatch({ type: GET_FAVORITE_CAMPUSES_REQUEST });
return getFavoritePlaces()
return meService.getFavoritePlaces()
.then((favs) => {
favoriteCampusesDispatch({ type: GET_FAVORITE_CAMPUSES_RESPONSE, favoriteCampuses: favs });
});
@ -384,6 +390,13 @@ const ConvergeSettingsProvider: React.FC = ({ children }) => {
});
};
const setClickBuildingListLoading = (isLoading: boolean) => {
dispatch({
type: CLICK_UPDATE_BUILDINGS_LIST_LOADING,
payload: isLoading,
});
};
const setBuildingsListError = (isError: boolean) => {
dispatch({
type: UPDATE_BUILDINGS_LIST_ERROR,
@ -401,8 +414,9 @@ const ConvergeSettingsProvider: React.FC = ({ children }) => {
const loadBuildingsByDistance = (geoCoordinates: GeoCoordinates) => {
setBuildingListLoading(true);
setClickBuildingListLoading(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);
@ -410,17 +424,21 @@ const ConvergeSettingsProvider: React.FC = ({ children }) => {
setBuildingsByDistanceRadius(state.buildingsByRadiusDistance + 1000);
}
dispatch({ type: UPDATE_BUILDINGS_LIST, payload: response });
if (response.buildingsList.length !== 0) {
setBuildingListLoading(false);
setClickBuildingListLoading(false);
setBuildingsLoadingMessage(undefined);
}
})
.finally(() => {
.catch(() => {
setBuildingListLoading(false);
setBuildingsLoadingMessage(undefined);
})
.catch(() => setBuildingsListError(true));
setBuildingsListError(true);
});
};
const loadBuildingsByName = () => {
setBuildingListLoading(true);
getBuildingsByName()
buildingService.getBuildingsByName()
.then((response) => {
dispatch({ type: UPDATE_BUILDINGS_LIST, payload: response });
})
@ -428,13 +446,26 @@ const ConvergeSettingsProvider: React.FC = ({ children }) => {
.catch(() => setBuildingsListError(true));
};
const loadMoreBuildingsByDistance = (geoCoordinates: GeoCoordinates, distance: number) => {
setBuildingListLoading(true);
getBuildingsByDistance(`${geoCoordinates.latitude},${geoCoordinates.longitude}`, distance)
const loadMoreBuildingsByDistance = (geoCoordinates: GeoCoordinates, distance: number,
buildingLoading:boolean) => {
if (buildingLoading === true) {
setClickBuildingListLoading(true);
} else {
setBuildingListLoading(true);
}
buildingService.getBuildingsByDistance(`${geoCoordinates.latitude},${geoCoordinates.longitude}`, distance)
.then((response) => {
if (response.buildingsList.length === 0 && state.buildingsByRadiusDistance < 1000) {
setBuildingsByDistanceRadius(state.buildingsByRadiusDistance * 10);
} else if (response.buildingsList.length === 0 && state.buildingsByRadiusDistance < 4000) {
setBuildingsByDistanceRadius(state.buildingsByRadiusDistance + 1000);
}
dispatch({ type: UPDATE_BUILDINGS_LIST, payload: response });
if (response.buildingsList.length !== 0) {
setBuildingListLoading(false);
setClickBuildingListLoading(false);
}
})
.finally(() => setBuildingListLoading(false))
.catch(() => setBuildingsListError(true));
};
@ -442,7 +473,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,
@ -467,28 +498,31 @@ 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);
if (state.clickBuildingListLoading === true) {
loadMoreBuildingsByDistance(convergeSettings.geoCoordinates,
state.buildingsByRadiusDistance, true);
} else {
loadMoreBuildingsByDistance(convergeSettings.geoCoordinates,
state.buildingsByRadiusDistance, false);
}
}
}, [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);
});
}, []);
useEffect(() => {
getRecentBuildingsBasicDetails().then((basicRecentBuildings) => {
updateRecentBuildings(basicRecentBuildings);
});
}, [convergeSettings?.recentBuildingUpns]);
return (
<ConvergeSettingsContext.Provider
value={{
@ -500,16 +534,59 @@ const ConvergeSettingsProvider: React.FC = ({ children }) => {
favoriteCampuses,
getFavoriteCampuses: getFavoriteCampusesWrapper,
setBuildingListLoading,
setClickBuildingListLoading,
loadBuildingsByDistance,
loadBuildingsByName,
setBuildingsByDistanceRadius,
setBuildingsListError,
searchMoreBuildings,
updateSearchString,
updateRecentBuildings,
getRecentBuildings,
}}
>
{loading ? <Loader /> : children}
{loading && <Loader />}
{!loading && isError && (
<Alert
danger
styles={{ margin: "36px" }}
content={(
<Flex hAlign="center">
<Text
content="The application was unable to load."
styles={{
minWidth: "0px !important",
paddingTop: "0.4rem",
}}
/>
<Button
content={(
<Text
content="Try again"
styles={{
minWidth: "0px !important",
paddingTop: "0.4rem",
textAlign: "center",
}}
/>
)}
text
onClick={() => {
logEvent(USER_INTERACTION, [
{ name: UI_SECTION, value: UISections.ApplicationUnavailable },
{ name: DESCRIPTION, value: "refreshHomePage" },
]);
window.location.reload();
}}
color="red"
styles={{
minWidth: "0px !important", paddingTop: "0.2rem", textDecoration: "UnderLine", color: "rgb(196, 49, 75)",
}}
/>
</Flex>
)}
/>
)}
{!loading && !isError && children}
</ConvergeSettingsContext.Provider>
);
};

Просмотреть файл

@ -1,77 +0,0 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
import React, { useReducer } from "react";
type IErrorMessageState = {
message: string;
id: string;
};
type IToastAction =
{type: "SET_ERROR_ALERT"; payload: IErrorMessageState}
| {type: "HIDE_ALERT"; payload: IErrorMessageState}
type IErrorState = {
messages: IErrorMessageState[]
};
const emptyState: IErrorState = {
messages: [],
};
const actions = {
SET_ERROR_ALERT: "SET_ERROR_ALERT",
HIDE_ALERT: "HIDE_ALERT",
};
type IContextModel = {
errorState: IErrorState;
errorDispatch: React.Dispatch<IToastAction>;
};
const Context = React.createContext({} as IContextModel);
const reducer = (errorState: IErrorState, action: IToastAction) => {
let updatedState = { ...errorState };
switch (action.type) {
case actions.SET_ERROR_ALERT:
updatedState = {
messages: [...updatedState.messages, action.payload],
};
break;
case actions.HIDE_ALERT:
updatedState = {
...updatedState,
messages: updatedState.messages.filter((item) => item.id !== action.payload.id),
};
break;
default:
break;
}
return { ...updatedState };
};
const ErrorAlertProvider: React.FC = ({ children }) => {
const [errorState, errorDispatch] = useReducer(reducer, emptyState);
return (
<Context.Provider value={{ errorState, errorDispatch }}>
{children}
</Context.Provider>
);
};
const useProvider = (): IContextModel => React.useContext(Context);
const helpers = {
getDefaultToastObject: (message: string, id: string): IErrorMessageState => ({
message,
id,
}),
};
export {
ErrorAlertProvider, useProvider, helpers,
};

Просмотреть файл

@ -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,

Просмотреть файл

@ -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,

Просмотреть файл

@ -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 };

Просмотреть файл

@ -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";
@ -31,7 +31,6 @@ const SET_VENUE_SKIP = "SET_VENUE_SKIP";
const SEARCH_PLACES_LOAD_MORE_RESPONSE = "SEARCH_PLACES_LOAD_MORE_RESPONSE";
const SEARCH_PLACES_LOAD_MORE_REQUEST = "SEARCH_PLACES_LOAD_MORE_REQUEST";
const SET_CAMPUS_SEARCH_RANGE = "SET_CAMPUS_SEARCH_RANGE";
const SET_CAMPUS_SEARCH_WAITING = "SET_CAMPUS_SEARCH_WAITING";
interface SetStartTimeAction {
type: typeof SET_START_TIME;
@ -114,11 +113,6 @@ interface SetCampusSearchRangeAction {
payload: number;
}
interface SetCampusSearchWaitingAction {
type: typeof SET_CAMPUS_SEARCH_WAITING;
payload: boolean;
}
type ISearchAction =
SetStartTimeAction
| SetEndTimeAction
@ -136,8 +130,7 @@ type ISearchAction =
| SetVenueSkipAction
| LoadMorePlacesResponseAction
| LoadMorePlacesRequestAction
| SetCampusSearchRangeAction
| SetCampusSearchWaitingAction;
| SetCampusSearchRangeAction;
interface ISearchState {
placesToCollaborate: (CampusToCollaborate|VenueToCollaborate)[];
@ -154,7 +147,6 @@ interface ISearchState {
venueSkip: number;
loadMorePlacesLoading: boolean;
campusSearchRangeInMiles: number;
campusSearchWaiting: boolean;
}
interface ISearchProviderModel {
@ -164,15 +156,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;
setCampusSearchWaiting: (waitState: boolean) => void;
setCampusSearchRange: (range: number) => void;
}
const iState: ISearchState = {
@ -190,7 +181,6 @@ const iState: ISearchState = {
venueSkip: 0,
loadMorePlacesLoading: false,
campusSearchRangeInMiles: 10,
campusSearchWaiting: false,
};
const Context = createContext({} as ISearchProviderModel);
@ -268,6 +258,7 @@ const reducer = (state: ISearchState, action: ISearchAction): ISearchState => {
placesToCollaborate: [],
venueType: CollaborationVenueType.Workspace,
placesLoading: false,
campusSearchRangeInMiles: 10,
};
case SET_VENUE_SKIP:
return {
@ -291,24 +282,43 @@ const reducer = (state: ISearchState, action: ISearchAction): ISearchState => {
...state,
campusSearchRangeInMiles: action.payload,
};
case SET_CAMPUS_SEARCH_WAITING:
return {
...state,
campusSearchWaiting: action.payload,
};
default:
return state;
}
};
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(
reducer,
{ ...iState },
);
const setPlacesLoading = (placesLoading: boolean) => {
dispatch({ type: SET_PLACES_LOADING, payload: placesLoading });
};
const searchPlaces = (
searchState: ISearchState,
specificRange?: number,
): Promise<CampusesToCollaborateResponse | VenuesToCollaborateResponse> => {
const userList: string[] = [];
userList.push(searchState.loginUser);
@ -329,11 +339,11 @@ 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 searchCampusesToCollaborate(request);
return searchService.searchCampusesToCollaborate(request);
}
return searchVenuesToCollaborate({
return searchService.searchVenuesToCollaborate({
teamMembers: userList,
venueType: getCollaborationVenueTypeString(searchState.venueType as CollaborationVenueType),
endTime: searchState.endTime.utc().toDate(),
@ -345,53 +355,31 @@ const SearchContextProvider: React.FC = ({ children }) => {
});
};
const setCampusSearchWaiting = (waitState: boolean) => {
const setCampusSearchRange = (range: number) => {
dispatch({
type: SET_CAMPUS_SEARCH_WAITING,
payload: waitState,
type: SET_CAMPUS_SEARCH_RANGE,
payload: range,
});
};
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 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 ((response as CampusesToCollaborateResponse)
?.campusesToCollaborateList
?.length === 0
) {
if (setCampusSearchNextRange()) {
searchPlacesToCollaborate();
const currentRange = specificRange || state.campusSearchRangeInMiles;
if (currentRange < 4000) {
const nextRange = getCampusSearchNextRange(currentRange);
setCampusSearchRange(nextRange);
searchPlacesToCollaborate(true, nextRange);
} else {
const payload = (response as CampusesToCollaborateResponse).campusesToCollaborateList
|| (response as VenuesToCollaborateResponse).venuesToCollaborateList;
@ -404,7 +392,7 @@ const SearchContextProvider: React.FC = ({ children }) => {
}
})
.catch(() => dispatch({ type: SEARCH_PLACES_ERROR }))
.finally(() => setCampusSearchWaiting(false));
.finally(() => setPlacesLoading(false));
}
};
@ -432,10 +420,6 @@ const SearchContextProvider: React.FC = ({ children }) => {
dispatch({ type: SET_MAP_PLACES, payload: mapPlaces });
};
const setPlacesLoading = (placesLoading: boolean) => {
dispatch({ type: SET_PLACES_LOADING, payload: placesLoading });
};
const clearPlaceSearch = () => {
dispatch({ type: CLEAR_PLACES_SEARCH });
};
@ -499,8 +483,7 @@ const SearchContextProvider: React.FC = ({ children }) => {
clearPlaceSearch,
setStartAndEndTime,
setVenueSkip,
setCampusSearchNextRange,
setCampusSearchWaiting,
setCampusSearchRange,
}}
>
{children}

Просмотреть файл

@ -3,21 +3,19 @@
import React, { createContext, useContext, useReducer } from "react";
import * as MicrosoftGraph from "@microsoft/microsoft-graph-types";
import {
getMyList, getPeople, getWorkgroup,
} from "../api/meService";
import { searchUsers, searchUsersByPage } 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 {
MyList = "My List",
Suggested = "Suggested",
MyOrganization = "My Organization",
MyList = "My List",
All = "All",
}
@ -27,6 +25,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 +54,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 +104,11 @@ interface SetMoreTeammateLoadingAction {
payload: boolean,
}
interface SetTeammatesDropdownAction {
type: typeof SET_TEAMMATES_DROPDOWN,
payload: TeammateList[],
}
type ITeammateAction = UpdateTeammateLocationAction
| GetTeammatesRequestAction
| GetTeammatesResponseAction
@ -95,7 +118,8 @@ type ITeammateAction = UpdateTeammateLocationAction
| UpdateSearchString
| UpdateSearchQueryOptions
| SetTeammateLocationAction
| SetMoreTeammateLoadingAction;
| SetMoreTeammateLoadingAction
| SetTeammatesDropdownAction;
type ITeammateState = {
list: TeammateList;
@ -108,15 +132,16 @@ 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;
getTeammates: (list: TeammateList, searchString?: string) => void;
updateSearchString: (list: TeammateList, searchString?: string) => void;
updateSearchQueryOptions:(searchQueryOptions?: QueryOption[]) => void;
searchMoreTeammates:(
searchString?: string,
@ -125,17 +150,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 +275,57 @@ 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 { meService, userService } = useApiProvider();
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,34 +333,57 @@ 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) => {
const getTeammates = (list: TeammateList, searchString?: string) => {
dispatch({ type: TEAMMATES_REQUEST });
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;
break;
case TeammateList.All:
requestMethod = searchUsers;
requestMethod = meService.getWorkgroup;
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) => {
if (searchString !== undefined && searchString.length > 0) {
const payload = teammates.filter((x) => {
if (x.displayName) {
return x.displayName.toLowerCase().indexOf(
searchString.toLowerCase(),
) > -1;
}
return false;
}).map((teammate) => ({
user: teammate,
}));
dispatch({ type: TEAMMATES_RESPONSE, payload });
} else {
const payload = teammates.map((teammate) => ({
user: teammate,
}));
dispatch({ type: TEAMMATES_RESPONSE, payload });
}
})
.catch(() => dispatch({ type: TEAMMATES_ERROR }));
} else {
userService.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) => {
@ -312,43 +395,48 @@ const TeammateFilterProvider: React.FC = ({ children }) => {
qOptions?: QueryOption[],
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);
})
.catch(() => {
dispatch({ type: TEAMMATES_ERROR });
setMoreTeammatesLoading(false);
});
if (searchString?.length) {
setMoreTeammatesLoading(true);
userService.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);
});
} else {
dispatch({ type: TEAMMATES_RESPONSE, payload: [] });
}
};
const updateSearchString = (searchString?: string) => {
const updateSearchString = (list: TeammateList, searchString?: string) => {
dispatch({ type: UPDATE_SEARCH_STRING, payload: searchString });
if (state.list === TeammateList.All) {
if (list === TeammateList.All) {
const qOptions: QueryOption[] | undefined = undefined;
const resetTeam: Teammate[] | undefined = undefined;
searchMoreTeammates(searchString, qOptions, resetTeam);
} else {
getTeammates(list, searchString);
}
};
const updateList = (list: TeammateList) => {
dispatch({ type: UPDATE_LIST, payload: list });
if (list !== TeammateList.All) {
getTeammates(list, state.date, state.searchString);
} else {
updateSearchString(state.searchString);
}
updateSearchString(list, state.searchString);
};
const updateDate = (date: Date) => {
dispatch({ type: UPDATE_DATE, payload: date });
getTeammates(state.list, date, state.searchString);
};
const updateSearchQueryOptions = (queryOptions?: QueryOption[]) => {
@ -359,6 +447,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 (
<Context.Provider value={{
state,
@ -371,6 +463,7 @@ const TeammateFilterProvider: React.FC = ({ children }) => {
searchMoreTeammates,
setTeammateLocation,
setMoreTeammatesLoading,
setTeammatesDropdown,
}}
>
{children}

Просмотреть файл

@ -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";
@ -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 { usePlacePhotos } from "../../../providers/PlacePhotosProvider";
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> = (props) => {
const { meService, buildingService } = useApiProvider();
const {
convergeSettings,
setConvergeSettings,
@ -39,15 +40,12 @@ const CampusPlacePanel: React.FC<Props> = (props) => {
} = props;
const classes = CampusPlacePanelStyles();
const [,
placePhotos = [],,
getPlacePhotos,
] = usePlacePhotos();
const [placePhotos, setPlacePhotos] = useState<PlacePhotosResult | undefined>(undefined);
const images = useMemo<string[]>(() => {
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 +57,8 @@ const CampusPlacePanel: React.FC<Props> = (props) => {
useEffect(() => {
if (place.sharePointID) {
getPlacePhotos([place.sharePointID]);
buildingService.getPlacePhotos(place.sharePointID)
.then(setPlacePhotos);
}
}, [place.sharePointID]);
@ -126,7 +125,7 @@ const CampusPlacePanel: React.FC<Props> = (props) => {
favoriteCampusesToCollaborate,
};
setConvergeSettings(newSettings);
setSettings(newSettings)
meService.setSettings(newSettings)
.then(() => {
if (!isFavorite) {
logEvent(USER_INTERACTION, [

Просмотреть файл

@ -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> = (props) => {
const { calendarService, meService } = useApiProvider();
const classes = CollaborationPlaceDetailsStyles();
const {
isOpen,
@ -199,7 +199,7 @@ const CollaborationPlaceDetails: React.FC<Props> = (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> = (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(),

Просмотреть файл

@ -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,19 +88,26 @@ const CollaborationPlaceResults: React.FC<Props> = (props) => {
) && (
<Flex hAlign="center" vAlign="center" style={{ marginTop: "8px" }}>
{state.loadMorePlacesLoading
? (<Loader />)
: (
<Button
content="Show more"
onClick={() => {
loadMoreResults();
logEvent(USER_INTERACTION, [
{ name: UI_SECTION, value: UISections.CollaborateResults },
{ name: DESCRIPTION, value: "loadMoreResults" },
]);
}}
/>
)}
&& (<Loader />)}
{state.venueSkip < 1000 && !state.loadMorePlacesLoading && (
<Button
content="Show more"
onClick={() => {
loadMoreResults();
logEvent(USER_INTERACTION, [
{ name: UI_SECTION, value: UISections.CollaborateResults },
{ name: DESCRIPTION, value: "loadMoreResults" },
]);
}}
/>
)}
{state.venueSkip > 1000 && !state.loadMorePlacesLoading && (
<Text
className={classes.textNoMore}
>
No more results
</Text>
)}
</Flex>
)}

Просмотреть файл

@ -15,25 +15,31 @@ 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 {
places: (CampusToCollaborate | VenueToCollaborate)[];
openPanel: () => void;
setSelectedPlace: (selectedPlace: CampusToCollaborate | VenueToCollaborate) => void;
forceVenueShowMore?: boolean;
recommendationSearchRadius?: number;
moreRecommendationsfetcher?: () => void;
}
const CollaborationPlaceResultsPaged: React.FC<Props> = (props) => {
const {
places, openPanel, setSelectedPlace,
forceVenueShowMore,
recommendationSearchRadius,
moreRecommendationsfetcher,
} = props;
const classes = CollaborationPlaceResultsPagedStyles();
const {
state,
setVenueSkip,
setCampusSearchNextRange,
setCampusSearchWaiting,
setCampusSearchRange,
setPlacesLoading,
searchPlacesToCollaborate,
} = useSearchContextProvider();
@ -42,9 +48,14 @@ const CollaborationPlaceResultsPaged: React.FC<Props> = (props) => {
};
const loadFartherPlaces = () => {
setCampusSearchWaiting(true);
setCampusSearchNextRange();
searchPlacesToCollaborate();
if (moreRecommendationsfetcher === undefined) {
setPlacesLoading(true);
const increasedSearchRange = getCampusSearchNextRange(state.campusSearchRangeInMiles);
setCampusSearchRange(increasedSearchRange);
searchPlacesToCollaborate(true, increasedSearchRange);
} else {
moreRecommendationsfetcher();
}
};
return (
@ -92,10 +103,13 @@ const CollaborationPlaceResultsPaged: React.FC<Props> = (props) => {
state.venueType === CollaborationVenueType.FoodAndDrink
|| state.venueType === CollaborationVenueType.ParksAndRecreation
) && (
<Flex hAlign="center" vAlign="center" style={{ marginTop: "8px" }}>
{state.loadMorePlacesLoading
? (<Loader />)
: (
<>
<Flex hAlign="center" vAlign="center" style={{ marginTop: "8px" }}>
{state.loadMorePlacesLoading === true
&& (<Loader />)}
</Flex>
<Flex hAlign="center" vAlign="center" style={{ marginTop: "8px" }}>
{state.venueSkip < 1000 && !state.loadMorePlacesLoading && (
<Button
content="Show more"
onClick={() => {
@ -107,15 +121,24 @@ const CollaborationPlaceResultsPaged: React.FC<Props> = (props) => {
}}
/>
)}
</Flex>
{state.venueSkip > 1000 && !state.loadMorePlacesLoading && (
<Text
className={classes.textNoMore}
>
No more results
</Text>
)}
</Flex>
</>
)}
</Box>
<Box className={classes.loadBtnContainer}>
{(
(state.venueType === CollaborationVenueType.Workspace
(forceVenueShowMore
|| state.venueType === CollaborationVenueType.Workspace
|| state.venueType === CollaborationVenueType.ConferenceRoom)
) && (
state.campusSearchRangeInMiles < 4000 ? (
(recommendationSearchRadius ?? state.campusSearchRangeInMiles) < 4000 ? (
<Button
onClick={() => {
loadFartherPlaces();
@ -125,8 +148,8 @@ const CollaborationPlaceResultsPaged: React.FC<Props> = (props) => {
]);
}}
className={classes.showMoreBtn}
disabled={state.campusSearchWaiting}
loading={state.campusSearchWaiting}
disabled={state.placesLoading}
loading={state.placesLoading}
content="Show more"
/>
)

Просмотреть файл

@ -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 { createCachedVenueDetailsQuery } from "../../../api/searchService";
import VenueDetails from "../../../types/VenueDetails";
import CollaborationPlaceResults from "./CollaborationPlaceResults";
import CollaborationPlaceDetails from "./CollaborationPlaceDetails";
import { createCachedPlaceDetailsQuery } 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;
@ -50,10 +49,12 @@ function createVenueToCollaborate(v: VenueDetails): VenueToCollaborate {
transactions: v.transactions,
};
}
const { getItems: getPlaceDetails } = createCachedPlaceDetailsQuery();
const { getItems: getVenueDetails } = createCachedVenueDetailsQuery();
const FavoritesToCollaborate: React.FC<Props> = (props) => {
const {
searchService,
buildingService,
} = useApiProvider();
const classes = FavoritesToCollaborateStyles();
const [open, setOpen] = useState<boolean>(false);
const [isError, setIsError] = useState<boolean>(false);
@ -78,8 +79,7 @@ const FavoritesToCollaborate: React.FC<Props> = (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) => buildingService.getPlaceDetails(v, { start: new Date(), end: dayjs().utc().add(30, "minute").toDate() })
.catch(() => {
setIsError(true);
const isErrorPlace = 0 as unknown as CampusToCollaborate;
@ -90,8 +90,7 @@ const FavoritesToCollaborate: React.FC<Props> = (props) => {
}
if (convergeSettings?.favoriteVenuesToCollaborate) {
const venueDetails = await Promise.all(
convergeSettings.favoriteVenuesToCollaborate.map((v) => getVenueDetails([v])
.then((results) => results[0])
convergeSettings.favoriteVenuesToCollaborate.map((v) => searchService.getVenueDetails(v)
.catch(() => {
setIsError(true);
const isErrorVenue = 0 as unknown as VenueToCollaborate;

Просмотреть файл

@ -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<User>,
@ -39,11 +39,14 @@ const Map: React.FC<Props> = ({
updateUserRecord,
setUsersMissingCoordinates,
}) => {
const {
userService,
} = useApiProvider();
const {
convergeSettings,
} = useConvergeSettingsContextProvider();
const { teamsContext } = useTeamsContext();
const userCoordinateService = useMemo(createUserCoordinateService, []);
const userCoordinateService = useMemo(userService.createUserCoordinateService, []);
const {
state,
} = useSearchContextProvider();
@ -67,6 +70,7 @@ const Map: React.FC<Props> = ({
>(undefined);
const [isOpen, { setTrue: openPanel, setFalse: dismissPanel }] = useBoolean(false);
const { appSettings } = useAppSettingsProvider();
const [userCoordsFetchCompleted, setCoordsFetchCompleted] = useState<boolean>(false);
useEffect(() => {
if (appSettings?.bingAPIKey && appSettings?.bingAPIKey !== "") {
@ -86,11 +90,14 @@ const Map: React.FC<Props> = ({
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);
}
};
@ -150,7 +157,14 @@ const Map: React.FC<Props> = ({
}
};
const updateCurrentUserPhotoRecord = async () => {
if (teamsContext?.userPrincipalName && !photoRecord[teamsContext.userPrincipalName]) {
updatePhotoRecord(teamsContext.userPrincipalName);
}
};
useEffect(() => {
updateCurrentUserPhotoRecord();
updateCurrentUserRecord();
}, [teamsContext]);
@ -158,7 +172,7 @@ const Map: React.FC<Props> = ({
updateSelectedUserRecords();
updateSelectedUserPhotos();
updateSelectedUserCoords();
}, [state.selectedUsers, mapLoading, teamsContext]);
}, [state.selectedUsers, mapLoading, teamsContext, state.startTime]);
const updatePushpins = async () => {
if (!mapLoading && teamsContext && userCoords?.userPrincipalName) {
@ -209,7 +223,7 @@ const Map: React.FC<Props> = ({
return (
<Box className={classes.root}>
{finishedLoading ? (
{finishedLoading && userCoordsFetchCompleted ? (
<ErrorBoundary errorMessage="Oops! We can't load the map right now. Please try again later.">
<BingMaps
coordinates={userCoords}

Просмотреть файл

@ -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> = (props) => {
setMessage,
err,
} = props;
const { userService } = useApiProvider();
const classes = NewEventModalStyles();
const [attendeesLoading, setAttendeesLoading] = useState<boolean>(false);
const [attendeeItems, setAttendeeItems] = useState<string[]>([]);
@ -169,14 +170,14 @@ const NewEventModal: React.FC<Props> = (props) => {
) => {
if (data?.searchQuery) {
setAttendeesLoading(true);
searchUsers(data.searchQuery.toString())
.then((users) => {
userService.searchUsers(data.searchQuery.toString())
.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 {

Просмотреть файл

@ -9,10 +9,8 @@ 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";
import VenueToCollaborate from "../../../types/VenueToCollaborate";
import RecommendedToCollaborateStyles from "../styles/RecommendedToCollaborateStyles";
import { useTeamsContext } from "../../../providers/TeamsContextProvider";
@ -20,6 +18,9 @@ import {
DESCRIPTION, UISections, UI_SECTION, USER_INTERACTION,
} from "../../../types/LoggerTypes";
import { logEvent } from "../../../utilities/LogWrapper";
import { useApiProvider } from "../../../providers/ApiProvider";
import CollaborationPlaceResultsPaged from "./CollaborationPlaceResultsPaged";
import { getCampusSearchNextRange } from "../../../providers/SearchProvider";
interface Props {
setMapPlaces: (places: (CampusToCollaborate | VenueToCollaborate)[]) => void;
@ -33,6 +34,7 @@ const RecommendedToCollaborate: React.FC<Props> = (props) => {
placesLoading,
setPlacesLoading,
} = props;
const { searchService } = useApiProvider();
const { teamsContext } = useTeamsContext();
const classes = RecommendedToCollaborateStyles();
const [open, setOpen] = useState<boolean>(false);
@ -43,46 +45,56 @@ const RecommendedToCollaborate: React.FC<Props> = (props) => {
>(undefined);
const [isOpen, { setTrue: openPanel, setFalse: dismissPanel }] = useBoolean(false);
const [placesToCollaborate, setPlacesToCollaborate] = useState<CampusToCollaborate[]>([]);
useEffect(() => {
if (teamsContext?.userPrincipalName) {
setUserPrincipalName(teamsContext.userPrincipalName);
setPlacesLoading(true);
searchCampusesToCollaborate({
teamMembers: [teamsContext.userPrincipalName],
startTime: dayjs().utc().add(5, "minutes").toDate(),
endTime: dayjs().utc().add(35, "minutes").toDate(),
capacitySortOrder: "Asc",
placeType: "space",
})
.then((data) => {
setPlacesToCollaborate(data.campusesToCollaborateList);
setMapPlaces(data.campusesToCollaborateList);
}).catch(() => setIsError(true))
.finally(() => setPlacesLoading(false));
}
}, []);
const [recommendationsRadius, setRecommendationsRadius] = useState<number>(10);
const getRecommendations = () => {
setIsError(false);
const getRecommendedPlaces = () => {
setPlacesLoading(true);
searchCampusesToCollaborate({
teamMembers: [upn],
searchService.searchCampusesToCollaborate({
teamMembers: teamsContext?.userPrincipalName ? [teamsContext.userPrincipalName] : [upn],
startTime: dayjs().utc().add(5, "minutes").toDate(),
endTime: dayjs().utc().add(35, "minutes").toDate(),
capacitySortOrder: "Asc",
placeType: "space",
distanceFromSource: recommendationsRadius,
})
.then((data) => {
setPlacesToCollaborate(data.campusesToCollaborateList);
setMapPlaces(data.campusesToCollaborateList);
if (!data?.campusesToCollaborateList?.length) {
const newSearchRange = getCampusSearchNextRange(recommendationsRadius);
if (newSearchRange !== recommendationsRadius) {
setRecommendationsRadius(newSearchRange);
} else {
setPlacesToCollaborate(data.campusesToCollaborateList);
setMapPlaces(data.campusesToCollaborateList);
}
} else {
setPlacesToCollaborate(data.campusesToCollaborateList);
setMapPlaces(data.campusesToCollaborateList);
}
}).catch(() => setIsError(true))
.finally(() => setPlacesLoading(false));
};
if (placesLoading) {
return <Loader />;
}
return (
useEffect(() => {
if (teamsContext?.userPrincipalName) {
setUserPrincipalName(teamsContext.userPrincipalName);
getRecommendedPlaces();
}
}, [recommendationsRadius]);
const moreRecommendationsSearch = () => {
const newSearchRange = getCampusSearchNextRange(recommendationsRadius);
if (newSearchRange !== recommendationsRadius) {
setRecommendationsRadius(newSearchRange);
}
};
const getRecommendations = () => {
setIsError(false);
setPlacesLoading(true);
getRecommendedPlaces();
};
return (placesLoading ? <Loader /> : (
<div className={classes.recommendations}>
{!isError && placesToCollaborate.length === 0
&& <Text content="No results in the recommended Places. " className={!isError ? classes.noResult : classes.isError} />}
@ -108,10 +120,13 @@ const RecommendedToCollaborate: React.FC<Props> = (props) => {
</Box>
</Box>
)}
<CollaborationPlaceResults
<CollaborationPlaceResultsPaged
places={placesToCollaborate}
openPanel={openPanel}
setSelectedPlace={setSelectedPlace}
forceVenueShowMore
recommendationSearchRadius={recommendationsRadius}
moreRecommendationsfetcher={moreRecommendationsSearch}
/>
{selectedPlace && (
<CollaborationPlaceDetails
@ -123,7 +138,7 @@ const RecommendedToCollaborate: React.FC<Props> = (props) => {
/>
)}
</div>
);
));
};
export default RecommendedToCollaborate;

Просмотреть файл

@ -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> = (props) => {
const { userService } = useApiProvider();
const {
selectedUsers,
onSelectedUsersChange,
@ -84,16 +85,18 @@ const UserSearchDropdown:React.FC<Props> = (props) => {
) => {
if (data?.searchQuery) {
setLoading(true);
searchUsers(data.searchQuery.toString())
.then((users) => {
setInputItems(users.filter((u) => !!u.displayName).map((u) => u.displayName as string));
userService.searchUsers(data.searchQuery.toString())
.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 {

Просмотреть файл

@ -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> = (props) => {
const { searchService } = useApiProvider();
const {
convergeSettings,
setConvergeSettings,
@ -50,7 +51,7 @@ const PlacePanel: React.FC<Props> = (props) => {
useEffect(() => {
setLoading(true);
getVenueDetails(place.venueId)
searchService.getVenueDetails(place.venueId)
.then(
(response) => setVenueDetails(
response,

Просмотреть файл

@ -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> = (props) => {
const { place } = props;
const { searchService } = useApiProvider();
const classes = VenueReviewsStyles();
const [reviews, setReviews] = useState<YelpReview[]>([]);
useEffect(() => {
getReviews(place.venueId).then((yelpReviews) => {
searchService.getReviews(place.venueId).then((yelpReviews) => {
setReviews(yelpReviews.response.reviews);
});
}, [place.venueId]);

Просмотреть файл

@ -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<boolean>(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)

Просмотреть файл

@ -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 } 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,17 +121,14 @@ 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);
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");
}
return getBuildingSchedule(
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(),
endBuilding.toISOString(),
@ -137,19 +137,20 @@ 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())
meService.getMyRecommendation(day.year(), day.month() + 1, day.date())
.then((recommendation) => {
setMyRecommendation(recommendation);
if (recommendation !== "Remote" && recommendation !== "Out of Office") {
const building = buildingsList.find((b) => b.displayName === recommendation);
if (building) {
getSchedule(building);
setSelectedBuilding(building);
}
buildingService.getBuildingByDisplayName(recommendation).then((building) => {
if (building) {
getSchedule(building);
setSelectedBuilding(building);
}
}).catch(() => setIsError(true));
}
}).catch(() => setIsError(true));
};
@ -169,7 +170,7 @@ const BookWorkspace: React.FC = () => {
useEffect(() => {
if (selectedBuilding) {
getBuildingPlaces(
buildingService.getBuildingPlaces(
selectedBuilding.identity,
PlaceType.Space,
{
@ -183,7 +184,7 @@ const BookWorkspace: React.FC = () => {
}
}, [selectedBuilding]);
const handleDropdownChange = (bldg:string | undefined) => {
const handleDropdownChange = (bldg: string | undefined) => {
if (bldg !== "Remote" && bldg !== "Out of Office") {
setSelectedBuildingName(bldg);
} else {
@ -217,11 +218,21 @@ 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);
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") {
@ -237,7 +248,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
@ -259,7 +270,7 @@ const BookWorkspace: React.FC = () => {
<Box className={classes.root}>
<Flex gap="gap.small" wrap className={classes.actions}>
<Box className={classes.halfWidthDropDown}>
<PopupMenuWrapper headerTitle={RECOMMENDED} handleDropdownChange={handleDropdownChange} buildingList={buildingsList.map((x) => x.displayName)} locationBuildingName={myRecommendation !== "Remote" && myRecommendation !== "Out of Office" ? selectedBuilding?.displayName : myRecommendation} width="320px" marginContent="5.1rem" value={selectedBuildingName === RECOMMENDED ? "" : selectedBuildingName} placeholderTitle={RECOMMENDED} buttonTitle="See more" />
<PopupMenuWrapper headerTitle={RECOMMENDED} handleDropdownChange={handleDropdownChange} buildingList={buildingsList.map((x) => x.displayName)} locationBuildingName={myRecommendation} width="320px" marginContent="5.1rem" value={selectedBuildingName === RECOMMENDED ? "" : selectedBuildingName} placeholderTitle={RECOMMENDED} buttonTitle="See more" otherOptionsList={[]} maxHeight="320px" clearTextBox={onClearTextBox} />
</Box>
<Box className={classes.datePickerStyles}>
<DatePickerPrimary
@ -269,23 +280,23 @@ const BookWorkspace: React.FC = () => {
</Box>
</Flex>
{isError
&& selectedBuildingName === RECOMMENDED && (
<Box className={classes.errBox}>
<Text content="Something went wrong." color="red" />
<Button
content="Try again"
text
onClick={() => {
refreshRecommended();
logEvent(USER_INTERACTION, [
{ name: UI_SECTION, value: UISections.WorkspaceHome },
{ name: DESCRIPTION, value: "refreshRecommended" },
]);
}}
color="red"
className={classes.retryBtn}
/>
</Box>
&& selectedBuildingName === RECOMMENDED && (
<Box className={classes.errBox}>
<Text content="Something went wrong." color="red" />
<Button
content="Try again"
text
onClick={() => {
refreshRecommended();
logEvent(USER_INTERACTION, [
{ name: UI_SECTION, value: UISections.WorkspaceHome },
{ name: DESCRIPTION, value: "refreshRecommended" },
]);
}}
color="red"
className={classes.retryBtn}
/>
</Box>
)}
{!isError && selectedBuildingName === RECOMMENDED && myRecommendation === "Out of Office"
&& (
@ -295,219 +306,219 @@ const BookWorkspace: React.FC = () => {
/>
)}
{!isError && selectedBuildingName === RECOMMENDED && myRecommendation === "Remote"
&& (
<RemoteCard
title="Work from home"
description="Go into an office if you want, but save yourself the commute. "
/>
)}
&& (
<RemoteCard
title="Work from home"
description="Go into an office if you want, but save yourself the commute. "
/>
)}
{selectedBuilding && (
<Box className={classes.boxStyle}>
<Flex vAlign="center" space="between" gap="gap.small">
<Flex vAlign="end" gap="gap.small" className={classes.displayNameBox}>
<Header
as="h3"
content={selectedBuilding.displayName}
className={classes.displayName}
/>
<BuildingCapacity availableSpace={schedule?.available || 100} />
<Box className={classes.boxStyle}>
<Flex vAlign="center" space="between" gap="gap.small">
<Flex vAlign="end" gap="gap.small" className={classes.displayNameBox}>
<Header
as="h3"
content={selectedBuilding.displayName}
className={classes.displayName}
/>
<BuildingCapacity availableSpace={schedule?.available || 100} />
</Flex>
</Flex>
</Flex>
{convergeSettings
&& convergeSettings.zipCode
&& address
&& <TravelTimes start={convergeSettings?.zipCode} end={address} />}
{convergeSettings
&& convergeSettings.zipCode
&& address
&& <TravelTimes start={convergeSettings?.zipCode} end={address} />}
{isError
&& selectedBuildingName !== RECOMMENDED && (
<Box className={classes.errBox}>
<Text content="Something went wrong." color="red" />
<Button
content="Try again"
text
onClick={() => {
refreshWorkSpace();
logEvent(USER_INTERACTION, [
{ name: UI_SECTION, value: UISections.WorkspaceHome },
{ name: DESCRIPTION, value: "refreshWorkSpace" },
]);
}}
color="red"
className={classes.retryBtn}
/>
</Box>
)}
<Flex hAlign="center">
{loading && <Box className={classes.loaderBox}><Loader /></Box>}
{!loading && schedule && <AvailabilityChart schedule={schedule} />}
</Flex>
<Flex
vAlign="center"
space="between"
gap="gap.small"
padding="padding.medium"
className={isError ? classes.errorMessage : classes.hideErrorMessage}
>
<Provider
theme={{
componentVariables: {
Dialog: ({ colorScheme }: SiteVariablesPrepared) => ({
rootWidth: "795px",
headerFontSize: "18px",
rootBackground: colorScheme.default.background,
color: colorScheme.default.background3,
}),
},
}}
>
<Dialog
open={open}
onOpen={() => {
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 && (
<BookPlaceModal
place={flexiblePlace}
bookable={bookable}
setBookable={setBookable}
buildingName={selectedBuildingName}
err={err}
start={start}
end={end}
setStart={setStart}
setEnd={setEnd}
isAllDay={isAllDay}
setIsAllDay={setIsAllDay}
isFlexible={!!flexiblePlace}
/>
)
)}
header={(
<Text
as="h2"
content="Book a workspace"
className={classes.header}
/>
)}
trigger={(
&& selectedBuildingName !== RECOMMENDED && (
<Box className={classes.errBox}>
<Text content="Something went wrong." color="red" />
<Button
primary
content="Try again"
text
disabled={!flexiblePlace}
>
Flexible seating
</Button>
)}
headerAction={{
icon: <CloseIcon />,
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}
/>
</Box>
)}
<Flex hAlign="center">
{loading && <Box className={classes.loaderBox}><Loader /></Box>}
{!loading && schedule && <AvailabilityChart schedule={schedule} />}
</Flex>
<Flex
vAlign="center"
space="between"
gap="gap.small"
padding="padding.medium"
className={isError ? classes.errorMessage : classes.hideErrorMessage}
>
<Provider
theme={{
componentVariables: {
Dialog: ({ colorScheme }: SiteVariablesPrepared) => ({
rootWidth: "795px",
headerFontSize: "18px",
rootBackground: colorScheme.default.background,
color: colorScheme.default.background3,
}),
},
}}
className={classes.dialog}
/>
</Provider>
<Button
onClick={() => {
goToWorkspaces();
logEvent(USER_INTERACTION, [
{ name: UI_SECTION, value: UISections.WorkspaceHome },
{ name: DESCRIPTION, value: "go_to_search_workspaces" },
]);
}}
>
Search spaces
</Button>
</Flex>
</Box>
>
<Dialog
open={open}
onOpen={() => {
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();
}
calendarService.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(() => {
meService.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 && (
<BookPlaceModal
place={flexiblePlace}
bookable={bookable}
setBookable={setBookable}
buildingName={selectedBuildingName}
err={err}
start={start}
end={end}
setStart={setStart}
setEnd={setEnd}
isAllDay={isAllDay}
setIsAllDay={setIsAllDay}
isFlexible={!!flexiblePlace}
/>
)
)}
header={(
<Text
as="h2"
content="Book a workspace"
className={classes.header}
/>
)}
trigger={(
<Button
primary
text
disabled={!flexiblePlace}
>
Flexible seating
</Button>
)}
headerAction={{
icon: <CloseIcon />,
title: "Close",
onClick: () => {
logEvent(USER_INTERACTION, [
{ name: UI_SECTION, value: UISections.WorkspaceHome },
{ name: DESCRIPTION, value: "close_workspace_dialog" },
]);
clearEvent();
setOpen(false);
},
}}
className={classes.dialog}
/>
</Provider>
<Button
onClick={() => {
goToWorkspaces();
logEvent(USER_INTERACTION, [
{ name: UI_SECTION, value: UISections.WorkspaceHome },
{ name: DESCRIPTION, value: "go_to_search_workspaces" },
]);
}}
>
Search spaces
</Button>
</Flex>
</Box>
)}
<IsThisHelpful logId="e0510597" sectionName={UISections.WorkspaceHome} />
<Box className={classes.changeLocation}>

Просмотреть файл

@ -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);
@ -55,7 +56,7 @@ const ConnectTeammates: React.FC = () => {
const [widget, setWidget] = React.useState<IWidget[]>([]);
useEffect(() => {
if (state.list !== TeammateList.All) getTeammates(state.list, state.date);
if (state.list !== TeammateList.All) getTeammates(state.list);
else searchMoreTeammates(state.searchString);
}, []);
@ -75,9 +76,6 @@ const ConnectTeammates: React.FC = () => {
) => {
const eventData = data?.value?.toString() as TeammateList;
updateList(eventData);
if (eventData === TeammateList.All && state.searchString && state.searchString.length > 0) {
searchMoreTeammates(state.searchString);
}
logEvent(USER_INTERACTION, [
{ name: UI_SECTION, value: UISections.ConnectWithTeammates },
{ name: DESCRIPTION, value: `dropdown_change_${data?.value}` },
@ -87,7 +85,7 @@ const ConnectTeammates: React.FC = () => {
const handleInputChange: ComponentEventHandler<InputProps & {
value: string;
}> = (event, data) => {
updateSearchString(data?.value);
updateSearchString(state.list, data?.value);
logEvent(USER_INTERACTION, [
{ name: UI_SECTION, value: UISections.ConnectWithTeammates },
{ name: DESCRIPTION, value: "input_change_search_users" },
@ -95,11 +93,11 @@ const ConnectTeammates: React.FC = () => {
};
const refreshPageTeammates = async () => {
setIsError(false);
getTeammates(state.list, state.date);
getTeammates(state.list);
};
const getMyConvergeCalendar = async () => {
getConvergeCalendar()
meService.getConvergeCalendar()
.then((ConvergeCalendar) => {
if (ConvergeCalendar === null || ConvergeCalendar === undefined) {
setConvergeCalendar(true);
@ -113,7 +111,8 @@ const ConnectTeammates: React.FC = () => {
getMyConvergeCalendar();
}, []);
const setupNewUserWrapper = (settings: ConvergeSettings): Promise<void> => setupNewUser(settings)
const setupNewUserWrapper = (settings: ConvergeSettings):
Promise<void> => meService.setupNewUser(settings)
.then(() => {
setConvergeCalendar(false);
})
@ -162,7 +161,6 @@ const ConnectTeammates: React.FC = () => {
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}
@ -277,11 +275,7 @@ const ConnectTeammates: React.FC = () => {
className={classes.header}
>
<PrimaryDropdown
items={[
TeammateList.Suggested,
TeammateList.MyOrganization,
TeammateList.MyList,
TeammateList.All]}
items={state.teammatesDropdown}
handleDropdownChange={handleDropdownChange}
value={state.list}
width="168px"

Просмотреть файл

@ -39,16 +39,22 @@ 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 RELOAD_ROWS = "RELOAD_ROWS";
const USER_AVAILABILITY_REQUEST = "USER_AVAILABILITY_REQUEST";
const USER_AVAILABILITY_RESPONSE = "USER_AVAILABILITY_RESPONSE";
const TOGGLE_ITEM = "TOGGLE_ITEM";
const TOGGLE_ALL = "TOGGLE_ALL";
interface ReloadRowsAction {
type: typeof RELOAD_ROWS,
payload: Teammate[],
}
interface UserAvailabilityRequestAction {
type: typeof USER_AVAILABILITY_REQUEST,
payload: string
@ -72,7 +78,8 @@ interface ToggleAllAction {
type SelectableTableAction = UserAvailabilityRequestAction
| UserAvailabilityResponseAction
| ToggleAllAction
| ToggleItemAction;
| ToggleItemAction
| ReloadRowsAction;
type SelectableTableState = {
rows: Record<string, boolean>;
@ -81,10 +88,28 @@ type SelectableTableState = {
selectedAvailableTimes: Record<string, TimeLimit[]>;
}
const getRowKeys = (teammateList: Teammate[]): Record<string, boolean> => {
const teammateRows = teammateList.reduce((items: Record<string, boolean>, teammate) => {
// eslint-disable-next-line no-param-reassign
items[teammate.user.id as string] = false;
return items;
}, {});
return teammateRows;
};
const selectableTableStateReducer: React.Reducer<SelectableTableState, SelectableTableAction> = (
state, action,
) => {
switch (action.type) {
case RELOAD_ROWS: {
const newState: SelectableTableState = {
rows: getRowKeys(action.payload),
availableTimes: {},
loadingAvailableTimes: {},
selectedAvailableTimes: {},
};
return newState;
}
case TOGGLE_ITEM: {
const newState = {
...state,
@ -163,6 +188,7 @@ interface Props {
}
const SelectableTable: React.FC<Props> = (props) => {
const { userService } = useApiProvider();
const { teammates } = props;
const {
convergeSettings,
@ -187,7 +213,7 @@ const SelectableTable: React.FC<Props> = (props) => {
setConvergeSettings(newSettings).then(() => {
if (state.list === TeammateList.MyList) {
setLoading(false);
getTeammates(state.list, state.date, state.searchString);
getTeammates(state.list, state.searchString);
}
}).catch(() => {
if (state.list === TeammateList.MyList) {
@ -239,11 +265,7 @@ const SelectableTable: React.FC<Props> = (props) => {
];
const initialState: SelectableTableState = {
rows: teammates.reduce((items: Record<string, boolean>, teammate) => {
// eslint-disable-next-line no-param-reassign
items[teammate.user.id as string] = false;
return items;
}, {}),
rows: getRowKeys(teammates),
availableTimes: {},
loadingAvailableTimes: {},
selectedAvailableTimes: {},
@ -275,7 +297,7 @@ const SelectableTable: React.FC<Props> = (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,
@ -308,9 +330,10 @@ const SelectableTable: React.FC<Props> = (props) => {
React.useEffect(() => {
if (teammates.length) {
dispatch({ type: RELOAD_ROWS, payload: teammates });
getAvailability(teammates);
}
}, [teammates.length]);
}, [teammates.length, state.date]);
const refreshPageTeammates = async () => {
getAvailability(teammates);
@ -344,7 +367,7 @@ const SelectableTable: React.FC<Props> = (props) => {
}}
>
<ResponsiveTableContainer columns={responsiveColumnsConfig}>
<Table aria-label="Selectable table" accessibility={gridNestedBehavior}>
<Table aria-label="Selectable table" accessibility={gridNestedBehavior} className={classes.tableheight}>
<Table.Row
header
accessibility={gridRowBehavior}

Просмотреть файл

@ -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,15 @@ interface Props {
const UserLocationCell: React.FC<Props> = (props) => {
const { teammate } = props;
const { userService } = useApiProvider();
const [loading, setLoading] = useState<boolean>(true);
const { state, setTeammateLocation } = useTeammateProvider();
const [isError, setIsError] = React.useState(false);
useEffect(() => {
setLoading(true);
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);
@ -30,11 +32,8 @@ const UserLocationCell: React.FC<Props> = (props) => {
setLoading(false);
}
}, [teammate.user.id, state.date]);
if (loading) {
return <Loader />;
}
return (
<span>{isError ? "Unknown" : teammate.location}</span>
return (loading ? (<Loader />)
: (<span>{isError ? "Unknown" : teammate.location}</span>)
);
};

Просмотреть файл

@ -3,28 +3,59 @@
import React, { useState } from "react";
import {
Box, Button, Form, FormButton, FormLabel, FormField, Input,
Box, Button, Form, FormButton, FormLabel, FormField, Input, Alert,
} from "@fluentui/react-northstar";
import WelcomeBanner from "./WelcomeBanner";
import { logEvent } from "../../utilities/LogWrapper";
import {
DESCRIPTION, UISections, UI_SECTION, USER_INTERACTION,
DESCRIPTION, ImportantActions, IMPORTANT_ACTION, UISections, UI_SECTION, USER_INTERACTION,
} from "../../types/LoggerTypes";
import InitialLoader from "../../InitialLoader";
import WelcomeStyles from "./styles/WelcomeStyles";
import { useConvergeSettingsContextProvider } from "../../providers/ConvergeSettingsProvider";
type Props = {
onZipCodeSubmission: (zipCode: string) => Promise<void>
}
const Welcome: React.FC<Props> = (props) => {
const Welcome: React.FC = () => {
const {
convergeSettings,
setupNewUser,
} = useConvergeSettingsContextProvider();
const [zipCode, setZipCode] = useState("");
const [getStarted, setGetStarted] = useState(false);
const [loading, setLoading] = useState(false);
const [isErr, setIsErr] = useState(false);
const classes = WelcomeStyles();
const getStartedCallback = () => {
const handleGetStartedButton = () => {
setGetStarted(true);
logEvent(USER_INTERACTION, [
{ name: UI_SECTION, value: UISections.Welcome },
{ name: DESCRIPTION, value: "submit_zipcode" },
]);
};
const handleFormSubmission = () => {
setLoading(true);
setIsErr(false);
logEvent(USER_INTERACTION, [
{ name: "didSubmitZipCode", value: (!!zipCode).toString() },
{ name: UI_SECTION, value: UISections.Welcome },
{ name: DESCRIPTION, value: "zip_code_submit" },
]);
const convergeSettingsCopy = convergeSettings ? { ...convergeSettings } : {};
setupNewUser({
...convergeSettingsCopy,
isConvergeUser: true,
zipCode,
})
.then(() => {
if (zipCode && zipCode !== "") {
logEvent(USER_INTERACTION, [
{ name: IMPORTANT_ACTION, value: ImportantActions.AddZipCode },
]);
}
})
.catch(() => setIsErr(true))
.finally(() => setLoading(false));
};
const descriptiontext1 = "Converge needs to know your most frequent remote work location zipcode to determine office recommendations, commute times, and team collaboration opportunities. ";
@ -44,17 +75,7 @@ const Welcome: React.FC<Props> = (props) => {
<p className={classes.description}>{descriptiontext1}</p>
<p className={classes.description}>{descriptiontext2}</p>
<Box>
<Form
onSubmit={() => {
setLoading(true);
logEvent(USER_INTERACTION, [
{ name: "didSubmitZipCode", value: (!!zipCode).toString() },
{ name: UI_SECTION, value: UISections.Welcome },
{ name: DESCRIPTION, value: "zip_code_submit" },
]);
props.onZipCodeSubmission(zipCode).finally(() => setLoading(false));
}}
>
<Form onSubmit={handleFormSubmission}>
<p className={classes.contentText}>
Where are you most likely to work remote from?
</p>
@ -80,6 +101,7 @@ const Welcome: React.FC<Props> = (props) => {
}}
/>
</FormField>
{isErr && <Alert danger content="There was a problem setting up your account. Please try again." />}
<Box className={classes.btnWrapper}>
<FormButton
content="Done"
@ -106,13 +128,7 @@ const Welcome: React.FC<Props> = (props) => {
primary
className={classes.getStartedBtn}
styles={{ padding: "12px 0" }}
onClick={() => {
getStartedCallback();
logEvent(USER_INTERACTION, [
{ name: UI_SECTION, value: UISections.Welcome },
{ name: DESCRIPTION, value: "submit_zipcode" },
]);
}}
onClick={handleGetStartedButton}
/>
</Box>
)

Просмотреть файл

@ -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> = (props) => {
const { start, end } = props;
const { routeService } = useApiProvider();
const [driveTime, setDriveTime] = useState("");
const [transitTime, setTransitTime] = useState("");
const [isError, setIsError] = useState<boolean>(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());

Просмотреть файл

@ -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,23 +92,20 @@ const getAvailabilityBorderColor = (
};
const WorkgroupAvatar: React.FC<Props> = (props) => {
const { userService } = useApiProvider();
const {
user,
} = props;
const [image, setImage] = useState<string | undefined>(undefined);
const [image, setImage] = useState<string | undefined | null>(undefined);
const [IsDisplayName, setDisplayName] = useState<string>("");
const classes = WorkgroupAvatarStyles();
const [presence, setPresence] = useState<ApiPresence>({} as ApiPresence);
useEffect(() => {
if (user.userPrincipalName) {
const response = 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]}`);
@ -117,6 +114,10 @@ const WorkgroupAvatar: React.FC<Props> = (props) => {
setDisplayName(`${user.userPrincipalName.split(" ")[0]}${user.userPrincipalName.split(" ")[1]}`);
}
});
const responsePresence = userService.getUserProfile(user.userPrincipalName);
responsePresence.then((userPresence) => {
setPresence(userPresence.presence);
});
}
return () => {

Просмотреть файл

@ -10,29 +10,13 @@ import { TeammateFilterProvider } from "../../providers/TeammateFilterProvider";
import { PlaceContextProvider } from "../../providers/PlaceFilterProvider";
import NPSDialog from "./components/NPSDialog";
import UnknownZipcodeAlert from "./UnknownZipcodeAlert";
import { logEvent } from "../../utilities/LogWrapper";
import { useConvergeSettingsContextProvider } from "../../providers/ConvergeSettingsProvider";
import { ImportantActions, IMPORTANT_ACTION, USER_INTERACTION } from "../../types/LoggerTypes";
const Home: React.FC = () => {
const {
convergeSettings,
setupNewUser,
} = useConvergeSettingsContextProvider();
const handleZipCodeSubmission = (zipCode: string): Promise<void> => setupNewUser({
...convergeSettings,
isConvergeUser: true,
zipCode,
})
.then(() => {
if (zipCode && zipCode !== "") {
logEvent(USER_INTERACTION, [
{ name: IMPORTANT_ACTION, value: ImportantActions.AddZipCode },
]);
}
});
return (
<>
{convergeSettings?.isConvergeUser ? (
@ -67,7 +51,7 @@ const Home: React.FC = () => {
</Box>
</>
) : (
<Welcome onZipCodeSubmission={handleZipCodeSubmission} />
<Welcome />
)}
</>
);

Просмотреть файл

@ -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",

Просмотреть файл

@ -64,6 +64,12 @@ const SelectableTableStyles = makeStyles(() => ({
fontSize: "small",
color: "red important",
},
tableheight: {
maxHeight: "67vh",
"@media (max-width: 1366px)": {
height: "auto",
},
},
}));
export default SelectableTableStyles;

Просмотреть файл

@ -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";
@ -49,6 +48,7 @@ const Places: React.FC<Props> = (props) => {
const [err, setErr] = useState<boolean>(isError);
const classes = PlacesStyles();
const { teamsContext } = useTeamsContext();
const [skipTokenString, setSkipTokenString] = useState<string>("");
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"),
@ -86,6 +86,10 @@ const Places: React.FC<Props> = (props) => {
window.location.reload();
};
const onSkipToken = (skipToken:string) => {
setSkipTokenString(skipToken);
};
return (
<DisplayBox
descriptionContent="Find somewhere to get things done"
@ -168,6 +172,7 @@ const Places: React.FC<Props> = (props) => {
buildingUpn={state.location}
placeType={placeType}
key={state.location + placeType}
skipToken={skipTokenString}
/>
)}
{!!state.location && state.location === "Favorites"
@ -189,13 +194,11 @@ const Places: React.FC<Props> = (props) => {
<CustomizedPlaceCollectionAccordian
closestBuilding={buildingsList[0]}
favoriteCampuses={favoriteCampuses}
getSkipToken={onSkipToken}
/>
)}
{convergeState.buildingListLoading && <Loader />}
<Box className={classes.isThisHelpful}>
<IsThisHelpful logId="3938cd30" sectionName={UISections.PlaceResults} />
</Box>
</DisplayBox>
);
};

Просмотреть файл

@ -13,8 +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 getPlaceMaxReserved, { getRoomAvailability } from "../../../api/placeService";
import ExchangePlace, { PlaceType } from "../../../types/ExchangePlace";
import { logEvent } from "../../../utilities/LogWrapper";
import {
USER_INTERACTION, UISections, UI_SECTION, DESCRIPTION, IMPORTANT_ACTION, ImportantActions,
@ -23,9 +22,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 { useApiProvider } from "../../../providers/ApiProvider";
import { PlacePhotosResult } from "../../../api/buildingService";
type Props = {
place: ExchangePlace,
@ -57,12 +56,14 @@ const BookPlaceModal: React.FC<Props> = (props) => {
setIsAllDay,
isFlexible,
} = props;
const {
meService,
placeService,
buildingService,
} = useApiProvider();
const [maxReserved, setMaxReserved] = useState<number>(0);
const [isAvailable, setIsAvailable] = useState<boolean>(true);
const [,
placePhotos,,
getPlacePhotos,
] = usePlacePhotos();
const [placePhotos, setPlacePhotos] = useState<PlacePhotosResult | undefined>(undefined);
const [photoUrl, setPhotoUrl] = useState<string | undefined>(undefined);
const [floorPlanUrl, setFloorPlanUrl] = useState<string | undefined>(undefined);
const [otherPhotos, setOtherPhotos] = useState<string[]>([]);
@ -157,44 +158,38 @@ const BookPlaceModal: React.FC<Props> = (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(startDay).utc().toISOString(),
dayjs(endDay).utc().toISOString(),
dayjs(start).utc().toISOString(),
dayjs(end).utc().toISOString(),
).then(setIsAvailable);
}
}, [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]);
buildingService.getPlacePhotos(place.sharePointID).then(setPlacePhotos);
}
}, [place.sharePointID]);
@ -397,7 +392,7 @@ const BookPlaceModal: React.FC<Props> = (props) => {
favoriteCampusesToCollaborate,
};
setConvergeSettings(newSettings);
setSettings(newSettings)
meService.setSettings(newSettings)
.then(() => {
if (!isFavorite) {
logEvent(USER_INTERACTION, [

Просмотреть файл

@ -6,8 +6,10 @@ import {
Button,
Dropdown,
DropdownProps,
ErrorIcon,
Flex,
FormLabel,
Loader,
Text,
useFluentContext,
} from "@fluentui/react-northstar";
@ -18,34 +20,41 @@ import { PlaceType } from "../../../types/ExchangePlace";
import {
DESCRIPTION, UISections, UI_SECTION, USER_INTERACTION,
} from "../../../types/LoggerTypes";
import Await from "../../../utilities/Await";
import { logEvent } from "../../../utilities/LogWrapper";
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 {
buildingUpn: string;
skipToken:string;
placeType?: PlaceType
}
const BuildingPlaces: React.FC<IPlaceResultSetProps> = ({ buildingUpn, placeType }) => {
const BuildingPlaces: React.FC<IPlaceResultSetProps> = ({
buildingUpn, placeType,
skipToken,
}) => {
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,
];
const [itemsPerPage, setItemsPerPage] = useState<number>(pageSizeOptions[0]);
const [count, setCount] = useState<number>(0);
const [skipTokenString, setSkipTokenString] = useState<string>(skipToken);
const handleItemCountChange = (
event: React.MouseEvent<Element, MouseEvent> | React.KeyboardEvent<Element> | null,
data: DropdownProps,
@ -63,16 +72,17 @@ const BuildingPlaces: React.FC<IPlaceResultSetProps> = ({ buildingUpn, placeType
hasDisplay: state.attributeFilter.indexOf("displayDeviceName") > -1,
hasVideo: state.attributeFilter.indexOf("videoDeviceName") > -1,
},
skipTokenString,
true,
);
).then((s) => {
setCount(s.exchangePlacesList.length);
setSkipTokenString(s.skipToken);
});
}, [buildingUpn, state.attributeFilter]);
return (
<Flex
column
styles={{
height: "240px",
}}
>
<Flex space="between">
<Flex>
@ -111,57 +121,91 @@ const BuildingPlaces: React.FC<IPlaceResultSetProps> = ({ buildingUpn, placeType
/>
</Flex>
</Flex>
<Await
loading={placesLoading}
error={placesError as Error}
>
<Box styles={{ padding: "16px 0" }}>
<RepeatingBox>
{(places ?? []).map(((place, index) => (
<PlaceCard
place={place}
buildingName={place.building}
key={place.identity + place.capacity + index.toString()}
/>
)))}
</RepeatingBox>
</Box>
<Flex
hAlign="center"
style={{
margin: "16px, 0",
}}
>
{hasMore ? (
<Button
content="Show more"
onClick={() => {
requestBuildingPlaces(
placeType ?? PlaceType.Room,
itemsPerPage,
{
isWheelchairAccessible: state.attributeFilter.indexOf("isWheelChairAccessible") > -1,
hasAudio: state.attributeFilter.indexOf("audioDeviceName") > -1,
hasDisplay: state.attributeFilter.indexOf("displayDeviceName") > -1,
hasVideo: state.attributeFilter.indexOf("videoDeviceName") > -1,
},
);
logEvent(USER_INTERACTION, [
{ name: UI_SECTION, value: UISections.BookPlaceModal },
{ name: DESCRIPTION, value: "requestWorkspaces" },
]);
}}
<Flex hAlign="center" vAlign="center" style={{ marginTop: "8px" }}>
{placesLoading === false && hasMore === false && count === 0
&& (
<>
<ErrorIcon styles={{ paddingLeft: "5rem" }} />
<Text
error
styles={{ paddingLeft: "0.5rem" }}
>
There was a problem loading
{" "}
{buildingUpn}
.Please choose another building from the menu
</Text>
</>
)}
</Flex>
<Flex hAlign="center" vAlign="center" style={{ marginTop: "8px" }}>
{placesLoading === true && skipTokenString === null
&& (<Loader />)}
</Flex>
<Box styles={{ padding: "16px 0" }}>
<RepeatingBox>
{(places ?? []).map(((place, index) => (
<PlaceCard
place={place}
buildingName={place.building}
key={place.identity + place.capacity + index.toString()}
/>
) : (
<Text style={{
color: theme.siteVariables.colors.grey[400],
)))}
</RepeatingBox>
</Box>
<Flex hAlign="center" vAlign="center" style={{ marginTop: "8px" }}>
{placesLoading === true && skipTokenString !== null
&& (<Loader />)}
</Flex>
<Flex
hAlign="center"
style={{
margin: "16px, 0",
}}
>
{hasMore && (
<Button
content="Show more"
onClick={() => {
requestBuildingPlaces(
placeType ?? PlaceType.Room,
itemsPerPage,
{
isWheelchairAccessible: state.attributeFilter.indexOf("isWheelChairAccessible") > -1,
hasAudio: state.attributeFilter.indexOf("audioDeviceName") > -1,
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" },
]);
}}
>
No more results
</Text>
)}
</Flex>
</Await>
/>
) }
</Flex>
<Flex hAlign="center" vAlign="center" style={{ marginTop: "8px" }}>
{placesLoading === false && hasMore === false && skipTokenString === null
&& (
<Text style={{
color: theme.siteVariables.colors.grey[400],
}}
>
No more results
</Text>
)}
</Flex>
<Box className={classes.isThisHelpful}>
<IsThisHelpful logId="3938cd30" sectionName={UISections.PlaceResults} />
</Box>
</Flex>
);
};

Просмотреть файл

@ -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> = (props) => {
const { placeService } = useApiProvider();
const classes = CampusPlaceEventTitleStyles();
const {
place,
@ -29,13 +30,19 @@ const CampusPlaceEventTitle:React.FC<Props> = (props) => {
const [isAvailable, setIsAvailable] = useState<boolean>(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");
}
getRoomAvailability(
placeService.getRoomAvailability(
place.identity,
startDay.utc().toISOString(),
endDay.utc().toISOString(),

Просмотреть файл

@ -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,9 @@ 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 { PlacePhotosResult } from "../../../api/buildingService";
import { useApiProvider } from "../../../providers/ApiProvider";
type Props = {
placeToCollaborate: CampusToCollaborate,
@ -24,20 +25,23 @@ type Props = {
const CollaborationCampusPlaceCard:React.FC<Props> = (props) => {
const { placeToCollaborate, onPlaceClick } = props;
const { buildingService } = useApiProvider();
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<PlacePhotosResult | undefined>(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);
buildingService.getPlacePhotos(placeToCollaborate.sharePointID)
.then(setPlacePhotos)
.catch(() => setPlacePhotosError(true))
.finally(() => setPlacePhotosLoading(false));
}
}, [placeToCollaborate.sharePointID]);

Просмотреть файл

@ -16,22 +16,26 @@ 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 {
closestBuilding: BuildingBasicInfo;
favoriteCampuses: ExchangePlace[];
getSkipToken:(skipToken:string)=>void;
}
const CustomizedPlaceCollectionAccordian: React.FC<Props> = (props) => {
const { closestBuilding, favoriteCampuses } = props;
const { closestBuilding, favoriteCampuses, getSkipToken } = props;
const { state, updateLocation } = PlaceFilterProvider();
const { buildingService } = useApiProvider();
const {
placesLoading,
places,
placesError,
requestBuildingPlaces,
hasMore,
} = useBuildingPlaces(closestBuilding.identity);
} = useBuildingPlaces(buildingService, closestBuilding.identity);
useEffect(() => {
requestBuildingPlaces(
@ -43,8 +47,11 @@ const CustomizedPlaceCollectionAccordian: React.FC<Props> = (props) => {
hasDisplay: state.attributeFilter.indexOf("displayDeviceName") > -1,
hasVideo: state.attributeFilter.indexOf("videoDeviceName") > -1,
},
null,
true,
);
).then((s) => {
getSkipToken(s.skipToken);
});
}, [
closestBuilding.identity,
state.place,
@ -58,12 +65,12 @@ const CustomizedPlaceCollectionAccordian: React.FC<Props> = (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,
{
@ -72,8 +79,11 @@ const CustomizedPlaceCollectionAccordian: React.FC<Props> = (props) => {
hasDisplay: state.attributeFilter.indexOf("displayDeviceName") > -1,
hasVideo: state.attributeFilter.indexOf("videoDeviceName") > -1,
},
null,
true,
);
).then((s) => {
getSkipToken(s.skipToken);
});
};
const determineFavoritePanel = () => {
@ -214,6 +224,9 @@ const CustomizedPlaceCollectionAccordian: React.FC<Props> = (props) => {
defaultActiveIndex={newCustomizedPanels.map((p, i) => i)}
onActiveIndexChange={handleCustomizedAccordionChange}
/>
<Box>
<IsThisHelpful logId="3938cd30" sectionName={UISections.PlaceResults} />
</Box>
</Box>
);
};

Просмотреть файл

@ -30,7 +30,7 @@ const LocationFilter: React.FC<Props> = (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> = (props) => {
value={buildings.find((b) => b.identity === state.location)?.displayName}
placeholderTitle="Select a building"
buttonTitle="Show more"
otherOptionsList={[]}
maxHeight="260px"
/>
</FormField>
);

Просмотреть файл

@ -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,7 +43,8 @@ const NewConfRoomEvent:React.FC<Props> = (props) => {
clearPlaceCard,
getAvailability,
} = props;
const { createReservation } = PlaceFilterProvider();
const { calendarService } = useApiProvider();
const { createReservation, loadUpcomingReservations } = PlaceFilterProvider();
const [isAllDay, setIsAllDay] = useState<boolean>(false);
const [subject, setSubject] = useState<string>("");
const [message, setMessage] = useState<string>("");
@ -103,7 +104,7 @@ const NewConfRoomEvent:React.FC<Props> = (props) => {
endDate = dayjs(end.add(1, "day").format("YYYY-MM-DD")).toDate();
}
createEvent({
calendarService.createEvent({
isAllDay,
start: startDate,
end: endDate,
@ -143,6 +144,7 @@ const NewConfRoomEvent:React.FC<Props> = (props) => {
};
setConvergeSettings(newSettings);
createReservation(calendarEvent);
loadUpcomingReservations(start, end);
getAvailability();
Notifications.show({
duration: 5000,

Просмотреть файл

@ -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 { usePlacePhotos } from "../../../providers/PlacePhotosProvider";
import { useApiProvider } from "../../../providers/ApiProvider";
import { useConvergeSettingsContextProvider } from "../../../providers/ConvergeSettingsProvider";
import { AddRecentBuildings } from "../../../utilities/RecentBuildingsManager";
import getPlaceMaxReserved, { getRoomAvailability } from "../../../api/placeService";
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> = (props) => {
const { place, buildingName } = props;
const {
calendarService,
meService,
placeService,
buildingService,
} = useApiProvider();
const classes = PlaceCardStyles();
const {
convergeSettings,
@ -45,7 +49,7 @@ const PlaceCard: React.FC<Props> = (props) => {
const [bookable, setBookable] = useState<boolean>(true);
const [loading, setLoading] = useState<boolean>(false);
const [err, setErr] = useState<string | undefined>(undefined);
const { state } = PlaceProvider();
const { state, loadUpcomingReservations } = PlaceProvider();
const [start, setStart] = useState<Dayjs>(state.startDate);
const [end, setEnd] = useState<Dayjs>(state.endDate);
const [isAllDay, setIsAllDay] = useState<boolean>(false);
@ -53,15 +57,12 @@ const PlaceCard: React.FC<Props> = (props) => {
const [availability, setAvailability] = useState(0);
const [isAvailable, setIsAvailable] = useState(false);
const [availabilityLoading, setAvailabilityLoading] = useState(false);
const [placePhotos, setPlacePhotos] = useState<PlacePhotosResult | undefined>(undefined);
const [,
placePhotos,,
getPlacePhotos,
] = usePlacePhotos();
const photoUrl = placePhotos?.[0].coverPhoto?.url;
const photoUrl = placePhotos?.coverPhoto?.url;
useEffect(() => {
if (place.sharePointID) {
getPlacePhotos([place.sharePointID]);
buildingService.getPlacePhotos(place.sharePointID).then(setPlacePhotos);
}
}, [place.sharePointID]);
@ -84,7 +85,7 @@ const PlaceCard: React.FC<Props> = (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(),
@ -93,7 +94,7 @@ const PlaceCard: React.FC<Props> = (props) => {
})
.finally(() => setAvailabilityLoading(false));
} else {
getRoomAvailability(
placeService.getRoomAvailability(
place.identity,
(state.startDate).utc().toISOString(),
(state.endDate).utc().toISOString(),
@ -218,7 +219,7 @@ const PlaceCard: React.FC<Props> = (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,
@ -244,7 +245,7 @@ const PlaceCard: React.FC<Props> = (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(),
@ -257,6 +258,10 @@ const PlaceCard: React.FC<Props> = (props) => {
clearPlaceCard();
setOpen(false);
getAvailability();
loadUpcomingReservations(
state.upcomingReservationsStartDate,
state.upcomingReservationsEndDate,
);
Notifications.show({
duration: 5000,
title: "You reserved a workspace.",

Просмотреть файл

@ -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 (
<Box styles={{
width: "100%",
display: "grid",
gap: "14px",
gridTemplateColumns: "repeat(auto-fill, 32%)",
"@media (min-width: 1366px)": {
gridTemplateColumns: "repeat(auto-fill, 310px)",
},
}}
<Box
styles={{
width: "100%",
display: "grid",
gap: "14px",
gridTemplateColumns: "repeat(auto-fill, 32%)",
"@media (min-width: 1366px)": {
gridTemplateColumns: "repeat(auto-fill, 310px)",
},
}}
className={state.location === undefined ? classes.cardBox : classes.placeCardBox}
>
{children}
</Box>

Просмотреть файл

@ -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> = (props) => {
const { reservation } = props;
const { calendarService } = useApiProvider();
const { cancelReservation } = PlaceFilterProvider();
const [openCancelDialog, setOpenCancelDialog] = React.useState<boolean>(false);
const [canceling, setCanceling] = React.useState<boolean>(false);
@ -54,7 +55,7 @@ const Reservation: React.FC<Props> = (props) => {
value: "cancel_reservation",
},
]);
deleteEvent(reservation.id, message).then(() => {
calendarService.deleteEvent(reservation.id, message).then(() => {
Notifications.show({
duration: 5000,
title: "You cancelled a reservation.",

Просмотреть файл

@ -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) {

Просмотреть файл

@ -16,6 +16,9 @@ const BuildingPlacesStyles = makeStyles(() => ({
overflowX: "hidden",
},
},
isThisHelpful: {
marginBottom: "2rem",
},
}));
export default BuildingPlacesStyles;

Просмотреть файл

@ -12,6 +12,12 @@ const PlacesStyles = makeStyles(() => ({
isThisHelpful: {
margin: "2.5em 0 0",
},
placeCardBox: {
height: "49vh",
overflowX: "auto",
},
cardBox: {
},
}));
export default PlacesStyles;

Просмотреть файл

@ -0,0 +1,9 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
import ExchangePlace from "./ExchangePlace";
export interface IExchangePlacesResponse {
exchangePlacesList: ExchangePlace[];
skipToken: string;
}

Просмотреть файл

@ -24,7 +24,8 @@ export enum UISections {
Places="Places",
PopupMenuContent="PopupMenuContent",
PopupMenuWrapper="PopupMenuWrapper",
Toast="Toast"
Toast="Toast",
ApplicationUnavailable="ApplicationUnavailable"
}
export const UI_SECTION = "UI_SECTION";

Просмотреть файл

@ -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<P> = (searchStrings: string[], params: P) => Promise<void>;
type UpdateFunction<P> = (searchStrings: string[], params: P) => Promise<void>;
type UseCachedServiceHook<T, P = void> = () => [
boolean,
T[] | undefined | null,
unknown,
GetFunction<P>,
UpdateFunction<P>
];
type CachedServiceProviderReturnType<T, P = void> = [
CachedServiceProvider,
UseCachedServiceHook<T, P>
];
/**
* 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 = <T, P = void>(
cachedService: CachedQuery<T, P>,
): CachedServiceProviderReturnType<T, P> => {
const Context = React.createContext<CachedQuery<T, P>>(cachedService);
const CachedServiceProvider: React.FC = ({ children }) => {
const value = useContext(Context);
return (
<Context.Provider value={value}>
{children}
</Context.Provider>
);
};
const useCachedService: UseCachedServiceHook<T, P> = () => {
const { getItems, forceUpdate } = useContext(Context);
const [
loading,
result,
error,
waitFor,
] = usePromise<T[]>();
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;

Просмотреть файл

@ -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 { useApiProvider } from "../providers/ApiProvider";
import PopupMenuWrapper from "./popupMenuWrapper";
const ChangeLocationModalStyles = makeStyles(() => ({
triggerBtn: {
@ -55,20 +55,59 @@ const ChangeLocationModal: React.FC<Props> = (props) => {
date,
refreshRecommendation,
} = props;
const {
meService,
} = useApiProvider();
const [open, setOpen] = useState<boolean>(false);
const [location, setLocation] = useState<string>(recommendation || "");
const [loading, setLoading] = useState<boolean>(false);
const classes = ChangeLocationModalStyles();
const handleLocationChange = (
event: React.MouseEvent<Element, MouseEvent> | React.KeyboardEvent<Element> | 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;
}
}
meService.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 +137,7 @@ const ChangeLocationModal: React.FC<Props> = (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 +179,11 @@ const ChangeLocationModal: React.FC<Props> = (props) => {
?
</Box>
<Text content="Location" className={classes.locationText} />
<PrimaryDropdown
placeholder="Select one"
items={buildings.map((b) => b.displayName).concat(["Remote", "Out of Office"])}
value={location}
handleDropdownChange={handleLocationChange}
clearable
width="212px"
/>
<Box>
<PopupMenuWrapper headerTitle="Other Options" handleDropdownChange={handleLocationChange} buildingList={buildings.map((b) => b.displayName)} locationBuildingName="" otherOptionsList={["Remote", "Out Of Office"]} width="320px" marginContent="3.8rem" value={location} placeholderTitle="Select One" buttonTitle="See more" maxHeight="90px" />
</Box>
</Flex>
)}
)}
/>
</Provider>
);

Просмотреть файл

@ -0,0 +1,6 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
export default {
TWO_HOURS_IN_MILLISECONDS: 120 * 60 * 1000,
};

Просмотреть файл

@ -55,12 +55,13 @@ const EnterZipcode: React.FC<Props> = (props) => {
...convergeSettings,
zipCode: newZipCode,
};
setConvergeSettings(newSettings).then(() => {
setLoading(false);
setOpen(false);
});
if (updateWidgetActions) updateWidgetActions(newZipCode);
}).catch(() => setErr(true));
if (updateWidgetActions) {
updateWidgetActions(newZipCode);
}
};
return (
@ -124,7 +125,7 @@ const EnterZipcode: React.FC<Props> = (props) => {
if (inputProps) { setZipCode(inputProps?.value); }
}}
/>
{err && (<Text error content="Please enter a valid zipcode." />)}
{err && (<Text error content="Invalid Zip Code. Please try again." />)}
</Flex>
)}
header="Remote work"

Просмотреть файл

@ -1,50 +0,0 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
import React, { useState, useEffect } from "react";
import { Alert, Box } from "@fluentui/react-northstar";
import { useProvider as errorAlertProvider } from "../providers/ErrorAlertProvider";
type IErrorMessageState = {
message: string;
id: string;
};
const ErrorAlert: React.FC = () => {
const { errorState, errorDispatch } = errorAlertProvider();
const [visible, setVisible] = useState<IErrorMessageState>({ message: "", id: "" });
useEffect(() => {
errorDispatch({
type: "HIDE_ALERT",
payload: { message: visible.message, id: visible.id },
});
}, [visible]);
return (
<>
{
errorState.messages.length > 0
&& errorState.messages.map((error) => (
<Box styles={{
display: "flex",
flexDirection: "column",
padding: "1em 0",
}}
>
<Alert
dismissible
danger
content={`${error.id} : ${error.message}`}
key={error.id}
onVisibleChange={() => setVisible(error)}
/>
</Box>
))
}
</>
);
};
export default ErrorAlert;

Просмотреть файл

@ -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<ILog> = (props) => {
convergeSettings,
setConvergeSettings,
} = useConvergeSettingsContextProvider();
const {
meService,
} = useApiProvider();
const likeSection = () => {
const didLike = convergeSettings?.likedSections?.includes(sectionName) || false;
@ -75,7 +78,7 @@ const IsThisHelpful: React.FC<ILog> = (props) => {
likedSections: newLikedSections,
dislikedSections: newDislikedSections,
};
setSettings(newSettings);
meService.setSettings(newSettings);
setConvergeSettings(newSettings);
};
const likeIsTrue = convergeSettings?.likedSections?.includes(sectionName) || false;

Просмотреть файл

@ -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";
@ -15,31 +15,48 @@ import {
interface Props {
headerTitle: string,
locationBuildingName:string | undefined;
buildingList:ShorthandCollection<DropdownItemProps, Record<string, unknown>>;
handleDropdownChange:(bldg:string| undefined)=>void;
buttonTitle:string;
locationBuildingName: string | undefined;
otherOptionsList: string[];
buildingList: ShorthandCollection<DropdownItemProps, Record<string, unknown>>;
handleDropdownChange: (bldg: string | undefined) => void;
buttonTitle: string;
maxHeight: string;
}
const PopupMenuContent: React.FunctionComponent<Props> = (props) => {
const {
headerTitle, buildingList, locationBuildingName, buttonTitle,
headerTitle, buildingList, locationBuildingName, buttonTitle, otherOptionsList, maxHeight,
} = props;
const {
state, setBuildingsByDistanceRadius,
state,
setBuildingsByDistanceRadius,
getRecentBuildings,
setClickBuildingListLoading,
convergeSettings,
} = useConvergeSettingsContextProvider();
const classes = BookWorkspaceStyles();
const location = useLocation();
const [recentBuildingsLoading, setRecentBuildingsLoading] = useState(false);
const onClickSeeMore = () => {
if (state.buildingsByRadiusDistance < 1000) {
setClickBuildingListLoading(true);
setBuildingsByDistanceRadius(state.buildingsByRadiusDistance * 10);
} else if (state.buildingsByRadiusDistance < 4000) {
setClickBuildingListLoading(true);
setBuildingsByDistanceRadius(state.buildingsByRadiusDistance + 1000);
}
};
useEffect(() => {
if (!state.recentBuildings.length) {
setRecentBuildingsLoading(true);
}
getRecentBuildings()
.finally(() => setRecentBuildingsLoading(false));
}, [convergeSettings?.recentBuildingUpns]);
return (
<Provider>
<Box>
@ -48,7 +65,7 @@ const PopupMenuContent: React.FunctionComponent<Props> = (props) => {
</Flex>
</Box>
{location.pathname !== "/workspace" && locationBuildingName !== ""
&& (
&& (
<Box>
<>
<Button
@ -79,90 +96,128 @@ const PopupMenuContent: React.FunctionComponent<Props> = (props) => {
</Button>
</>
</Box>
)}
)}
{location.pathname === "/workspace" && recentBuildingsLoading && <Loader />}
{location.pathname === "/workspace" && state.recentBuildings.length > 0
&& (
<Box>
{state.recentBuildings.map((item) => (
<>
<Flex>
<Button
text
styles={{ minWidth: "0rem !important", maxWidth: "230px !important" }}
onClick={() => {
props.handleDropdownChange(item?.displayName);
logEvent(USER_INTERACTION, [
{
name: UI_SECTION,
value: UISections.PopupMenuContent,
},
{
name: DESCRIPTION,
value: "handleDropdownChange",
},
]);
}}
>
<Text
content={item.displayName}
title={item?.displayName}
weight="semilight"
styles={{
whiteSpace: "nowrap", textOverflow: "ellipsis", overflow: "hidden", marginLeft: "0.6rem", marginTop: "1rem",
}}
/>
</Button>
</Flex>
</>
))}
</Box>
)}
&& (
<Box>
{state.recentBuildings.map((item) => (
<>
<Flex>
<Button
text
styles={{ minWidth: "0rem !important", maxWidth: "230px !important" }}
onClick={() => {
props.handleDropdownChange(item?.displayName);
logEvent(USER_INTERACTION, [
{
name: UI_SECTION,
value: UISections.PopupMenuContent,
},
{
name: DESCRIPTION,
value: "handleDropdownChange",
},
]);
}}
>
<Text
content={item.displayName}
title={item?.displayName}
weight="semilight"
styles={{
whiteSpace: "nowrap", textOverflow: "ellipsis", overflow: "hidden", marginLeft: "0.6rem", marginTop: "1rem",
}}
/>
</Button>
</Flex>
</>
))}
</Box>
)}
{location.pathname !== "/workspace" && otherOptionsList.length > 0
&& (
<Box>
{otherOptionsList.map((item) => (
<>
<Flex>
<Button
text
styles={{ minWidth: "0rem !important", maxWidth: "230px !important" }}
onClick={() => {
props.handleDropdownChange(item);
logEvent(USER_INTERACTION, [
{
name: UI_SECTION,
value: UISections.PopupMenuContent,
},
{
name: DESCRIPTION,
value: "handleDropdownChange",
},
]);
}}
>
<Text
content={item}
title={item}
weight="semilight"
styles={{
whiteSpace: "nowrap", textOverflow: "ellipsis", overflow: "hidden", marginLeft: "0.6rem", marginTop: "1rem",
}}
/>
</Button>
</Flex>
</>
))}
</Box>
)}
<Divider className="filter-popup-menu-divider" styles={{ marginTop: "0.4rem" }} />
<Box className={location.pathname === "/workspace" ? classes.WorkSpacebuildingContent : classes.buildingContent}>
<Box className={classes.WorkSpacebuildingContent} styles={{ maxHeight }}>
<Box>
<Flex gap="gap.small" vAlign="center">
<Text content="Buildings near you" weight="semibold" styles={{ marginLeft: "1rem", marginTop: "0.6rem" }} />
</Flex>
</Box>
{buildingList.length > 0
&& (
<Box>
{buildingList.map((item) => (
<>
<Flex>
<Button
text
styles={{ minWidth: "0rem !important", maxWidth: "230px !important" }}
onClick={() => {
props.handleDropdownChange(item?.toString());
logEvent(USER_INTERACTION, [
{
name: UI_SECTION,
value: UISections.PopupMenuContent,
},
{
name: DESCRIPTION,
value: "handleDropdownChange",
},
]);
}}
>
<Text
content={item}
title={item?.toString()}
weight="semilight"
styles={{
whiteSpace: "nowrap", textOverflow: "ellipsis", overflow: "hidden", marginLeft: "0.6rem", marginTop: "1rem",
}}
/>
</Button>
</Flex>
</>
))}
&& (
<Box>
{buildingList.map((item) => (
<>
<Flex>
<Button
text
styles={{ minWidth: "0rem !important", maxWidth: "230px !important" }}
onClick={() => {
props.handleDropdownChange(item?.toString());
logEvent(USER_INTERACTION, [
{
name: UI_SECTION,
value: UISections.PopupMenuContent,
},
{
name: DESCRIPTION,
value: "handleDropdownChange",
},
]);
}}
>
<Text
content={item}
title={item?.toString()}
weight="semilight"
styles={{
whiteSpace: "nowrap", textOverflow: "ellipsis", overflow: "hidden", marginLeft: "0.6rem", marginTop: "1rem",
}}
/>
</Button>
</Flex>
</>
))}
</Box>
)}
</Box>
)}
</Box>
{state.buildingListLoading && <Loader label={state.buildingsLoadingMessage} />}
{state.clickBuildingListLoading && <Loader label={state.buildingsLoadingMessage} />}
<Divider className="filter-popup-menu-divider" styles={{ marginTop: "0.4rem" }} />
<Flex gap="gap.small" vAlign="center" hAlign="start" styles={{ marginBottom: "1.5%" }}>
<Button

Просмотреть файл

@ -16,20 +16,23 @@ import { logEvent } from "./LogWrapper";
interface Props {
headerTitle: string,
locationBuildingName:string | undefined,
buildingList:ShorthandCollection<DropdownItemProps, Record<string, unknown>>,
handleDropdownChange:(bldg:string | undefined)=>void;
marginContent:string,
width:string,
value:string|undefined,
placeholderTitle:string,
buttonTitle:string,
locationBuildingName: string | undefined,
otherOptionsList: string[];
buildingList: ShorthandCollection<DropdownItemProps, Record<string, unknown>>,
handleDropdownChange: (bldg: string | undefined) => void;
marginContent: string,
width: string,
value: string | undefined,
placeholderTitle: string,
buttonTitle: string,
maxHeight: string,
clearTextBox?: (isValid:boolean) => void;
}
const PopupMenuWrapper: React.FunctionComponent<Props> = (props) => {
const {
headerTitle, buildingList, locationBuildingName, width,
marginContent, value, placeholderTitle, buttonTitle,
marginContent, value, placeholderTitle, buttonTitle, otherOptionsList, maxHeight,
} = props;
const { updateLocation } = PlaceProvider();
@ -41,14 +44,15 @@ const PopupMenuWrapper: React.FunctionComponent<Props> = (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);
updateLocation(selectedBuilding?.identity);
props.handleDropdownChange(bldg);
props.clearTextBox?.(false);
};
useEffect(() => {
@ -60,11 +64,12 @@ const PopupMenuWrapper: React.FunctionComponent<Props> = (props) => {
else setPopup(true);
setSelectedBuildingName("");
};
const handleTextboxChange = (searchText:string | undefined) => {
const handleTextboxChange = (searchText: string | undefined) => {
setPopup(true);
setSelectedBuildingName(searchText || "");
updateLocation(undefined);
updateSearchString(searchText);
props.clearTextBox?.(true);
};
return (
@ -93,13 +98,13 @@ const PopupMenuWrapper: React.FunctionComponent<Props> = (props) => {
]);
}}
/>
)}
)}
value={selectedBuildingName}
clearable
onChange={((event, data) => handleTextboxChange(data?.value))}
placeholder={placeholderTitle}
/>
)}
)}
content={{
styles: {
width,
@ -118,6 +123,8 @@ const PopupMenuWrapper: React.FunctionComponent<Props> = (props) => {
handleDropdownChange={handleDropdownChange}
locationBuildingName={locationBuildingName}
buttonTitle={buttonTitle}
otherOptionsList={otherOptionsList}
maxHeight={maxHeight}
/>
),
}}

Просмотреть файл

@ -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"
@ -5661,6 +5679,11 @@
"locate-path" "^6.0.0"
"path-exists" "^4.0.0"
"flagged@^2.0.1":
"integrity" "sha512-cKkJdbHruBbi4CAufayqDs1X6PYMKE+MZmG/3mYBPZw1M0dY+tA14tDTkg5+TISQwZ7tTIzwubFZNv4lm7XENw=="
"resolved" "https://registry.npmjs.org/flagged/-/flagged-2.0.1.tgz"
"version" "2.0.1"
"flat-cache@^3.0.4":
"integrity" "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg=="
"resolved" "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz"
@ -5674,7 +5697,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 +6268,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 +6320,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 +6383,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 +7364,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 +7483,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 +7527,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 +7555,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 +8711,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"
@ -8901,7 +8920,7 @@
"loose-envify" "^1.4.0"
"prop-types" "^15.6.2"
"react@*", "react@^0.14.0 || ^15.0.0 || ^16.0.0 || ^17.0.0", "react@^16.0.0", "react@^16.3.0", "react@^16.8.0", "react@^16.8.0 || ^17", "react@^17.0.1", "react@>= 16", "react@>=0.14.9", "react@>=15", "react@>=16.6.0", "react@>=16.8.0 <17.0.0", "react@>=16.8.0 <18.0.0", "react@16.10":
"react@*", "react@^0.14.0 || ^15.0.0 || ^16.0.0 || ^17.0.0", "react@^16.0.0", "react@^16.3.0", "react@^16.8.0", "react@^16.8.0 || ^17", "react@^17.0.1", "react@>= 16", "react@>=0.14.9", "react@>=15", "react@>=16", "react@>=16.6.0", "react@>=16.8.0 <17.0.0", "react@>=16.8.0 <18.0.0", "react@16.10":
"integrity" "sha512-MFVIq0DpIhrHFyqLU0S3+4dIcBhhOvBE8bJ/5kHPVOVaGdo0KuiQzpcjCPsf585WvhypqtrMILyoE2th6dT+Lw=="
"resolved" "https://registry.npmjs.org/react/-/react-16.10.2.tgz"
"version" "16.10.2"

Просмотреть файл

@ -5,28 +5,22 @@ using Converge.Models;
using Converge.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using System;
using System.Threading.Tasks;
namespace Converge.Controllers
{
[Authorize]
[Route("api/buildings")]
[ApiController]
public class BuildingsController : Controller
[Route("api/v1.0/buildings")]
public class BuildingsV1Controller : Controller
{
/// <summary>
/// Send logs to telemetry service
/// </summary>
private readonly ILogger<BuildingsController> logger;
private readonly BuildingsService buildingsService;
public BuildingsController(
ILogger<BuildingsController> logger,
BuildingsService buildingsService)
public BuildingsV1Controller(BuildingsService buildingsService)
{
this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
this.buildingsService = buildingsService;
}
@ -35,23 +29,15 @@ namespace Converge.Controllers
/// defaulted to first 100 buildings if there is no value on how many records to skip.
/// </summary>
/// <param name="topCount">Number of records after the skipCount used to skip the number of records.</param>
/// <param name="skipTokenString">Skip-token option as string to get next set of records.</param>
/// <param name="skip">The number of records to skip.</param>
/// <returns>BuildingsResponse: Containing the list of Buildings records and the count of those.</returns>
[HttpGet]
[Route("sortByName")]
public async Task<ActionResult<BasicBuildingsResponse>> GetBuildings(int? topCount = null, string skipTokenString = null)
public async Task<ActionResult<BasicBuildingsResponse>> GetBuildingsByName(int? topCount = 10, int? skip = 0)
{
try
{
buildingsService.SetPrincipalUserIdentity(User.Identity);
var result = await buildingsService.GetBuildings(topCount, skipTokenString);
return Ok(result);
}
catch (Exception ex)
{
logger.LogError(ex, "Error occurred while getting the Buildings list.");
throw;
}
buildingsService.SetPrincipalUserIdentity(User.Identity);
var result = await buildingsService.GetBuildingsByName(topCount, skip);
return Ok(result);
}
/// <summary>
@ -63,19 +49,24 @@ namespace Converge.Controllers
/// <returns>BuildingsResponse: Containing the list of Buildings records and the count of those.</returns>
[HttpGet]
[Route("sortByDistance")]
public async Task<ActionResult<BasicBuildingsResponse>> GetBuildings(string sourceGeoCoordinates, double? distanceFromSource)
public async Task<ActionResult<BasicBuildingsResponse>> GetBuildingsByDistance(string sourceGeoCoordinates, double? distanceFromSource)
{
try
{
buildingsService.SetPrincipalUserIdentity(User.Identity);
var result = await buildingsService.GetBuildings(sourceGeoCoordinates, distanceFromSource);
return Ok(result);
}
catch (Exception ex)
{
logger.LogError(ex, "Error occurred while getting the Buildings list.");
throw;
}
buildingsService.SetPrincipalUserIdentity(User.Identity);
var result = await buildingsService.GetBuildingsByDistance(sourceGeoCoordinates, distanceFromSource);
return Ok(result);
}
/// <summary>
/// Get Building details by given display-name of Building.
/// </summary>
/// <param name="buildingDisplayName">Building-name</param>
/// <returns>Building and its details</returns>
[HttpGet]
[Route("buildingByName/{buildingDisplayName}")]
public async Task<ActionResult<BuildingBasicInfo>> GetBuildingByDisplayName(string buildingDisplayName)
{
var result = await buildingsService.GetBuildingByDisplayName(buildingDisplayName);
return Ok(result);
}
/// <summary>
@ -84,7 +75,7 @@ namespace Converge.Controllers
/// </summary>
/// <param name="buildingUpn">UPN of Building.</param>
/// <param name="topCount">Number of records after the skipCount used to skip the number of records.</param>
/// <param name="skipTokenString">Skip-token option as string to get next set of records.</param>
/// <param name="skipToken">Skip-token option as string to get next set of records.</param>
/// <param name="hasVideo">Whether to return places with a video display device.</param>
/// <param name="hasAudio">Whether to return places with an audio device.</param>
/// <param name="hasDisplay">Whether to return places with a display device.</param>
@ -92,33 +83,23 @@ namespace Converge.Controllers
/// <returns>ExchangePlacesResponse: Containing the Conference-rooms list and reference to Link-to-next-page.</returns>
[HttpGet]
[Route("{buildingUpn}/rooms")]
public async Task<ActionResult<GraphExchangePlacesResponse>> GetBuildingConferenceRooms(
string buildingUpn,
int? topCount = null,
string skipTokenString = null,
bool hasVideo = false,
bool hasAudio = false,
bool hasDisplay = false,
bool isWheelchairAccessible = false
)
public async Task<ActionResult<GraphExchangePlacesResponse>> GetBuildingConferenceRooms(string buildingUpn,
int? topCount = null,
string skipToken = null,
bool hasVideo = false,
bool hasAudio = false,
bool hasDisplay = false,
bool isWheelchairAccessible = false)
{
try
ListItemFilterOptions listItemFilterOptions = new ListItemFilterOptions
{
ListItemFilterOptions listItemFilterOptions = new ListItemFilterOptions
{
HasAudio = hasAudio,
HasVideo = hasVideo,
HasDisplay = hasDisplay,
IsWheelChairAccessible = isWheelchairAccessible,
};
var result = await buildingsService.GetPlacesOfBuilding(buildingUpn, PlaceType.Room, topCount, skipTokenString, listItemFilterOptions);
return Ok(result);
}
catch (Exception ex)
{
logger.LogError(ex, $"Error occurred while getting Conference-Rooms of the Building for upn: {buildingUpn}.");
throw;
}
HasAudio = hasAudio,
HasVideo = hasVideo,
HasDisplay = hasDisplay,
IsWheelChairAccessible = isWheelchairAccessible,
};
var result = await buildingsService.GetPlacesOfBuilding(buildingUpn, PlaceType.Room, topCount, skipToken, listItemFilterOptions);
return Ok(result);
}
/// <summary>
@ -127,7 +108,7 @@ namespace Converge.Controllers
/// </summary>
/// <param name="buildingUpn">UPN of Building.</param>
/// <param name="topCount">Number of records after the skipCount used to skip the number of records.</param>
/// <param name="skipTokenString">Skip-token option as string to get next set of records.</param>
/// <param name="skipToken">Skip-token option as string to get next set of records.</param>
/// <param name="hasVideo">Whether to return places with a video display device.</param>
/// <param name="hasAudio">Whether to return places with an audio device.</param>
/// <param name="hasDisplay">Whether to return places with a display device.</param>
@ -136,35 +117,25 @@ namespace Converge.Controllers
/// <returns>ExchangePlacesResponse: Containing the Workspaces list and reference to Link-to-next-page.</returns>
[HttpGet]
[Route("{buildingUpn}/spaces")]
public async Task<ActionResult<GraphExchangePlacesResponse>> GetBuildingWorkspaces(
string buildingUpn,
int? topCount = null,
string skipTokenString = null,
bool hasVideo = false,
bool hasAudio = false,
bool hasDisplay = false,
bool isWheelchairAccessible = false,
string displayNameSearchString = null
)
public async Task<ActionResult<GraphExchangePlacesResponse>> GetBuildingWorkspaces(string buildingUpn,
int? topCount = null,
string skipToken = null,
bool hasVideo = false,
bool hasAudio = false,
bool hasDisplay = false,
bool isWheelchairAccessible = false,
string displayNameSearchString = null)
{
try
ListItemFilterOptions listItemFilterOptions = new ListItemFilterOptions
{
ListItemFilterOptions listItemFilterOptions = new ListItemFilterOptions
{
HasAudio = hasAudio,
HasVideo = hasVideo,
HasDisplay = hasDisplay,
IsWheelChairAccessible = isWheelchairAccessible,
DisplayNameSearchString = displayNameSearchString,
};
var result = await buildingsService.GetPlacesOfBuilding(buildingUpn, PlaceType.Space, topCount, skipTokenString, listItemFilterOptions);
return Ok(result);
}
catch (Exception ex)
{
logger.LogError(ex, $"Error occurred while getting Workspaces of the Building for upn: {buildingUpn}.");
throw;
}
HasAudio = hasAudio,
HasVideo = hasVideo,
HasDisplay = hasDisplay,
IsWheelChairAccessible = isWheelchairAccessible,
DisplayNameSearchString = displayNameSearchString,
};
var result = await buildingsService.GetPlacesOfBuilding(buildingUpn, PlaceType.Space, topCount, skipToken, listItemFilterOptions);
return Ok(result);
}
/// <summary>
@ -176,16 +147,8 @@ namespace Converge.Controllers
[Route("rooms/{roomUpn}")]
public async Task<ActionResult<ExchangePlace>> GetConferenceRoom(string roomUpn)
{
try
{
var result = await buildingsService.GetPlaceByUpn(roomUpn, PlaceType.Room);
return Ok(result);
}
catch (Exception ex)
{
logger.LogError(ex, $"Error occurred while getting Conference-Room-details for upn: {roomUpn}.");
throw;
}
var result = await buildingsService.GetPlaceByUpn(roomUpn, PlaceType.Room);
return Ok(result);
}
/// <summary>
@ -197,16 +160,8 @@ namespace Converge.Controllers
[Route("spaces/{spaceUpn}")]
public async Task<ActionResult<ExchangePlace>> GetWorkspace(string spaceUpn)
{
try
{
var result = await buildingsService.GetPlaceByUpn(spaceUpn, PlaceType.Space);
return Ok(result);
}
catch (Exception ex)
{
logger.LogError(ex, $"Error occurred while getting Workspace-details for upn: {spaceUpn}.");
throw;
}
var result = await buildingsService.GetPlaceByUpn(spaceUpn, PlaceType.Space);
return Ok(result);
}
/// <summary>
@ -220,30 +175,14 @@ namespace Converge.Controllers
[Route("{buildingUpn}/schedule")]
public async Task<ActionResult<ConvergeSchedule>> GetWorkspacesSchedule(string buildingUpn, string start, string end)
{
try
{
return await buildingsService.GetWorkspacesScheduleForBuilding(buildingUpn, start, end);
}
catch (Exception ex)
{
logger.LogError(ex, $"Error occurred while getting Schedule for buildingUpn:{buildingUpn} with start:{start} and end:{end}.");
throw;
}
return await buildingsService.GetWorkspacesScheduleForBuilding(buildingUpn, start, end);
}
[HttpGet]
[Route("searchForBuildings/{searchString}")]
public async Task<ActionResult<BuildingSearchInfo>> SearchForBuildings(string searchString, int? topCount = null, string skipToken = null)
public async Task<ActionResult<BuildingSearchInfo>> SearchForBuildings(string searchString, int? topCount = 10, int? skip = 0)
{
try
{
return await buildingsService.SearchForBuildings(searchString, topCount, skipToken);
}
catch (Exception ex)
{
logger.LogError(ex, $"Error occurred while searching Buildings for: {searchString}.");
throw;
}
return await buildingsService.SearchForBuildings(searchString, topCount, skip);
}
}
}

Просмотреть файл

@ -1,159 +0,0 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using Converge.Helpers;
using Converge.Models;
using Converge.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using Microsoft.Graph;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace Converge.Controllers
{
[Authorize]
[Route("api/calendar")]
[ApiController]
public class CalendarController : Controller
{
/// <summary>
/// Send logs to telemetry service
/// </summary>
private readonly ILogger<CalendarController> logger;
private readonly UserGraphService userGraphService;
private readonly BuildingsService buildingsService;
public CalendarController(ILogger<CalendarController> logger,
UserGraphService graphService,
BuildingsService buildingsSvc)
{
this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
this.userGraphService = graphService;
this.buildingsService = buildingsSvc;
}
/// <summary>
/// Gets the working hours of the current user
/// </summary>
/// <returns>User working hours in UTC</returns>
[HttpGet]
[Route("mailboxSettings/workingHours")]
public async Task<ActionResult<WorkingStartEnd>> GetWorkingHours()
{
try
{
WorkingHours workingHours = await userGraphService.GetMyWorkingHours();
return new WorkingStartEnd(workingHours);
}
catch (Exception ex)
{
logger.LogError(ex, $"Error occurred while getting Working-hours by the user '{User.Identity.Name}'.");
throw;
}
}
/// <summary>
/// Get all upcoming reservations within set duration
/// </summary>
/// <param name="startDateTime">Duration start time</param>
/// <param name="endDateTime">Duration end time</param>
/// <param name="top">retrieves only the specified top number of results</param>
/// <param name="skip">skips the specified no of results before retrieving the top results</param>
/// <returns>List of all reservations as calendar events</returns>
[HttpGet]
[Route("upcomingReservations")]
public async Task<UpcomingReservationsResponse> GetUpcomingReservations(string startDateTime, string endDateTime, int top = 100, int skip = 0)
{
try
{
string filter = "IsOrganizer eq true";
IUserCalendarViewCollectionPage calendarPage = await userGraphService.GetMyEvents(startDateTime, endDateTime, filter, top, skip);
List<Event> events = calendarPage.CurrentPage as List<Event>;
var targetEvents = events.Where(e => e.Location != null && !string.IsNullOrWhiteSpace(e.Location.LocationUri));
List<CalendarEvent> calendarEventsList = targetEvents.Where(e => e.Locations.Any(l => l.LocationType == LocationType.ConferenceRoom))
.Select(e => new CalendarEvent(e)).ToList();
List<string> placeUpnsList = new List<string>();
calendarEventsList.Select(p => p.Location).ToList().ForEach(x => placeUpnsList.Add(x.LocationUri));
GraphExchangePlacesResponse exchangePlacesResponse = await buildingsService.GetPlacesByUpnsList(placeUpnsList);
Parallel.ForEach(calendarEventsList, calendarEvent =>
{
var targetPlace = exchangePlacesResponse.ExchangePlacesList?.FirstOrDefault(pl => pl.Identity.SameAs(calendarEvent.Location.LocationUri));
if (targetPlace != null)
{
calendarEvent.IsWorkspaceBooking = targetPlace.Type == PlaceType.Space;
calendarEvent.BuildingName = targetPlace.Building;
}
});
return new UpcomingReservationsResponse { Reservations = calendarEventsList, LoadMore = calendarPage.NextPageRequest != null };
}
catch (Exception ex)
{
logger.LogError(ex, $"Error occurred while getting Upcoming-reservations by the user '{User.Identity.Name}' for start-date:{startDateTime} and end-date:{endDateTime}.");
throw;
}
}
/// <summary>
/// Creates Calendar Event
/// </summary>
/// <param name="calendarEvent">Calendar Event specifications</param>
/// <returns></returns>
[HttpPost]
[Route("event")]
public async Task<ActionResult<CalendarEvent>> CreateEvent([FromBody] CalendarEventRequest calendarEvent)
{
try
{
userGraphService.SetPrincipalUserIdentity(User.Identity);
Event e = await userGraphService.CreateEvent(calendarEvent);
CalendarEvent returnEvent = new CalendarEvent(e);
if (calendarEvent.Location?.LocationEmailAddress != null)
{
GraphExchangePlacesResponse exchangePlacesResponse = await buildingsService.GetPlacesByUpnsList(new List<string> { calendarEvent.Location.LocationEmailAddress });
var targetPlace = exchangePlacesResponse.ExchangePlacesList?.FirstOrDefault();
if (targetPlace != null)
{
returnEvent.IsWorkspaceBooking = targetPlace.Type == PlaceType.Space;
returnEvent.BuildingName = targetPlace.Building;
}
}
return returnEvent;
}
catch (Exception ex)
{
logger.LogError(ex, $"Error occurred while creating an event for request: {JsonConvert.SerializeObject(calendarEvent)}.");
throw;
}
}
/// <summary>
/// Cancels or deletes the calendar event
/// </summary>
/// <param name="eventId">Id of the calendar event to cancel/delete in string</param>
/// <param name="messageComment">Comment for event cancellation message to inform participants</param>
/// <returns></returns>
[HttpGet]
[Route("events/{eventId}/deleteEvent")]
public async Task<ActionResult> DeleteEvent(string eventId, string messageComment)
{
try
{
await userGraphService.DeleteEvent(eventId, messageComment);
return NoContent();
}
catch (Exception ex)
{
logger.LogError(ex, $"Error occurred while deleting an event with id: {eventId}.");
throw;
}
}
}
}

Просмотреть файл

@ -0,0 +1,121 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using Converge.Helpers;
using Converge.Models;
using Converge.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Graph;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace Converge.Controllers
{
[Authorize]
[Route("api/v1.0/calendar")]
[ApiController]
public class CalendarV1Controller : Controller
{
/// <summary>
/// Send logs to telemetry service
/// </summary>
private readonly UserGraphService userGraphService;
private readonly BuildingsService buildingsService;
public CalendarV1Controller(UserGraphService graphService,
BuildingsService buildingsSvc)
{
this.userGraphService = graphService;
this.buildingsService = buildingsSvc;
}
/// <summary>
/// Gets the working hours of the current user
/// </summary>
/// <returns>User working hours in UTC</returns>
[HttpGet]
[Route("mailboxSettings/workingHours")]
public async Task<ActionResult<WorkingStartEnd>> GetWorkingHours()
{
WorkingHours workingHours = await userGraphService.GetMyWorkingHours();
return new WorkingStartEnd(workingHours);
}
/// <summary>
/// Get all upcoming reservations within set duration
/// </summary>
/// <param name="startDateTime">Duration start time</param>
/// <param name="endDateTime">Duration end time</param>
/// <param name="top">retrieves only the specified top number of results</param>
/// <param name="skip">skips the specified no of results before retrieving the top results</param>
/// <returns>List of all reservations as calendar events</returns>
[HttpGet]
[Route("upcomingReservations")]
public async Task<UpcomingReservationsResponse> GetUpcomingReservations(string startDateTime, string endDateTime, int top = 100, int skip = 0)
{
string filter = "IsOrganizer eq true";
IUserCalendarViewCollectionPage calendarPage = await userGraphService.GetMyEvents(startDateTime, endDateTime, filter, top, skip);
List<Event> events = calendarPage.CurrentPage as List<Event>;
var targetEvents = events.Where(e => e.Location != null && !string.IsNullOrWhiteSpace(e.Location.LocationUri));
List<CalendarEvent> calendarEventsList = targetEvents.Where(e => e.Locations.Any(l => l.LocationType == LocationType.ConferenceRoom))
.Select(e => new CalendarEvent(e)).ToList();
List<string> placeUpnsList = new List<string>();
calendarEventsList.Select(p => p.Location).ToList().ForEach(x => placeUpnsList.Add(x.LocationUri));
GraphExchangePlacesResponse exchangePlacesResponse = await buildingsService.GetPlacesByUpnsList(placeUpnsList);
Parallel.ForEach(calendarEventsList, calendarEvent =>
{
var targetPlace = exchangePlacesResponse.ExchangePlacesList?.FirstOrDefault(pl => pl.Identity.SameAs(calendarEvent.Location.LocationUri));
if (targetPlace != null)
{
calendarEvent.IsWorkspaceBooking = targetPlace.Type == PlaceType.Space;
calendarEvent.BuildingName = targetPlace.Building;
}
});
return new UpcomingReservationsResponse { Reservations = calendarEventsList, LoadMore = calendarPage.NextPageRequest != null };
}
/// <summary>
/// Creates Calendar Event
/// </summary>
/// <param name="calendarEvent">Calendar Event specifications</param>
/// <returns></returns>
[HttpPost]
[Route("event")]
public async Task<ActionResult<CalendarEvent>> CreateEvent([FromBody] CalendarEventRequest calendarEvent)
{
userGraphService.SetPrincipalUserIdentity(User.Identity);
Event e = await userGraphService.CreateEvent(calendarEvent);
CalendarEvent returnEvent = new CalendarEvent(e);
if (calendarEvent.Location?.LocationEmailAddress != null)
{
GraphExchangePlacesResponse exchangePlacesResponse = await buildingsService.GetPlacesByUpnsList(new List<string> { calendarEvent.Location.LocationEmailAddress });
var targetPlace = exchangePlacesResponse.ExchangePlacesList?.FirstOrDefault();
if (targetPlace != null)
{
returnEvent.IsWorkspaceBooking = targetPlace.Type == PlaceType.Space;
returnEvent.BuildingName = targetPlace.Building;
}
}
return returnEvent;
}
/// <summary>
/// Cancels or deletes the calendar event
/// </summary>
/// <param name="eventId">Id of the calendar event to cancel/delete in string</param>
/// <param name="messageComment">Comment for event cancellation message to inform participants</param>
/// <returns></returns>
[HttpGet]
[Route("events/{eventId}/deleteEvent")]
public async Task<ActionResult> DeleteEvent(string eventId, string messageComment)
{
await userGraphService.DeleteEvent(eventId, messageComment);
return NoContent();
}
}
}

Просмотреть файл

@ -0,0 +1,22 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using Converge.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace Converge.Controllers
{
[Authorize]
[ApiController]
[Route("api/v1.0/groupChat")]
public class GroupChatV1Controller : Controller
{
private readonly ChatGraphService graphChatService;
public GroupChatV1Controller(ChatGraphService paramGraphChatService)
{
graphChatService = paramGraphChatService;
}
}
}

Просмотреть файл

@ -1,438 +0,0 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using Converge.Helpers;
using Converge.Models;
using Converge.Models.Enums;
using Converge.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using Microsoft.Graph;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
namespace Converge.Controllers
{
[Authorize]
[Route("api/me")]
[ApiController]
public class MeController : Controller
{
/// <summary>
/// Send logs to telemetry service
/// </summary>
private readonly ILogger<MeController> logger;
private readonly UserGraphService userGraphService;
private readonly PredictionService predictionService;
private readonly IConfiguration configuration;
private readonly TelemetryService telemetryService;
private readonly BuildingsService buildingsService;
private readonly PlacesService placesService;
public MeController(ILogger<MeController> logger,
IConfiguration configuration,
UserGraphService userGraphService,
PredictionService predictionService,
TelemetryService telemetryService,
BuildingsService buildingsService,
PlacesService placesService)
{
this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
this.configuration = configuration;
this.userGraphService = userGraphService;
this.predictionService = predictionService;
this.telemetryService = telemetryService;
this.buildingsService = buildingsService;
this.placesService = placesService;
}
/// <summary>
/// Gets converge settings for the current user.
/// </summary>
/// <returns></returns>
[HttpGet]
[Route("convergeSettings")]
public async Task<ActionResult<ConvergeSettings>> GetMyConvergeSettings()
{
try
{
return await userGraphService.GetConvergeSettings();
}
catch (Exception ex)
{
logger.LogError(ex, $"Error while getting Converge-settings by the user '{User.Identity.Name}'.");
throw;
}
}
/// <summary>
/// Creates/Updates Converge settings for the current user.
/// </summary>
/// <param name="convergeSettings">Settings info</param>
/// <returns></returns>
[HttpPost]
[Route("convergeSettings")]
public async Task<ActionResult> SetMyConvergeSettings(ConvergeSettings convergeSettings)
{
try
{
if (string.IsNullOrEmpty(convergeSettings.ZipCode))
{
this.telemetryService.TrackEvent(TelemetryService.USER_NO_ZIP_CODE);
}
ConvergeSettings settings = await userGraphService.GetConvergeSettings();
if (settings == null)
{
await userGraphService.SaveConvergeSettings(convergeSettings, DataOperationType.IsAdd);
}
else
{
await userGraphService.SaveConvergeSettings(convergeSettings, DataOperationType.IsUpdate);
}
return Ok();
}
catch (Exception ex)
{
logger.LogError(ex, $"Error while setting Converge-settings by the user '{User.Identity.Name}' for request: {JsonConvert.SerializeObject(convergeSettings)}.");
throw;
}
}
/// <summary>
/// Gets current user's workgroup in the organization.
/// </summary>
/// <returns></returns>
[HttpGet]
[Route("workgroup")]
public async Task<List<DirectoryObject>> GetMyWorkgroup()
{
try
{
var result = new List<Microsoft.Graph.DirectoryObject>();
var manager = await userGraphService.GetMyManager();
if (manager != null)
{
result.Add(manager);
var colleagues = await userGraphService.GetReports(manager.UserPrincipalName);
result.AddRange(colleagues);
}
else
{
this.logger.LogInformation("Manager information is null.");
}
var reports = await userGraphService.GetMyReports();
if (reports != null)
{
result.AddRange(reports);
}
else
{
this.logger.LogInformation("Reports are null.");
}
var userId = User.FindFirst("http://schemas.microsoft.com/identity/claims/objectidentifier")?.Value;
return result.Where(u => u.Id != userId).ToList();
}
catch (Exception ex)
{
logger.LogError(ex, $"Error while getting Workgroup by the user '{User.Identity.Name}'.");
throw;
}
}
/// <summary>
/// Gets a list of people as suggestions for the current user.
/// </summary>
/// <returns></returns>
[HttpGet]
[Route("people")]
public async Task<List<Person>> GetMyPeople()
{
try
{
List<Person> people = await userGraphService.GetMyPeople();
if (people == null)
{
this.logger.LogInformation("People information is null.");
return new List<Person>();
}
string userPrincipalName = User.Claims.ToList().Find(claim => claim.Type == "preferred_username")?.Value;
userPrincipalName ??= User.FindFirst("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/upn")?.Value;
userPrincipalName ??= string.Empty;
Regex tenantRegex = new Regex(@"@(.+)");
MatchCollection matches = tenantRegex.Matches(userPrincipalName);
string tenant = (matches.Count > 0) ? matches[^1].Value : string.Empty;
people.RemoveAll(p => string.IsNullOrEmpty(p.UserPrincipalName) || p.UserPrincipalName.Equals(userPrincipalName) ||
(p.PersonType != null && !p.PersonType.Class.SameAs("Person")));
return people.Where(p => p.UserPrincipalName.EndsWith(tenant)).ToList();
}
catch (Exception ex)
{
logger.LogError(ex, $"Error while getting People-information by the user '{User.Identity.Name}'.");
throw;
}
}
/// <summary>
/// Current user's list of users.
/// </summary>
/// <returns></returns>
[HttpGet]
[Route("list")]
public async Task<List<DirectoryObject>> GetMyList()
{
try
{
ConvergeSettings convergeSettings = await userGraphService.GetConvergeSettings();
List<DirectoryObject> result = new List<DirectoryObject>();
if (convergeSettings.MyList == null)
{
this.logger.LogInformation("Users list is null.");
return result;
}
foreach (string upn in convergeSettings.MyList)
{
DirectoryObject user = await userGraphService.GetUser(upn);
if (user != null)
{
result.Add(user);
}
}
return result;
}
catch (Exception ex)
{
logger.LogError(ex, $"Error while getting Users-list by the user '{User.Identity.Name}'.");
throw;
}
}
/// <summary>
/// Get Recommended Locations to collaborate for the current user.
/// </summary>
/// <param name="year"></param>
/// <param name="month"></param>
/// <param name="day"></param>
/// <returns></returns>
[HttpGet]
[Route("recommendation")]
public async Task<string> GetMyRecommendedLocation(int year, int month, int day)
{
try
{
Microsoft.Graph.Calendar calendar = await userGraphService.GetMyConvergeCalendar();
if (calendar == null)
{
this.logger.LogInformation("user's calendar is null.");
return "Remote";
}
Event prediction = await userGraphService.GetMyConvergePrediction(calendar.Id, year, month, day);
return prediction?.Location?.DisplayName ?? "Remote";
}
catch (Exception ex)
{
logger.LogError(ex, $"Error while getting Recommended-location by the user '{User.Identity.Name}' for date {year}-{month}-{day}.");
throw;
}
}
/// <summary>
/// Sets up a new converge user by adding or updating the
/// converge settings, calendar and default location predictions
/// </summary>
/// <param name="convergeSettings">converge settings</param>
/// <returns></returns>
[HttpPost]
[Route("setup")]
public async Task SetupNewUser(ConvergeSettings convergeSettings)
{
try
{
ConvergeSettings settings = await userGraphService.GetConvergeSettings();
if (settings == null)
{
await userGraphService.SaveConvergeSettings(convergeSettings, DataOperationType.IsAdd);
}
else
{
await userGraphService.SaveConvergeSettings(convergeSettings, DataOperationType.IsUpdate);
}
var calendar = await userGraphService.GetMyConvergeCalendar();
if (calendar == null)
{
await userGraphService.CreateMyConvergeCalendar();
}
List<OutlookCategory> categories = await userGraphService.GetMyCalendarCategories();
if (categories.Find(c => c.DisplayName == userGraphService.ConvergeDisplayName) == null)
{
await userGraphService.CreateMyCalendarCategory(new OutlookCategory
{
DisplayName = userGraphService.ConvergeDisplayName,
Color = CategoryColor.Preset9,
});
}
var userId = User.FindFirst("http://schemas.microsoft.com/identity/claims/objectidentifier")?.Value;
WorkingHours workingHours = await userGraphService.GetMyWorkingHours();
PredictionMetrics predictionMetrics = new PredictionMetrics();
Dictionary<string, ExchangePlace> placesDictionary = new Dictionary<string, ExchangePlace>();
// Perform prediction for the given user.
await predictionService.PerformPrediction(userId, workingHours, placesDictionary, predictionMetrics);
//If there is a failure only 1 Exception is expected. Log the failure, but do not throw to the user. They can continue to use Converge.
if (predictionMetrics.ExceptionsList.Count > 0)
{
logger.LogError(predictionMetrics.ExceptionsList[0], $"Error while predicting future locations for request: {JsonConvert.SerializeObject(convergeSettings)}");
}
}
catch (Exception ex)
{
logger.LogError(ex, $"Error while setting-up New User for request: {JsonConvert.SerializeObject(convergeSettings)}.");
throw;
}
}
/// <summary>
/// Updates current user's predicted location.
/// </summary>
/// <param name="request"></param>
/// <returns></returns>
[HttpPut]
[Route("updatePredictedLocation")]
public async Task<ActionResult> UpdatePredictedLocationChosenByUser(UserPredictedLocationRequest request)
{
try
{
var userId = User.FindFirst("http://schemas.microsoft.com/identity/claims/objectidentifier")?.Value;
var startDate = new DateTime(request.Year, request.Month, request.Day);
bool isUpdated = await predictionService.UpdatePredictedLocationChosenByUser(startDate, userId, request.UserPredictedLocation);
return isUpdated ? Ok() : StatusCode(500);
}
catch (Exception ex)
{
logger.LogError(ex, $"Error while updating Predicted-location by the user '{User.Identity.Name}' for request: {JsonConvert.SerializeObject(request)}.");
throw;
}
}
/// <summary>
/// Gets converge calendar for the current user.
/// </summary>
/// <returns></returns>
[HttpGet]
[Route("convergeCalendar")]
public async Task<Microsoft.Graph.Calendar> GetMyConvergeCalendar()
{
try
{
return await userGraphService.GetMyConvergeCalendar();
}
catch (Exception ex)
{
logger.LogError(ex, $"Error while getting user's calendar by the user '{User.Identity.Name}'.");
throw;
}
}
/// <summary>
/// Current user's recent buildings.
/// </summary>
/// <returns>Building Basic Information of current user's recent buildings</returns>
[HttpGet]
[Route("recentBuildings")]
public async Task<List<BuildingBasicInfo>> GetRecentBuildings()
{
try
{
ConvergeSettings convergeSettings = await userGraphService.GetConvergeSettings();
if (convergeSettings == null)
{
logger.LogInformation($"Converge settings is unavailable for the user '{User.Identity.Name}'.");
return new List<BuildingBasicInfo>();
}
if (convergeSettings.RecentBuildingUpns == null)
{
logger.LogInformation($"There are no saved recent buildings for the user '{User.Identity.Name}'.");
return new List<BuildingBasicInfo>();
}
var recentBuildingUpns = convergeSettings.RecentBuildingUpns.Distinct().ToList();
List<BuildingBasicInfo> buildingsBasicInfoList = await buildingsService.GetBuildingsBasicInfo(recentBuildingUpns);
if (buildingsBasicInfoList.Count != recentBuildingUpns.Count)
{
var missingBuildings = recentBuildingUpns.Except(buildingsBasicInfoList.Select(x => x.Identity));
logger.LogInformation($"Unable to find Buildings by UPNs: {string.Join(", ", missingBuildings)}.");
}
else
{
logger.LogInformation($"Successfully found {buildingsBasicInfoList.Count} out of {recentBuildingUpns.Count} recent buildings.");
}
return buildingsBasicInfoList;
}
catch (Exception ex)
{
logger.LogError(ex, $"Error while getting Recent Buildings by the user '{User.Identity.Name}'.");
throw;
}
}
/// <summary>
/// Gets the detailed list of Current user's favorite campuses to collaborate
/// </summary>
/// <returns>Favorite campuses as a collection of Exchange Places</returns>
[HttpGet]
[Route("favoriteCampusesDetails")]
public async Task<List<ExchangePlace>> GetFavoriteCampuses()
{
try
{
ConvergeSettings convergeSettings = await userGraphService.GetConvergeSettings();
if(convergeSettings == null)
{
logger.LogInformation($"Converge settings is unavailable for the user '{User.Identity.Name}'.");
return new List<ExchangePlace>();
}
if (convergeSettings.FavoriteCampusesToCollaborate == null || convergeSettings.FavoriteCampusesToCollaborate.Count == 0)
{
logger.LogInformation($"There are no favorite campuses for the user '{User.Identity.Name}'.");
return new List<ExchangePlace>();
}
var favoritePlacesUpns = convergeSettings.FavoriteCampusesToCollaborate.Distinct().ToList();
var placesResponse = await placesService.GetPlacesByPlaceUpns(favoritePlacesUpns);
if (placesResponse.ExchangePlacesList.Count != favoritePlacesUpns.Count)
{
var missingBuildings = favoritePlacesUpns.Except(placesResponse.ExchangePlacesList.Select(x => x.Identity));
logger.LogInformation($"Unable to find favorite campuses by UPNs: {string.Join(", ", missingBuildings)}.");
}
else
{
logger.LogInformation($"Successfully found {placesResponse.ExchangePlacesList.Count} out of {favoritePlacesUpns.Count} favorite campuses.");
}
return placesResponse.ExchangePlacesList;
}
catch (Exception ex)
{
logger.LogError(ex, $"Error while getting favorite campuses by the user '{User.Identity.Name}'.");
throw;
}
}
}
}

Просмотреть файл

@ -0,0 +1,350 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using Converge.Helpers;
using Converge.Models;
using Converge.Models.Enums;
using Converge.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using Microsoft.Graph;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
namespace Converge.Controllers
{
[Authorize]
[Route("api/v1.0/me")]
[ApiController]
public class MeV1Controller : Controller
{
/// <summary>
/// Send logs to telemetry service
/// </summary>
private readonly ILogger<MeV1Controller> logger;
private readonly UserGraphService userGraphService;
private readonly PredictionService predictionService;
private readonly IConfiguration configuration;
private readonly TelemetryService telemetryService;
private readonly BuildingsService buildingsService;
private readonly PlacesService placesService;
public MeV1Controller(ILogger<MeV1Controller> logger,
IConfiguration configuration,
UserGraphService userGraphService,
PredictionService predictionService,
TelemetryService telemetryService,
BuildingsService buildingsService,
PlacesService placesService)
{
this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
this.configuration = configuration;
this.userGraphService = userGraphService;
this.predictionService = predictionService;
this.telemetryService = telemetryService;
this.buildingsService = buildingsService;
this.placesService = placesService;
}
/// <summary>
/// Gets converge settings for the current user.
/// </summary>
/// <returns></returns>
[HttpGet]
[Route("convergeSettings")]
public async Task<ActionResult<ConvergeSettings>> GetMyConvergeSettings()
{
return await userGraphService.GetConvergeSettings();
}
/// <summary>
/// Creates/Updates Converge settings for the current user.
/// </summary>
/// <param name="convergeSettings">Settings info</param>
/// <returns></returns>
[HttpPost]
[Route("convergeSettings")]
public async Task<ActionResult> SetMyConvergeSettings(ConvergeSettings convergeSettings)
{
if (string.IsNullOrEmpty(convergeSettings.ZipCode))
{
this.telemetryService.TrackEvent(TelemetryService.USER_NO_ZIP_CODE);
}
ConvergeSettings settings = await userGraphService.GetConvergeSettings();
if (settings == null)
{
await userGraphService.SaveConvergeSettings(convergeSettings, DataOperationType.IsAdd);
}
else
{
await userGraphService.SaveConvergeSettings(convergeSettings, DataOperationType.IsUpdate);
}
return Ok();
}
/// <summary>
/// Gets current user's workgroup in the organization.
/// </summary>
/// <returns></returns>
[HttpGet]
[Route("workgroup")]
public async Task<List<DirectoryObject>> GetMyWorkgroup()
{
var result = new List<Microsoft.Graph.DirectoryObject>();
var manager = await userGraphService.GetMyManager();
if (manager != null)
{
result.Add(manager);
var colleagues = await userGraphService.GetReports(manager.UserPrincipalName);
result.AddRange(colleagues);
}
else
{
this.logger.LogInformation("Manager information is null.");
}
var reports = await userGraphService.GetMyReports();
if (reports != null)
{
result.AddRange(reports);
}
else
{
this.logger.LogInformation("Reports are null.");
}
var userId = User.FindFirst("http://schemas.microsoft.com/identity/claims/objectidentifier")?.Value;
return result.Where(u => u.Id != userId).ToList();
}
/// <summary>
/// Gets a list of people as suggestions for the current user.
/// </summary>
/// <returns></returns>
[HttpGet]
[Route("people")]
public async Task<List<Person>> GetMyPeople()
{
List<Person> people = await userGraphService.GetMyPeople();
if (people == null)
{
this.logger.LogInformation("People information is null.");
return new List<Person>();
}
string userPrincipalName = User.Claims.ToList().Find(claim => claim.Type == "preferred_username")?.Value;
userPrincipalName ??= User.FindFirst("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/upn")?.Value;
userPrincipalName ??= string.Empty;
Regex tenantRegex = new Regex(@"@(.+)");
MatchCollection matches = tenantRegex.Matches(userPrincipalName);
string tenant = (matches.Count > 0) ? matches[^1].Value : string.Empty;
people.RemoveAll(p => string.IsNullOrEmpty(p.UserPrincipalName) || p.UserPrincipalName.Equals(userPrincipalName) ||
(p.PersonType != null && !p.PersonType.Class.SameAs("Person")));
return people.Where(p => p.UserPrincipalName.EndsWith(tenant)).ToList();
}
/// <summary>
/// Current user's list of users.
/// </summary>
/// <returns></returns>
[HttpGet]
[Route("list")]
public async Task<List<DirectoryObject>> GetMyList()
{
ConvergeSettings convergeSettings = await userGraphService.GetConvergeSettings();
List<DirectoryObject> result = new List<DirectoryObject>();
if (convergeSettings.MyList == null)
{
this.logger.LogInformation("Users list is null.");
return result;
}
foreach (string upn in convergeSettings.MyList)
{
DirectoryObject user = await userGraphService.GetUser(upn);
if (user != null)
{
result.Add(user);
}
}
return result;
}
/// <summary>
/// Get Recommended Locations to collaborate for the current user.
/// </summary>
/// <param name="year"></param>
/// <param name="month"></param>
/// <param name="day"></param>
/// <returns></returns>
[HttpGet]
[Route("recommendation")]
public async Task<string> GetMyRecommendedLocation(int year, int month, int day)
{
Microsoft.Graph.Calendar calendar = await userGraphService.GetMyConvergeCalendar();
if (calendar == null)
{
this.logger.LogInformation("user's calendar is null.");
return "Remote";
}
Event prediction = await userGraphService.GetMyConvergePrediction(calendar.Id, year, month, day);
return prediction?.Location?.DisplayName ?? "Remote";
}
/// <summary>
/// Sets up a new converge user by adding or updating the
/// converge settings, calendar and default location predictions
/// </summary>
/// <param name="convergeSettings">converge settings</param>
/// <returns></returns>
[HttpPost]
[Route("setup")]
public async Task SetupNewUser(ConvergeSettings convergeSettings)
{
ConvergeSettings settings = await userGraphService.GetConvergeSettings();
if (settings == null)
{
await userGraphService.SaveConvergeSettings(convergeSettings, DataOperationType.IsAdd);
}
else
{
await userGraphService.SaveConvergeSettings(convergeSettings, DataOperationType.IsUpdate);
}
var calendar = await userGraphService.GetMyConvergeCalendar();
if (calendar == null)
{
await userGraphService.CreateMyConvergeCalendar();
}
List<OutlookCategory> categories = await userGraphService.GetMyCalendarCategories();
if (categories.Find(c => c.DisplayName == userGraphService.ConvergeDisplayName) == null)
{
await userGraphService.CreateMyCalendarCategory(new OutlookCategory
{
DisplayName = userGraphService.ConvergeDisplayName,
Color = CategoryColor.Preset9,
});
}
var userId = User.FindFirst("http://schemas.microsoft.com/identity/claims/objectidentifier")?.Value;
WorkingHours workingHours = await userGraphService.GetMyWorkingHours();
PredictionMetrics predictionMetrics = new PredictionMetrics();
Dictionary<string, ExchangePlace> placesDictionary = new Dictionary<string, ExchangePlace>();
// Perform prediction for the given user.
await predictionService.PerformPrediction(userId, workingHours, placesDictionary, predictionMetrics);
//If there is a failure only 1 Exception is expected. Log the failure, but do not throw to the user. They can continue to use Converge.
if (predictionMetrics.ExceptionsList.Count > 0)
{
logger.LogError(predictionMetrics.ExceptionsList[0], $"Error while predicting future locations for request: {JsonConvert.SerializeObject(convergeSettings)}");
}
}
/// <summary>
/// Updates current user's predicted location.
/// </summary>
/// <param name="request"></param>
/// <returns></returns>
[HttpPut]
[Route("updatePredictedLocation")]
public async Task<ActionResult> UpdatePredictedLocationChosenByUser(UserPredictedLocationRequest request)
{
var userId = User.FindFirst("http://schemas.microsoft.com/identity/claims/objectidentifier")?.Value;
var startDate = new DateTime(request.Year, request.Month, request.Day);
bool isUpdated = await predictionService.UpdatePredictedLocationChosenByUser(startDate, userId, request.UserPredictedLocation);
return isUpdated ? Ok() : StatusCode(500);
}
/// <summary>
/// Gets converge calendar for the current user.
/// </summary>
/// <returns></returns>
[HttpGet]
[Route("convergeCalendar")]
public async Task<Microsoft.Graph.Calendar> GetMyConvergeCalendar()
{
return await userGraphService.GetMyConvergeCalendar();
}
/// <summary>
/// Current user's recent buildings.
/// </summary>
/// <returns>Building Basic Information of current user's recent buildings</returns>
[HttpGet]
[Route("recentBuildings")]
public async Task<List<BuildingBasicInfo>> GetRecentBuildings()
{
ConvergeSettings convergeSettings = await userGraphService.GetConvergeSettings();
if (convergeSettings == null)
{
logger.LogInformation($"Converge settings is unavailable for the user '{User.Identity.Name}'.");
return new List<BuildingBasicInfo>();
}
if (convergeSettings.RecentBuildingUpns == null)
{
logger.LogInformation($"There are no saved recent buildings for the user '{User.Identity.Name}'.");
return new List<BuildingBasicInfo>();
}
var recentBuildingUpns = convergeSettings.RecentBuildingUpns.Distinct().ToList();
List<BuildingBasicInfo> buildingsBasicInfoList = await buildingsService.GetBuildingsBasicInfo(recentBuildingUpns);
if (buildingsBasicInfoList.Count != recentBuildingUpns.Count)
{
var missingBuildings = recentBuildingUpns.Except(buildingsBasicInfoList.Select(x => x.Identity));
logger.LogInformation($"Unable to find Buildings by UPNs: {string.Join(", ", missingBuildings)}.");
}
else
{
logger.LogInformation($"Successfully found {buildingsBasicInfoList.Count} out of {recentBuildingUpns.Count} recent buildings.");
}
return buildingsBasicInfoList;
}
/// <summary>
/// Gets the detailed list of Current user's favorite campuses to collaborate
/// </summary>
/// <returns>Favorite campuses as a collection of Exchange Places</returns>
[HttpGet]
[Route("favoriteCampusesDetails")]
public async Task<List<ExchangePlace>> GetFavoriteCampuses()
{
ConvergeSettings convergeSettings = await userGraphService.GetConvergeSettings();
if(convergeSettings == null)
{
logger.LogInformation($"Converge settings is unavailable for the user '{User.Identity.Name}'.");
return new List<ExchangePlace>();
}
if (convergeSettings.FavoriteCampusesToCollaborate == null || convergeSettings.FavoriteCampusesToCollaborate.Count == 0)
{
logger.LogInformation($"There are no favorite campuses for the user '{User.Identity.Name}'.");
return new List<ExchangePlace>();
}
var favoritePlacesUpns = convergeSettings.FavoriteCampusesToCollaborate.Distinct().ToList();
var placesResponse = await placesService.GetPlacesByPlaceUpns(favoritePlacesUpns);
if (placesResponse.ExchangePlacesList.Count != favoritePlacesUpns.Count)
{
var missingBuildings = favoritePlacesUpns.Except(placesResponse.ExchangePlacesList.Select(x => x.Identity));
logger.LogInformation($"Unable to find favorite campuses by UPNs: {string.Join(", ", missingBuildings)}.");
}
else
{
logger.LogInformation($"Successfully found {placesResponse.ExchangePlacesList.Count} out of {favoritePlacesUpns.Count} favorite campuses.");
}
return placesResponse.ExchangePlacesList;
}
}
}

Просмотреть файл

@ -5,7 +5,6 @@ using Converge.Models;
using Converge.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
@ -13,16 +12,14 @@ using System.Threading.Tasks;
namespace Converge.Controllers
{
[Authorize]
[Route("api/places")]
[Route("api/v1.0/places")]
[ApiController]
public class PlacesController : Controller
public class PlacesV1Controller : Controller
{
private readonly ILogger<PlacesController> logger;
private readonly PlacesService placesService;
public PlacesController(ILogger<PlacesController> logger, PlacesService placesService)
public PlacesV1Controller(PlacesService placesService)
{
this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
this.placesService = placesService;
}
@ -38,15 +35,7 @@ namespace Converge.Controllers
[Route("{upn}/maxReserved")]
public async Task<ActionResult<int>> GetMaxReserved(string upn, string start, string end)
{
try
{
return await placesService.GetMaxReserved(upn, start, end);
}
catch (Exception ex)
{
logger.LogError(ex, $"Error occurred while getting Max-Reserved for upn:{upn} with start:{start} and end:{end}.");
throw;
}
return await placesService.GetMaxReserved(upn, start, end);
}
/// <summary>
@ -60,15 +49,7 @@ namespace Converge.Controllers
[Route("{upn}/availability")]
public async Task<ActionResult<bool>> GetAvailability(string upn, string start, string end)
{
try
{
return await placesService.GetAvailability(upn, start, end);
}
catch (Exception ex)
{
logger.LogError(ex, $"Error occurred while getting Availability for upn:{upn} with start:{start} and end:{end}.");
throw;
}
return await placesService.GetAvailability(upn, start, end);
}
/// <summary>
@ -82,15 +63,7 @@ namespace Converge.Controllers
[Route("{upn}/details")]
public async Task<ActionResult<ExchangePlace>> GetPlaceDetails(string upn, DateTime start, DateTime end)
{
try
{
return await placesService.GetPlace(upn, start, end);
}
catch (Exception ex)
{
logger.LogError(ex, $"Error occurred while getting Place-details for placeUpn:{upn} with start:{start} and end:{end}.");
throw;
}
return await placesService.GetPlace(upn, start, end);
}
/// <summary>
@ -102,15 +75,7 @@ namespace Converge.Controllers
[Route("{sharePointID}/photos")]
public async Task<ActionResult<List<ExchangePlacePhoto>>> GetPlacePhotos(string sharePointID)
{
try
{
return await placesService.GetPlacePhotos(sharePointID);
}
catch (Exception ex)
{
logger.LogError(ex, $"Error occurred while getting place photos for sharepoint-id:{sharePointID}.");
throw;
}
return await placesService.GetPlacePhotos(sharePointID);
}
}
}

Просмотреть файл

@ -1,56 +0,0 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using Converge.Models;
using Converge.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using System;
using System.Threading.Tasks;
namespace Converge.Controllers
{
[Authorize]
[Route("api/route")]
[ApiController]
public class RouteController : Controller
{
private readonly ILogger<RouteController> logger;
private readonly RouteService routeService;
public RouteController(ILogger<RouteController> logger, RouteService routeService)
{
this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
this.routeService = routeService;
}
/// <summary>
/// Gets the travel times on various modes
/// between two different places
/// </summary>
/// <param name="start">start denotes "the place from"</param>
/// <param name="end">end denotes "the place to"</param>
/// <returns></returns>
[HttpGet]
[Route("travelTime")]
public async Task<ActionResult<RouteResponse>> GetTravelTimes(string start, string end)
{
try
{
double transit = await routeService.GetTransitTime(start, end);
double drive = await routeService.GetDriveTime(start, end);
return new RouteResponse
{
TransitTravelTimeInSeconds = transit,
DriveTravelTimeInSeconds = drive,
};
}
catch (Exception ex)
{
logger.LogError(ex, $"Error occurred while getting Travel-times by the user '{User.Identity.Name}' with start:{start} and end:{end}.");
throw;
}
}
}
}

Просмотреть файл

@ -0,0 +1,44 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using Converge.Models;
using Converge.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using System.Threading.Tasks;
namespace Converge.Controllers
{
[Authorize]
[Route("api/v1.0/route")]
[ApiController]
public class RouteV1Controller : Controller
{
private readonly RouteService routeService;
public RouteV1Controller(RouteService routeService)
{
this.routeService = routeService;
}
/// <summary>
/// Gets the travel times on various modes
/// between two different places
/// </summary>
/// <param name="start">start denotes "the place from"</param>
/// <param name="end">end denotes "the place to"</param>
/// <returns></returns>
[HttpGet]
[Route("travelTime")]
public async Task<ActionResult<RouteResponse>> GetTravelTimes(string start, string end)
{
double transit = await routeService.GetTransitTime(start, end);
double drive = await routeService.GetDriveTime(start, end);
return new RouteResponse
{
TransitTravelTimeInSeconds = transit,
DriveTravelTimeInSeconds = drive,
};
}
}
}

Просмотреть файл

@ -5,25 +5,20 @@ using Converge.Models;
using Converge.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace Converge.Controllers
{
[Authorize]
[Route("api/search")]
[Route("api/v1.0/search")]
[ApiController]
public class SearchController : Controller
public class SearchV1Controller : Controller
{
private readonly ILogger<SearchController> logger;
private readonly SearchService searchService;
public SearchController(ILogger<SearchController> logger, SearchService searchSvc)
public SearchV1Controller(SearchService searchSvc)
{
this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
searchService = searchSvc;
}
@ -40,19 +35,11 @@ namespace Converge.Controllers
{
searchService.SetPrincipalUserIdentity(User.Identity);
try
List<VenuesToCollaborate> venuesToCollaborateList = await searchService.GetVenuesToCollaborate(request);
return new VenuesToCollaborateResponse()
{
List<VenuesToCollaborate> venuesToCollaborateList = await searchService.GetVenuesToCollaborate(request);
return new VenuesToCollaborateResponse()
{
VenuesToCollaborateList = venuesToCollaborateList
};
}
catch (Exception ex)
{
logger.LogError(ex, $"Error occurred while getting Venues list for request: {JsonConvert.SerializeObject(request)}.");
throw;
}
VenuesToCollaborateList = venuesToCollaborateList
};
}
/// <summary>
@ -65,15 +52,7 @@ namespace Converge.Controllers
[Route("venues/{venueId}/details")]
public async Task<VenueDetails> GetVenueDetails(string venueId)
{
try
{
return await searchService.GetVenueDetails(venueId);
}
catch (Exception ex)
{
logger.LogError(ex, $"Error occurred while getting Venue details for venue-id: {venueId}.");
throw;
}
return await searchService.GetVenueDetails(venueId);
}
/// <summary>
@ -87,15 +66,7 @@ namespace Converge.Controllers
[Route("venues/{venueId}/reviews")]
public async Task<ServiceJsonResponse> GetVenueReviews(string venueId)
{
try
{
return await searchService.GetVenueReviews(venueId);
}
catch (Exception ex)
{
logger.LogError(ex, $"Error occurred while getting Venue reviews for venue-id: {venueId}.");
throw;
}
return await searchService.GetVenueReviews(venueId);
}
/// <summary>
@ -110,15 +81,7 @@ namespace Converge.Controllers
{
searchService.SetPrincipalUserIdentity(User.Identity);
try
{
return await searchService.GetCampusesListToCollaborate(request);
}
catch (Exception ex)
{
logger.LogError(ex, $"Error occurred while getting Campuses for request: {JsonConvert.SerializeObject(request)}.");
throw;
}
return await searchService.GetCampusesListToCollaborate(request);
}
}
}

Просмотреть файл

@ -1,50 +0,0 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using Converge.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using System;
namespace Converge.Controllers
{
[Route("api/settings")]
[ApiController]
public class SettingsController : Controller
{
private readonly ILogger<SettingsController> logger;
private readonly IConfiguration configuration;
public SettingsController(ILogger<SettingsController> logger, IConfiguration configuration)
{
this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
this.configuration = configuration;
}
/// <summary>
/// Gets the Application Settings
/// </summary>
/// <returns></returns>
[HttpGet("appSettings")]
public ActionResult<AppSettings> GetAppSettings()
{
try
{
var result = new AppSettings
{
ClientId = this.configuration["AzureAd:ClientId"],
InstrumentationKey = this.configuration["AppInsightsInstrumentationKey"],
BingAPIKey = this.configuration["BingMapsAPIKey"],
AppBanner = this.configuration["AppBannerMessage"]
};
return Ok(result);
}
catch (Exception ex)
{
logger.LogError(ex, $"Error occurred while getting App-settings by the user '{User.Identity.Name}'.");
throw;
}
}
}
}

Просмотреть файл

@ -0,0 +1,40 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using Converge.Models;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
namespace Converge.Controllers
{
[Authorize]
[Route("api/v1.0/settings")]
[ApiController]
public class SettingsV1Controller : Controller
{
private readonly IConfiguration configuration;
public SettingsV1Controller(IConfiguration configuration)
{
this.configuration = configuration;
}
/// <summary>
/// Gets the Application Settings
/// </summary>
/// <returns></returns>
[HttpGet("appSettings")]
public ActionResult<AppSettings> GetAppSettings()
{
var result = new AppSettings
{
ClientId = this.configuration["AzureAd:ClientId"],
InstrumentationKey = this.configuration["AppInsightsInstrumentationKey"],
BingAPIKey = this.configuration["BingMapsAPIKey"],
AppBanner = this.configuration["AppBannerMessage"]
};
return Ok(result);
}
}
}

Просмотреть файл

@ -1,239 +0,0 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using AutoWrapper.Filters;
using Converge.Models;
using Converge.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using Microsoft.Graph;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace Converge.Controllers
{
[Authorize]
[Route("api/users")]
[ApiController]
public class UsersController : Controller
{
private readonly ILogger<UsersController> logger;
private readonly AppGraphService appGraphService;
private readonly UserGraphService userGraphService;
public UsersController(ILogger<UsersController> logger, AppGraphService appGraphService, UserGraphService userGraphService)
{
this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
this.appGraphService = appGraphService;
this.userGraphService = userGraphService;
}
/// <summary>
/// Gets the User identified by the provided upn
/// </summary>
/// <param name="upn">upn</param>
/// <returns></returns>
[HttpGet]
[Route("{upn}")]
public async Task<ActionResult<SerializedUser>> GetUser(string upn)
{
try
{
var user = await userGraphService.GetUserByUpn(upn);
if (user == null)
{
return NotFound();
}
return new SerializedUser(user);
}
catch (Exception ex)
{
logger.LogError(ex, $"Error while getting the user for upn: {upn}.");
throw;
}
}
/// <summary>
/// Gets the
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
[HttpGet]
[Route("{id}/presence")]
public async Task<ActionResult<ApiPresence>> GetUserPresence(string id)
{
try
{
return await userGraphService.GetPresence(id);
}
catch (Exception ex)
{
logger.LogError(ex, $"Error while getting User-presence for id: {id}.");
throw;
}
}
/// <summary>
/// Gets the profile photo of the user
/// identified by the provided id
/// </summary>
/// <param name="id">id</param>
/// <returns></returns>
[HttpGet]
[Route("{id}/photo")]
[AutoWrapIgnore]
public async Task<ActionResult<System.IO.Stream>> GetPersonPhoto(string id)
{
try
{
return await userGraphService.GetUserPhoto(id);
}
catch (Exception ex)
{
logger.LogError(ex, $"Error while getting User-photo for id: {id}.");
throw;
}
}
/// <summary>
/// Gets the user profile of the user
/// identified by the provided id
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
[HttpGet]
[Route("{id}/userProfile")]
public async Task<UserProfile> GetUserProfile(string id)
{
try
{
return await userGraphService.GetUserProfile(id);
}
catch (Exception ex)
{
logger.LogError(ex, $"Error while getting User-profile for id: {id}.");
throw;
}
}
/// <summary>
/// Gets specific User Location for the given date
/// </summary>
/// <param name="id">User id</param>
/// <param name="year">year</param>
/// <param name="month">month</param>
/// <param name="day">day</param>
/// <returns></returns>
[HttpGet]
[Route("{id}/location")]
public async Task<string> GetUserLocation(string id, int year, int month, int day)
{
try
{
Calendar calendar = await appGraphService.GetConvergeCalendar(id);
if (calendar == null)
{
return "Unknown";
}
return await appGraphService.GetUserLocation(id, calendar.Id, year, month, day);
}
catch (Exception ex)
{
logger.LogError(ex, $"Error while getting User-location for id: '{id}' for date {year}-{month}-{day}.");
throw;
}
}
/// <summary>
/// Gets the co-ordinates of multiple users
/// </summary>
/// <param name="request"></param>
/// <returns></returns>
[HttpPost]
[Route("coordinates")]
public async Task<UserCoordinatesResponse> GetUsersCoordinates([FromBody] MultiUserAvailableTimesRequest request)
{
try
{
userGraphService.SetPrincipalUserIdentity(User.Identity);
List<UserCoordinates> userCoordinatesList = await userGraphService.GetUsersCoordinates(request);
return new UserCoordinatesResponse
{
UserCoordinatesList = userCoordinatesList,
};
}
catch (Exception ex)
{
logger.LogError(ex, $"Error while getting User-coordinates for request: {JsonConvert.SerializeObject(request)}.");
throw;
}
}
/// <summary>
/// Search Users that match the provided search string
/// </summary>
/// <param name="searchString"></param>
/// <returns>List of users <see cref="User"/> whose DisplayName/UserPrincipalName starts with input string></returns>
[HttpGet]
[Route("search/{searchString}")]
public async Task<ActionResult<List<User>>> 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;
}
}
/// <summary>
/// Search Users that match the provided search string and
/// filter results based on the provided query options
/// </summary>
/// <param name="searchString">search string</param>
/// <param name="queryOptions">query options</param>
/// <returns>List of users <see cref="User"/> whose DisplayName/UserPrincipalName starts with input string></returns>
[HttpGet]
[Route("searchAndPage")]
public async Task<ActionResult<UserSearchPaginatedResponse>> SearchUsersByPage(string searchString, string queryOptions)
{
try
{
return await userGraphService.SearchUsers(searchString, queryOptions, User);
}
catch (Exception ex)
{
logger.LogError(ex, $"Error while searching Users for '{searchString}' and queryOptions: {queryOptions}.");
throw;
}
}
/// <summary>
/// Gets availability information for multiple users
/// </summary>
/// <param name="request"></param>
/// <returns></returns>
[HttpPost]
[Route("multi/availableTimes")]
public async Task<MultiUserAvailableTimesResponse> GetMultiUserAvailabilityTimes([FromBody] MultiUserAvailableTimesRequest request)
{
try
{
return await userGraphService.GetMultiUserAvailabilityTimes(request);
}
catch (Exception ex)
{
logger.LogError(ex, $"Error while getting multi-user available-times for request: {JsonConvert.SerializeObject(request)}.");
throw;
}
}
}
}

Просмотреть файл

@ -0,0 +1,148 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using AutoWrapper.Filters;
using Converge.Models;
using Converge.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Graph;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace Converge.Controllers
{
[Authorize]
[Route("api/v1.0/users")]
[ApiController]
public class UsersV1Controller : Controller
{
private readonly AppGraphService appGraphService;
private readonly UserGraphService userGraphService;
public UsersV1Controller(AppGraphService appGraphService, UserGraphService userGraphService)
{
this.appGraphService = appGraphService;
this.userGraphService = userGraphService;
}
/// <summary>
/// Gets the User identified by the provided upn
/// </summary>
/// <param name="upn">upn</param>
/// <returns></returns>
[HttpGet]
[Route("{upn}")]
public async Task<ActionResult<SerializedUser>> GetUser(string upn)
{
var user = await userGraphService.GetUserByUpn(upn);
if (user == null)
{
return NotFound();
}
return new SerializedUser(user);
}
/// <summary>
/// Gets the
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
[HttpGet]
[Route("{id}/presence")]
public async Task<ActionResult<ApiPresence>> GetUserPresence(string id)
{
return await userGraphService.GetPresence(id);
}
/// <summary>
/// Gets the profile photo of the user
/// identified by the provided id
/// </summary>
/// <param name="id">id</param>
/// <returns></returns>
[HttpGet]
[Route("{id}/photo")]
[AutoWrapIgnore]
public async Task<ActionResult<System.IO.Stream>> GetPersonPhoto(string id)
{
return await userGraphService.GetUserPhoto(id);
}
/// <summary>
/// Gets the user profile of the user
/// identified by the provided id
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
[HttpGet]
[Route("{id}/userProfile")]
public async Task<UserProfile> GetUserProfile(string id)
{
return await userGraphService.GetUserProfile(id);
}
/// <summary>
/// Gets specific User Location for the given date
/// </summary>
/// <param name="id">User id</param>
/// <param name="year">year</param>
/// <param name="month">month</param>
/// <param name="day">day</param>
/// <returns></returns>
[HttpGet]
[Route("{id}/location")]
public async Task<string> GetUserLocation(string id, int year, int month, int day)
{
Calendar calendar = await appGraphService.GetConvergeCalendar(id);
if (calendar == null)
{
return "Unknown";
}
return await appGraphService.GetUserLocation(id, calendar.Id, year, month, day);
}
/// <summary>
/// Gets the co-ordinates of multiple users
/// </summary>
/// <param name="request"></param>
/// <returns></returns>
[HttpPost]
[Route("coordinates")]
public async Task<UserCoordinatesResponse> GetUsersCoordinates([FromBody] MultiUserAvailableTimesRequest request)
{
userGraphService.SetPrincipalUserIdentity(User.Identity);
List<UserCoordinates> userCoordinatesList = await userGraphService.GetUsersCoordinates(request);
return new UserCoordinatesResponse
{
UserCoordinatesList = userCoordinatesList,
};
}
/// <summary>
/// Search Users that match the provided search string
/// </summary>
/// <param name="searchString">The string to user for user search</param>
/// <param name="queryOptions">query options</param>
/// <returns>List of users <see cref="User"/> whose DisplayName/UserPrincipalName starts with input string></returns>
[HttpGet]
[Route("search")]
public async Task<ActionResult<UserSearchPaginatedResponse>> SearchUsers(string searchString, string queryOptions)
{
return await userGraphService.SearchUsers(searchString, queryOptions, User);
}
/// <summary>
/// Gets availability information for multiple users
/// </summary>
/// <param name="request"></param>
/// <returns></returns>
[HttpPost]
[Route("multi/availableTimes")]
public async Task<MultiUserAvailableTimesResponse> GetMultiUserAvailabilityTimes([FromBody] MultiUserAvailableTimesRequest request)
{
return await userGraphService.GetMultiUserAvailabilityTimes(request);
}
}
}

Просмотреть файл

@ -77,40 +77,8 @@
<Message Importance="high" Text="Restoring dependencies using 'npm'. This may take several minutes..." />
<Exec WorkingDirectory="$(SpaRoot)" Command="npm install" />
</Target>
<Target Name="PublishRunWebpackStaging" AfterTargets="ComputeFilesToPublish" Condition=" '$(ASPNETCORE_ENVIRONMENT)' == 'Staging'">
<!-- As part of publishing, ensure the JS resources are freshly built in production mode -->
<Exec WorkingDirectory="$(SpaRoot)" Command="npm install" />
<Exec WorkingDirectory="$(SpaRoot)" Command="npm run build-stage" />
<!-- Include the newly-built files in the publish output -->
<ItemGroup>
<DistFiles Include="$(SpaRoot)build\**" />
<ResolvedFileToPublish Include="@(DistFiles->'%(FullPath)')" Exclude="@(ResolvedFileToPublish)">
<RelativePath>%(DistFiles.Identity)</RelativePath>
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
</ResolvedFileToPublish>
</ItemGroup>
</Target>
<Target Name="PublishRunWebpackTest" AfterTargets="ComputeFilesToPublish" Condition=" '$(ASPNETCORE_ENVIRONMENT)' == 'Test'">
<!-- As part of publishing, ensure the JS resources are freshly built in production mode -->
<Exec WorkingDirectory="$(SpaRoot)" Command="npm install" />
<Exec WorkingDirectory="$(SpaRoot)" Command="npm run build-test" />
<!-- Include the newly-built files in the publish output -->
<ItemGroup>
<DistFiles Include="$(SpaRoot)build\**" />
<ResolvedFileToPublish Include="@(DistFiles->'%(FullPath)')" Exclude="@(ResolvedFileToPublish)">
<RelativePath>%(DistFiles.Identity)</RelativePath>
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
</ResolvedFileToPublish>
</ItemGroup>
</Target>
<Target Name="PublishRunWebpack" AfterTargets="ComputeFilesToPublish" Condition=" '$(ASPNETCORE_ENVIRONMENT)' == 'Production'">
<Target Name="PublishRunWebpack" AfterTargets="ComputeFilesToPublish" Condition=" '$(ASPNETCORE_ENVIRONMENT)' != 'Development'">
<!-- As part of publishing, ensure the JS resources are freshly built in production mode -->
<Exec WorkingDirectory="$(SpaRoot)" Command="npm install" />
<Exec WorkingDirectory="$(SpaRoot)" Command="npm run build" />

Просмотреть файл

@ -25,6 +25,17 @@ namespace Converge.Helpers
return (string.Compare(firstString, secondString, true) == 0);
}
/// <summary>
/// Case-insensitive Sub-string functionality.
/// </summary>
/// <param name="actualString"></param>
/// <param name="partString"></param>
/// <returns></returns>
public static bool Comprises(this string actualString, string partString)
{
return actualString.ToLower().Contains(partString.ToLower());
}
/// <summary>
/// Case-insensitive search for an element in a given collection.
/// </summary>

Просмотреть файл

@ -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;
}
}
}

Просмотреть файл

@ -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);

Просмотреть файл

@ -0,0 +1,19 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using Microsoft.AspNetCore.Builder;
namespace Converge.Middleware
{
// Extension method used to add the middleware to the HTTP request pipeline.
public static class MiddlewareExtension
{
public static IApplicationBuilder UseMiddlewareExtensions(this IApplicationBuilder builder)
{
//Add all Middleware extension classes here.
builder.UseMiddleware<UserConsentErrorMiddleware>();
return builder;
}
}
}

Просмотреть файл

@ -0,0 +1,47 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using Converge.Models;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Options;
using Microsoft.Identity.Web;
using System;
using System.Threading.Tasks;
namespace Converge.Middleware
{
// You may need to install the Microsoft.AspNetCore.Http.Abstractions package into your project
public class UserConsentErrorMiddleware
{
private readonly RequestDelegate _next;
private readonly IOptions<MicrosoftGraphOptions> _graphOptions;
public UserConsentErrorMiddleware(RequestDelegate next, IOptions<MicrosoftGraphOptions> graphOptions)
{
_next = next;
_graphOptions = graphOptions;
}
public async Task InvokeAsync(HttpContext httpContext, ITokenAcquisition tokenAcquisition)
{
try
{
await _next(httpContext);
}
catch (Exception ex)
{
if (ex is MicrosoftIdentityWebChallengeUserException || ex.InnerException is MicrosoftIdentityWebChallengeUserException)
{
var accessException = ex as MicrosoftIdentityWebChallengeUserException;
accessException ??= ex.InnerException as MicrosoftIdentityWebChallengeUserException;
tokenAcquisition.ReplyForbiddenWithWwwAuthenticateHeader(Constant.ScopesToAccessGraphApi, accessException.MsalUiRequiredException);
return;
}
else
{
throw;
}
}
}
}
}

Просмотреть файл

@ -9,13 +9,10 @@ namespace Converge.Models
public class BasicBuildingsResponse
{
public List<BuildingBasicInfo> BuildingsList { get; set; }
public QueryOption SkipToken { get; set; }
public BasicBuildingsResponse(List<BuildingBasicInfo> buildingsList, QueryOption skipToken = null)
public BasicBuildingsResponse(List<BuildingBasicInfo> buildingsList)
{
BuildingsList = buildingsList;
SkipToken = skipToken;
}
}
}

Просмотреть файл

@ -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<BuildingBasicInfo> BuildingInfoList { get; set; }
public QueryOption SkipToken { get; set; }
public BuildingSearchInfo(List<BuildingBasicInfo> buildingInfoList, QueryOption skipToken)
public BuildingSearchInfo(List<BuildingBasicInfo> buildingInfoList)
{
BuildingInfoList = buildingInfoList;
SkipToken = skipToken;
}
}
}

Просмотреть файл

@ -1,8 +1,6 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using System;
namespace Converge.Models
{
public static class Constant
@ -18,7 +16,13 @@ namespace Converge.Models
"Presence.Read.All",
"User.Read.All",
"User.ReadWrite",
"Place.Read.All",
"Place.Read.All"
};
public static readonly string[] GraphChatScopes = new string[] {
"Chat.Create",
"Chat.ReadWrite",
"ChatMessage.Send"
};
public static string TimeZonePST = "Pacific Standard Time";

Просмотреть файл

@ -0,0 +1,10 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
namespace Converge.Models.Enums
{
public enum SharePointContentType
{
ExchangePlacePhotoUrls,
}
}

Просмотреть файл

@ -10,12 +10,9 @@ namespace Converge.Models
{
public List<Place> RoomsList { get; set; }
public QueryOption SkipToken { get; set; }
public GraphRoomsListResponse(List<Place> roomsList, QueryOption skipToken)
public GraphRoomsListResponse(List<Place> roomsList)
{
RoomsList = roomsList;
SkipToken = skipToken;
}
}
}

Некоторые файлы не были показаны из-за слишком большого количества измененных файлов Показать больше