From 6a291804217fab2464aa46415b8d33884478a95f Mon Sep 17 00:00:00 2001 From: Adam Coster Date: Sat, 3 Jul 2021 10:15:17 -0500 Subject: [PATCH] feat: Add fetching and caching of custom fields. --- README.md | 13 +-- src/lib/BravoClient.ts | 30 +++++- src/lib/clientLib/BravoClientCache.ts | 30 +++++- src/lib/clientLib/BravoResponse.ts | 13 ++- src/lib/entities/BravoCustomField.ts | 60 ++++++++++++ src/types/FavroApiTypes.ts | 2 +- src/types/FavroCardTypes.ts | 133 +++++++++++++++++++++++-- src/types/FavroCustomFieldTypes.ts | 136 +++++--------------------- 8 files changed, 279 insertions(+), 138 deletions(-) create mode 100644 src/lib/entities/BravoCustomField.ts diff --git a/README.md b/README.md index bdbcf24..fe6af2a 100644 --- a/README.md +++ b/README.md @@ -73,14 +73,15 @@ As environment variables: - ✔ List cards - ✔ Find card by name - ✔ Delete a card (from a board or from EVERYWHERE) - - 🔜 Find card by field value (tricky, requires handling "Custom Fields") - - 🔜 Add an attachment + - 🔜 Change field values on a card, including Custom Fields + - 🔜 Find card by field value, including Custom Fields + - ❓ Add an attachment - ❓ Cache cards to reduce API calls (cards change frequently, so this might be a bad idea anyway) - Custom Fields - - 🔜 Fetch and cache Custom Fields - - ❓ Create Custom Field - - ❓ Delete Custom Field - - ❓ Update a Custom Field + - ✔ Fetch and cache Custom Field definitions + - ~~Create Custom Field~~ (No API endpoint for this) + - ~~Delete Custom Field~~ (No API endpoint for this) + - ~~Update a Custom Field~~ (No API endpoint for this) - Comments - 🔜 Create a comment - 🔜 List comments diff --git a/src/lib/BravoClient.ts b/src/lib/BravoClient.ts index 86e5552..6deea01 100644 --- a/src/lib/BravoClient.ts +++ b/src/lib/BravoClient.ts @@ -33,6 +33,8 @@ import { FavroApiPostCard, } from '$/types/FavroCardTypes.js'; import { BravoCard } from './entities/BravoCard.js'; +import { BravoCustomField } from './entities/BravoCustomField.js'; +import { DataFavroCustomField } from '$/types/FavroCustomFieldTypes.js'; /** * The `BravoClient` class should be singly-instanced for a given @@ -502,7 +504,8 @@ export class BravoClient extends FavroClient { } /** - * Fetch cards. **Note**: not cached! + * Fetch cards. **Note**: not cached! Cards are lazy-loaded + * to reduce API calls. * * {@link https://favro.com/developer/#get-all-cards} */ @@ -540,6 +543,31 @@ export class BravoClient extends FavroClient { //#endregion + //#region CUSTOM FIELDS + + /** + * Get and cache *all* Custom Fields. Lazy-loaded to reduce + * API calls. + * + * (The Favro API does not provide any filter options, so + * Custom Fields can be obtained 1 at a time or 1 page at a time.) + * + * {@link https://favro.com/developer/#get-all-custom-fields} + */ + async listCustomFields() { + if (!this.cache.customFields) { + const res = (await this.requestWithReturnedEntities( + `customfields`, + { method: 'get' }, + BravoCustomField, + )) as BravoResponseEntities; + this.cache.customFields = res; + } + return this.cache.customFields!; + } + + //#endregion + private async deleteEntity(url: string) { const res = await this.request(url, { method: 'delete', diff --git a/src/lib/clientLib/BravoClientCache.ts b/src/lib/clientLib/BravoClientCache.ts index a528a14..8a47360 100644 --- a/src/lib/clientLib/BravoClientCache.ts +++ b/src/lib/clientLib/BravoClientCache.ts @@ -1,19 +1,24 @@ -import { BravoOrganization } from '$entities/BravoOrganization.js'; -import { BravoUser } from '$entities/users'; -import { BravoCollection } from '$entities/BravoCollection.js'; -import type { BravoResponseWidgets } from './BravoResponse.js'; import { assertBravoClaim } from '$lib/errors.js'; -import { BravoColumn } from '../entities/BravoColumn.js'; +import type { BravoOrganization } from '$entities/BravoOrganization.js'; +import type { BravoUser } from '$entities/users'; +import type { BravoCollection } from '$entities/BravoCollection.js'; +import type { + BravoResponseCustomFields, + BravoResponseWidgets, +} from './BravoResponse.js'; +import type { BravoColumn } from '../entities/BravoColumn.js'; export class BravoClientCache { protected _organizations?: BravoOrganization[]; protected _users?: BravoUser[]; protected _collections?: BravoCollection[]; + /** * Widget paging results keyed by collectionId, with the empty string `''` * used to key the paging result from not using a collectionId (global). */ protected _widgets: Map = new Map(); + /** * Widget columns are fetched separately via the API. They can * be fetched directly, but more likely will all be fetched at once @@ -22,6 +27,13 @@ export class BravoClientCache { */ protected _columns: Map = new Map(); + /** + * Custom Field definitions are needed to get actual values and + * field names (plus possible values) from cards. Favro has no + * filtering options for Custom Fields + */ + protected _customFields?: BravoResponseCustomFields; + get collections() { // @ts-expect-error return this._collections ? [...this._collections] : undefined; @@ -46,6 +58,14 @@ export class BravoClientCache { this._organizations = orgs; } + get customFields() { + // @ts-expect-error + return this._customFields; + } + set customFields(customFields: BravoResponseCustomFields) { + this._customFields = customFields; + } + /** * Get the widget paging object from a get-all-widgets * search, keyed by collectionId. If the collectionId diff --git a/src/lib/clientLib/BravoResponse.ts b/src/lib/clientLib/BravoResponse.ts index c1d7433..eeaebaf 100644 --- a/src/lib/clientLib/BravoResponse.ts +++ b/src/lib/clientLib/BravoResponse.ts @@ -1,14 +1,21 @@ -import type { DataFavroWidget } from '$/types/FavroWidgetTypes.js'; -import type { BravoWidget } from '$entities/BravoWidget.js'; import { BravoClient } from '$lib/BravoClient'; import { BravoEntity } from '$lib/BravoEntity'; -import { FavroResponse } from './FavroResponse'; +import type { FavroResponse } from './FavroResponse'; +import type { DataFavroCustomField } from '$/types/FavroCustomFieldTypes.js'; +import type { DataFavroWidget } from '$/types/FavroWidgetTypes.js'; +import type { BravoWidget } from '$entities/BravoWidget.js'; +import type { BravoCustomField } from '../entities/BravoCustomField.js'; export type BravoResponseWidgets = BravoResponseEntities< DataFavroWidget, BravoWidget >; +export type BravoResponseCustomFields = BravoResponseEntities< + DataFavroCustomField, + BravoCustomField +>; + export type BravoResponseEntitiesMatchFunction = ( entity: Entity, idx?: number, diff --git a/src/lib/entities/BravoCustomField.ts b/src/lib/entities/BravoCustomField.ts new file mode 100644 index 0000000..a4e81c9 --- /dev/null +++ b/src/lib/entities/BravoCustomField.ts @@ -0,0 +1,60 @@ +import type { DataFavroCustomField } from '$/types/FavroCustomFieldTypes.js'; +import { BravoEntity } from '$lib/BravoEntity.js'; + +export type BravoCustomFieldTypeName = + typeof BravoCustomField['typeNames'][number]; + +export class BravoCustomField extends BravoEntity { + get name() { + return this._data.name; + } + + get customFieldId() { + return this._data.customFieldId; + } + + get type() { + return this._data.type; + } + + /** + * For choice-based custom fields (e.g. tags, multi-select), + * the set of possible values and their associated IDs. + * Needed when looking up id-based field values on card data. + */ + get customFieldItems() { + return this._data.customFieldItems; + } + + get organizationId() { + return this._data.organizationId; + } + + get enabled() { + return this._data.enabled; + } + + equals(org: BravoCustomField) { + return ( + this.hasSameConstructor(org) && this.customFieldId === org.customFieldId + ); + } + + static get typeNames() { + return [ + 'Number', + 'Time', + 'Text', + 'Rating', + 'Vote', + 'Checkbox', + 'Date', + 'Timeline', + 'Link', + 'Members', + 'Tags', + 'Status', + 'Multiple select', + ] as const; + } +} diff --git a/src/types/FavroApiTypes.ts b/src/types/FavroApiTypes.ts index 90e9df0..5334ed7 100644 --- a/src/types/FavroApiTypes.ts +++ b/src/types/FavroApiTypes.ts @@ -44,8 +44,8 @@ export interface DataFavroCollection { /** The id of the collection. */ collectionId: string; /** The id of the organization that this collection exists in. */ - /** The name of the collection. */ organizationId: string; + /** The name of the collection. */ name: string; /** The array of collection members that the collection is shared to. */ sharedToUsers: DataFavroCollectionMember[]; diff --git a/src/types/FavroCardTypes.ts b/src/types/FavroCardTypes.ts index ed989bc..f03a2f1 100644 --- a/src/types/FavroCardTypes.ts +++ b/src/types/FavroCardTypes.ts @@ -1,4 +1,3 @@ -import type { DataFavroCustomFieldLink } from './FavroCustomFieldTypes.js'; import type { OptionWidgetType } from './FavroWidgetTypes.js'; export type OptionFavroDescriptionFormat = 'plaintext' | 'markdown'; @@ -47,21 +46,135 @@ export type FavroApiGetCardsParams = | FavroApiGetCardsByWidgetCommonId | FavroApiGetCardsByCollectionId; +// FOR SETTING VALUES ON A CARD + +interface DataFavroCustomFieldMembers { + /** The list of members, that will be added to the card custom field (array of userIds). Optional. */ + addUserIds: string[]; + /** The list of members, that will be removed from card custom field (array of userIds). Optional. */ + removeUserIds: string[]; + /** The list of card assignment, that will update their statuses on the custom field accordingly. Optional. */ + completeUsers: string[]; +} +interface DataFavroCustomFieldTags { + /** The list of tag names or card tags that will be added to this card custom field. If the tag does not exist in the organization it will be created. Optional. */ + addTags: string[]; + /** A list of tagIds that will be added to this card custom field. Optional. */ + addTagIds: string[]; + /** The list of tag names, that will be removed from this card custom field. Optional. */ + removeTags: string[]; + /** The list of tag IDs, that will be removed from this card custom field. Optional. */ + removeTagIds: string[]; +} + +/** + * + * + * {@link https://favro.com/developer/#card-custom-fields} + */ +export type DataFavroCardCustomField = { customFieldId: string } & ( + | DataFavroCardFieldCheckbox + | DataFavroCardFieldDate + | DataFavroCardFieldLink + | DataFavroCardFieldMembers + | DataFavroCardFieldMultipleSelect + | DataFavroCardFieldNumber + | DataFavroCardFieldRating + | DataFavroCardFieldStatus + | DataFavroCardFieldTags + | DataFavroCardFieldText + | DataFavroCardFieldTime + | DataFavroCardFieldTimeline + | DataFavroCardFieldVote +); + +interface DataFavroCardFieldNumber { + /** The total value of the field. */ + total: number; +} +interface DataFavroCardFieldTime { + /** The total value of all time reported by all users. */ + total: number; + /** + * The values reported by each user. + * The object key represents the userId of the user. + * The value is an array of user entries. Refer to custom field time user reports. */ + reports: { + [userId: string]: { + /** The id of the user entry. */ + reportId: string; + /** The user entry value. Passing 0 will remove report entry. For custom fields with type "Time", this value is in milliseconds. */ + value: number; + /** The description of the time report entry. */ + description: string; + /** The report date in ISO format. */ + createdAt: string; + }[]; + }; +} +interface DataFavroCardFieldText { + /** The value of the field. */ + value: string; +} +interface DataFavroCardFieldRating { + /** The value of the field. Valid value is integer from 0 to 5. */ + total: 0 | 1 | 2 | 3 | 4 | 5; +} +interface DataFavroCardFieldVote { + /** The id array of users that vote for the field. */ + value: string[]; +} +interface DataFavroCardFieldCheckbox { + /** The value of the field. */ + value: boolean; +} +interface DataFavroCardFieldDate { + /** The date value in ISO format. */ + value: string; +} +interface DataFavroCardFieldTimeline { + /** The value options of the field. See custom field timeline. */ + timeline: { + /** The value of start date in ISO format. Required. */ + startDate: string; + /** The value of due date in ISO format. Required. */ + dueDate: string; + /** The value to determine display text of field should include time or not. */ + showTime: boolean; + }; +} +interface DataFavroCardFieldLink { + /** The value options of the field. See custom field link. */ + link: { + /** The url of the field. Required. */ + url: string; + /** The display text of the field. Optional. */ + text: string; + }; +} +interface DataFavroCardFieldMembers { + /** The id array of users that are assigned to card. */ + value: string[]; +} +interface DataFavroCardFieldTags { + /** The id array of tags that are added to card. */ + value: string[]; +} +interface DataFavroCardFieldStatus { + /** The id array of item that are added to card. */ + value: string[]; +} +interface DataFavroCardFieldMultipleSelect { + /** The id array of item that are added to card. */ + value: string[]; +} + /** {@link https://favro.com/developer/#card-assignment} */ interface DataFavroCardAssignment { userId: string; completed: boolean; } -/** {@link https://favro.com/developer/#card-custom-fields} */ -interface DataFavroCardCustomField { - customFieldId: string; - /** A lot of types seem to just use an ID or array of IDs */ - value?: string | string[]; - /** If a Link type */ - link?: DataFavroCustomFieldLink; -} - /** {@link https://favro.com/developer/#card-attachment} */ export interface DataFavroCardAttachment { name: string; diff --git a/src/types/FavroCustomFieldTypes.ts b/src/types/FavroCustomFieldTypes.ts index 088cd23..9fb3707 100644 --- a/src/types/FavroCustomFieldTypes.ts +++ b/src/types/FavroCustomFieldTypes.ts @@ -10,117 +10,29 @@ * {@link https://favro.com/developer/#custom-field-types} */ -export type DataFavroCardField = - | DataFavroCardFieldNumber - | DataFavroCardFieldTime - | DataFavroCardFieldText - | DataFavroCardFieldRating - | DataFavroCardFieldVote - | DataFavroCardFieldCheckbox - | DataFavroCardFieldDate - | DataFavroCardFieldTimeline - | DataFavroCardFieldLink - | DataFavroCardFieldMembers - | DataFavroCardFieldTags - | DataFavroCardFieldStatus - | DataFavroCardFieldMultipleSelect; +import type { BravoCustomFieldTypeName } from '$/lib/entities/BravoCustomField.js'; -export interface DataFavroCardFieldNumber { - /** The total value of the field. */ - total: number; -} -export interface DataFavroCardFieldTime { - /** The total value of all time reported by all users. */ - total: number; - /** - * The values reported by each user. - * The object key represents the userId of the user. - * The value is an array of user entries. Refer to custom field time user reports. */ - reports: { [userId: string]: DataFavroCustomFieldTimeUserReports }; -} -export interface DataFavroCardFieldText { - /** The value of the field. */ - value: string; -} -export interface DataFavroCardFieldRating { - /** The value of the field. Valid value is integer from 0 to 5. */ - total: 0 | 1 | 2 | 3 | 4 | 5; -} -export interface DataFavroCardFieldVote { - /** The id array of users that vote for the field. */ - value: string[]; -} -export interface DataFavroCardFieldCheckbox { - /** The value of the field. */ - value: boolean; -} -export interface DataFavroCardFieldDate { - /** The date value in ISO format. */ - value: string; -} -export interface DataFavroCardFieldTimeline { - /** The value options of the field. See custom field timeline. */ - timeline: DataFavroCustomFieldTimeline; -} -export interface DataFavroCardFieldLink { - /** The value options of the field. See custom field link. */ - link: DataFavroCustomFieldLink; -} -export interface DataFavroCardFieldMembers { - /** The id array of users that are assigned to card. */ - value: string[]; -} -export interface DataFavroCardFieldTags { - /** The id array of tags that are added to card. */ - value: string[]; -} -export interface DataFavroCardFieldStatus { - /** The id array of item that are added to card. */ - value: string[]; -} -export interface DataFavroCardFieldMultipleSelect { - /** The id array of item that are added to card. */ - value: string[]; -} -export interface DataFavroCustomFieldTimeUserReports { - /** The id of the user entry. */ - reportId: string; - /** The user entry value. Passing 0 will remove report entry. For custom fields with type "Time", this value is in milliseconds. */ - value: number; - /** The description of the time report entry. */ - description: string; - /** The report date in ISO format. */ - createdAt: string; -} -export interface DataFavroCustomFieldTimeline { - /** The value of start date in ISO format. Required. */ - startDate: string; - /** The value of due date in ISO format. Required. */ - dueDate: string; - /** The value to determine display text of field should include time or not. */ - showTime: boolean; -} -export interface DataFavroCustomFieldLink { - /** The url of the field. Required. */ - url: string; - /** The display text of the field. Optional. */ - text: string; -} -export interface DataFavroCustomFieldMembers { - /** The list of members, that will be added to the card custom field (array of userIds). Optional. */ - addUserIds: string[]; - /** The list of members, that will be removed from card custom field (array of userIds). Optional. */ - removeUserIds: string[]; - /** The list of card assignment, that will update their statuses on the custom field accordingly. Optional. */ - completeUsers: string[]; -} -export interface DataFavroCustomFieldTags { - /** The list of tag names or card tags that will be added to this card custom field. If the tag does not exist in the organization it will be created. Optional. */ - addTags: string[]; - /** A list of tagIds that will be added to this card custom field. Optional. */ - addTagIds: string[]; - /** The list of tag names, that will be removed from this card custom field. Optional. */ - removeTags: string[]; - /** The list of tag IDs, that will be removed from this card custom field. Optional. */ - removeTagIds: string[]; +/** + * The definition data for a custom field. + * + * {@link https://favro.com/developer/#custom-field} + */ +export interface DataFavroCustomField { + /** The id of the organization that this custom field exists in. */ + organizationId: string; + /** The id of the custom field. */ + customFieldId: string; + /** Custom field type. */ + type: BravoCustomFieldTypeName; + /** The name of the custom field. */ + name: string; + /** True if the custom field is currently enabled for the organization. */ + enabled: boolean; + /** The list of items that this custom field can have in case it is a selectable one. */ + customFieldItems?: { + /** The id of the custom field item. */ + customFieldItemId: string; + /** The name of the custom field item. */ + name: string; + }[]; }