diff --git a/src/common/types/annotations.js b/src/common/types/annotations.js.flow similarity index 100% rename from src/common/types/annotations.js rename to src/common/types/annotations.js.flow diff --git a/src/common/types/annotations.ts b/src/common/types/annotations.ts new file mode 100644 index 0000000000..d2173878ab --- /dev/null +++ b/src/common/types/annotations.ts @@ -0,0 +1,98 @@ +import type { BoxItemVersionMini, Reply, User } from './core'; +import type { ActionItemError, Comment, FeedItemStatus } from './feed'; + +export type Frame = { + type: 'frame'; + value: number; +}; + +export type Page = { + type: 'page'; + value: number; +}; + +export type Rect = { + fill?: { + color: string; + }; + height: number; + stroke?: { + color: string; + size: number; + }; + type: 'rect'; + width: number; + x: number; + y: number; +}; + +export type TargetDrawing = { + location: Frame | Page; + type: 'drawing'; +}; + +export type TargetHighlight = { + location: Page; + text?: string; + type: 'highlight'; +}; + +export type TargetRegion = { + location: Frame | Page; + shape?: Rect; + type: 'region'; +}; + +export type TargetPoint = { + location: Page; + type: 'point'; + x: number; + y: number; +}; + +export type Target = TargetDrawing | TargetHighlight | TargetPoint | TargetRegion; + +export type AnnotationPermission = { + can_delete?: boolean; + can_edit?: boolean; + can_reply?: boolean; + can_resolve?: boolean; +}; + +export type Resolution = { + resolved_at?: string | null; + resolved_by?: User | null; +}; + +export type Annotation = { + created_at: string; + created_by: User; + description?: Reply; + error?: ActionItemError; // Doesn't come from the API but used in the FeedItems, + file_version: BoxItemVersionMini | null; + id: string; + isPending?: boolean; // Doesn't come from the API but used in the FeedItems, + isRepliesLoading?: boolean; + modified_at: string; + modified_by: User; + permissions: AnnotationPermission; + replies?: Array; + resolution?: Resolution | null; + status?: FeedItemStatus; + target: Target; + total_reply_count?: number; + type: 'annotation'; +}; + +export type Annotations = { + entries: Array; + limit: number; + next_marker: string | null; +}; + +export type NewAnnotation = { + description?: { + message: string; + }; + target: Target; +}; diff --git a/src/common/types/api.js b/src/common/types/api.js.flow similarity index 90% rename from src/common/types/api.js rename to src/common/types/api.js.flow index 4aa79992ce..e350f998f5 100644 --- a/src/common/types/api.js +++ b/src/common/types/api.js.flow @@ -86,17 +86,19 @@ type APIOptions = { /** * Optional regional metadata API host (e.g. "https://api-jp.box.com"). * - * Affects ONLY metadata *instance* endpoints (file/folder - * `/metadata/...`). Templates, taxonomies, suggestions, options, and - * metadata queries always use `apiHost` regardless of this value. + * Affects metadata *instance* endpoints (file/folder `/metadata/...`) + * and metadata *template* endpoints (`/metadata_templates/...`). + * Taxonomies, suggestions, options, and metadata queries always use + * `apiHost` regardless of this value. * * Forward-compatible by design: when undefined, empty, or equal to * `apiHost`, the resulting URLs are identical to those produced when * the option is not set. Dropping the option later is therefore a * no-op for any consumer. * - * Transitional: this option lets host applications pin metadata- - * instance traffic to a regional Box gateway while the global API host + * Transitional: this option lets host applications pin metadata + * instance and template traffic to a regional Box gateway while the + * global API host * is not yet regionalized end-to-end. It is expected to be retired in * a future major version. */ diff --git a/src/common/types/api.ts b/src/common/types/api.ts new file mode 100644 index 0000000000..6debef4c14 --- /dev/null +++ b/src/common/types/api.ts @@ -0,0 +1,133 @@ +/* eslint-disable @typescript-eslint/no-explicit-any -- Preserve Flow any and Function contracts. */ +import type { AxiosError } from 'axios'; +import { HTTP_GET, HTTP_POST, HTTP_PUT, HTTP_DELETE, HTTP_OPTIONS, HTTP_HEAD } from '../../constants'; +import { JSON_PATCH_OP_ADD, JSON_PATCH_OP_REMOVE, JSON_PATCH_OP_REPLACE, JSON_PATCH_OP_TEST } from '../constants'; +import type { ElementOrigin } from '../../elements/common/flowTypes'; +import type { StringAnyMap, StringMap, Token } from './core'; +import type APICache from '../../utils/Cache'; + +type Method = + | typeof HTTP_DELETE + | typeof HTTP_GET + | typeof HTTP_POST + | typeof HTTP_OPTIONS + | typeof HTTP_HEAD + | typeof HTTP_PUT; + +type JSONPatch = { + op: + | typeof JSON_PATCH_OP_ADD + | typeof JSON_PATCH_OP_REMOVE + | typeof JSON_PATCH_OP_REPLACE + | typeof JSON_PATCH_OP_TEST; + path: string; + value?: any; +}; + +type JSONPatchOperations = Array; + +type RequestOptions = { + fields?: Array; + forceFetch?: boolean; + noPagination?: boolean; + refreshCache?: boolean; + // Extra X-Rep-Hints brackets appended to the default header (e.g. '[waveform]') + repHints?: string; +}; + +type PayloadType = StringAnyMap | Array; + +type RequestData = { + data: PayloadType; + headers?: StringMap; + id?: string; + params?: StringAnyMap; + url: string; +}; + +type ErrorResponseData = { + code: string; + context_info: any; + help_url: string; + message: string; + request_id: string; + status: number; + type: 'error'; +}; + +type ElementsXhrError = AxiosError | ErrorResponseData; + +type ElementsError = { + code: string; + context_info: any; + message: string; + origin: ElementOrigin; + type: 'error'; +}; + +type ErrorContextProps = { + onError: (error: ElementsXhrError | Error, code: string, contextInfo?: any, origin?: ElementOrigin) => void; +}; + +type ElementsErrorCallback = (e: ElementsXhrError, code: string, contextInfo?: any) => void; + +type ElementsSuccess = { + code: string; + showNotification: boolean; +}; + +type APIOptions = { + apiHost?: string; + cache?: APICache; + clientName?: string; + consoleError?: boolean; + consoleLog?: boolean; + id?: string; + language?: string; + /** + * Optional regional metadata API host (e.g. "https://api-jp.box.com"). + * + * Affects metadata *instance* endpoints (file/folder `/metadata/...`) + * and metadata *template* endpoints (`/metadata_templates/...`). + * Taxonomies, suggestions, options, and metadata queries always use + * `apiHost` regardless of this value. + * + * Forward-compatible by design: when undefined, empty, or equal to + * `apiHost`, the resulting URLs are identical to those produced when + * the option is not set. Dropping the option later is therefore a + * no-op for any consumer. + * + * Transitional: this option lets host applications pin metadata + * instance and template traffic to a regional Box gateway while the + * global API host + * is not yet regionalized end-to-end. It is expected to be retired in + * a future major version. + */ + metadataApiHost?: string; + metadataNamespaceMode?: string; + requestInterceptor?: any; + responseInterceptor?: any; + retryableStatusCodes?: Array; + sharedLink?: string; + sharedLinkPassword?: string; + shouldRetry?: boolean; + token: Token; + uploadHost?: string; + version?: string; +}; + +export type { + APIOptions, + ElementOrigin, + ElementsError, + ElementsErrorCallback, + ElementsSuccess, + ElementsXhrError, + ErrorContextProps, + ErrorResponseData, + RequestData, + RequestOptions, + JSONPatchOperations, + Method, + PayloadType, +}; diff --git a/src/common/types/feed.js b/src/common/types/feed.js.flow similarity index 100% rename from src/common/types/feed.js rename to src/common/types/feed.js.flow diff --git a/src/common/types/feed.ts b/src/common/types/feed.ts new file mode 100644 index 0000000000..fde4fc9e99 --- /dev/null +++ b/src/common/types/feed.ts @@ -0,0 +1,249 @@ +import type { MessageDescriptor } from 'react-intl'; +import { + ACTIVITY_FILTER_OPTION_ALL, + ACTIVITY_FILTER_OPTION_RESOLVED, + ACTIVITY_FILTER_OPTION_TASKS, + ACTIVITY_FILTER_OPTION_UNRESOLVED, + COMMENT_STATUS_OPEN, + COMMENT_STATUS_RESOLVED, + FEED_ITEM_TYPE_ANNOTATION, + FEED_ITEM_TYPE_APP_ACTIVITY, + FEED_ITEM_TYPE_COMMENT, + FEED_ITEM_TYPE_VERSION, + FEED_ITEM_TYPE_TASK, + FILE_ACTIVITY_TYPE_ANNOTATION, + FILE_ACTIVITY_TYPE_APP_ACTIVITY, + FILE_ACTIVITY_TYPE_COMMENT, + FILE_ACTIVITY_TYPE_ENHANCED_ANNOTATION, + FILE_ACTIVITY_TYPE_ENHANCED_COMMENT, + FILE_ACTIVITY_TYPE_ENHANCED_COMMENT_TIMESPAN, + FILE_ACTIVITY_TYPE_TASK, + FILE_ACTIVITY_TYPE_VERSION, +} from '../../constants'; +import type { BoxItemPermission, BoxItemVersion, Reply, User } from './core'; +import type { Annotation, AnnotationPermission, Annotations, Resolution } from './annotations'; +import type { TaskNew } from './tasks'; + +type FeedItemType = + | typeof FEED_ITEM_TYPE_ANNOTATION + | typeof FEED_ITEM_TYPE_APP_ACTIVITY + | typeof FEED_ITEM_TYPE_COMMENT + | typeof FEED_ITEM_TYPE_VERSION + | typeof FEED_ITEM_TYPE_TASK; + +// Feed item types that can receive deeplinks inline in the feed +type FocusableFeedItemType = + | typeof FEED_ITEM_TYPE_TASK + | typeof FEED_ITEM_TYPE_COMMENT + | typeof FEED_ITEM_TYPE_ANNOTATION; + +// Feed item types that represent user's written response (that also can have replies) +type CommentFeedItemType = typeof FEED_ITEM_TYPE_COMMENT | typeof FEED_ITEM_TYPE_ANNOTATION; + +type BoxCommentPermission = { + can_delete?: boolean; + can_edit?: boolean; + can_reply?: boolean; + can_resolve?: boolean; +}; + +type BoxTaskPermission = { + can_delete?: boolean; + can_update?: boolean; +}; + +type BaseFeedItem = { + created_at: string; + created_by: User; + id: string; +}; + +// Used in Annotation and Comment +type FeedItemStatus = typeof COMMENT_STATUS_OPEN | typeof COMMENT_STATUS_RESOLVED; + +// this is a subset of TaskNew, which imports as `any` +type Task = BaseFeedItem & { + permissions: BoxTaskPermission; + type: typeof FEED_ITEM_TYPE_TASK; +}; + +type Tasks = { + entries: Array; + next_marker: string | null | undefined; +}; + +type Comment = BaseFeedItem & { + isPending?: boolean; + isRepliesLoading?: boolean; + is_reply_comment?: boolean; + message?: string; + modified_at: string; + parent?: { + id: string; + type: CommentFeedItemType; + }; + permissions: BoxCommentPermission; + replies?: Array; + resolution?: Resolution | null; + status?: FeedItemStatus; + tagged_message: string; + total_reply_count?: number; + type: typeof FEED_ITEM_TYPE_COMMENT; +}; + +type Comments = { + entries: Array; + total_count: number; +}; + +type ThreadedComments = { + entries: Array; + limit: number; + next_marker: string; +}; + +type ActivityTemplateItem = { + id: string; + type: 'activity_template'; +}; + +type AppItem = { + icon_url: string; + id: string; + name: string; + type: 'app'; +}; + +type BaseAppActivityItem = { + activity_template: ActivityTemplateItem; + app: AppItem; + created_by: User; + id: string; + rendered_text: string; + type: typeof FEED_ITEM_TYPE_APP_ACTIVITY; +}; + +type AppActivityAPIItem = { + occurred_at: string; +} & BaseAppActivityItem; + +type AppActivityAPIItems = { + entries: Array; + total_count: number; +}; + +type AppActivityItem = { + created_at: string; + occurred_at?: string; + permissions: BoxItemPermission; +} & BaseAppActivityItem; + +type AppActivityItems = { + entries: Array; + total_count: number; +}; + +type FeedItem = Annotation | Comment | Task | BoxItemVersion | AppActivityItem; + +type FeedItems = Array; + +type FocusableFeedItem = Annotation | Comment | Task; + +type CommentFeedItem = Annotation | Comment; + +type ActionItemError = { + action?: { + onAction: () => void; + text: MessageDescriptor; + }; + message: MessageDescriptor; + title: MessageDescriptor; +}; + +type ActivityFilterOption = + | typeof ACTIVITY_FILTER_OPTION_ALL + | typeof ACTIVITY_FILTER_OPTION_UNRESOLVED + | typeof ACTIVITY_FILTER_OPTION_RESOLVED + | typeof ACTIVITY_FILTER_OPTION_TASKS; + +type ActivityFilterItemType = + | typeof ACTIVITY_FILTER_OPTION_ALL + | typeof COMMENT_STATUS_OPEN + | typeof COMMENT_STATUS_RESOLVED + | typeof FEED_ITEM_TYPE_TASK; + +// FILE_ACTIVITY_TYPE_ENHANCED_* are wire-only; the parser remaps them to the legacy variants +type FileActivityTypes = + | typeof FILE_ACTIVITY_TYPE_ANNOTATION + | typeof FILE_ACTIVITY_TYPE_APP_ACTIVITY + | typeof FILE_ACTIVITY_TYPE_COMMENT + | typeof FILE_ACTIVITY_TYPE_ENHANCED_ANNOTATION + | typeof FILE_ACTIVITY_TYPE_ENHANCED_COMMENT + | typeof FILE_ACTIVITY_TYPE_ENHANCED_COMMENT_TIMESPAN + | typeof FILE_ACTIVITY_TYPE_TASK + | typeof FILE_ACTIVITY_TYPE_VERSION; + +type FileActivitySource = + | { + annotation: Annotation; + } + | { + app_activity: AppActivityItem; + } + | { + comment: Comment; + } + | { + enhanced_annotation: Annotation; + } + | { + enhanced_comment: Comment; + } + | { + enhanced_comment_timespan: Comment; + } + | { + task: TaskNew; + } + | { + versions: BoxItemVersion; + }; + +type FileActivity = { + activity_type: FileActivityTypes; + source: FileActivitySource; + type: 'activity'; +}; + +export type { + ActionItemError, + ActivityFilterItemType, + ActivityFilterOption, + ActivityTemplateItem, + Annotation, + AnnotationPermission, + Annotations, + AppActivityAPIItem, + AppActivityAPIItems, + AppActivityItem, + AppActivityItems, + AppItem, + BoxCommentPermission, + Comment, + CommentFeedItem, + CommentFeedItemType, + Comments, + FeedItem, + FeedItems, + FeedItemStatus, + FeedItemType, + FileActivity, + FileActivityTypes, + FocusableFeedItem, + FocusableFeedItemType, + Reply, + Resolution, + Task, + Tasks, + ThreadedComments, +}; diff --git a/src/common/types/metadata.js b/src/common/types/metadata.js.flow similarity index 99% rename from src/common/types/metadata.js rename to src/common/types/metadata.js.flow index 0dcd65372e..8476436029 100644 --- a/src/common/types/metadata.js +++ b/src/common/types/metadata.js.flow @@ -75,7 +75,7 @@ type MetadataFields = { [string]: MetadataFieldValue }; type MetadataQueryInstanceTypeField = { displayName: string, key: string, - options?: MetadataTemplateFieldOption, + options?: Array, type: string, value: ?MetadataFieldValue, }; diff --git a/src/common/types/metadata.ts b/src/common/types/metadata.ts new file mode 100644 index 0000000000..b8de27e83f --- /dev/null +++ b/src/common/types/metadata.ts @@ -0,0 +1,254 @@ +import { + FIELD_TYPE_DATE, + FIELD_TYPE_ENUM, + FIELD_TYPE_FLOAT, + FIELD_TYPE_MULTISELECT, + FIELD_TYPE_STRING, + FIELD_TYPE_TAXONOMY, + FIELD_TYPE_USER, +} from '../../features/metadata-instance-fields/constants'; +import type { SkillCards } from './skills'; + +type MetadataFieldType = + | typeof FIELD_TYPE_DATE + | typeof FIELD_TYPE_ENUM + | typeof FIELD_TYPE_FLOAT + | typeof FIELD_TYPE_MULTISELECT + | typeof FIELD_TYPE_STRING + | typeof FIELD_TYPE_TAXONOMY + | typeof FIELD_TYPE_USER; + +type MetadataTemplateFieldOption = { + id?: string; + key: string; +}; + +export type TaxonomyLevel = { + description: string; + displayName: string; + level: number; +}; + +type MetadataTemplateField = { + description?: string; + displayName: string; + hidden?: boolean; + id: string; + isHidden?: boolean; + key: string; // V2, + options?: Array; // V3, + type: MetadataFieldType; + levels?: Array; + namespace?: string; + taxonomyKey?: string; + taxonomy_key?: string; +}; + +type MetadataTemplate = { + displayName?: string; + fields?: Array; + hidden?: boolean; + id: string; + isHidden?: boolean; + namespace?: string; // MIGRATION/FINAL modes, + scope: string; // V2; absent for namespace-only templates in FINAL mode, + templateKey: string; // V3 +}; + +type MetadataTemplateSchemaResponse = { + data?: MetadataTemplate; +}; + +type MetadataSkillsTemplate = { + archivedItemTemplate?: { + archiveDate: string; + }; + boxSkillsCards?: SkillCards; +}; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Preserve Flow any and Function contracts. +type MetadataFieldValue = string | number | Array; + +type MetadataFields = { + [key: string]: MetadataFieldValue; +}; + +type MetadataQueryInstanceTypeField = { + displayName: string; + key: string; + options?: MetadataTemplateFieldOption[]; + type: string; + value: MetadataFieldValue | null | undefined; +}; + +type MetadataQueryInstanceTemplate = { + fields: Array; + id: string; +}; + +type MetadataType = { + enterprise?: MetadataQueryInstanceTemplate; + global?: MetadataSkillsTemplate; +}; + +type MetadataCascadePolicyConfiguration = { + agent: string; +}; + +type MetadataCascadePolicy = { + canEdit?: boolean; + id?: string; + cascadePolicyType?: string; + cascadePolicyConfiguration?: MetadataCascadePolicyConfiguration; +}; + +type MetadataCascadingPolicyData = { + id?: string; + isEnabled: boolean; + overwrite: boolean; + cascadePolicyConfiguration: MetadataCascadePolicyConfiguration | null; +}; + +type MetadataInstance = { + canEdit: boolean; + cascadePolicy?: MetadataCascadePolicy; + data: MetadataFields; + id: string; +}; + +type MetadataInstanceV2 = { + $canEdit: boolean; + $id: string; + $namespace?: string; // MIGRATION/FINAL modes, + $parent: string; + $scope?: string; // absent for namespace-only instances in MIGRATION/FINAL modes, + $template: string; + $type: string; + $typeVersion: number; + $version: number; +}; + +type MetadataEditor = { + hasError?: boolean; + instance: MetadataInstance; + isDirty?: boolean; + template: MetadataTemplate; +}; + +type MetadataSuggestion = { + $scope: string; + $templateKey: string; + suggestions: { + [key: string]: string | number | string[]; + }; +}; + +type MetadataOptionEntryAncestor = { + id: string; + display_name: string; + level: string; +}; + +type MetadataOptionEntry = { + id: string; + display_name: string; + level: number; + ancestors: MetadataOptionEntryAncestor[]; + deprecated: boolean; + selectable: boolean; +}; + +type MetadataOptions = { + entries: MetadataOptionEntry[]; + next_marker: string | null; + result_count: number; +}; + +type MetadataConfidenceScoreData = { + value: number; + level: string; + isAccepted: boolean; +}; + +type MetadataBoundingBox = { + left: number; + top: number; + right: number; + bottom: number; +}; + +type MetadataTargetLocationEntry = { + itemId: string; + page: number; + text: string; + boundingBox?: MetadataBoundingBox; +}; + +type MetadataDetailedFieldDetails = { + updatedAt: number; + updatedBy: string; + updatedAppId: string; + confidenceScore?: number; + confidenceLevel?: string; + process?: string; + targetLocation?: string; +}; + +type MetadataDetailedFieldValue = { + values: MetadataFieldValue; + details?: MetadataDetailedFieldDetails; +}; + +type MetadataTemplateInstanceField = { + confidenceScore?: MetadataConfidenceScoreData; + isExtracted?: boolean; + description?: string; + displayName?: string; + hidden?: boolean; + id?: string; + key: string; // V2, + levels?: Array; + options?: Array; // V3, + targetLocation?: Array; + type: MetadataFieldType; + value: MetadataFieldValue; +}; + +type MetadataTemplateInstance = { + canEdit: boolean; + isExternallyOwned?: boolean; + displayName?: string; + hidden?: boolean; + id: string; + fields: MetadataTemplateInstanceField[]; + namespace?: string; + scope?: string; + templateKey: string; + type: string; +}; + +export type { + MetadataBoundingBox, + MetadataConfidenceScoreData, + MetadataDetailedFieldDetails, + MetadataDetailedFieldValue, + MetadataTargetLocationEntry, + MetadataTemplateInstanceField, + MetadataTemplateInstance, + MetadataFieldType, + MetadataTemplateFieldOption, + MetadataTemplateField, + MetadataTemplate, + MetadataTemplateSchemaResponse, + MetadataFieldValue, + MetadataFields, + MetadataQueryInstanceTypeField, + MetadataType, + MetadataCascadePolicy, + MetadataCascadePolicyConfiguration, + MetadataCascadingPolicyData, + MetadataInstanceV2, + MetadataEditor, + MetadataSuggestion, + MetadataOptions, +}; diff --git a/src/common/types/metadataQueries.js b/src/common/types/metadataQueries.js.flow similarity index 100% rename from src/common/types/metadataQueries.js rename to src/common/types/metadataQueries.js.flow diff --git a/src/common/types/metadataQueries.ts b/src/common/types/metadataQueries.ts new file mode 100644 index 0000000000..41b5768b7c --- /dev/null +++ b/src/common/types/metadataQueries.ts @@ -0,0 +1,49 @@ +import type { MetadataInstanceV2 } from './metadata'; +import type { SortDirection, BoxItem } from './core'; + +type MetadataQueryResponseEntryEnterprise = { + [key: string]: MetadataInstanceV2; +}; + +type MetadataQueryResponseData = { + entries: Array; + next_marker?: string; +}; + +type MetadataQueryOrderByClause = { + direction: SortDirection; + field_key: string; +}; + +type MetadataFieldConfig = { + canEdit?: boolean; + displayName?: string; + key: string; +}; + +// FieldsToShow array items could be simple strings or objects +// e.g. const fieldsToShow: FieldsToShow = [ 'name', { key: 'desc', displayName: 'Desc', canEdit: true } ]; +type FieldsToShow = Array; + +type MetadataQuery = { + ancestor_folder_id: string; + fields?: Array; + from: string; + limit?: number; + marker?: string; + order_by?: Array; + query?: string; + query_params?: { + [key: string]: boolean | number | string; + }; + use_index?: string; +}; + +export type { + MetadataFieldConfig, + FieldsToShow, + MetadataQuery, + MetadataQueryOrderByClause, + MetadataQueryResponseData, + MetadataQueryResponseEntryEnterprise, +}; diff --git a/src/common/types/skills.js b/src/common/types/skills.js.flow similarity index 100% rename from src/common/types/skills.js rename to src/common/types/skills.js.flow diff --git a/src/common/types/skills.ts b/src/common/types/skills.ts new file mode 100644 index 0000000000..5d7115d554 --- /dev/null +++ b/src/common/types/skills.ts @@ -0,0 +1,55 @@ +import { SKILLS_KEYWORD, SKILLS_TIMELINE, SKILLS_TRANSCRIPT, SKILLS_FACE, SKILLS_STATUS } from '../../constants'; +import type { BoxItemVersion } from './core'; + +type SkillCardType = + | typeof SKILLS_KEYWORD + | typeof SKILLS_TIMELINE + | typeof SKILLS_TRANSCRIPT + | typeof SKILLS_FACE + | typeof SKILLS_STATUS; + +type SkillCardEntryType = 'text' | 'image'; + +type SkillCardLocalizableType = { + code?: string; + message?: string; +}; + +type SkillCardEntryTimeSlice = { + end?: number; + start: number; +}; + +type SkillCardEntry = { + appears?: Array; + image_url?: string; + label?: string; + text?: string; + type?: SkillCardEntryType; +}; + +type SkillCard = { + duration?: number; + entries: Array; + error?: string; + file_version: BoxItemVersion; + id?: string; + skill_card_title: SkillCardLocalizableType; + skill_card_type: SkillCardType; + status?: SkillCardLocalizableType; + title?: string; + type: 'skill_card'; +}; + +type SkillCards = { + cards: Array; +}; + +export type { + SkillCardEntryType, + SkillCardLocalizableType, + SkillCardEntryTimeSlice, + SkillCardEntry, + SkillCard, + SkillCards, +}; diff --git a/src/common/types/tasks.js b/src/common/types/tasks.js.flow similarity index 100% rename from src/common/types/tasks.js rename to src/common/types/tasks.js.flow diff --git a/src/common/types/tasks.ts b/src/common/types/tasks.ts new file mode 100644 index 0000000000..243b8e47d3 --- /dev/null +++ b/src/common/types/tasks.ts @@ -0,0 +1,115 @@ +import type { + FileMini, + FolderMini, + ISODate, + UserMini, + GroupMini, + MarkerPaginatedCollection, + SelectorItems, +} from './core'; + +type ID = string; + +type TaskCollabStatus = 'APPROVED' | 'REJECTED' | 'COMPLETED' | 'NOT_STARTED'; + +type TaskStatus = TaskCollabStatus | 'IN_PROGRESS'; + +type TaskCompletionRule = 'ALL_ASSIGNEES' | 'ANY_ASSIGNEE'; + +type TaskMini = { + created_at: ISODate; + id: ID; + modified_at: ISODate; + status: TaskStatus; + type: 'task'; +}; + +type TaskCollab = { + completed_at?: ISODate | null | undefined; + id: ID; + role: R; + status: TaskCollabStatus; + target: UserMini; + task?: TaskMini; + type: 'task_collaborator'; +}; + +type TaskCollabCreator = TaskCollab<'CREATOR'>; + +type TaskCollabAssignee = TaskCollab<'ASSIGNEE'> & { + permissions: { + can_delete: boolean; + can_update: boolean; + }; +}; + +type TaskLink = { + description?: string; + id: ID; + permissions: { + can_delete: boolean; + can_update: boolean; + }; + target?: FileMini | null | undefined | FolderMini | null | undefined | UserMini | null | undefined; + task?: TaskMini; + type: 'task_link'; +}; + +type TaskAssigneeCollection = MarkerPaginatedCollection; + +type TaskLinkCollection = MarkerPaginatedCollection; + +type TaskType = 'GENERAL' | 'APPROVAL'; +type TaskEditMode = 'CREATE' | 'EDIT'; + +type TaskNew = { + assigned_to: TaskAssigneeCollection; + completed_at?: ISODate | null | undefined; + completion_rule: TaskCompletionRule; + created_at: ISODate; + created_by: TaskCollabCreator; + description: string; + due_at?: ISODate | null | undefined; + id: ID; + modified_at?: ISODate; + permissions: { + can_create_task_collaborator: boolean; + can_create_task_link: boolean; + can_delete: boolean; + can_update: boolean; + }; + progress_at?: ISODate | null | undefined; + status: TaskStatus; + task_links: TaskLinkCollection; + task_type: TaskType; + type: 'task'; +}; + +type TaskPayload = { + description: string; + due_at?: string | null | undefined; + task_type: TaskType; +}; + +type TaskUpdatePayload = { + addedAssignees: SelectorItems; + completion_rule: TaskCompletionRule; + description: string; + due_at?: string | null | undefined; + id: string; + removedAssignees: Array; +}; + +export type { + TaskCollabStatus, + TaskCompletionRule, + TaskStatus, + TaskCollabAssignee, + TaskLink, + TaskAssigneeCollection, + TaskType, + TaskEditMode, + TaskNew, + TaskPayload, + TaskUpdatePayload, +}; diff --git a/src/common/types/upload.js b/src/common/types/upload.js.flow similarity index 100% rename from src/common/types/upload.js rename to src/common/types/upload.js.flow diff --git a/src/common/types/upload.ts b/src/common/types/upload.ts new file mode 100644 index 0000000000..fa1929bebc --- /dev/null +++ b/src/common/types/upload.ts @@ -0,0 +1,122 @@ +/* eslint-disable @typescript-eslint/no-explicit-any -- Preserve Flow any and Function contracts. */ +import { + STATUS_PENDING, + STATUS_IN_PROGRESS, + STATUS_STAGED, + STATUS_COMPLETE, + STATUS_ERROR, + STATUS_CANCELED, +} from '../../constants'; +import type MultiputUploadAPI from '../../api/uploads/MultiputUpload'; +import type PlainUploadAPI from '../../api/uploads/PlainUpload'; +import type { Token, BoxItem } from './core'; + +// TODO: replace with `UploadItemStatus` from @box/uploads-manager once 'inprogress' is aligned to 'uploading'. +type UploadStatus = + | typeof STATUS_PENDING + | typeof STATUS_IN_PROGRESS + | typeof STATUS_STAGED + | typeof STATUS_COMPLETE + | typeof STATUS_ERROR + | typeof STATUS_CANCELED; + +type FileSystemFileEntry = { + createReader: any; + file: any; + isDirectory: boolean; + isFile: boolean; + name: string; +}; + +type UploadFile = File & { + lastModifiedDate?: Date; + webkitRelativePath?: string; +}; + +type UploadItemAPIOptions = { + apiHost?: string; + fileId?: string; + folderId?: string; + token?: Token; + uploadInitTimestamp?: number; +}; + +type UploadDataTransferItemWithAPIOptions = { + item: DataTransferItem; + options?: UploadItemAPIOptions; +}; + +type UploadFileWithAPIOptions = { + file: UploadFile; + options?: UploadItemAPIOptions; +}; + +type DirectoryReader = { + readEntries: (arg1: any, arg2: any) => void; +}; + +type FolderUploadItem = { + boxFile?: BoxItem; + error?: any; + extension: string; + isFolder?: boolean; + name: string; + options?: UploadItemAPIOptions; + progress: number; + size: number; + status: UploadStatus; +}; + +type UploadItem = { + api: PlainUploadAPI | MultiputUploadAPI; + boxFile?: BoxItem; + bytesUploaded?: number; + bytesUploadedOnLastResume?: number; + dedupeKey?: string; + error?: any; + extension: string; + file: UploadFile; + isFolder?: boolean; + name: string; + options?: UploadItemAPIOptions; + progress: number; + remainingMs?: number; + size: number; + status: UploadStatus; + totalBytes?: number; +}; + +type MultiputConfig = { + digestReadahead: number; + initialRetryDelayMs: number; + maxRetryDelayMs: number; + parallelism: number; + requestTimeoutMs: number; + retries: number; +}; + +type MultiputPart = { + offset: number; + part_id: string; + sha1: string; + size: number; +}; + +type MultiputData = { + part?: MultiputPart; +}; + +export type { + UploadStatus, + FileSystemFileEntry, + UploadFile, + UploadItemAPIOptions, + UploadDataTransferItemWithAPIOptions, + UploadFileWithAPIOptions, + DirectoryReader, + FolderUploadItem, + UploadItem, + MultiputConfig, + MultiputPart, + MultiputData, +}; diff --git a/src/elements/common/current-user/withCurrentUser.tsx b/src/elements/common/current-user/withCurrentUser.tsx index 0534b12bd6..1f7d8f3111 100644 --- a/src/elements/common/current-user/withCurrentUser.tsx +++ b/src/elements/common/current-user/withCurrentUser.tsx @@ -14,7 +14,8 @@ import { ElementsXhrError, ErrorContextProps } from '../../../common/types/api'; import { Errors } from '../flowTypes'; // @ts-ignore: no ts definition // eslint-disable-next-line import/named -import { User } from '../../../common/types/core'; +import { BoxItem, User } from '../../../common/types/core'; +import type API from '../../../api'; export type WithCurrentUserProps = { currentUser?: User; @@ -76,7 +77,7 @@ export default function withCurrentUser

( * @return {void} */ fetchCurrentUser = (user?: User, shouldDestroy = false): void => { - const { api, file } = this.props; + const { api, file } = this.props as P & WithCurrentUserProps & { api: API; file?: BoxItem }; if (!file) { throw getBadItemError(); diff --git a/src/elements/content-explorer/ContentExplorer.tsx b/src/elements/content-explorer/ContentExplorer.tsx index cb5c59a42e..1660c6353b 100644 --- a/src/elements/content-explorer/ContentExplorer.tsx +++ b/src/elements/content-explorer/ContentExplorer.tsx @@ -323,7 +323,7 @@ class ContentExplorer extends Component { isUploadModalOpen: false, markers: [], metadataFilters: {}, - metadataTemplate: {}, + metadataTemplate: {} as MetadataTemplate, rootName: '', selectedItemIds: new Set(), searchQuery: '', @@ -459,7 +459,7 @@ class ContentExplorer extends Component { * @return {void} */ showMetadataQueryResults() { - const { features, metadataQuery = {} }: ContentExplorerProps = this.props; + const { features, metadataQuery = {} as MetadataQuery }: ContentExplorerProps = this.props; const { currentPageNumber, markers, metadataFilters, sortBy, sortDirection }: State = this.state; const metadataQueryClone = cloneDeep(metadataQuery); diff --git a/src/elements/content-explorer/MetadataQueryAPIHelper.ts b/src/elements/content-explorer/MetadataQueryAPIHelper.ts index 3687a2be85..4318382e7e 100644 --- a/src/elements/content-explorer/MetadataQueryAPIHelper.ts +++ b/src/elements/content-explorer/MetadataQueryAPIHelper.ts @@ -24,6 +24,8 @@ import type { MetadataTemplateSchemaResponse, MetadataTemplate, MetadataFieldValue, + MetadataFields, + MetadataInstanceV2, MetadataType, MetadataQueryInstanceTypeField, } from '../../common/types/metadata'; @@ -36,6 +38,7 @@ import { getStringFilter, mergeQueries, mergeQueryParams, + type QueryParams, } from './MetadataQueryBuilder'; import type { ExternalFilterValues } from './MetadataViewContainer'; @@ -133,7 +136,10 @@ export default class MetadataQueryAPIHelper { flattenMetadata = (metadata?: MetadataType): MetadataType => { const templateFields = getProp(this.metadataTemplate, 'fields', []); - const instance = getProp(metadata, `${this.templateScope}.${this.templateKey}`); + const instance: (MetadataInstanceV2 & MetadataFields) | undefined = getProp( + metadata, + `${this.templateScope}.${this.templateKey}`, + ); if (!instance) { return {}; @@ -290,7 +296,7 @@ export default class MetadataQueryAPIHelper { successCallback: () => void, errorCallback: ErrorCallback, ): Promise => { - const operations: JSONPatchOperations = []; + const operations: JSONPatchOperations[] = []; items.forEach(item => { const operation = this.generateOperations(item, templateOldFields, templateNewFields); operations.push(operation); @@ -303,7 +309,7 @@ export default class MetadataQueryAPIHelper { buildMetadataQueryParams = (filters: ExternalFilterValues) => { let argIndex = 0; let queries: string[] = []; - let queryParams: { [key: string]: number | Date | string } = {}; + let queryParams: QueryParams = {}; if (filters) { Object.keys(filters).forEach(key => { diff --git a/src/elements/content-explorer/MetadataQueryBuilder.ts b/src/elements/content-explorer/MetadataQueryBuilder.ts index e4f7205fd3..a605262cb6 100644 --- a/src/elements/content-explorer/MetadataQueryBuilder.ts +++ b/src/elements/content-explorer/MetadataQueryBuilder.ts @@ -1,8 +1,10 @@ import isNil from 'lodash/isNil'; import { getFileExtensions } from './utils'; +export type QueryParams = { [key: string]: boolean | number | string }; + type QueryResult = { - queryParams: { [key: string]: number | Date | string }; + queryParams: QueryParams; queries: string[]; keysGenerated: number; }; @@ -18,10 +20,7 @@ type SimpleRangeType = { // Union type for filter values type SimpleFilterValue = string[] | SimpleRangeType; -export const mergeQueryParams = ( - targetParams: { [key: string]: number | Date | string }, - sourceParams: { [key: string]: number | Date | string }, -): { [key: string]: number | Date | string } => { +export const mergeQueryParams = (targetParams: QueryParams, sourceParams: QueryParams): QueryParams => { return { ...targetParams, ...sourceParams }; }; @@ -134,7 +133,7 @@ export const getMimeTypeFilter = (filterValue: string[], fieldKey: string, argIn } let currentArgIndex = argIndexStart; - const queryParams: { [key: string]: number | Date | string } = {}; + const queryParams: QueryParams = {}; const queries: string[] = []; // Handle specific extensions and folder type diff --git a/src/elements/content-explorer/MetadataSidePanel.tsx b/src/elements/content-explorer/MetadataSidePanel.tsx index b21f3bbf6a..f60820027e 100644 --- a/src/elements/content-explorer/MetadataSidePanel.tsx +++ b/src/elements/content-explorer/MetadataSidePanel.tsx @@ -10,6 +10,7 @@ import { JSONPatchOperations, MetadataInstance, MetadataInstanceForm, + type MetadataTemplate as EditorMetadataTemplate, type MetadataTemplateField, } from '@box/metadata-editor'; @@ -59,7 +60,11 @@ const MetadataSidePanel = ({ selectedItemIds === 'all' ? currentCollection.items : currentCollection.items.filter(item => selectedItemIds.has(item.id)); - const templateInstance = useTemplateInstance(metadataTemplate, selectedItems, isEditing); + const templateInstance = useTemplateInstance( + metadataTemplate as unknown as EditorMetadataTemplate, + selectedItems, + isEditing, + ); const handleMetadataInstanceEdit = () => { onEditingChange(true); diff --git a/src/elements/content-explorer/MetadataViewContainer.tsx b/src/elements/content-explorer/MetadataViewContainer.tsx index d7b524ea48..8206ee446d 100644 --- a/src/elements/content-explorer/MetadataViewContainer.tsx +++ b/src/elements/content-explorer/MetadataViewContainer.tsx @@ -185,7 +185,8 @@ const MetadataViewContainer = ({ const filterGroups = React.useMemo(() => { const clonedTemplate = cloneDeep(metadataTemplate); - let fields = clonedTemplate?.fields || []; + let fields: Pick[] = + clonedTemplate?.fields || []; // Filter fields to only include those that have corresponding columns const columnIds = newColumns.map(col => col.id); @@ -207,7 +208,6 @@ const MetadataViewContainer = ({ key: ITEM_FILTER_NAME, displayName: formatMessage(messages.name), type: 'string', - shouldRenderChip: true, }, ...fields, ]; @@ -223,6 +223,7 @@ const MetadataViewContainer = ({ name: field.displayName, fieldType: field.type, options: field.options?.map(({ key }) => key) || [], + selected: false, shouldRenderChip: true, }; }) || [], diff --git a/src/elements/content-sharing/ContentSharingV2.tsx b/src/elements/content-sharing/ContentSharingV2.tsx index 4d113da1dc..0a94614ea7 100644 --- a/src/elements/content-sharing/ContentSharingV2.tsx +++ b/src/elements/content-sharing/ContentSharingV2.tsx @@ -126,7 +126,7 @@ function ContentSharingV2({ let errorMessage; if (error.status) { errorMessage = messages[CONTENT_SHARING_ERRORS[error.status]]; - } else if (error.response && error.response.status) { + } else if ('response' in error && error.response && error.response.status) { errorMessage = messages[CONTENT_SHARING_ERRORS[error.response.status]]; } else { errorMessage = messages.loadingError; diff --git a/src/elements/content-sidebar/fetchers/metadataTaxonomyFetcher.ts b/src/elements/content-sidebar/fetchers/metadataTaxonomyFetcher.ts index 06d6c578e1..e3167458e8 100644 --- a/src/elements/content-sidebar/fetchers/metadataTaxonomyFetcher.ts +++ b/src/elements/content-sidebar/fetchers/metadataTaxonomyFetcher.ts @@ -11,7 +11,17 @@ import type { } from '@box/metadata-taxonomy-picker'; import type { CreateTaxonomyItemsService } from '@box/metadata-editor/lib/components/metadata-editor-fields/components/metadata-taxonomy-field/types.js'; import type API from '../../../api'; -import type { MetadataOptionEntry } from '../../../common/types/metadata'; + +/** + * `/options` entry as consumed by `metadataTaxonomyFetcher`: a `TaxonomyEntry` plus the + * tree fields passed through to the combobox. Ancestors are always objects here. + */ +type MetadataOptionListEntry = Omit & { + parentId?: string | null; + nodePath?: string[] | null; + deprecated?: boolean; + ancestors?: TaxonomyEntryAncestor[]; +}; type MetadataTaxonomyListEntry = { id?: string; @@ -77,7 +87,7 @@ export const metadataTaxonomyFetcher = async ( const { marker = null } = options; return { - options: metadataOptions.entries.map((metadataOption: MetadataOptionEntry) => ({ + options: metadataOptions.entries.map((metadataOption: MetadataOptionListEntry) => ({ value: metadataOption.id, displayValue: metadataOption.display_name || metadataOption.displayName, level: metadataOption.level, diff --git a/src/elements/content-sidebar/hooks/useSidebarMetadataFetcher.ts b/src/elements/content-sidebar/hooks/useSidebarMetadataFetcher.ts index ea81c02376..3536024ba2 100644 --- a/src/elements/content-sidebar/hooks/useSidebarMetadataFetcher.ts +++ b/src/elements/content-sidebar/hooks/useSidebarMetadataFetcher.ts @@ -107,7 +107,7 @@ function useSidebarMetadataFetcher( const isValidError = isUserCorrectableError(errorStatus); setStatus(STATUS.ERROR); setErrorMessage(message); - onError(error, code, { + onError(error as Error, code, { error, isErrorDisplayed: isValidError, }); diff --git a/src/elements/content-uploader/ContentUploader.tsx b/src/elements/content-uploader/ContentUploader.tsx index f65575087b..4627ef6463 100644 --- a/src/elements/content-uploader/ContentUploader.tsx +++ b/src/elements/content-uploader/ContentUploader.tsx @@ -97,7 +97,7 @@ export interface ContentUploaderProps { onClickResume: (item: UploadItem) => void; onClickRetry: (item: UploadItem) => void; onClose: () => void; - onComplete: (items: UploadItem[]) => void; + onComplete: (items: BoxItem[] | UploadItem[]) => void; onItemShare?: (itemId: string) => void; onItemOpen?: (itemId: string) => void; onError: Function; @@ -585,6 +585,7 @@ class ContentUploader extends Component { }; }); + // @ts-expect-error no file property this.addToQueue(folderUploads, itemUpdateCallback); }; @@ -637,7 +638,7 @@ class ContentUploader extends Component { addFolderToUploadQueue = (folderUpload: FolderUpload, itemUpdateCallback: Function, apiOptions: Object): void => { this.addToQueue( [ - // $FlowFixMe no file property + // @ts-expect-error no file property { api: folderUpload, extension: '', diff --git a/src/elements/content-uploader/ItemList.tsx b/src/elements/content-uploader/ItemList.tsx index f6d9eb7191..c9edcaa02f 100644 --- a/src/elements/content-uploader/ItemList.tsx +++ b/src/elements/content-uploader/ItemList.tsx @@ -18,7 +18,7 @@ const ACTION_CELL_WIDTH = 32; export interface ItemListProps { isResumableUploadsEnabled?: boolean; items: UploadItem[]; - onClick: React.MouseEventHandler; + onClick: (item: UploadItem) => void; onRemoveClick?: (item: UploadItem) => void; onUpgradeCTAClick?: () => void; } diff --git a/src/elements/content-uploader/ItemRemove.tsx b/src/elements/content-uploader/ItemRemove.tsx index 65e235f68f..6071f23afb 100644 --- a/src/elements/content-uploader/ItemRemove.tsx +++ b/src/elements/content-uploader/ItemRemove.tsx @@ -3,14 +3,14 @@ import { useIntl } from 'react-intl'; import { IconButton, Tooltip } from '@box/blueprint-web'; import { XMark } from '@box/blueprint-web-assets/icons/Fill'; -import type { UploadItem, UploadStatus } from '../../common/types/upload'; +import type { UploadStatus } from '../../common/types/upload'; import { STATUS_ERROR, STATUS_IN_PROGRESS, STATUS_STAGED } from '../../constants'; import messages from '../common/messages'; export interface ItemRemoveProps { - onClick: (item: UploadItem) => void; + onClick: React.MouseEventHandler; status: UploadStatus; } diff --git a/src/elements/content-uploader/UploadsManager.tsx b/src/elements/content-uploader/UploadsManager.tsx index f427afa861..e1f7b7d5d5 100644 --- a/src/elements/content-uploader/UploadsManager.tsx +++ b/src/elements/content-uploader/UploadsManager.tsx @@ -15,7 +15,7 @@ export interface UploadsManagerProps { isResumableUploadsEnabled: boolean; isVisible: boolean; items: UploadItem[]; - onItemActionClick: React.MouseEventHandler; + onItemActionClick: (item: UploadItem) => void; onRemoveActionClick: (item: UploadItem) => void; onUpgradeCTAClick?: () => void; onUploadsManagerActionClick: (status: UploadStatus) => void; diff --git a/src/elements/content-uploader/actionCellRenderer.tsx b/src/elements/content-uploader/actionCellRenderer.tsx index cdc8660a7a..35c238e3c9 100644 --- a/src/elements/content-uploader/actionCellRenderer.tsx +++ b/src/elements/content-uploader/actionCellRenderer.tsx @@ -1,10 +1,11 @@ import * as React from 'react'; import ItemAction from './ItemAction'; import { cellRendererProps } from './ItemList'; +import type { UploadItem } from '../../common/types/upload'; export default ( isResumableUploadsEnabled: boolean, - onClick: React.MouseEventHandler, + onClick: (item: UploadItem) => void, onUpgradeCTAClick?: () => void, ) => ({ rowData }: cellRendererProps) => ( diff --git a/src/utils/Xhr.js.flow b/src/utils/Xhr.js.flow index bfc2654012..242e2b8ebb 100644 --- a/src/utils/Xhr.js.flow +++ b/src/utils/Xhr.js.flow @@ -42,7 +42,7 @@ class Xhr { language: ?string; - token: Token; + token: ?Token; version: ?string; diff --git a/src/utils/Xhr.ts b/src/utils/Xhr.ts index 6a987d2450..e4a5bbb6f6 100644 --- a/src/utils/Xhr.ts +++ b/src/utils/Xhr.ts @@ -24,6 +24,11 @@ const DEFAULT_UPLOAD_TIMEOUT_MS = 120000; const MAX_NUM_RETRIES = 3; const RETRYABLE_HTTP_METHODS = [HTTP_GET, HTTP_OPTIONS, HTTP_HEAD].map(lowerCase); +/** Xhr constructor options; `token` is optional (e.g. `new Xhr()` in tests). */ +type XhrConstructorOptions = Omit & { + token?: Token; +}; + class Xhr { id: string | null | undefined; @@ -35,7 +40,7 @@ class Xhr { language: string | null | undefined; - token: Token; + token: Token | undefined; version: string | null | undefined; @@ -74,7 +79,7 @@ class Xhr { requestInterceptor, retryableStatusCodes = [HTTP_STATUS_CODE_RATE_LIMIT], shouldRetry = true, - }: APIOptions = {}) { + }: XhrConstructorOptions = {}) { this.clientName = clientName; this.id = id; this.language = language; diff --git a/src/utils/__tests__/sorter.test.ts b/src/utils/__tests__/sorter.test.ts index e26a5255f8..02fb9c0ee1 100644 --- a/src/utils/__tests__/sorter.test.ts +++ b/src/utils/__tests__/sorter.test.ts @@ -2,6 +2,7 @@ import Cache from '../Cache'; import sort, { sortFeedItems } from '../sorter'; import { FEED_ITEM_TYPE_COMMENT, FEED_ITEM_TYPE_TASK, SORT_ASC, SORT_DESC } from '../../constants'; import { annotation as mockAnnotation } from '../../__mocks__/annotations'; +import type { Annotations, Comments, Tasks } from '../../common/types/feed'; let cache; let item; @@ -316,7 +317,11 @@ describe('util/sorter', () => { }; test('should sort items based on date', () => { - const sorted = sortFeedItems(comments, tasks, annotations); + const sorted = sortFeedItems( + comments as unknown as Comments, + tasks as unknown as Tasks, + annotations as unknown as Annotations, + ); expect(sorted[0].id).toEqual(annotations.entries[0].id); expect(sorted[1].id).toEqual(tasks.entries[0].id); expect(sorted[2].id).toEqual(comments.entries[0].id); diff --git a/src/utils/__tests__/uploads.test.ts b/src/utils/__tests__/uploads.test.ts index 02dd75d144..8aaa7d273d 100644 --- a/src/utils/__tests__/uploads.test.ts +++ b/src/utils/__tests__/uploads.test.ts @@ -19,16 +19,17 @@ import { getFileId, getDataTransferItemId, } from '../uploads'; +import type { UploadFile, UploadItemAPIOptions } from '../../common/types/upload'; -const mockFile = { name: 'hi' }; +const mockFile = { name: 'hi' } as UploadFile; const entry = { name: 'hi', file: fn => { fn(mockFile); }, }; -const mockItem = { kind: 'file', webkitGetAsEntry: () => entry }; -const options = { options: true }; +const mockItem = { kind: 'file', webkitGetAsEntry: () => entry } as unknown as DataTransferItem; +const options = { options: true } as unknown as UploadItemAPIOptions; describe('util/uploads', () => { describe('toISOStringNoMS()', () => { @@ -100,6 +101,10 @@ describe('util/uploads', () => { test('should return false when argument is UploadFile type', () => { expect(doesFileContainAPIOptions(mockFile)).toBeFalsy(); }); + + test('should return true when wrapper has file but no options', () => { + expect(doesFileContainAPIOptions({ file: mockFile })).toBeTruthy(); + }); }); describe('doesDataTransferItemContainAPIOptions()', () => { @@ -115,6 +120,10 @@ describe('util/uploads', () => { test('should return false when argument is DataTransferItem type', () => { expect(doesDataTransferItemContainAPIOptions(mockItem)).toBeFalsy(); }); + + test('should return true when wrapper has item but no options', () => { + expect(doesDataTransferItemContainAPIOptions({ item: mockItem })).toBeTruthy(); + }); }); describe('getFile()', () => { @@ -160,6 +169,10 @@ describe('util/uploads', () => { test('should return DEFAULT_API_OPTIONS when argument is UploadFile type', () => { expect(getFileAPIOptions(mockFile)).toEqual(DEFAULT_API_OPTIONS); }); + + test('should return DEFAULT_API_OPTIONS when wrapper has file but no options', () => { + expect(getFileAPIOptions({ file: mockFile })).toEqual(DEFAULT_API_OPTIONS); + }); }); describe('getDataTransferItemAPIOptions()', () => { @@ -175,6 +188,10 @@ describe('util/uploads', () => { test('should return DEFAULT_API_OPTIONS when argument is DataTransferItem type', () => { expect(getDataTransferItemAPIOptions(mockItem)).toEqual(DEFAULT_API_OPTIONS); }); + + test('should return DEFAULT_API_OPTIONS when wrapper has item but no options', () => { + expect(getDataTransferItemAPIOptions({ item: mockItem })).toEqual(DEFAULT_API_OPTIONS); + }); }); describe('getFileFromDataTransferItem()', () => { @@ -196,7 +213,7 @@ describe('util/uploads', () => { const mockPackageItem = { getAsFile: jest.fn(() => mockFile), webkitGetAsEntry: () => entry, - }; + } as unknown as DataTransferItem; const itemData = { item: mockPackageItem, options, @@ -218,7 +235,7 @@ describe('util/uploads', () => { const folderItem = { kind: '', webkitGetAsEntry: () => folderEntry, - }; + } as unknown as DataTransferItem; const itemData = { item: folderItem, @@ -237,7 +254,7 @@ describe('util/uploads', () => { const folderItem = { kind: 'file', webkitGetAsEntry: () => fileEntry, - }; + } as unknown as DataTransferItem; const itemData = { item: folderItem, @@ -257,7 +274,7 @@ describe('util/uploads', () => { kind: 'file', type: 'application/zip', webkitGetAsEntry: () => packageEntry, - }; + } as unknown as DataTransferItem; const itemData = { item: folderItem, @@ -276,7 +293,7 @@ describe('util/uploads', () => { const folderItem = { kind: 'file', webkitGetAsEntry: () => packageEntry, - }; + } as unknown as DataTransferItem; const itemData = { item: folderItem, @@ -295,7 +312,7 @@ describe('util/uploads', () => { const folderItem = { kind: '', webkitGetAsEntry: () => folderEntry, - }; + } as unknown as DataTransferItem; const itemData = { item: folderItem, @@ -309,7 +326,7 @@ describe('util/uploads', () => { const fileEntry = { isDirectory: false, }; - const fileItem = { kind: '', webkitGetAsEntry: () => fileEntry }; + const fileItem = { kind: '', webkitGetAsEntry: () => fileEntry } as unknown as DataTransferItem; const itemData = { item: fileItem, @@ -320,7 +337,7 @@ describe('util/uploads', () => { }); test('should return false if item does not have an entry', () => { - const fileItem = { kind: '', webkitGetAsEntry: () => undefined }; + const fileItem = { kind: '', webkitGetAsEntry: () => undefined } as unknown as DataTransferItem; expect(isDataTransferItemAFolder(fileItem)).toBeFalsy(); }); @@ -330,7 +347,7 @@ describe('util/uploads', () => { test('should return file id correctly when file does not contain API options', () => { const file = { name: 'hi', - }; + } as UploadFile; expect(getFileId(file, '0')).toBe('hi'); }); @@ -339,7 +356,7 @@ describe('util/uploads', () => { const file = { file: { name: 'hi', - }, + } as UploadFile, options: { folderId: '0', uploadInitTimestamp: 123123, @@ -348,29 +365,9 @@ describe('util/uploads', () => { expect(getFileId(file, '0')).toBe('hi_0_123123'); }); - }); - - describe('getFileId()', () => { - test('should return file id correctly when file does not contain API options', () => { - const file = { - name: 'hi', - }; - - expect(getFileId(file, '0')).toBe('hi'); - }); - - test('should return file id correctly when file does contain API options', () => { - const file = { - file: { - name: 'hi', - }, - options: { - folderId: '0', - uploadInitTimestamp: 123123, - }, - }; - expect(getFileId(file, '0')).toBe('hi_0_123123'); + test('should return file name when wrapper has file but no options', () => { + expect(getFileId({ file: mockFile }, '0')).toBe('hi'); }); }); @@ -394,6 +391,10 @@ describe('util/uploads', () => { expect(getDataTransferItemId(item, rootFolderId)).toBe('hi_0_123123'); }); + + test('should return item name when wrapper has item but no options', () => { + expect(getDataTransferItemId({ item: mockItem }, rootFolderId)).toBe('hi'); + }); }); describe('isMultiputSupported()', () => { diff --git a/src/utils/uploads.js.flow b/src/utils/uploads.js.flow index cf95b9f41c..7e89550861 100644 --- a/src/utils/uploads.js.flow +++ b/src/utils/uploads.js.flow @@ -24,8 +24,8 @@ const DEFAULT_API_OPTIONS = {}; * @returns {boolean} */ function doesFileContainAPIOptions(file: UploadFile | UploadFileWithAPIOptions): boolean { - // $FlowFixMe UploadFileWithAPIOptions has `file` and `options` properties - return !!(file.options && file.file); + // $FlowFixMe UploadFileWithAPIOptions has a `file` property; `options` is optional + return !!file.file; } /** @@ -35,8 +35,8 @@ function doesFileContainAPIOptions(file: UploadFile | UploadFileWithAPIOptions): * @returns {boolean} */ function doesDataTransferItemContainAPIOptions(item: DataTransferItem | UploadDataTransferItemWithAPIOptions): boolean { - // $FlowFixMe UploadDataTransferItemWithAPIOptions has `item` and `options` properties - return !!(item.options && item.item); + // $FlowFixMe UploadDataTransferItemWithAPIOptions has an `item` property; `options` is optional + return !!item.item; } /** @@ -313,6 +313,10 @@ function getFileId(file: UploadFileWithAPIOptions | UploadFile, rootFolderId: st } const fileWithOptions = ((file: any): UploadFileWithAPIOptions); + if (!fileWithOptions.options) { + return fileWithOptions.file.name; + } + const folderId = getProp(fileWithOptions, 'options.folderId', rootFolderId); const uploadInitTimestamp = getProp(fileWithOptions, 'options.uploadInitTimestamp', Date.now()); const fileName = fileWithOptions.file.webkitRelativePath || fileWithOptions.file.name; @@ -337,7 +341,8 @@ function getDataTransferItemId( ): string { const item = getDataTransferItem(itemData); const { name } = getEntryFromDataTransferItem(item); - if (!doesDataTransferItemContainAPIOptions(itemData)) { + // $FlowFixMe UploadDataTransferItemWithAPIOptions has an optional `options` property + if (!doesDataTransferItemContainAPIOptions(itemData) || !itemData.options) { return name; } diff --git a/src/utils/uploads.ts b/src/utils/uploads.ts index dd7d4a4d38..302afaa054 100644 --- a/src/utils/uploads.ts +++ b/src/utils/uploads.ts @@ -13,37 +13,39 @@ import type { const DEFAULT_API_OPTIONS: UploadItemAPIOptions = {}; /** Returns true if file contains API options. */ -function doesFileContainAPIOptions(file: UploadFile | UploadFileWithAPIOptions): boolean { - return !!(file.options && file.file); +function doesFileContainAPIOptions(file: UploadFile | UploadFileWithAPIOptions): file is UploadFileWithAPIOptions { + return 'file' in file && file.file != null; } /** Returns true if item contains API options. */ -function doesDataTransferItemContainAPIOptions(item: DataTransferItem | UploadDataTransferItemWithAPIOptions): boolean { - return !!(item.options && item.item); +function doesDataTransferItemContainAPIOptions( + item: DataTransferItem | UploadDataTransferItemWithAPIOptions, +): item is UploadDataTransferItemWithAPIOptions { + return 'item' in item && item.item != null; } /** Converts UploadFile or UploadFileWithAPIOptions to UploadFile. */ function getFile(file: UploadFile | UploadFileWithAPIOptions): UploadFile { if (doesFileContainAPIOptions(file)) { - return (file as UploadFileWithAPIOptions).file; + return file.file; } - return file as UploadFile; + return file; } /** Converts DataTransferItem or UploadDataTransferItemWithAPIOptions to DataTransferItem. */ function getDataTransferItem(item: DataTransferItem | UploadDataTransferItemWithAPIOptions): DataTransferItem { if (doesDataTransferItemContainAPIOptions(item)) { - return (item as UploadDataTransferItemWithAPIOptions).item; + return item.item; } - return item as DataTransferItem; + return item; } /** Get API Options from file. */ function getFileAPIOptions(file: UploadFile | UploadFileWithAPIOptions): UploadItemAPIOptions { if (doesFileContainAPIOptions(file)) { - return (file as UploadFileWithAPIOptions).options || DEFAULT_API_OPTIONS; + return file.options || DEFAULT_API_OPTIONS; } return DEFAULT_API_OPTIONS; @@ -54,7 +56,7 @@ function getDataTransferItemAPIOptions( item: DataTransferItem | UploadDataTransferItemWithAPIOptions, ): UploadItemAPIOptions { if (doesDataTransferItemContainAPIOptions(item)) { - return (item as UploadDataTransferItemWithAPIOptions).options || DEFAULT_API_OPTIONS; + return item.options || DEFAULT_API_OPTIONS; } return DEFAULT_API_OPTIONS; @@ -221,13 +223,16 @@ function getPackageFileFromDataTransferItem( */ function getFileId(file: UploadFileWithAPIOptions | UploadFile, rootFolderId: string): string { if (!doesFileContainAPIOptions(file)) { - return (file as UploadFile).name; + return file.name; + } + + if (!file.options) { + return file.file.name; } - const fileWithOptions = file as UploadFileWithAPIOptions; - const folderId = getProp(fileWithOptions, 'options.folderId', rootFolderId); - const uploadInitTimestamp = getProp(fileWithOptions, 'options.uploadInitTimestamp', Date.now()); - const fileName = fileWithOptions.file.webkitRelativePath || fileWithOptions.file.name; + const folderId = getProp(file, 'options.folderId', rootFolderId); + const uploadInitTimestamp = getProp(file, 'options.uploadInitTimestamp', Date.now()); + const fileName = file.file.webkitRelativePath || file.file.name; return `${fileName}_${folderId}_${uploadInitTimestamp}`; } @@ -245,7 +250,7 @@ function getDataTransferItemId( ): string { const item = getDataTransferItem(itemData); const { name } = getEntryFromDataTransferItem(item); - if (!doesDataTransferItemContainAPIOptions(itemData)) { + if (!doesDataTransferItemContainAPIOptions(itemData) || !itemData.options) { return name; }