TeamCollectionAdapter.ts (hoppscotch-2.2.1) | : | TeamCollectionAdapter.ts (hoppscotch-3.0.0) | ||
---|---|---|---|---|
import { BehaviorSubject } from "rxjs" | import * as E from "fp-ts/Either" | |||
import { gql } from "graphql-tag" | import { BehaviorSubject, Subscription } from "rxjs" | |||
import pull from "lodash/pull" | ||||
import remove from "lodash/remove" | ||||
import { translateToNewRequest } from "@hoppscotch/data" | import { translateToNewRequest } from "@hoppscotch/data" | |||
import { pull, remove } from "lodash-es" | ||||
import { Subscription as WSubscription } from "wonka" | ||||
import { runGQLQuery, runGQLSubscription } from "../backend/GQLClient" | ||||
import { TeamCollection } from "./TeamCollection" | import { TeamCollection } from "./TeamCollection" | |||
import { TeamRequest } from "./TeamRequest" | import { TeamRequest } from "./TeamRequest" | |||
import { | import { | |||
rootCollectionsOfTeam, | RootCollectionsOfTeamDocument, | |||
getCollectionChildren, | TeamCollectionAddedDocument, | |||
getCollectionRequests, | TeamCollectionUpdatedDocument, | |||
} from "./utils" | TeamCollectionRemovedDocument, | |||
import { apolloClient } from "~/helpers/apollo" | TeamRequestAddedDocument, | |||
TeamRequestUpdatedDocument, | ||||
/* | TeamRequestDeletedDocument, | |||
* NOTE: These functions deal with REFERENCES to objects and mutates them, for a | GetCollectionChildrenDocument, | |||
simpler implementation. | GetCollectionRequestsDocument, | |||
* Be careful when you play with these. | } from "~/helpers/backend/graphql" | |||
* | ||||
* I am not a fan of mutating references but this is so much simpler compared to | const TEAMS_BACKEND_PAGE_SIZE = 10 | |||
mutating clones | ||||
* - Andrew | ||||
*/ | ||||
/** | /** | |||
* Finds the parent of a collection and returns the REFERENCE (or null) | * Finds the parent of a collection and returns the REFERENCE (or null) | |||
* | * | |||
* @param {TeamCollection[]} tree - The tree to look in | * @param {TeamCollection[]} tree - The tree to look in | |||
* @param {string} collID - ID of the collection to find the parent of | * @param {string} collID - ID of the collection to find the parent of | |||
* @param {TeamCollection} currentParent - (used for recursion, do not set) The parent in the current iteration (undefined if root) | * @param {TeamCollection} currentParent - (used for recursion, do not set) The parent in the current iteration (undefined if root) | |||
* | * | |||
* @returns REFERENCE to the collecton or null if not found or the collection is in root | * @returns REFERENCE to the collection or null if not found or the collection i s in root | |||
*/ | */ | |||
function findParentOfColl( | function findParentOfColl( | |||
tree: TeamCollection[], | tree: TeamCollection[], | |||
collID: string, | collID: string, | |||
currentParent?: TeamCollection | currentParent?: TeamCollection | |||
): TeamCollection | null { | ): TeamCollection | null { | |||
for (const coll of tree) { | for (const coll of tree) { | |||
// If the root is parent, return null | // If the root is parent, return null | |||
if (coll.id === collID) return currentParent || null | if (coll.id === collID) return currentParent || null | |||
skipping to change at line 79 | skipping to change at line 79 | |||
const result = findCollInTree(coll.children, targetID) | const result = findCollInTree(coll.children, targetID) | |||
if (result) return result | if (result) return result | |||
} | } | |||
} | } | |||
// If nothing matched, return null | // If nothing matched, return null | |||
return null | return null | |||
} | } | |||
/** | /** | |||
* Finds and returns a REFERENCE to the collection containing a given request ID in tree (or null) | * Deletes a collection in the tree | |||
* | * | |||
* @param {TeamCollection[]} tree - The tree to look in | * @param {TeamCollection[]} tree - The tree to delete in (THIS WILL BE MUTATED! | |||
* @param {string} reqID - The ID of the request to look for | ) | |||
* @param {string} targetID - ID of the collection to delete | ||||
*/ | ||||
function deleteCollInTree(tree: TeamCollection[], targetID: string) { | ||||
// Get the parent owning the collection | ||||
const parent = findParentOfColl(tree, targetID) | ||||
// If we found a parent, update it | ||||
if (parent && parent.children) { | ||||
parent.children = parent.children.filter((coll) => coll.id !== targetID) | ||||
} | ||||
// If there is no parent, it could mean: | ||||
// 1. The collection with that ID does not exist | ||||
// 2. The collection is in root (therefore, no parent) | ||||
// Let's look for element, if not exist, then stop | ||||
const el = findCollInTree(tree, targetID) | ||||
if (!el) return | ||||
// Collection exists, so this should be in root, hence removing element | ||||
pull(tree, el) | ||||
} | ||||
/** | ||||
* Updates a collection in the tree with the specified data | ||||
* | * | |||
* @returns REFERENCE to the collection or null if request not found | * @param {TeamCollection[]} tree - The tree to update in (THIS WILL BE MUTATED! | |||
) | ||||
* @param {Partial<TeamCollection> & Pick<TeamCollection, "id">} updateColl - An | ||||
object defining all the fields that should be updated (ID is required to find t | ||||
he target collection) | ||||
*/ | */ | |||
function findCollWithReqIDInTree( | function updateCollInTree( | |||
tree: TeamCollection[], | tree: TeamCollection[], | |||
reqID: string | updateColl: Partial<TeamCollection> & Pick<TeamCollection, "id"> | |||
): TeamCollection | null { | ) { | |||
for (const coll of tree) { | const el = findCollInTree(tree, updateColl.id) | |||
// Check in root collections (if expanded) | ||||
if (coll.requests) { | ||||
if (coll.requests.find((req) => req.id === reqID)) return coll | ||||
} | ||||
// Check in children of collections | // If no match, stop the operation | |||
if (coll.children) { | if (!el) return | |||
const result = findCollWithReqIDInTree(coll.children, reqID) | ||||
if (result) return result | ||||
} | ||||
} | ||||
// No matches | // Update all the specified keys | |||
return null | Object.assign(el, updateColl) | |||
} | } | |||
/** | /** | |||
* Finds and returns a REFERENCE to the request with the given ID (or null) | * Finds and returns a REFERENCE to the request with the given ID (or null) | |||
* | * | |||
* @param {TeamCollection[]} tree - The tree to look in | * @param {TeamCollection[]} tree - The tree to look in | |||
* @param {string} reqID - The ID of the request to look for | * @param {string} reqID - The ID of the request to look for | |||
* | * | |||
* @returns REFERENCE to the request or null if request not found | * @returns REFERENCE to the request or null if request not found | |||
*/ | */ | |||
skipping to change at line 138 | skipping to change at line 155 | |||
const match = findReqInTree(coll.children, reqID) | const match = findReqInTree(coll.children, reqID) | |||
if (match) return match | if (match) return match | |||
} | } | |||
} | } | |||
// No matches | // No matches | |||
return null | return null | |||
} | } | |||
/** | /** | |||
* Updates a collection in the tree with the specified data | * Finds and returns a REFERENCE to the collection containing a given request ID in tree (or null) | |||
* | * | |||
* @param {TeamCollection[]} tree - The tree to update in (THIS WILL BE MUTATED! | * @param {TeamCollection[]} tree - The tree to look in | |||
) | * @param {string} reqID - The ID of the request to look for | |||
* @param {Partial<TeamCollection> & Pick<TeamCollection, "id">} updateColl - An | * | |||
object defining all the fields that should be updated (ID is required to find t | * @returns REFERENCE to the collection or null if request not found | |||
he target collection) | ||||
*/ | */ | |||
function updateCollInTree( | function findCollWithReqIDInTree( | |||
tree: TeamCollection[], | tree: TeamCollection[], | |||
updateColl: Partial<TeamCollection> & Pick<TeamCollection, "id"> | reqID: string | |||
) { | ): TeamCollection | null { | |||
const el = findCollInTree(tree, updateColl.id) | for (const coll of tree) { | |||
// Check in root collections (if expanded) | ||||
if (coll.requests) { | ||||
if (coll.requests.find((req) => req.id === reqID)) return coll | ||||
} | ||||
// If no match, stop the operation | // Check in children of collections | |||
if (!el) return | if (coll.children) { | |||
const result = findCollWithReqIDInTree(coll.children, reqID) | ||||
if (result) return result | ||||
} | ||||
} | ||||
// Update all the specified keys | // No matches | |||
Object.assign(el, updateColl) | return null | |||
} | } | |||
/** | type EntityType = "request" | "collection" | |||
* Deletes a collection in the tree | type EntityID = `${EntityType}-${string}` | |||
* | ||||
* @param {TeamCollection[]} tree - The tree to delete in (THIS WILL BE MUTATED! | ||||
) | ||||
* @param {string} targetID - ID of the collection to delete | ||||
*/ | ||||
function deleteCollInTree(tree: TeamCollection[], targetID: string) { | ||||
// Get the parent owning the collection | ||||
const parent = findParentOfColl(tree, targetID) | ||||
// If we found a parent, update it | ||||
if (parent && parent.children) { | ||||
parent.children = parent.children.filter((coll) => coll.id !== targetID) | ||||
} | ||||
// If there is no parent, it could mean: | ||||
// 1. The collection with that ID does not exist | ||||
// 2. The collection is in root (therefore, no parent) | ||||
// Let's look for element, if not exist, then stop | export default class NewTeamCollectionAdapter { | |||
const el = findCollInTree(tree, targetID) | collections$: BehaviorSubject<TeamCollection[]> | |||
if (!el) return | ||||
// Collection exists, so this should be in root, hence removing element | // Stream to the list of collections/folders that are being loaded in | |||
pull(tree, el) | loadingCollections$: BehaviorSubject<string[]> | |||
} | ||||
/** | ||||
* TeamCollectionAdapter provides a reactive collections list for a specific tea | ||||
m | ||||
*/ | ||||
export default class TeamCollectionAdapter { | ||||
/** | /** | |||
* The reactive list of collections | * Stores the entity (collection/request/folder) ids of all the loaded entitie | |||
* | s. | |||
* A new value is emitted when there is a change | * Used for preventing duplication of data which definitely is not possible (d | |||
* (Use views instead) | uplication due to network problems etc.) | |||
*/ | */ | |||
collections$: BehaviorSubject<TeamCollection[]> | private entityIDs: Set<EntityID> | |||
// Fields for subscriptions, used for destroying once not needed | private teamCollectionAdded$: Subscription | null | |||
private teamCollectionAdded$: ZenObservable.Subscription | null | private teamCollectionUpdated$: Subscription | null | |||
private teamCollectionUpdated$: ZenObservable.Subscription | null | private teamCollectionRemoved$: Subscription | null | |||
private teamCollectionRemoved$: ZenObservable.Subscription | null | private teamRequestAdded$: Subscription | null | |||
private teamRequestAdded$: ZenObservable.Subscription | null | private teamRequestUpdated$: Subscription | null | |||
private teamRequestUpdated$: ZenObservable.Subscription | null | private teamRequestDeleted$: Subscription | null | |||
private teamRequestDeleted$: ZenObservable.Subscription | null | ||||
private teamCollectionAddedSub: WSubscription | null | ||||
private teamCollectionUpdatedSub: WSubscription | null | ||||
private teamCollectionRemovedSub: WSubscription | null | ||||
private teamRequestAddedSub: WSubscription | null | ||||
private teamRequestUpdatedSub: WSubscription | null | ||||
private teamRequestDeletedSub: WSubscription | null | ||||
/** | ||||
* @constructor | ||||
* | ||||
* @param {string | null} teamID - ID of the team to listen to, or null if non | ||||
e decided and the adapter should stand by | ||||
*/ | ||||
constructor(private teamID: string | null) { | constructor(private teamID: string | null) { | |||
this.collections$ = new BehaviorSubject<TeamCollection[]>([]) | this.collections$ = new BehaviorSubject<TeamCollection[]>([]) | |||
this.loadingCollections$ = new BehaviorSubject<string[]>([]) | ||||
this.entityIDs = new Set() | ||||
this.teamCollectionAdded$ = null | this.teamCollectionAdded$ = null | |||
this.teamCollectionUpdated$ = null | this.teamCollectionUpdated$ = null | |||
this.teamCollectionRemoved$ = null | this.teamCollectionRemoved$ = null | |||
this.teamRequestAdded$ = null | this.teamRequestAdded$ = null | |||
this.teamRequestDeleted$ = null | this.teamRequestDeleted$ = null | |||
this.teamRequestUpdated$ = null | this.teamRequestUpdated$ = null | |||
this.teamCollectionAddedSub = null | ||||
this.teamCollectionUpdatedSub = null | ||||
this.teamCollectionRemovedSub = null | ||||
this.teamRequestAddedSub = null | ||||
this.teamRequestDeletedSub = null | ||||
this.teamRequestUpdatedSub = null | ||||
if (this.teamID) this.initialize() | if (this.teamID) this.initialize() | |||
} | } | |||
/** | ||||
* Updates the team the adapter is looking at | ||||
* | ||||
* @param {string | null} newTeamID - ID of the team to listen to, or null if | ||||
none decided and the adapter should stand by | ||||
*/ | ||||
changeTeamID(newTeamID: string | null) { | changeTeamID(newTeamID: string | null) { | |||
this.teamID = newTeamID | ||||
this.collections$.next([]) | this.collections$.next([]) | |||
this.entityIDs.clear() | ||||
this.teamID = newTeamID | this.loadingCollections$.next([]) | |||
this.unsubscribeSubscriptions() | ||||
if (this.teamID) this.initialize() | if (this.teamID) this.initialize() | |||
} | } | |||
/** | /** | |||
* Unsubscribes from the subscriptions | * Unsubscribes from the subscriptions | |||
* NOTE: Once this is called, no new updates to the tree will be detected | * NOTE: Once this is called, no new updates to the tree will be detected | |||
*/ | */ | |||
unsubscribeSubscriptions() { | unsubscribeSubscriptions() { | |||
this.teamCollectionAdded$?.unsubscribe() | this.teamCollectionAdded$?.unsubscribe() | |||
this.teamCollectionUpdated$?.unsubscribe() | this.teamCollectionUpdated$?.unsubscribe() | |||
this.teamCollectionRemoved$?.unsubscribe() | this.teamCollectionRemoved$?.unsubscribe() | |||
this.teamRequestAdded$?.unsubscribe() | this.teamRequestAdded$?.unsubscribe() | |||
this.teamRequestDeleted$?.unsubscribe() | this.teamRequestDeleted$?.unsubscribe() | |||
this.teamRequestUpdated$?.unsubscribe() | this.teamRequestUpdated$?.unsubscribe() | |||
this.teamCollectionAddedSub?.unsubscribe() | ||||
this.teamCollectionUpdatedSub?.unsubscribe() | ||||
this.teamCollectionRemovedSub?.unsubscribe() | ||||
this.teamRequestAddedSub?.unsubscribe() | ||||
this.teamRequestDeletedSub?.unsubscribe() | ||||
this.teamRequestUpdatedSub?.unsubscribe() | ||||
} | } | |||
/** | ||||
* Initializes the adapter | ||||
*/ | ||||
private async initialize() { | private async initialize() { | |||
await this.loadRootCollections() | await this.loadRootCollections() | |||
this.registerSubscriptions() | this.registerSubscriptions() | |||
} | } | |||
/** | /** | |||
* Loads the root collections | ||||
*/ | ||||
private async loadRootCollections(): Promise<void> { | ||||
const colls = await rootCollectionsOfTeam(apolloClient, this.teamID) | ||||
this.collections$.next(colls) | ||||
} | ||||
/** | ||||
* Performs addition of a collection to the tree | * Performs addition of a collection to the tree | |||
* | * | |||
* @param {TeamCollection} collection - The collection to add to the tree | * @param {TeamCollection} collection - The collection to add to the tree | |||
* @param {string | null} parentCollectionID - The parent of the new collectio n, pass null if this collection is in root | * @param {string | null} parentCollectionID - The parent of the new collectio n, pass null if this collection is in root | |||
*/ | */ | |||
private addCollection( | private addCollection( | |||
collection: TeamCollection, | collection: TeamCollection, | |||
parentCollectionID: string | null | parentCollectionID: string | null | |||
) { | ) { | |||
const tree = this.collections$.value | const tree = this.collections$.value | |||
skipping to change at line 288 | skipping to change at line 298 | |||
if (!parentCollection) return | if (!parentCollection) return | |||
if (parentCollection.children != null) { | if (parentCollection.children != null) { | |||
parentCollection.children.push(collection) | parentCollection.children.push(collection) | |||
} else { | } else { | |||
parentCollection.children = [collection] | parentCollection.children = [collection] | |||
} | } | |||
} | } | |||
// Add to entity ids set | ||||
this.entityIDs.add(`collection-${collection.id}`) | ||||
this.collections$.next(tree) | this.collections$.next(tree) | |||
} | } | |||
private async loadRootCollections() { | ||||
if (this.teamID === null) throw new Error("Team ID is null") | ||||
this.loadingCollections$.next([ | ||||
...this.loadingCollections$.getValue(), | ||||
"root", | ||||
]) | ||||
const totalCollections: TeamCollection[] = [] | ||||
while (true) { | ||||
const result = await runGQLQuery({ | ||||
query: RootCollectionsOfTeamDocument, | ||||
variables: { | ||||
teamID: this.teamID, | ||||
cursor: | ||||
totalCollections.length > 0 | ||||
? totalCollections[totalCollections.length - 1].id | ||||
: undefined, | ||||
}, | ||||
}) | ||||
if (E.isLeft(result)) { | ||||
this.loadingCollections$.next( | ||||
this.loadingCollections$.getValue().filter((x) => x !== "root") | ||||
) | ||||
throw new Error(`Error fetching root collections: ${result}`) | ||||
} | ||||
totalCollections.push( | ||||
...result.right.rootCollectionsOfTeam.map( | ||||
(x) => | ||||
<TeamCollection>{ | ||||
...x, | ||||
children: null, | ||||
requests: null, | ||||
} | ||||
) | ||||
) | ||||
if (result.right.rootCollectionsOfTeam.length !== TEAMS_BACKEND_PAGE_SIZE) | ||||
break | ||||
} | ||||
this.loadingCollections$.next( | ||||
this.loadingCollections$.getValue().filter((x) => x !== "root") | ||||
) | ||||
// Add all the collections to the entity ids list | ||||
totalCollections.forEach((coll) => | ||||
this.entityIDs.add(`collection-${coll.id}`) | ||||
) | ||||
this.collections$.next(totalCollections) | ||||
} | ||||
/** | /** | |||
* Updates an existing collection in tree | * Updates an existing collection in tree | |||
* | * | |||
* @param {Partial<TeamCollection> & Pick<TeamCollection, "id">} collectionUpd ate - Object defining the fields that need to be updated (ID is required to find the target) | * @param {Partial<TeamCollection> & Pick<TeamCollection, "id">} collectionUpd ate - Object defining the fields that need to be updated (ID is required to find the target) | |||
*/ | */ | |||
private updateCollection( | private updateCollection( | |||
collectionUpdate: Partial<TeamCollection> & Pick<TeamCollection, "id"> | collectionUpdate: Partial<TeamCollection> & Pick<TeamCollection, "id"> | |||
) { | ) { | |||
const tree = this.collections$.value | const tree = this.collections$.value | |||
skipping to change at line 316 | skipping to change at line 386 | |||
/** | /** | |||
* Removes a collection from the tree | * Removes a collection from the tree | |||
* | * | |||
* @param {string} collectionID - ID of the collection to remove | * @param {string} collectionID - ID of the collection to remove | |||
*/ | */ | |||
private removeCollection(collectionID: string) { | private removeCollection(collectionID: string) { | |||
const tree = this.collections$.value | const tree = this.collections$.value | |||
deleteCollInTree(tree, collectionID) | deleteCollInTree(tree, collectionID) | |||
this.entityIDs.delete(`collection-${collectionID}`) | ||||
this.collections$.next(tree) | this.collections$.next(tree) | |||
} | } | |||
/** | /** | |||
* Adds a request to the tree | * Adds a request to the tree | |||
* | * | |||
* @param {TeamRequest} request - The request to add to the tree | * @param {TeamRequest} request - The request to add to the tree | |||
*/ | */ | |||
private addRequest(request: TeamRequest) { | private addRequest(request: TeamRequest) { | |||
// Check if we have it already in the entity tree, if so, we don't need it a | ||||
gain | ||||
if (this.entityIDs.has(`request-${request.id}`)) return | ||||
const tree = this.collections$.value | const tree = this.collections$.value | |||
// Check if we have the collection (if not, then not loaded?) | // Check if we have the collection (if not, then not loaded?) | |||
const coll = findCollInTree(tree, request.collectionID) | const coll = findCollInTree(tree, request.collectionID) | |||
if (!coll) return // Ignore add request | if (!coll) return // Ignore add request | |||
// Collection is not expanded | // Collection is not expanded | |||
if (!coll.requests) return | if (!coll.requests) return | |||
// Collection is expanded hence append request | // Collection is expanded hence append request | |||
coll.requests.push(request) | coll.requests.push(request) | |||
this.collections$.next(tree) | // Update the Entity IDs list | |||
} | this.entityIDs.add(`request-${request.id}`) | |||
/** | ||||
* Removes a request from the tree | ||||
* | ||||
* @param {string} requestID - ID of the request to remove | ||||
*/ | ||||
private removeRequest(requestID: string) { | ||||
const tree = this.collections$.value | ||||
// Find request in tree, don't attempt if no collection or no requests (expa | ||||
nsion?) | ||||
const coll = findCollWithReqIDInTree(tree, requestID) | ||||
if (!coll || !coll.requests) return | ||||
// Remove the collection | ||||
remove(coll.requests, (req) => req.id === requestID) | ||||
// Publish new tree | ||||
this.collections$.next(tree) | this.collections$.next(tree) | |||
} | } | |||
/** | /** | |||
* Updates the request in tree | * Updates the request in tree | |||
* | * | |||
* @param {Partial<TeamRequest> & Pick<TeamRequest, 'id'>} requestUpdate - Obj ect defining all the fields to update in request (ID of the request is required) | * @param {Partial<TeamRequest> & Pick<TeamRequest, 'id'>} requestUpdate - Obj ect defining all the fields to update in request (ID of the request is required) | |||
*/ | */ | |||
private updateRequest( | private updateRequest( | |||
requestUpdate: Partial<TeamRequest> & Pick<TeamRequest, "id"> | requestUpdate: Partial<TeamRequest> & Pick<TeamRequest, "id"> | |||
skipping to change at line 379 | skipping to change at line 438 | |||
// Find request, if not present, don't update | // Find request, if not present, don't update | |||
const req = findReqInTree(tree, requestUpdate.id) | const req = findReqInTree(tree, requestUpdate.id) | |||
if (!req) return | if (!req) return | |||
Object.assign(req, requestUpdate) | Object.assign(req, requestUpdate) | |||
this.collections$.next(tree) | this.collections$.next(tree) | |||
} | } | |||
/** | /** | |||
* Registers the subscriptions to listen to team collection updates | * Removes a request from the tree | |||
* | ||||
* @param {string} requestID - ID of the request to remove | ||||
*/ | */ | |||
registerSubscriptions() { | private removeRequest(requestID: string) { | |||
this.teamCollectionAdded$ = apolloClient | const tree = this.collections$.value | |||
.subscribe({ | ||||
query: gql` | ||||
subscription TeamCollectionAdded($teamID: ID!) { | ||||
teamCollectionAdded(teamID: $teamID) { | ||||
id | ||||
title | ||||
parent { | ||||
id | ||||
} | ||||
} | ||||
} | ||||
`, | ||||
variables: { | ||||
teamID: this.teamID, | ||||
}, | ||||
}) | ||||
.subscribe(({ data }) => { | ||||
this.addCollection( | ||||
{ | ||||
id: data.teamCollectionAdded.id, | ||||
children: null, | ||||
requests: null, | ||||
title: data.teamCollectionAdded.title, | ||||
}, | ||||
data.teamCollectionAdded.parent?.id | ||||
) | ||||
}) | ||||
this.teamCollectionUpdated$ = apolloClient | // Find request in tree, don't attempt if no collection or no requests (expa | |||
.subscribe({ | nsion?) | |||
query: gql` | const coll = findCollWithReqIDInTree(tree, requestID) | |||
subscription TeamCollectionUpdated($teamID: ID!) { | if (!coll || !coll.requests) return | |||
teamCollectionUpdated(teamID: $teamID) { | ||||
id | ||||
title | ||||
parent { | ||||
id | ||||
} | ||||
} | ||||
} | ||||
`, | ||||
variables: { | ||||
teamID: this.teamID, | ||||
}, | ||||
}) | ||||
.subscribe(({ data }) => { | ||||
this.updateCollection({ | ||||
id: data.teamCollectionUpdated.id, | ||||
title: data.teamCollectionUpdated.title, | ||||
}) | ||||
}) | ||||
this.teamCollectionRemoved$ = apolloClient | // Remove the collection | |||
.subscribe({ | remove(coll.requests, (req) => req.id === requestID) | |||
query: gql` | ||||
subscription TeamCollectionRemoved($teamID: ID!) { | ||||
teamCollectionRemoved(teamID: $teamID) | ||||
} | ||||
`, | ||||
variables: { | ||||
teamID: this.teamID, | ||||
}, | ||||
}) | ||||
.subscribe(({ data }) => { | ||||
this.removeCollection(data.teamCollectionRemoved) | ||||
}) | ||||
this.teamRequestAdded$ = apolloClient | // Remove from entityIDs set | |||
.subscribe({ | this.entityIDs.delete(`request-${requestID}`) | |||
query: gql` | ||||
subscription TeamRequestAdded($teamID: ID!) { | ||||
teamRequestAdded(teamID: $teamID) { | ||||
id | ||||
collectionID | ||||
request | ||||
title | ||||
} | ||||
} | ||||
`, | ||||
variables: { | ||||
teamID: this.teamID, | ||||
}, | ||||
}) | ||||
.subscribe(({ data }) => { | ||||
this.addRequest({ | ||||
id: data.teamRequestAdded.id, | ||||
collectionID: data.teamRequestAdded.collectionID, | ||||
request: translateToNewRequest( | ||||
JSON.parse(data.teamRequestAdded.request) | ||||
), | ||||
title: data.teamRequestAdded.title, | ||||
}) | ||||
}) | ||||
this.teamRequestUpdated$ = apolloClient | // Publish new tree | |||
.subscribe({ | this.collections$.next(tree) | |||
query: gql` | } | |||
subscription TeamRequestUpdated($teamID: ID!) { | ||||
teamRequestUpdated(teamID: $teamID) { | private registerSubscriptions() { | |||
id | if (!this.teamID) return | |||
collectionID | ||||
request | const [teamCollAdded$, teamCollAddedSub] = runGQLSubscription({ | |||
title | query: TeamCollectionAddedDocument, | |||
} | variables: { | |||
} | teamID: this.teamID, | |||
`, | }, | |||
variables: { | }) | |||
teamID: this.teamID, | ||||
this.teamCollectionAddedSub = teamCollAddedSub | ||||
this.teamCollectionAdded$ = teamCollAdded$.subscribe((result) => { | ||||
if (E.isLeft(result)) | ||||
throw new Error(`Team Collection Added Error: ${result.left}`) | ||||
this.addCollection( | ||||
{ | ||||
id: result.right.teamCollectionAdded.id, | ||||
children: null, | ||||
requests: null, | ||||
title: result.right.teamCollectionAdded.title, | ||||
}, | }, | |||
result.right.teamCollectionAdded.parent?.id ?? null | ||||
) | ||||
}) | ||||
const [teamCollUpdated$, teamCollUpdatedSub] = runGQLSubscription({ | ||||
query: TeamCollectionUpdatedDocument, | ||||
variables: { | ||||
teamID: this.teamID, | ||||
}, | ||||
}) | ||||
this.teamCollectionUpdatedSub = teamCollUpdatedSub | ||||
this.teamCollectionUpdated$ = teamCollUpdated$.subscribe((result) => { | ||||
if (E.isLeft(result)) | ||||
throw new Error(`Team Collection Updated Error: ${result.left}`) | ||||
this.updateCollection({ | ||||
id: result.right.teamCollectionUpdated.id, | ||||
title: result.right.teamCollectionUpdated.title, | ||||
}) | }) | |||
.subscribe(({ data }) => { | }) | |||
this.updateRequest({ | ||||
id: data.teamRequestUpdated.id, | ||||
collectionID: data.teamRequestUpdated.collectionID, | ||||
request: JSON.parse(data.teamRequestUpdated.request), | ||||
title: data.teamRequestUpdated.title, | ||||
}) | ||||
}) | ||||
this.teamRequestDeleted$ = apolloClient | const [teamCollRemoved$, teamCollRemovedSub] = runGQLSubscription({ | |||
.subscribe({ | query: TeamCollectionRemovedDocument, | |||
query: gql` | variables: { | |||
subscription TeamRequestDeleted($teamID: ID!) { | teamID: this.teamID, | |||
teamRequestDeleted(teamID: $teamID) | }, | |||
} | }) | |||
`, | ||||
variables: { | this.teamCollectionRemovedSub = teamCollRemovedSub | |||
teamID: this.teamID, | this.teamCollectionRemoved$ = teamCollRemoved$.subscribe((result) => { | |||
}, | if (E.isLeft(result)) | |||
throw new Error(`Team Collection Removed Error: ${result.left}`) | ||||
this.removeCollection(result.right.teamCollectionRemoved) | ||||
}) | ||||
const [teamReqAdded$, teamReqAddedSub] = runGQLSubscription({ | ||||
query: TeamRequestAddedDocument, | ||||
variables: { | ||||
teamID: this.teamID, | ||||
}, | ||||
}) | ||||
this.teamRequestAddedSub = teamReqAddedSub | ||||
this.teamRequestAdded$ = teamReqAdded$.subscribe((result) => { | ||||
if (E.isLeft(result)) | ||||
throw new Error(`Team Request Added Error: ${result.left}`) | ||||
this.addRequest({ | ||||
id: result.right.teamRequestAdded.id, | ||||
collectionID: result.right.teamRequestAdded.collectionID, | ||||
request: translateToNewRequest( | ||||
JSON.parse(result.right.teamRequestAdded.request) | ||||
), | ||||
title: result.right.teamRequestAdded.title, | ||||
}) | }) | |||
.subscribe(({ data }) => { | }) | |||
this.removeRequest(data.teamRequestDeleted) | ||||
const [teamReqUpdated$, teamReqUpdatedSub] = runGQLSubscription({ | ||||
query: TeamRequestUpdatedDocument, | ||||
variables: { | ||||
teamID: this.teamID, | ||||
}, | ||||
}) | ||||
this.teamRequestUpdatedSub = teamReqUpdatedSub | ||||
this.teamRequestUpdated$ = teamReqUpdated$.subscribe((result) => { | ||||
if (E.isLeft(result)) | ||||
throw new Error(`Team Request Updated Error: ${result.left}`) | ||||
this.updateRequest({ | ||||
id: result.right.teamRequestUpdated.id, | ||||
collectionID: result.right.teamRequestUpdated.collectionID, | ||||
request: JSON.parse(result.right.teamRequestUpdated.request), | ||||
title: result.right.teamRequestUpdated.title, | ||||
}) | }) | |||
}) | ||||
const [teamReqDeleted$, teamReqDeleted] = runGQLSubscription({ | ||||
query: TeamRequestDeletedDocument, | ||||
variables: { | ||||
teamID: this.teamID, | ||||
}, | ||||
}) | ||||
this.teamRequestUpdatedSub = teamReqDeleted | ||||
this.teamRequestDeleted$ = teamReqDeleted$.subscribe((result) => { | ||||
if (E.isLeft(result)) | ||||
throw new Error(`Team Request Deleted Error ${result.left}`) | ||||
this.removeRequest(result.right.teamRequestDeleted) | ||||
}) | ||||
} | } | |||
/** | /** | |||
* Expands a collection on the tree | * Expands a collection on the tree | |||
* | * | |||
* When a collection is loaded initially in the adapter, children and requests are not loaded (they will be set to null) | * When a collection is loaded initially in the adapter, children and requests are not loaded (they will be set to null) | |||
* Upon expansion those two fields will be populated | * Upon expansion those two fields will be populated | |||
* | * | |||
* @param {string} collectionID - The ID of the collection to expand | * @param {string} collectionID - The ID of the collection to expand | |||
*/ | */ | |||
async expandCollection(collectionID: string): Promise<void> { | async expandCollection(collectionID: string): Promise<void> { | |||
// TODO: While expanding one collection, block (or queue) the expansion of t he other, to avoid race conditions | // TODO: While expanding one collection, block (or queue) the expansion of t he other, to avoid race conditions | |||
const tree = this.collections$.value | const tree = this.collections$.value | |||
const collection = findCollInTree(tree, collectionID) | const collection = findCollInTree(tree, collectionID) | |||
if (!collection) return | if (!collection) return | |||
if (collection.children != null) return | if (collection.children != null) return | |||
const collections: TeamCollection[] = ( | const collections: TeamCollection[] = [] | |||
await getCollectionChildren(apolloClient, collectionID) | ||||
).map<TeamCollection>((el) => { | this.loadingCollections$.next([ | |||
return { | ...this.loadingCollections$.getValue(), | |||
id: el.id, | collectionID, | |||
title: el.title, | ]) | |||
children: null, | ||||
requests: null, | while (true) { | |||
const data = await runGQLQuery({ | ||||
query: GetCollectionChildrenDocument, | ||||
variables: { | ||||
collectionID, | ||||
cursor: | ||||
collections.length > 0 | ||||
? collections[collections.length - 1].id | ||||
: undefined, | ||||
}, | ||||
}) | ||||
if (E.isLeft(data)) { | ||||
this.loadingCollections$.next( | ||||
this.loadingCollections$.getValue().filter((x) => x !== collectionID) | ||||
) | ||||
throw new Error( | ||||
`Child Collection Fetch Error for ${collectionID}: ${data.left}` | ||||
) | ||||
} | } | |||
}) | ||||
const requests: TeamRequest[] = ( | collections.push( | |||
await getCollectionRequests(apolloClient, collectionID) | ...data.right.collection!.children.map( | |||
).map<TeamRequest>((el) => { | (el) => | |||
return { | <TeamCollection>{ | |||
id: el.id, | id: el.id, | |||
collectionID, | title: el.title, | |||
title: el.title, | children: null, | |||
request: translateToNewRequest(JSON.parse(el.request)), | requests: null, | |||
} | ||||
) | ||||
) | ||||
if (data.right.collection!.children.length !== TEAMS_BACKEND_PAGE_SIZE) | ||||
break | ||||
} | ||||
const requests: TeamRequest[] = [] | ||||
while (true) { | ||||
const data = await runGQLQuery({ | ||||
query: GetCollectionRequestsDocument, | ||||
variables: { | ||||
collectionID, | ||||
cursor: | ||||
requests.length > 0 ? requests[requests.length - 1].id : undefined, | ||||
}, | ||||
}) | ||||
if (E.isLeft(data)) { | ||||
this.loadingCollections$.next( | ||||
this.loadingCollections$.getValue().filter((x) => x !== collectionID) | ||||
) | ||||
throw new Error(`Child Request Fetch Error for ${data}: ${data.left}`) | ||||
} | } | |||
}) | ||||
requests.push( | ||||
...data.right.requestsInCollection.map<TeamRequest>((el) => { | ||||
return { | ||||
id: el.id, | ||||
collectionID, | ||||
title: el.title, | ||||
request: translateToNewRequest(JSON.parse(el.request)), | ||||
} | ||||
}) | ||||
) | ||||
if (data.right.requestsInCollection.length !== TEAMS_BACKEND_PAGE_SIZE) | ||||
break | ||||
} | ||||
collection.children = collections | collection.children = collections | |||
collection.requests = requests | collection.requests = requests | |||
// Add to the entity ids set | ||||
collections.forEach((coll) => this.entityIDs.add(`collection-${coll.id}`)) | ||||
requests.forEach((req) => this.entityIDs.add(`request-${req.id}`)) | ||||
this.loadingCollections$.next( | ||||
this.loadingCollections$.getValue().filter((x) => x !== collectionID) | ||||
) | ||||
this.collections$.next(tree) | this.collections$.next(tree) | |||
} | } | |||
} | } | |||
End of changes. 56 change blocks. | ||||
284 lines changed or deleted | 407 lines changed or added |