Skip to content

Commit

Permalink
feat: Add fetching and caching of custom fields.
Browse files Browse the repository at this point in the history
  • Loading branch information
adam-coster committed Jul 3, 2021
1 parent e5bed0c commit 6a29180
Show file tree
Hide file tree
Showing 8 changed files with 279 additions and 138 deletions.
13 changes: 7 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
30 changes: 29 additions & 1 deletion src/lib/BravoClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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}
*/
Expand Down Expand Up @@ -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<DataFavroCustomField, BravoCustomField>;
this.cache.customFields = res;
}
return this.cache.customFields!;
}

//#endregion

private async deleteEntity(url: string) {
const res = await this.request(url, {
method: 'delete',
Expand Down
30 changes: 25 additions & 5 deletions src/lib/clientLib/BravoClientCache.ts
Original file line number Diff line number Diff line change
@@ -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<string, BravoResponseWidgets> = new Map();

/**
* Widget columns are fetched separately via the API. They can
* be fetched directly, but more likely will all be fetched at once
Expand All @@ -22,6 +27,13 @@ export class BravoClientCache {
*/
protected _columns: Map<string, BravoColumn[]> = 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;
Expand All @@ -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
Expand Down
13 changes: 10 additions & 3 deletions src/lib/clientLib/BravoResponse.ts
Original file line number Diff line number Diff line change
@@ -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: Entity,
idx?: number,
Expand Down
60 changes: 60 additions & 0 deletions src/lib/entities/BravoCustomField.ts
Original file line number Diff line number Diff line change
@@ -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<DataFavroCustomField> {
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;
}
}
2 changes: 1 addition & 1 deletion src/types/FavroApiTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[];
Expand Down
133 changes: 123 additions & 10 deletions src/types/FavroCardTypes.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import type { DataFavroCustomFieldLink } from './FavroCustomFieldTypes.js';
import type { OptionWidgetType } from './FavroWidgetTypes.js';

export type OptionFavroDescriptionFormat = 'plaintext' | 'markdown';
Expand Down Expand Up @@ -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;
Expand Down
Loading

0 comments on commit 6a29180

Please sign in to comment.