diff --git a/packages/initiatives/package-lock.json b/packages/initiatives/package-lock.json index 3cc080e3175..407397bb750 100644 --- a/packages/initiatives/package-lock.json +++ b/packages/initiatives/package-lock.json @@ -3,46 +3,46 @@ "lockfileVersion": 1, "dependencies": { "@esri/arcgis-rest-auth": { - "version": "1.17.0", - "resolved": "https://registry.npmjs.org/@esri/arcgis-rest-auth/-/arcgis-rest-auth-1.17.0.tgz", - "integrity": "sha512-n9Omh1/JlWMfuQ98ZqdN+4Ku24CHaPxJkAHuz0AQwAau/SFH5qrlAC+yowFAI70nWPqB7IB4C7SaVd3ASvgK7Q==", + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/@esri/arcgis-rest-auth/-/arcgis-rest-auth-1.18.0.tgz", + "integrity": "sha512-xIeTZUPWYXXwRv1Nj8SmSOKopWautRKeSg9uHf5Q/GQGVDwtUJ7TGegkgpFnm3pTwsg8j6VfkWvHoCnGSzYveQ==", "requires": { "tslib": "^1.9.3" } }, "@esri/arcgis-rest-common-types": { - "version": "1.17.0", - "resolved": "https://registry.npmjs.org/@esri/arcgis-rest-common-types/-/arcgis-rest-common-types-1.17.0.tgz", - "integrity": "sha512-WJo3nYUEctkpwiECeeaii5zlfqYh1haMtTY6Z9yp9GxIRrKrzYWs4ud+SGnk1zWeFT6MdhHan5eBdSorpl++Bg==" + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/@esri/arcgis-rest-common-types/-/arcgis-rest-common-types-1.18.0.tgz", + "integrity": "sha512-ESSlZAP8Xd50jqlypRWZ99LVHiD+PC8zHiMJd91k1nurtFuUI6DWZ4WU3U/6LixK5bKQLKJNCGK0cP4Hk2whZw==" }, "@esri/arcgis-rest-groups": { - "version": "1.17.0", - "resolved": "https://registry.npmjs.org/@esri/arcgis-rest-groups/-/arcgis-rest-groups-1.17.0.tgz", - "integrity": "sha512-9pEtqmoqKIxW1/i3eQ2Nc7iN+7+3SWCMBNSICp5kC57N3TnD22zTqB817T29QXo0fK0T2sEciknz7VgjEWz/OA==", + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/@esri/arcgis-rest-groups/-/arcgis-rest-groups-1.18.0.tgz", + "integrity": "sha512-jhhtzHno/aYg9G1GO5w4m6zM6x7Jej3Nnid0ia4DYB4G0prTw1CTe0RwHa5/obV1DgBV/y7xCf0JWIgquZMq2w==", "requires": { "tslib": "^1.9.3" } }, "@esri/arcgis-rest-items": { - "version": "1.17.0", - "resolved": "https://registry.npmjs.org/@esri/arcgis-rest-items/-/arcgis-rest-items-1.17.0.tgz", - "integrity": "sha512-qZthiOrBj4tvvrzoJPmWhq9fv0/dwVzRHJfGVWLVtCpK5hKH1zu0Wx15sOfKhI9BD2T1VFEPctCzS+Dl/T8pwg==", + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/@esri/arcgis-rest-items/-/arcgis-rest-items-1.18.0.tgz", + "integrity": "sha512-c/qHXkn9Lqm7Ql+i1wGBvmQbN/gyaxLSq+cCB0SfZ9sbCFePlTw2hJqRKLuVMaNT6m5HX94uETK+PUtdNgtK/w==", "requires": { "tslib": "^1.9.3" } }, "@esri/arcgis-rest-request": { - "version": "1.17.0", - "resolved": "https://registry.npmjs.org/@esri/arcgis-rest-request/-/arcgis-rest-request-1.17.0.tgz", - "integrity": "sha512-6Rm0WEAQb/efiDq8aik6qlIyIYqA8OKdj4dHpQ6aVmmlFBCYUkf29Sy9whFhH3VtU62KULpQaaLSVjYP6m5tuw==", + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/@esri/arcgis-rest-request/-/arcgis-rest-request-1.18.0.tgz", + "integrity": "sha512-PyRBYzcX1yMXBrYKw4AE8/4GTLsHkAyCgIJ2v/18jI/RqVqdrqfHXJFP79Zmi1BBuY9clhY2UOeHIAAsQPnphA==", "requires": { "tslib": "^1.9.3" } }, "@esri/arcgis-rest-sharing": { - "version": "1.17.0", - "resolved": "https://registry.npmjs.org/@esri/arcgis-rest-sharing/-/arcgis-rest-sharing-1.17.0.tgz", - "integrity": "sha512-awMK47KY6P19dewokz9JruQtBvR8Hzmg7UkzDtwYUEjGS1qZkWE+aqHbVzv/7O5b1Kg56qgP4lkNotTLxUInLw==", + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/@esri/arcgis-rest-sharing/-/arcgis-rest-sharing-1.18.0.tgz", + "integrity": "sha512-6Ch8VpTbh6GRPZks5nQVXNIjBKXKIjVYHAqfbYoN1PO61jvrk5bSxPHfdj8TmDpFQMasYeXiKrkTRQuD6wAXDA==", "requires": { "tslib": "^1.9.3" } diff --git a/packages/initiatives/package.json b/packages/initiatives/package.json index 7ccc0829003..e78c40e1b25 100644 --- a/packages/initiatives/package.json +++ b/packages/initiatives/package.json @@ -9,12 +9,12 @@ "types": "dist/esm/index.d.ts", "license": "Apache-2.0", "dependencies": { - "@esri/arcgis-rest-auth": "^1.17.0", - "@esri/arcgis-rest-common-types": "^1.17.0", - "@esri/arcgis-rest-groups": "^1.17.0", - "@esri/arcgis-rest-items": "^1.17.0", - "@esri/arcgis-rest-request": "^1.17.0", - "@esri/arcgis-rest-sharing": "^1.17.0", + "@esri/arcgis-rest-auth": "^1.18.0", + "@esri/arcgis-rest-common-types": "^1.18.0", + "@esri/arcgis-rest-groups": "^1.18.0", + "@esri/arcgis-rest-items": "^1.18.0", + "@esri/arcgis-rest-request": "^1.18.0", + "@esri/arcgis-rest-sharing": "^1.18.0", "tslib": "^1.9.3" }, "peerDependencies": { diff --git a/packages/initiatives/src/follow.ts b/packages/initiatives/src/follow.ts new file mode 100644 index 00000000000..6f8191903bc --- /dev/null +++ b/packages/initiatives/src/follow.ts @@ -0,0 +1,98 @@ +import { + request, + getPortalUrl, + IRequestOptions +} from "@esri/arcgis-rest-request"; +import { UserSession, getUserUrl } from "@esri/arcgis-rest-auth"; +import { IUser } from "@esri/arcgis-rest-common-types"; + +export interface IFollowInitiativeRequestOptions extends IRequestOptions { + initiativeId: string; + authentication: UserSession; +} + +const getTag = (initiativeId: string) => `hubInitiativeId|${initiativeId}`; + +const getUpdateUrl = (session: UserSession) => `${getUserUrl(session)}/update`; + +export const currentlyFollowedInitiatives = (user: IUser): string[] => + user.tags.map(tag => tag.replace(/^hubInitiativeId\|/, "")); + +export const isUserFollowing = (user: IUser, initiativeId: string): boolean => + currentlyFollowedInitiatives(user).indexOf(initiativeId) > -1; + +/** + * ```js + * import { followInitiative } from "@esri/hub-initiatives"; + * // + * followInitiative({ + * initiativeId, + * authentication + * }) + * .then(response) + * ``` + * Follow an initiative. + */ +export function followInitiative( + requestOptions: IFollowInitiativeRequestOptions +): Promise<{ success: boolean; username: string }> { + // we dont call getUser() because the tags are cached and will be mutating + return request(getUserUrl(requestOptions.authentication), { + authentication: requestOptions.authentication + }).then(user => { + // don't update if already following + if (isUserFollowing(user, requestOptions.initiativeId)) { + return Promise.reject(`user is already following this initiative.`); + } + const tag = getTag(requestOptions.initiativeId); + const tags = JSON.parse(JSON.stringify(user.tags)); + tags.push(tag); + + return request(getUpdateUrl(requestOptions.authentication), { + params: { tags }, + authentication: requestOptions.authentication + }); + }); +} + +/** + * ```js + * import { unfollowInitiative } from "@esri/hub-initiatives"; + * // + * unfollowInitiative({ + * initiativeId, + * authentication + * }) + * .then(response) + * ``` + * Follow an initiative. + */ +export function unfollowInitiative( + requestOptions: IFollowInitiativeRequestOptions +): Promise<{ success: boolean; username: string }> { + // we dont call getUser() because the tags are cached and will be mutating + return request(getUserUrl(requestOptions.authentication), { + authentication: requestOptions.authentication + }).then(user => { + // don't update if already following + if (!isUserFollowing(user, requestOptions.initiativeId)) { + return Promise.reject(`user is not following this initiative.`); + } + + const tag = getTag(requestOptions.initiativeId); + const tags = JSON.parse(JSON.stringify(user.tags)); + // https://stackoverflow.com/questions/9792927/javascript-array-search-and-remove-string + const index = tags.indexOf(tag); + tags.splice(index, 1); + + // clear the last tag by passing ",". + if (tags.length === 0) { + tags.push(","); + } + + return request(getUpdateUrl(requestOptions.authentication), { + params: { tags }, + authentication: requestOptions.authentication + }); + }); +} diff --git a/packages/initiatives/src/groups.ts b/packages/initiatives/src/groups.ts index 80e6efa2a51..8d6d327bc8d 100644 --- a/packages/initiatives/src/groups.ts +++ b/packages/initiatives/src/groups.ts @@ -12,7 +12,7 @@ import { import { IRequestOptions } from "@esri/arcgis-rest-request"; /** - * Create an initiative collaboration or ope data groups + * Create an initiative collaboration or open data group * Note: This does not ensure a group with the proposed name does not exist. Please use * `checkGroupExists * diff --git a/packages/initiatives/src/index.ts b/packages/initiatives/src/index.ts index 91e885f5d6c..93a5b5b0d69 100644 --- a/packages/initiatives/src/index.ts +++ b/packages/initiatives/src/index.ts @@ -11,6 +11,7 @@ export * from "./activate"; export * from "./remove"; export * from "./detach-site"; export * from "./search"; -// TODO: Move to module in ArcGIS-REST-JS +export * from "./follow"; +// TODO: Move to module in ArcGIS REST JS import geometryService from "./geometry"; export { geometryService }; diff --git a/packages/initiatives/test/follow.test.ts b/packages/initiatives/test/follow.test.ts new file mode 100644 index 00000000000..0bc1e485371 --- /dev/null +++ b/packages/initiatives/test/follow.test.ts @@ -0,0 +1,321 @@ +/* Copyright (c) 2018 Environmental Systems Research Institute, Inc. + * Apache-2.0 */ + +import { IUser } from "@esri/arcgis-rest-common-types"; +import { followInitiative, unfollowInitiative } from "../src/follow"; +import { MOCK_REQUEST_OPTIONS } from "./mocks/fake-session"; +import * as fetchMock from "fetch-mock"; + +const NON_FOLLOWER: IUser = { + username: "vader", + fullName: "Anakin Skywalker", + availableCredits: 54161.586, + assignedCredits: -1, + firstName: "Anakin", + lastName: "Skywalker", + preferredView: null, + description: null, + email: "darth@earthlink.net", + idpUsername: null, + favGroupId: "3746d652b4c44a3fa7b778631145298c", + lastLogin: 1552516637000, + mfaEnabled: false, + access: "org", + storageUsage: 1394828039, + storageQuota: 2199023255552, + orgId: "uCXeTVveQzP4IIcx", + role: "org_user", + privileges: [], + level: "2", + disabled: false, + tags: [], + culture: "en-US", + region: "WO", + units: "english", + thumbnail: null, + created: 1539911814000, + modified: 1545167865000, + provider: "arcgis", + groups: [] +}; + +const FOLLOWER: IUser = { + username: "vader", + fullName: "Anakin Skywalker", + availableCredits: 54161.586, + assignedCredits: -1, + firstName: "Anakin", + lastName: "Skywalker", + preferredView: null, + description: null, + email: "darth@earthlink.net", + idpUsername: null, + favGroupId: "3746d652b4c44a3fa7b778631145298c", + lastLogin: 1552516637000, + mfaEnabled: false, + access: "org", + storageUsage: 1394828039, + storageQuota: 2199023255552, + orgId: "uCXeTVveQzP4IIcx", + role: "org_user", + privileges: [], + level: "2", + disabled: false, + tags: ["hubInitiativeId|fe8"], + culture: "en-US", + region: "WO", + units: "english", + thumbnail: null, + created: 1539911814000, + modified: 1545167865000, + provider: "arcgis", + groups: [] +}; + +const ANOTHER_FOLLOWER: IUser = { + username: "vader", + fullName: "Anakin Skywalker", + availableCredits: 54161.586, + assignedCredits: -1, + firstName: "Anakin", + lastName: "Skywalker", + preferredView: null, + description: null, + email: "darth@earthlink.net", + idpUsername: null, + favGroupId: "3746d652b4c44a3fa7b778631145298c", + lastLogin: 1552516637000, + mfaEnabled: false, + access: "org", + storageUsage: 1394828039, + storageQuota: 2199023255552, + orgId: "uCXeTVveQzP4IIcx", + role: "org_user", + privileges: [], + level: "2", + disabled: false, + tags: ["I drive a Dodge Stratus", "hubInitiativeId|fe8"], + culture: "en-US", + region: "WO", + units: "english", + thumbnail: null, + created: 1539911814000, + modified: 1545167865000, + provider: "arcgis", + groups: [] +}; + +afterEach(() => { + fetchMock.restore(); +}); + +describe("follow/unfollowInitiative", () => { + it("should add user tags if they arent already following", done => { + // mock two fetch calls... + // user metadata + fetchMock.post( + `https://www.arcgis.com/sharing/rest/community/users/vader`, + NON_FOLLOWER + ); + + // user update + fetchMock.post( + `https://www.arcgis.com/sharing/rest/community/users/vader/update`, + { + success: true, + username: "vader" + } + ); + + followInitiative({ + initiativeId: `fe8`, + ...MOCK_REQUEST_OPTIONS + }).then(response => { + // check that the mocks were called + expect(fetchMock.done()).toBeTruthy(); + // inspect the POST call... + const [metadataUrl, metadataOptions]: [ + string, + RequestInit + ] = fetchMock.lastCall( + `https://www.arcgis.com/sharing/rest/community/users/vader` + ); + expect(metadataUrl).toEqual(metadataUrl); + expect(metadataOptions.method).toBe("POST"); + expect(metadataOptions.body).toContain("f=json"); + expect(metadataOptions.body).toContain("token=fake-token"); + + const [updateUrl, updateOptions]: [ + string, + RequestInit + ] = fetchMock.lastCall( + `https://www.arcgis.com/sharing/rest/community/users/vader/update` + ); + expect(updateUrl).toEqual(updateUrl); + expect(updateOptions.method).toBe("POST"); + expect(updateOptions.body).toContain("f=json"); + expect(updateOptions.body).toContain("token=fake-token"); + expect(updateOptions.body).toContain( + `tags=${encodeURIComponent("hubInitiativeId|fe8")}` + ); + expect(response.success).toBe(true); + done(); + }); + }); + + it("should not add user tags if they are already following", done => { + // mock two fetch calls... + // user metadata + fetchMock.post( + `https://www.arcgis.com/sharing/rest/community/users/vader`, + FOLLOWER + ); + + // user update + fetchMock.post( + `https://www.arcgis.com/sharing/rest/community/users/vader/update`, + { + success: true, + username: "vader" + } + ); + + followInitiative({ + initiativeId: `fe8`, + ...MOCK_REQUEST_OPTIONS + }).catch(err => { + expect(err).toBe(`user is already following this initiative.`); + done(); + }); + }); + + it("should remove a user tag if they were already following an initiative", done => { + // mock two fetch calls... + // user metadata + fetchMock.post( + `https://www.arcgis.com/sharing/rest/community/users/vader`, + FOLLOWER + ); + + // user update + fetchMock.post( + `https://www.arcgis.com/sharing/rest/community/users/vader/update`, + { + success: true, + username: "vader" + } + ); + + unfollowInitiative({ + initiativeId: `fe8`, + ...MOCK_REQUEST_OPTIONS + }).then(response => { + // check that the mocks were called + expect(fetchMock.done()).toBeTruthy(); + // inspect the POST call... + const [metadataUrl, metadataOptions]: [ + string, + RequestInit + ] = fetchMock.lastCall( + `https://www.arcgis.com/sharing/rest/community/users/vader` + ); + expect(metadataUrl).toEqual(metadataUrl); + expect(metadataOptions.method).toBe("POST"); + expect(metadataOptions.body).toContain("f=json"); + expect(metadataOptions.body).toContain("token=fake-token"); + + const [updateUrl, updateOptions]: [ + string, + RequestInit + ] = fetchMock.lastCall( + `https://www.arcgis.com/sharing/rest/community/users/vader/update` + ); + expect(updateUrl).toEqual(updateUrl); + expect(updateOptions.method).toBe("POST"); + expect(updateOptions.body).toContain("f=json"); + expect(updateOptions.body).toContain("token=fake-token"); + expect(updateOptions.body).toContain(`tags=${encodeURIComponent(",")}`); + expect(response.success).toBe(true); + done(); + }); + }); + + it("should remove a tag if they are already following and other tags are present", done => { + // mock two fetch calls... + // user metadata + fetchMock.post( + `https://www.arcgis.com/sharing/rest/community/users/vader`, + ANOTHER_FOLLOWER + ); + + // user update + fetchMock.post( + `https://www.arcgis.com/sharing/rest/community/users/vader/update`, + { + success: true, + username: "vader" + } + ); + + unfollowInitiative({ + initiativeId: `fe8`, + ...MOCK_REQUEST_OPTIONS + }).then(response => { + // check that the mocks were called + expect(fetchMock.done()).toBeTruthy(); + // inspect the POST call... + const [metadataUrl, metadataOptions]: [ + string, + RequestInit + ] = fetchMock.lastCall( + `https://www.arcgis.com/sharing/rest/community/users/vader` + ); + expect(metadataUrl).toEqual(metadataUrl); + expect(metadataOptions.method).toBe("POST"); + expect(metadataOptions.body).toContain("f=json"); + expect(metadataOptions.body).toContain("token=fake-token"); + + const [updateUrl, updateOptions]: [ + string, + RequestInit + ] = fetchMock.lastCall( + `https://www.arcgis.com/sharing/rest/community/users/vader/update` + ); + expect(updateUrl).toEqual(updateUrl); + expect(updateOptions.method).toBe("POST"); + expect(updateOptions.body).toContain("f=json"); + expect(updateOptions.body).toContain("token=fake-token"); + expect(updateOptions.body).toContain( + `tags=${encodeURIComponent("I drive a Dodge Stratus")}` + ); + expect(response.success).toBe(true); + done(); + }); + }); + + it("should not add user tags if they are already following", done => { + // mock two fetch calls... + // user metadata + fetchMock.post( + `https://www.arcgis.com/sharing/rest/community/users/vader`, + NON_FOLLOWER + ); + + // user update + fetchMock.post( + `https://www.arcgis.com/sharing/rest/community/users/vader/update`, + { + success: true, + username: "vader" + } + ); + + unfollowInitiative({ + initiativeId: `fe8`, + ...MOCK_REQUEST_OPTIONS + }).catch(err => { + expect(err).toBe(`user is not following this initiative.`); + done(); + }); + }); +});