From 9ca2e48db1826086596a69195a37161149a7c42b Mon Sep 17 00:00:00 2001 From: roman Date: Wed, 26 Aug 2026 15:10:36 +0200 Subject: [PATCH] refactor(common): migrate Common from Flow to TypeScript --- ...box-proptypes.js => box-proptypes.js.flow} | 0 src/common/box-proptypes.ts | 36 ++ .../{constants.js => constants.js.flow} | 0 src/common/constants.ts | 25 + ...oard-events.js => keyboard-events.js.flow} | 0 src/common/keyboard-events.ts | 8 + src/common/{messages.js => messages.js.flow} | 0 src/common/messages.ts | 101 ++++ .../{annotations.js => annotations.js.flow} | 0 src/common/types/annotations.ts | 98 ++++ src/common/types/{api.js => api.js.flow} | 0 src/common/types/api.ts | 129 +++++ src/common/types/{core.js => core.js.flow} | 0 src/common/types/core.ts | 544 ++++++++++++++++++ src/common/types/{feed.js => feed.js.flow} | 0 src/common/types/feed.ts | 244 ++++++++ .../{integrations.js => integrations.js.flow} | 0 src/common/types/integrations.ts | 61 ++ .../types/{logging.js => logging.js.flow} | 0 src/common/types/logging.ts | 28 + .../types/{metadata.js => metadata.js.flow} | 0 src/common/types/metadata.ts | 249 ++++++++ ...dataQueries.js => metadataQueries.js.flow} | 0 src/common/types/metadataQueries.ts | 49 ++ .../types/{skills.js => skills.js.flow} | 0 src/common/types/skills.ts | 55 ++ src/common/types/{tasks.js => tasks.js.flow} | 0 src/common/types/tasks.ts | 115 ++++ .../types/{upload.js => upload.js.flow} | 0 src/common/types/upload.ts | 122 ++++ .../{variables.js => variables.js.flow} | 0 src/common/variables.ts | 5 + .../grid-view/__tests__/GridView.test.tsx | 3 +- src/elements/common/__tests__/utils.test.ts | 35 +- .../__tests__/InlineBreadcrumbs.test.tsx | 7 +- .../__tests__/withCurrentUser.test.tsx | 35 +- .../common/current-user/withCurrentUser.tsx | 1 + .../item-grid/__tests__/ItemGrid.test.tsx | 17 +- .../item-list/__tests__/ItemList.test.tsx | 34 +- .../common/item/__tests__/ItemName.test.tsx | 3 +- .../item/__tests__/ItemOptions.test.tsx | 20 +- .../item/__tests__/ItemSubDetails.test.tsx | 7 +- .../item/__tests__/ItemTypeIcon.test.tsx | 13 +- .../__tests__/PreviewDialog.test.tsx | 2 +- .../__tests__/ShareAccessSelect.test.tsx | 13 +- .../content-explorer/ContentExplorer.tsx | 8 + .../MetadataQueryAPIHelper.ts | 8 + .../content-explorer/MetadataSidePanel.tsx | 1 + .../MetadataViewContainer.tsx | 3 + .../__tests__/Content.test.tsx | 23 +- .../__tests__/ContentExplorer.test.tsx | 2 +- .../DeleteConfirmationDialog.test.tsx | 8 +- .../__tests__/MetadataSidePanel.test.tsx | 14 +- .../__tests__/MetadataViewContainer.test.tsx | 4 +- .../__tests__/RenameDialog.test.tsx | 9 +- .../__tests__/ShareDialog.test.tsx | 8 +- .../content-sharing/ContentSharingV2.tsx | 2 + .../__tests__/convertCollaborators.test.ts | 65 ++- .../utils/convertCollaborators.ts | 1 + .../__tests__/DocGenSidebar.test.tsx | 14 +- ...ataSidebarRedesign-taxonomyPicker.test.tsx | 1 + .../MetadataSidebarRedesign.test.tsx | 1 + ...convertTemplateToTemplateInstance.test.tsx | 1 + .../__tests__/isFileLargerThan.test.ts | 8 +- .../__tests__/ActivityFeedV2.test.tsx | 23 +- .../fetchers/metadataTaxonomyFetcher.ts | 1 + .../hooks/useSidebarMetadataFetcher.ts | 1 + .../stories/__mocks__/TaskModalV2Mocks.tsx | 6 +- .../content-uploader/ContentUploader.tsx | 5 + .../content-uploader/DroppableContent.tsx | 6 +- src/elements/content-uploader/ItemRemove.tsx | 9 +- .../__tests__/CellRenderer.test.tsx | 33 +- .../__tests__/ItemList.test.tsx | 31 +- .../content-uploader/actionCellRenderer.tsx | 5 +- 74 files changed, 2190 insertions(+), 170 deletions(-) rename src/common/{box-proptypes.js => box-proptypes.js.flow} (100%) create mode 100644 src/common/box-proptypes.ts rename src/common/{constants.js => constants.js.flow} (100%) create mode 100644 src/common/constants.ts rename src/common/{keyboard-events.js => keyboard-events.js.flow} (100%) create mode 100644 src/common/keyboard-events.ts rename src/common/{messages.js => messages.js.flow} (100%) create mode 100644 src/common/messages.ts rename src/common/types/{annotations.js => annotations.js.flow} (100%) create mode 100644 src/common/types/annotations.ts rename src/common/types/{api.js => api.js.flow} (100%) create mode 100644 src/common/types/api.ts rename src/common/types/{core.js => core.js.flow} (100%) create mode 100644 src/common/types/core.ts rename src/common/types/{feed.js => feed.js.flow} (100%) create mode 100644 src/common/types/feed.ts rename src/common/types/{integrations.js => integrations.js.flow} (100%) create mode 100644 src/common/types/integrations.ts rename src/common/types/{logging.js => logging.js.flow} (100%) create mode 100644 src/common/types/logging.ts rename src/common/types/{metadata.js => metadata.js.flow} (100%) create mode 100644 src/common/types/metadata.ts rename src/common/types/{metadataQueries.js => metadataQueries.js.flow} (100%) create mode 100644 src/common/types/metadataQueries.ts rename src/common/types/{skills.js => skills.js.flow} (100%) create mode 100644 src/common/types/skills.ts rename src/common/types/{tasks.js => tasks.js.flow} (100%) create mode 100644 src/common/types/tasks.ts rename src/common/types/{upload.js => upload.js.flow} (100%) create mode 100644 src/common/types/upload.ts rename src/common/{variables.js => variables.js.flow} (100%) create mode 100644 src/common/variables.ts diff --git a/src/common/box-proptypes.js b/src/common/box-proptypes.js.flow similarity index 100% rename from src/common/box-proptypes.js rename to src/common/box-proptypes.js.flow diff --git a/src/common/box-proptypes.ts b/src/common/box-proptypes.ts new file mode 100644 index 0000000000..95a118fee0 --- /dev/null +++ b/src/common/box-proptypes.ts @@ -0,0 +1,36 @@ +import PropTypes from 'prop-types'; +import ImmutablePropTypes from 'react-immutable-proptypes'; + +const UserPropType = PropTypes.shape({ + avatarUrl: PropTypes.string, + id: PropTypes.number.isRequired, + name: PropTypes.string.isRequired, +}); + +const SelectorItemPropType = PropTypes.shape({ + id: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), + name: PropTypes.string.isRequired, +}); + +const ActionItemErrorPropType = PropTypes.shape({ + title: PropTypes.string.isRequired, + message: PropTypes.string.isRequired, + action: PropTypes.shape({ + text: PropTypes.string.isRequired, + onAction: PropTypes.func.isRequired, + }), +}); + +const SelectorItemsPropType = PropTypes.arrayOf(SelectorItemPropType); + +const OptionPropType = { + text: PropTypes.string.isRequired, + value: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired, +} as const; + +const OptionsPropType = PropTypes.oneOfType([ + PropTypes.arrayOf(PropTypes.shape(OptionPropType)), + ImmutablePropTypes.listOf(ImmutablePropTypes.recordOf(OptionPropType)), +]).isRequired; + +export { ActionItemErrorPropType, SelectorItemsPropType, SelectorItemPropType, UserPropType, OptionsPropType }; diff --git a/src/common/constants.js b/src/common/constants.js.flow similarity index 100% rename from src/common/constants.js rename to src/common/constants.js.flow diff --git a/src/common/constants.ts b/src/common/constants.ts new file mode 100644 index 0000000000..8ab4e25f2a --- /dev/null +++ b/src/common/constants.ts @@ -0,0 +1,25 @@ +const ITEM_TYPE_FILE = 'file' as const; +const ITEM_TYPE_FOLDER = 'folder' as const; +const ITEM_TYPE_HUBS = 'hubs' as const; +const ITEM_TYPE_WEBLINK = 'web_link' as const; + +const JSON_PATCH_OP_ADD = 'add' as const; +const JSON_PATCH_OP_REMOVE = 'remove' as const; +const JSON_PATCH_OP_REPLACE = 'replace' as const; +const JSON_PATCH_OP_TEST = 'test' as const; + +const METADATA_FIELD_TYPE_ENUM = 'enum' as const; +const METADATA_FIELD_TYPE_MULTISELECT = 'multiSelect' as const; + +export { + ITEM_TYPE_FILE, + ITEM_TYPE_FOLDER, + ITEM_TYPE_HUBS, + ITEM_TYPE_WEBLINK, + JSON_PATCH_OP_ADD, + JSON_PATCH_OP_REMOVE, + JSON_PATCH_OP_REPLACE, + JSON_PATCH_OP_TEST, + METADATA_FIELD_TYPE_ENUM, + METADATA_FIELD_TYPE_MULTISELECT, +}; diff --git a/src/common/keyboard-events.js b/src/common/keyboard-events.js.flow similarity index 100% rename from src/common/keyboard-events.js rename to src/common/keyboard-events.js.flow diff --git a/src/common/keyboard-events.ts b/src/common/keyboard-events.ts new file mode 100644 index 0000000000..d56be54a8d --- /dev/null +++ b/src/common/keyboard-events.ts @@ -0,0 +1,8 @@ +const ARROW_DOWN = 'ArrowDown'; +const ARROW_UP = 'ArrowUp'; +const ESCAPE = 'Escape'; +const ENTER = 'Enter'; +const SPACE = ' '; +const TAB = 'Tab'; + +export { ARROW_DOWN, ARROW_UP, ENTER, ESCAPE, SPACE, TAB }; diff --git a/src/common/messages.js b/src/common/messages.js.flow similarity index 100% rename from src/common/messages.js rename to src/common/messages.js.flow diff --git a/src/common/messages.ts b/src/common/messages.ts new file mode 100644 index 0000000000..52ed1c5b22 --- /dev/null +++ b/src/common/messages.ts @@ -0,0 +1,101 @@ +import { defineMessages } from 'react-intl'; + +const messages = defineMessages({ + cancel: { + defaultMessage: 'Cancel', + description: 'Cancel button text', + id: 'boxui.core.cancel', + }, + close: { + defaultMessage: 'Close', + description: 'Close button text', + id: 'boxui.core.close', + }, + copy: { + defaultMessage: 'Copy', + description: 'Copy button text', + id: 'boxui.core.copy', + }, + copied: { + defaultMessage: 'Copied', + description: 'Copy button text after user clicks on it', + id: 'boxui.core.copied', + }, + done: { + defaultMessage: 'Done', + description: 'Done button text', + id: 'boxui.core.done', + }, + okay: { + defaultMessage: 'Okay', + description: 'Okay button text', + id: 'boxui.core.okay', + }, + save: { + defaultMessage: 'Save', + description: 'Save button text', + id: 'boxui.core.save', + }, + send: { + defaultMessage: 'Send', + description: 'Send button text', + id: 'boxui.core.send', + }, + optional: { + defaultMessage: 'optional', + description: 'Optional text for labels', + id: 'boxui.core.optional', + }, + pillSelectorPlaceholder: { + defaultMessage: 'Add names or email addresses', + description: 'Placeholder text for the pill selector', + id: 'boxui.share.pillSelectorPlaceholder', + }, + messageSelectorPlaceholder: { + defaultMessage: 'Add a message', + description: 'Placeholder text for message section', + id: 'boxui.share.messageSelectorPlaceholder', + }, + invalidInputError: { + defaultMessage: 'Invalid Input', + description: 'Generic error message for a field is invalid', + id: 'boxui.validation.genericError', + }, + minLengthError: { + defaultMessage: 'Input must be at least {min} characters', + description: 'Error message for when an input value is too short. {min} is the minimum length', + id: 'boxui.validation.tooShortError', + }, + maxLengthError: { + defaultMessage: 'Input cannot exceed {max} characters', + description: 'Error message for when an input value is too long. {max} is the maximum length', + id: 'boxui.validation.tooLongError', + }, + invalidEmailError: { + defaultMessage: 'Invalid Email Address', + description: 'Error message for when an invalid email is entered', + id: 'boxui.validation.emailError', + }, + invalidURLError: { + defaultMessage: 'Invalid URL', + description: 'Error message for when an invalid URL is entered', + id: 'boxui.validation.URLError', + }, + invalidUserError: { + defaultMessage: 'Invalid User', + description: 'Error message for when an invalid user is entered', + id: 'boxui.validation.invalidUserError', + }, + requiredFieldError: { + defaultMessage: 'Required Field', + description: 'Error message for when a required field is missing', + id: 'boxui.validation.requiredError', + }, + invalidDateError: { + defaultMessage: 'Invalid Date', + description: 'Error message for when an invalid Date is entered', + id: 'boxui.validation.invalidDateError', + }, +}); + +export default messages; 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 100% rename from src/common/types/api.js rename to src/common/types/api.js.flow diff --git a/src/common/types/api.ts b/src/common/types/api.ts new file mode 100644 index 0000000000..c6a89f35e9 --- /dev/null +++ b/src/common/types/api.ts @@ -0,0 +1,129 @@ +/* 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; +}; + +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 ONLY metadata *instance* endpoints (file/folder + * `/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 + * is not yet regionalized end-to-end. It is expected to be retired in + * a future major version. + */ + metadataApiHost?: 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/core.js b/src/common/types/core.js.flow similarity index 100% rename from src/common/types/core.js rename to src/common/types/core.js.flow diff --git a/src/common/types/core.ts b/src/common/types/core.ts new file mode 100644 index 0000000000..af910dcf02 --- /dev/null +++ b/src/common/types/core.ts @@ -0,0 +1,544 @@ +/* eslint-disable @typescript-eslint/no-explicit-any -- Preserve Flow any and Function contracts. */ +import type React from 'react'; + +import { ITEM_TYPE_FOLDER, ITEM_TYPE_FILE, ITEM_TYPE_HUBS, ITEM_TYPE_WEBLINK } from '../constants'; +import { + ACCESS_OPEN, + ACCESS_COLLAB, + ACCESS_COMPANY, + ACCESS_NONE, + VIEW_SEARCH, + VIEW_FOLDER, + VIEW_ERROR, + VIEW_SELECTED, + VIEW_RECENTS, + VIEW_UPLOAD_EMPTY, + VIEW_UPLOAD_IN_PROGRESS, + VIEW_UPLOAD_SUCCESS, + VIEW_METADATA, + SORT_ASC, + SORT_DESC, + DELIMITER_SLASH, + DELIMITER_CARET, + SIZE_SMALL, + SIZE_MEDIUM, + SIZE_LARGE, + SIZE_VERY_LARGE, + FIELD_DATE, + FIELD_NAME, + FIELD_SIZE, + FIELD_RELEVANCE, + DEFAULT_VIEW_RECENTS, + DEFAULT_VIEW_FILES, + VERSION_RETENTION_DELETE_ACTION, + VERSION_RETENTION_INDEFINITE, + VERSION_RETENTION_REMOVE_ACTION, + PERMISSION_CAN_DOWNLOAD, + PERMISSION_CAN_PREVIEW, +} from '../../constants'; +import type { MetadataType } from './metadata'; + +type Token = null | undefined | string | ((...args: any[]) => any); +type TokenReadWrite = { + read: string; + write?: string; +}; +type TokenLiteral = null | typeof undefined | string | TokenReadWrite; + +type ClassComponent = new (...args: any[]) => React.Component; + +// TODO: Investigate some better types for these different maps, perhaps make use +// of generic types like: +// type GenericMap = { +// [K]: V, +// } +type StringMap = { + [key: string]: string; +}; +type StringAnyMap = { + [key: string]: any; +}; +type StringMixedMap = { + [key: string]: unknown; +}; +type StringBooleanMap = { + [key: string]: boolean; +}; +type NumberBooleanMap = { + [key: number]: boolean; +}; + +type DefaultView = typeof DEFAULT_VIEW_RECENTS | typeof DEFAULT_VIEW_FILES; +type View = + | typeof VIEW_ERROR + | typeof VIEW_SELECTED + | typeof VIEW_RECENTS + | typeof VIEW_FOLDER + | typeof VIEW_SEARCH + | typeof VIEW_UPLOAD_EMPTY + | typeof VIEW_UPLOAD_IN_PROGRESS + | typeof VIEW_UPLOAD_SUCCESS + | typeof VIEW_METADATA; + +type SortBy = typeof FIELD_DATE | typeof FIELD_NAME | typeof FIELD_RELEVANCE | typeof FIELD_SIZE; +type SortDirection = typeof SORT_ASC | typeof SORT_DESC; +type Delimiter = typeof DELIMITER_SLASH | typeof DELIMITER_CARET; +type Size = typeof SIZE_SMALL | typeof SIZE_MEDIUM | typeof SIZE_LARGE | typeof SIZE_VERY_LARGE; + +type Order = { + by: SortBy; + direction: SortDirection; +}; + +type Access = typeof ACCESS_COLLAB | typeof ACCESS_COMPANY | typeof ACCESS_OPEN | typeof ACCESS_NONE; + +type NoticeType = 'info' | 'error'; + +type InlineNoticeType = NoticeType | 'warning' | 'success' | 'generic'; + +type NotificationType = NoticeType | 'default' | 'warn'; + +type ItemType = typeof ITEM_TYPE_FOLDER | typeof ITEM_TYPE_FILE | typeof ITEM_TYPE_HUBS | typeof ITEM_TYPE_WEBLINK; + +type FileMini = { + id: string; + name: string; + type: typeof ITEM_TYPE_FILE; +}; + +type FolderMini = { + id: string; + name: string; + type: typeof ITEM_TYPE_FOLDER; +}; + +type UserMini = { + avatar_url?: string; + email?: string; + enterprise?: { + id: string; + name: string; + type: 'enterprise'; + }; + hostname?: string; + id: string; + login?: string; + name: string; + status?: string; + type: 'user'; +}; + +type ContactCollection = { + isLoaded?: boolean; + limit?: number; + next_marker?: string; + offset?: number; + order?: Array; + previous_marker?: string; + total_count?: number; +}; + +type User = UserMini; + +type UserCollection = ContactCollection & { + entries?: Array; +}; + +type GroupMini = { + id: string; + name: string; + permissions?: { + can_invite_as_collaborator: boolean; + }; + type: 'group'; +}; + +type GroupCollection = ContactCollection & { + entries?: Array; +}; + +type ISODate = string; + +type MarkerPaginatedCollection = { + entries: T[]; + limit: number; + next_marker: string | null | undefined; +}; + +// Used for things like collaborator search +// NOTE: PillSelectorDropdown requires an additional "text" or "displayText" field +type SelectorItem = { + id: string; + item?: T; // ie UserMini or GroupMini, + name: string; + value?: any; +}; + +type SelectorItems = Array>; + +type Crumb = { + id?: string; + name: string; +}; + +type BoxItemClassification = { + color: string; + definition: string; + name: string; +}; + +type BoxItemPermission = { + can_annotate?: boolean; + can_comment?: boolean; + can_create_annotations?: boolean; + can_delete?: boolean; + can_download?: boolean; + can_edit?: boolean; + can_invite_collaborator?: boolean; + can_preview?: boolean; + can_rename?: boolean; + can_set_share_access?: boolean; + can_share?: boolean; + can_upload?: boolean; + can_view_annotations?: boolean; + can_view_annotations_all?: boolean; + can_view_annotations_self?: boolean; +}; + +type BoxItemVersionPermission = { + can_delete?: boolean; + can_download?: boolean; + can_preview?: boolean; + can_upload?: boolean; +}; + +type BoxItemVersionRetentionPolicy = { + disposition_action: typeof VERSION_RETENTION_DELETE_ACTION | typeof VERSION_RETENTION_REMOVE_ACTION; + id: string; + policy_name: string; + retention_length: typeof VERSION_RETENTION_INDEFINITE | string; // length in days, + type: 'retention_policy'; +}; + +type BoxItemVersionRetention = { + applied_at: string; + disposition_at: string; + id: string; + type: 'file_version_retention'; + winning_retention_policy: BoxItemVersionRetentionPolicy; +}; + +type BoxItemVersion = { + action_by?: User[]; + action_type?: string; + authenticated_download_url?: string; + collaborators?: any; + created_at: string; + end?: any; + extension?: string; + id: string; + is_download_available?: boolean; + modified_at?: string; + modified_by: User | null | undefined; + name?: string; + permissions?: BoxItemVersionPermission; + promoted_by?: User | null; + restored_at?: string; + restored_by?: User | null; + retention?: BoxItemVersionRetention; + sha1?: string; + size?: number; + start?: any; + trashed_at: string | null | undefined; + trashed_by?: User | null; + type: 'file_version'; + uploader_display_name?: string; + version_end?: number; + version_number: string; + version_promoted?: string; + version_start?: number; + versions?: Array; +}; + +type BoxItemVersionMini = { + id: string; + type: 'version'; + version_number: string; +}; + +type BoxPathCollection = { + entries: Array; + total_count: number; +}; + +type FileRepresentation = { + content?: { + url_template: string; + }; + info?: { + url: string; + }; + properties?: { + dimensions: string; + paged: string; + thumb: string; + }; + representation?: string; + status: { + state: string | null | undefined; + }; +}; + +type FileRepresentationResponse = { + entries: Array; +}; + +type SharedLink = { + access: Access; + download_count?: number; + download_url?: string; + effective_access?: Access; + effective_permission?: typeof PERMISSION_CAN_DOWNLOAD | typeof PERMISSION_CAN_PREVIEW; + is_password_enabled?: boolean; + password?: string | null; // the API requires a null value to remove a password, + permissions?: BoxItemPermission; + preview_count?: number; + unshared_at?: string | null; + url: string; + vanity_name?: string; + vanity_url?: string; +}; + +type SharedLinkFeatures = { + download_url: boolean; + password: boolean; + vanity_name: boolean; +}; + +type BoxItem = { + allowed_shared_link_access_levels?: Array; + archive_type?: 'archive' | 'folder_archive' | 'file' | 'web_link'; + authenticated_download_url?: string; + content_created_at?: string; + content_modified_at?: string; + created_at?: string; + created_by?: User; + description?: string; + download_url?: string; + extension?: string; + file_version?: BoxItemVersion; + has_collaborations?: boolean; + id: string; + interacted_at?: string; + is_download_available?: boolean; + is_externally_owned?: boolean; + item_collection?: BoxItemCollection; + metadata?: MetadataType; + modified_at?: string; + modified_by?: User; + name?: string; + owned_by?: User; + parent?: BoxItem; + path_collection?: BoxPathCollection; + permissions?: BoxItemPermission; + representations?: FileRepresentationResponse; + restored_from?: BoxItemVersion; + selected?: boolean; + shared_link?: SharedLink; + shared_link_features?: SharedLinkFeatures; + size?: number; + thumbnailUrl?: string | null; + type?: ItemType; + url?: string; + version_limit?: number | null; + version_number?: string; +}; + +type BoxItemCollection = { + entries?: Array; + isLoaded?: boolean; + limit?: number; + next_marker?: string; + offset?: number; + order?: Array; + previous_marker?: string; + total_count?: number; +}; + +type FlattenedBoxItemCollection = { + entries?: Array; + isLoaded?: boolean; + limit?: number; + next_marker?: string; + offset?: number; + order?: Array; + previous_marker?: string; + total_count?: number; +}; + +type FlattenedBoxItem = { + allowed_shared_link_access_levels?: Array; + created_at?: string; + created_by?: User; + description?: string; + download_url?: string; + extension?: string; + file_version?: BoxItemVersion; + has_collaborations?: boolean; + id?: string; + interacted_at?: string; + is_externally_owned?: boolean; + item_collection?: FlattenedBoxItemCollection; + metadata?: MetadataType; + modified_at?: string; + modified_by?: User; + name?: string; + owned_by?: User; + parent?: BoxItem; + path_collection?: BoxPathCollection; + permissions?: BoxItemPermission; + selected?: boolean; + shared_link?: SharedLink; + size?: number; + type?: ItemType; + url?: string; +}; + +type Collection = { + boxItem?: FlattenedBoxItem; + breadcrumbs?: Array; + id?: string; + items?: Array; + name?: string; + nextMarker?: string | null; + offset?: number; + percentLoaded?: number; + permissions?: BoxItemPermission; + sortBy?: SortBy; + sortDirection?: SortDirection; + totalCount?: number; +}; + +type Recent = { + interacted_at: string; + item: BoxItem; +}; + +type RecentCollection = { + entries: Array; + order: Order; +}; + +type FileVersions = { + entries: Array; + total_count: number; +}; + +type Reply = { + created_at: string; + created_by: User; + id: string; + message: string; + parent: { + id: string; + type: string; + }; + type: 'reply'; +}; + +type Collaborators = { + entries: Array; + next_marker: string | null | undefined; +}; + +type AccessibleByUserOrGroup = { + id: number | string; + login: string; + name: string; + type: 'user' | 'group'; +}; + +type CollaborationOptions = { + expires_at: string | null; + id: number | string; + invite_email?: string; + role: string; + status?: string; +}; + +type Collaboration = CollaborationOptions & { + accessible_by: AccessibleByUserOrGroup; +}; + +type NewCollaboration = CollaborationOptions & { + accessible_by: Partial; +}; + +type Collaborations = { + entries: Array; + next_marker: string | null | undefined; +}; + +// reflects an IE11 specific object to support drag +// and drop for file uploads +type DOMStringList = { + contains: (strToSearch: string) => boolean; + item: (index: number) => string | null; + length: number; +}; + +export type { + Token, + TokenLiteral, + ClassComponent, + StringMap, + StringAnyMap, + StringMixedMap, + StringBooleanMap, + NumberBooleanMap, + Access, + DefaultView, + View, + SortBy, + SortDirection, + Order, + SharedLink, + InlineNoticeType, + ItemType, + Delimiter, + Size, + FileMini, + FolderMini, + UserMini, + User, + UserCollection, + GroupMini, + GroupCollection, + ISODate, + MarkerPaginatedCollection, + SelectorItem, + SelectorItems, + Crumb, + BoxItemClassification, + BoxItemPermission, + BoxItemVersionPermission, + BoxItemVersionRetention, + BoxItemVersion, + BoxItemVersionMini, + BoxItem, + BoxItemCollection, + FlattenedBoxItemCollection, + FlattenedBoxItem, + Collection, + Recent, + RecentCollection, + FileVersions, + FileRepresentation, + Reply, + NotificationType, + Collaborators, + Collaboration, + Collaborations, + NewCollaboration, + DOMStringList, +}; 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..b4537daae3 --- /dev/null +++ b/src/common/types/feed.ts @@ -0,0 +1,244 @@ +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_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_TASK + | typeof FILE_ACTIVITY_TYPE_VERSION; + +type FileActivitySource = + | { + annotation: Annotation; + } + | { + app_activity: AppActivityItem; + } + | { + comment: Comment; + } + | { + enhanced_annotation: Annotation; + } + | { + enhanced_comment: 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/integrations.js b/src/common/types/integrations.js.flow similarity index 100% rename from src/common/types/integrations.js rename to src/common/types/integrations.js.flow diff --git a/src/common/types/integrations.ts b/src/common/types/integrations.ts new file mode 100644 index 0000000000..f91dbe734e --- /dev/null +++ b/src/common/types/integrations.ts @@ -0,0 +1,61 @@ +import * as React from 'react'; +import { FormattedMessage } from 'react-intl'; +import { APP_INTEGRATION, HTTP_POST, HTTP_GET } from '../../constants'; +import BoxToolsInstallMessage from '../../elements/content-open-with/BoxToolsInstallMessage'; + +type AppIntegrationAPIMiniItem = { + id: string; + type: typeof APP_INTEGRATION; +}; + +type OpenWithAPIItem = { + app_integration: AppIntegrationAPIMiniItem; + disabled_reasons: Array; + display_description: string; + display_name: string; + display_order: number; + is_disabled: boolean; + should_show_consent_popup: boolean; +}; + +type OpenWithAPI = { + default_app_integration?: AppIntegrationAPIMiniItem; + disabled_reasons?: Array; + is_disabled?: boolean; + items: Array; + should_show_consent_popup?: boolean; +}; + +type ExecuteAPIParam = { + key: string; + value: string; +}; + +type ExecuteAPI = { + integration_type: string; + method: typeof HTTP_POST | typeof HTTP_GET; + params: Array | null | undefined; + url: string; +}; + +// TODO: is there a way to consolidate BoxToolsInstallMessage, +// which is just a wrapper around FormattedMessage? +type DisabledReason = + | string + | React.ReactElement> + | React.ReactElement>; + +type Integration = { + appIntegrationId: string; + disabledReasons: Array; + displayDescription: string; + displayName: string; + displayOrder: number; + extension?: string; + isDefault: boolean; + isDisabled: boolean; + requiresConsent: boolean; + type: typeof APP_INTEGRATION; +}; + +export type { OpenWithAPI, ExecuteAPI, DisabledReason, Integration }; diff --git a/src/common/types/logging.js b/src/common/types/logging.js.flow similarity index 100% rename from src/common/types/logging.js rename to src/common/types/logging.js.flow diff --git a/src/common/types/logging.ts b/src/common/types/logging.ts new file mode 100644 index 0000000000..58d18ab4aa --- /dev/null +++ b/src/common/types/logging.ts @@ -0,0 +1,28 @@ +/* eslint-disable @typescript-eslint/no-explicit-any -- Preserve Flow any and Function contracts. */ +import { + METRIC_TYPE_PREVIEW, + METRIC_TYPE_ELEMENTS_LOAD_METRIC, + METRIC_TYPE_ELEMENTS_PERFORMANCE_METRIC, +} from '../../constants'; + +type MetricType = + | typeof METRIC_TYPE_PREVIEW + | typeof METRIC_TYPE_ELEMENTS_LOAD_METRIC + | typeof METRIC_TYPE_ELEMENTS_PERFORMANCE_METRIC; + +type ElementsLoadMetricData = { + endMarkName: string; + startMarkName?: string; +}; + +type LoggerProps = { + logError?: (error: Error, errorCode: string, context?: any) => void; + onPreviewMetric: (data: any) => void; + onReadyMetric: (data: ElementsLoadMetricData) => void; +}; + +type WithLoggerProps = { + logger: LoggerProps; +}; + +export type { MetricType, ElementsLoadMetricData, LoggerProps, WithLoggerProps }; diff --git a/src/common/types/metadata.js b/src/common/types/metadata.js.flow similarity index 100% rename from src/common/types/metadata.js rename to src/common/types/metadata.js.flow diff --git a/src/common/types/metadata.ts b/src/common/types/metadata.ts new file mode 100644 index 0000000000..b5b26e69e2 --- /dev/null +++ b/src/common/types/metadata.ts @@ -0,0 +1,249 @@ +/* eslint-disable @typescript-eslint/no-explicit-any -- Preserve the Flow any array element contract. */ +import { + FIELD_TYPE_DATE, + FIELD_TYPE_ENUM, + FIELD_TYPE_FLOAT, + FIELD_TYPE_MULTISELECT, + FIELD_TYPE_STRING, + FIELD_TYPE_TAXONOMY, +} 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; + +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; + scope: string; // V2, + templateKey: string; // V3 +}; + +type MetadataTemplateSchemaResponse = { + data?: MetadataTemplate; +}; + +type MetadataSkillsTemplate = { + archivedItemTemplate?: { + archiveDate: string; + }; + boxSkillsCards?: SkillCards; +}; + +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; + $parent: string; + $scope: string; + $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[]; + 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..9ad0762952 --- /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; + 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 | FolderMini | UserMini | null; + 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; + completion_rule: TaskCompletionRule; + created_at: ISODate; + created_by: TaskCollabCreator; + description: string; + due_at?: ISODate | null; + 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; + status: TaskStatus; + task_links: TaskLinkCollection; + task_type: TaskType; + type: 'task'; +}; + +type TaskPayload = { + description: string; + due_at?: string | null; + task_type: TaskType; +}; + +type TaskUpdatePayload = { + addedAssignees: SelectorItems; + completion_rule: TaskCompletionRule; + description: string; + due_at?: string | null; + 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/common/variables.js b/src/common/variables.js.flow similarity index 100% rename from src/common/variables.js rename to src/common/variables.js.flow diff --git a/src/common/variables.ts b/src/common/variables.ts new file mode 100644 index 0000000000..a4f7e3a910 --- /dev/null +++ b/src/common/variables.ts @@ -0,0 +1,5 @@ +const BOX_BLUE = '#0061d5'; +const SEESEE = '#ccc'; +const RESIN_TAG_TARGET = 'data-resin-target'; + +export { BOX_BLUE, SEESEE, RESIN_TAG_TARGET }; diff --git a/src/components/grid-view/__tests__/GridView.test.tsx b/src/components/grid-view/__tests__/GridView.test.tsx index 3b48b38cdb..1148e67e6a 100644 --- a/src/components/grid-view/__tests__/GridView.test.tsx +++ b/src/components/grid-view/__tests__/GridView.test.tsx @@ -1,11 +1,12 @@ import * as React from 'react'; import { shallow } from 'enzyme'; +import type { Collection } from '../../../common/types/core'; import GridView from '../GridView'; describe('components/grid-view/GridView', () => { test('should render()', () => { - const collection = { items: [{ type: 'folder', id: '001', name: 'Example Folder' }] } as const; + const collection: Collection = { items: [{ type: 'folder', id: '001', name: 'Example Folder' }] }; const wrapper = shallow( { describe('isThumbnailAvailable()', () => { - test('returns `true` when the representation status is `success`', () => { - const item = { + test.each([ + { expected: true, state: 'success' }, + { expected: true, state: 'viewable' }, + { expected: false, state: 'pending' }, + ])('returns `$expected` when the representation status is `$state`', ({ expected, state }) => { + const item: BoxItem = { + id: '1', representations: { - entries: [{ status: { state: 'success' } }], + entries: [{ status: { state } }], }, }; - const result = isThumbnailAvailable(item); - expect(result).toBe(true); - }); - - test('returns `true` when the representation status is `viewable`', () => { - const item = { - representations: { - entries: [{ status: { state: 'viewable' } }], - }, - }; - const result = isThumbnailAvailable(item); - expect(result).toBe(true); - }); - - test('returns `false` when the representation response is not ready', () => { - const item = { - representations: { - entries: [{ status: { state: 'pending' } }], - }, - }; - const result = isThumbnailAvailable(item); - expect(result).toBe(false); + expect(isThumbnailAvailable(item)).toBe(expected); }); }); }); diff --git a/src/elements/common/breadcrumbs/__tests__/InlineBreadcrumbs.test.tsx b/src/elements/common/breadcrumbs/__tests__/InlineBreadcrumbs.test.tsx index 0a862132f5..2d516aba0c 100644 --- a/src/elements/common/breadcrumbs/__tests__/InlineBreadcrumbs.test.tsx +++ b/src/elements/common/breadcrumbs/__tests__/InlineBreadcrumbs.test.tsx @@ -1,11 +1,12 @@ import * as React from 'react'; import userEvent from '@testing-library/user-event'; +import type { BoxItem } from '../../../../common/types/core'; import { render, screen } from '../../../../test-utils/testing-library'; import InlineBreadcrumbs, { InlineBreadcrumbsProps } from '../InlineBreadcrumbs'; describe('elements/common/breadcrumbs/InlineBreadcrumbs', () => { const renderComponent = (props: Partial = {}) => { - const item = { + const item: BoxItem = { id: '123', name: 'Test Item', path_collection: { @@ -13,6 +14,7 @@ describe('elements/common/breadcrumbs/InlineBreadcrumbs', () => { { id: '0', name: 'All Files' }, { id: '1', name: 'Folder 1' }, ], + total_count: 2, }, }; return render(); @@ -37,7 +39,7 @@ describe('elements/common/breadcrumbs/InlineBreadcrumbs', () => { }); test('should render dropdown when there are at least 4 crumbs', () => { - const item = { + const item: BoxItem = { id: '123', name: 'Test Item', path_collection: { @@ -47,6 +49,7 @@ describe('elements/common/breadcrumbs/InlineBreadcrumbs', () => { { id: '2', name: 'Folder 2' }, { id: '3', name: 'Folder 3' }, ], + total_count: 4, }, }; renderComponent({ item }); diff --git a/src/elements/common/current-user/__tests__/withCurrentUser.test.tsx b/src/elements/common/current-user/__tests__/withCurrentUser.test.tsx index 89ee40d255..17111c440c 100644 --- a/src/elements/common/current-user/__tests__/withCurrentUser.test.tsx +++ b/src/elements/common/current-user/__tests__/withCurrentUser.test.tsx @@ -1,14 +1,12 @@ import React, { Component } from 'react'; import { shallow, ShallowWrapper } from 'enzyme'; +import type { ElementsXhrError } from '../../../../common/types/api'; import withCurrentUser, { ComponentWithCurrentUser, CurrentUserState, WithCurrentUserProps } from '../withCurrentUser'; // @ts-ignore no ts defintion import messages from '../../messages'; const { defaultErrorMaskSubHeaderMessage, currentUserErrorHeaderMessage } = messages; -type WrappedProps = Partial; -type WrapperType = ShallowWrapper; - jest.mock('../../api-context/withAPIContext', () => (div: React.Component) => div); describe('elements/common/current-user/withCurrentUser', () => { @@ -24,15 +22,34 @@ describe('elements/common/current-user/withCurrentUser', () => { id: 'id', }; - const MockComponent = (props: WithCurrentUserProps) =>
; - const WrappedComponent = withCurrentUser(MockComponent); + type MockComponentProps = WithCurrentUserProps & { + api: typeof api; + file: typeof file; + }; + + const MockComponent: React.FC = () =>
; + const WrappedComponent = withCurrentUser(MockComponent); + type WrappedProps = Partial>; + type WrapperType = ShallowWrapper; const getWrapper = (props: WrappedProps = {}): WrapperType => - shallow(); + shallow(); const currentUser = { id: 'foo', - }; + name: 'Foo User', + type: 'user', + } as const; + + const createError = (status: number): ElementsXhrError => ({ + code: 'error', + context_info: {}, + help_url: '', + message: 'Request failed', + request_id: 'request-id', + status, + type: 'error', + }); let instance: React.Component<{}, {}, {}> & ComponentWithCurrentUser; let wrapper: WrapperType; @@ -85,7 +102,7 @@ describe('elements/common/current-user/withCurrentUser', () => { }); test('should set a maskError if there is an error in fetching the current user', () => { - instance.fetchCurrentUserErrorCallback({}, '404'); + instance.fetchCurrentUserErrorCallback(createError(404), '404'); const inlineErrorState = wrapper.state().currentUserError.maskError; expect(typeof currentUserErrorHeaderMessage).toBe('object'); @@ -96,7 +113,7 @@ describe('elements/common/current-user/withCurrentUser', () => { test('should set the current user error and call the error callback', () => { instance.setState = jest.fn(); - instance.fetchCurrentUserErrorCallback({ status: 500 }, '500'); + instance.fetchCurrentUserErrorCallback(createError(500), '500'); expect(instance.setState).toBeCalledWith({ currentUser: undefined, currentUserError: expect.any(Object), diff --git a/src/elements/common/current-user/withCurrentUser.tsx b/src/elements/common/current-user/withCurrentUser.tsx index 0534b12bd6..6eb2736dbe 100644 --- a/src/elements/common/current-user/withCurrentUser.tsx +++ b/src/elements/common/current-user/withCurrentUser.tsx @@ -76,6 +76,7 @@ export default function withCurrentUser

( * @return {void} */ fetchCurrentUser = (user?: User, shouldDestroy = false): void => { + // @ts-expect-error -- API context and file props remain defined by the Flow-era HOC contract const { api, file } = this.props; if (!file) { diff --git a/src/elements/common/item-grid/__tests__/ItemGrid.test.tsx b/src/elements/common/item-grid/__tests__/ItemGrid.test.tsx index d6cbe74e7f..f4b9f97774 100644 --- a/src/elements/common/item-grid/__tests__/ItemGrid.test.tsx +++ b/src/elements/common/item-grid/__tests__/ItemGrid.test.tsx @@ -1,14 +1,15 @@ import React from 'react'; import userEvent from '@testing-library/user-event'; +import type { BoxItem } from '../../../../common/types/core'; import { render, screen } from '../../../../test-utils/testing-library'; -import ItemGrid from '../ItemGrid'; +import ItemGrid, { ItemGridProps } from '../ItemGrid'; import { isThumbnailAvailable } from '../../utils'; jest.mock('../../utils'); describe('elements/common/item-grid/ItemGrid', () => { - const renderComponent = (props = {}) => { - const defaultProps = { + const renderComponent = (props: Partial = {}) => { + const defaultProps: ItemGridProps = { items: [ { type: 'folder', id: '001', name: 'Shared folder', modified_at: '2021-10-18T09:00:00-07:00' }, { type: 'folder', id: '002', name: 'Documents', modified_at: '2021-10-18T09:00:00-07:00' }, @@ -33,7 +34,7 @@ describe('elements/common/item-grid/ItemGrid', () => { }); test('renders component correctly with item thumbnails', () => { - const items = [ + const items: BoxItem[] = [ { type: 'file', id: '003', @@ -59,7 +60,7 @@ describe('elements/common/item-grid/ItemGrid', () => { expect(screen.queryByRole('img', { name: 'Sample PDF' })).not.toBeInTheDocument(); }); - test.each(['folder', 'file', 'web_link'])( + test.each(['folder', 'file', 'web_link'] as const)( 'calls `onItemClick` when a %s item is clicked with preview enabled', async type => { const items = [{ type, id: '004', name: 'Box item', modified_at: '2021-10-18T09:00:00-07:00' }]; @@ -76,7 +77,9 @@ describe('elements/common/item-grid/ItemGrid', () => { ); test('does not call `onItemClick` when a file item is clicked with preview disabled', async () => { - const items = [{ type: 'file', id: '004', name: 'Box file', modified_at: '2021-10-18T09:00:00-07:00' }]; + const items: BoxItem[] = [ + { type: 'file', id: '004', name: 'Box file', modified_at: '2021-10-18T09:00:00-07:00' }, + ]; const onItemClick = jest.fn(); renderComponent({ canPreview: false, items, onItemClick }); @@ -86,7 +89,7 @@ describe('elements/common/item-grid/ItemGrid', () => { expect(onItemClick).not.toHaveBeenCalled(); }); - test.each(['file', 'web_link'])( + test.each(['file', 'web_link'] as const)( 'does not call `onItemClick` when a %s item is clicked on a touch device with preview enabled', async type => { const items = [{ type, id: '004', name: 'Box item', modified_at: '2021-10-18T09:00:00-07:00' }]; diff --git a/src/elements/common/item-list/__tests__/ItemList.test.tsx b/src/elements/common/item-list/__tests__/ItemList.test.tsx index e3d2c4126a..b544769f1d 100644 --- a/src/elements/common/item-list/__tests__/ItemList.test.tsx +++ b/src/elements/common/item-list/__tests__/ItemList.test.tsx @@ -1,11 +1,12 @@ import React from 'react'; import userEvent from '@testing-library/user-event'; +import type { BoxItem } from '../../../../common/types/core'; import { render, screen } from '../../../../test-utils/testing-library'; -import ItemList from '../ItemList'; +import ItemList, { ItemListProps } from '../ItemList'; describe('elements/common/item-list/ItemList', () => { - const renderComponent = (props = {}) => { - const defaultProps = { + const renderComponent = (props: Partial = {}) => { + const defaultProps: ItemListProps = { items: [ { type: 'folder', id: '001', name: 'Shared folder', modified_at: '2021-10-18T09:00:00-07:00' }, { type: 'folder', id: '002', name: 'Documents', modified_at: '2021-10-18T09:00:00-07:00' }, @@ -20,7 +21,7 @@ describe('elements/common/item-list/ItemList', () => { renderComponent(); expect(screen.getByRole('grid', { name: 'List view' })).toBeInTheDocument(); - expect(screen.getAllByRole('columnheader').length).toBe(4); + expect(screen.getAllByRole('columnheader')).toHaveLength(4); expect(screen.getByRole('columnheader', { name: 'NAME' })).toBeInTheDocument(); expect(screen.getByRole('columnheader', { name: 'UPDATED' })).toBeInTheDocument(); expect(screen.getByRole('columnheader', { name: 'SIZE' })).toBeInTheDocument(); @@ -33,7 +34,7 @@ describe('elements/common/item-list/ItemList', () => { test('renders component with correct columns for small screen devices', () => { renderComponent({ isSmall: true }); - expect(screen.getAllByRole('columnheader').length).toBe(2); + expect(screen.getAllByRole('columnheader')).toHaveLength(2); expect(screen.getByRole('columnheader', { name: 'DETAILS' })).toBeInTheDocument(); expect(screen.getByRole('columnheader', { name: 'ACTIONS' })).toBeInTheDocument(); }); @@ -41,23 +42,14 @@ describe('elements/common/item-list/ItemList', () => { test('renders component with correct columns for medium screen devices', () => { renderComponent({ isMedium: true }); - expect(screen.getAllByRole('columnheader').length).toBe(3); - expect(screen.getByRole('columnheader', { name: 'NAME' })).toBeInTheDocument(); - expect(screen.getByRole('columnheader', { name: 'UPDATED' })).toBeInTheDocument(); - expect(screen.getByRole('columnheader', { name: 'ACTIONS' })).toBeInTheDocument(); - }); - - test('renders component with correct columns for medium screen devices', () => { - renderComponent({ isMedium: true }); - - expect(screen.getAllByRole('columnheader').length).toBe(3); + expect(screen.getAllByRole('columnheader')).toHaveLength(3); expect(screen.getByRole('columnheader', { name: 'NAME' })).toBeInTheDocument(); expect(screen.getByRole('columnheader', { name: 'UPDATED' })).toBeInTheDocument(); expect(screen.getByRole('columnheader', { name: 'ACTIONS' })).toBeInTheDocument(); }); test('renders component with item details when device screen is small', () => { - const items = [ + const items: BoxItem[] = [ { type: 'file', id: '004', name: 'Box file', modified_at: '2021-10-18T09:00:00-07:00', size: 2048 }, ]; @@ -67,7 +59,7 @@ describe('elements/common/item-list/ItemList', () => { }); test('does not render component with item details when `view` is `recents`', () => { - const items = [ + const items: BoxItem[] = [ { type: 'file', id: '004', name: 'Box file', modified_at: '2021-10-18T09:00:00-07:00', size: 2048 }, ]; @@ -76,7 +68,7 @@ describe('elements/common/item-list/ItemList', () => { expect(screen.queryByText('Oct 18, 2021 • 2 KB')).not.toBeInTheDocument(); }); - test.each(['folder', 'file', 'web_link'])( + test.each(['folder', 'file', 'web_link'] as const)( 'calls `onItemClick` when a %s item is clicked with preview enabled', async type => { const items = [{ type, id: '004', name: 'Box item', modified_at: '2021-10-18T09:00:00-07:00' }]; @@ -93,7 +85,9 @@ describe('elements/common/item-list/ItemList', () => { ); test('does not call `onItemClick` when a file item is clicked with preview disabled', async () => { - const items = [{ type: 'file', id: '004', name: 'Box file', modified_at: '2021-10-18T09:00:00-07:00' }]; + const items: BoxItem[] = [ + { type: 'file', id: '004', name: 'Box file', modified_at: '2021-10-18T09:00:00-07:00' }, + ]; const onItemClick = jest.fn(); renderComponent({ canPreview: false, items, onItemClick }); @@ -103,7 +97,7 @@ describe('elements/common/item-list/ItemList', () => { expect(onItemClick).not.toHaveBeenCalled(); }); - test.each(['file', 'web_link'])( + test.each(['file', 'web_link'] as const)( 'does not call `onItemClick` when a %s item is clicked on a touch device with preview enabled', async type => { const items = [{ type, id: '004', name: 'Box item', modified_at: '2021-10-18T09:00:00-07:00' }]; diff --git a/src/elements/common/item/__tests__/ItemName.test.tsx b/src/elements/common/item/__tests__/ItemName.test.tsx index d2d8f1da54..29ef230f8d 100644 --- a/src/elements/common/item/__tests__/ItemName.test.tsx +++ b/src/elements/common/item/__tests__/ItemName.test.tsx @@ -4,8 +4,9 @@ import userEvent from '@testing-library/user-event'; import ItemName, { ItemNameProps } from '../ItemName'; import { fireEvent, render, screen } from '../../../../test-utils/testing-library'; import { TYPE_FOLDER, TYPE_WEBLINK } from '../../../../constants'; +import type { BoxItem } from '../../../../common/types/core'; -const mockItem = { +const mockItem: BoxItem = { id: '1', name: 'Test Item', type: 'file', diff --git a/src/elements/common/item/__tests__/ItemOptions.test.tsx b/src/elements/common/item/__tests__/ItemOptions.test.tsx index e3dcc5bf0c..eaf6f88b3d 100644 --- a/src/elements/common/item/__tests__/ItemOptions.test.tsx +++ b/src/elements/common/item/__tests__/ItemOptions.test.tsx @@ -1,7 +1,7 @@ import React from 'react'; import userEvent from '@testing-library/user-event'; import { render, screen } from '../../../../test-utils/testing-library'; -import ItemOptions from '../ItemOptions'; +import ItemOptions, { ItemOptionsProps } from '../ItemOptions'; jest.mock('@box/blueprint-web', () => ({ ...jest.requireActual('@box/blueprint-web'), @@ -9,8 +9,8 @@ jest.mock('@box/blueprint-web', () => ({ })); describe('elements/common/item/ItemOptions', () => { - const renderComponent = (props = {}) => { - const defaultProps = { + const renderComponent = (props: Partial = {}) => { + const defaultProps: ItemOptionsProps = { canDelete: true, canDownload: true, canPreview: true, @@ -90,9 +90,9 @@ describe('elements/common/item/ItemOptions', () => { renderComponent({ itemActions: [ - { label: 'Archive', type: 'folder' }, // Should be filtered since there are no folder items - { label: 'Email', type: 'file' }, - { filter: ({ extension }) => extension === 'pdf', label: 'Export' }, + { label: 'Archive', onAction, type: 'folder' }, // Should be filtered since there are no folder items + { label: 'Email', onAction, type: 'file' }, + { filter: ({ extension }) => extension === 'pdf', label: 'Export', onAction }, { label: 'Favorite', onAction, type: 'file' }, ], }); @@ -118,8 +118,8 @@ describe('elements/common/item/ItemOptions', () => { canRename: false, canShare: false, itemActions: [ - { label: 'Archive', type: 'folder' }, - { filter: ({ extension }) => extension === 'csv', label: 'Import' }, + { label: 'Archive', onAction: jest.fn(), type: 'folder' }, + { filter: ({ extension }) => extension === 'csv', label: 'Import', onAction: jest.fn() }, ], }); @@ -134,8 +134,8 @@ describe('elements/common/item/ItemOptions', () => { canRename: false, canShare: false, itemActions: [ - { label: 'Archive', type: 'folder' }, - { filter: ({ extension }) => extension === 'csv', label: 'Import' }, + { label: 'Archive', onAction: jest.fn(), type: 'folder' }, + { filter: ({ extension }) => extension === 'csv', label: 'Import', onAction: jest.fn() }, ], viewMode: 'list', }); diff --git a/src/elements/common/item/__tests__/ItemSubDetails.test.tsx b/src/elements/common/item/__tests__/ItemSubDetails.test.tsx index e6d682f2d3..0293851ce1 100644 --- a/src/elements/common/item/__tests__/ItemSubDetails.test.tsx +++ b/src/elements/common/item/__tests__/ItemSubDetails.test.tsx @@ -3,19 +3,20 @@ import React from 'react'; import { render, screen } from '../../../../test-utils/testing-library'; import ItemSubDetails, { ItemSubDetailsProps } from '../ItemSubDetails'; import { VIEW_RECENTS } from '../../../../constants'; +import type { BoxItem } from '../../../../common/types/core'; -const mockItem = { +const mockItem: BoxItem = { id: '1', name: 'Test Item', modified_at: '2023-10-10T10:00:00Z', - modified_by: { name: 'John Doe' }, + modified_by: { id: '2', name: 'John Doe', type: 'user' }, interacted_at: '', size: 12345, }; describe('ItemSubDetails', () => { const renderComponent = (props: Partial = {}) => { - const defaultProps = { + const defaultProps: ItemSubDetailsProps = { item: mockItem, view: 'default', ...props, diff --git a/src/elements/common/item/__tests__/ItemTypeIcon.test.tsx b/src/elements/common/item/__tests__/ItemTypeIcon.test.tsx index d28c8fce98..03edbf57f4 100644 --- a/src/elements/common/item/__tests__/ItemTypeIcon.test.tsx +++ b/src/elements/common/item/__tests__/ItemTypeIcon.test.tsx @@ -1,11 +1,12 @@ import React from 'react'; import { render, screen } from '../../../../test-utils/testing-library'; -import ItemTypeIcon from '../ItemTypeIcon'; +import ItemTypeIcon, { ItemTypeIconProps } from '../ItemTypeIcon'; describe('elements/common/item/ItemTypeIcon', () => { - const renderComponent = (props = {}) => { - const defaultProps = { + const renderComponent = (props: Partial = {}) => { + const defaultProps: ItemTypeIconProps = { item: { + id: '1', type: 'file', extension: 'pdf', }, @@ -20,13 +21,13 @@ describe('elements/common/item/ItemTypeIcon', () => { }); test('renders component correctly for a folder item', async () => { - renderComponent({ item: { type: 'folder' } }); + renderComponent({ item: { id: '1', type: 'folder' } }); expect(screen.getByRole('img', { name: 'Personal folder' })).toBeInTheDocument(); }); test('renders component correctly for a bookmark item', async () => { - renderComponent({ item: { type: 'web_link' } }); + renderComponent({ item: { id: '1', type: 'web_link' } }); expect(screen.getByRole('img', { name: 'Bookmark' })).toBeInTheDocument(); }); @@ -38,7 +39,7 @@ describe('elements/common/item/ItemTypeIcon', () => { ${{ is_externally_owned: true }} | ${'External folder'} ${{ has_collaborations: true }} | ${'Collaborated folder'} `('renders component when the folder is $label', ({ item, label }) => { - renderComponent({ item: { type: 'folder', ...item } }); + renderComponent({ item: { id: '1', type: 'folder', ...item } }); expect(screen.getByRole('img', { name: label })).toBeInTheDocument(); }); diff --git a/src/elements/common/preview-dialog/__tests__/PreviewDialog.test.tsx b/src/elements/common/preview-dialog/__tests__/PreviewDialog.test.tsx index ddf8137a8e..95f112bb6f 100644 --- a/src/elements/common/preview-dialog/__tests__/PreviewDialog.test.tsx +++ b/src/elements/common/preview-dialog/__tests__/PreviewDialog.test.tsx @@ -10,7 +10,7 @@ jest.mock('react-modal', () => { }); describe('elements/content-explorer/PreviewDialog', () => { - const defaultProps = { + const defaultProps: PreviewDialogProps = { appElement: document.body, apiHost: 'https://api.box.com', appHost: 'https://app.box.com', diff --git a/src/elements/common/share-access-select/__tests__/ShareAccessSelect.test.tsx b/src/elements/common/share-access-select/__tests__/ShareAccessSelect.test.tsx index 298819f023..61a27f741c 100644 --- a/src/elements/common/share-access-select/__tests__/ShareAccessSelect.test.tsx +++ b/src/elements/common/share-access-select/__tests__/ShareAccessSelect.test.tsx @@ -1,28 +1,31 @@ import React from 'react'; import { screen, render, fireEvent } from '../../../../test-utils/testing-library'; -import ShareAccessSelect from '../ShareAccessSelect'; +import type { BoxItem } from '../../../../common/types/core'; +import ShareAccessSelect, { ShareAccessSelectProps } from '../ShareAccessSelect'; import { ACCESS_NONE, ACCESS_OPEN, ACCESS_COLLAB, ACCESS_COMPANY } from '../../../../constants'; describe('elements/common/share-access-select/ShareAccessSelect', () => { - const getDefaultItem = (overrides = {}) => ({ + const getDefaultItem = (overrides: Partial = {}): BoxItem => ({ + id: '1', allowed_shared_link_access_levels: [ACCESS_OPEN, ACCESS_COLLAB, ACCESS_COMPANY], permissions: { can_set_share_access: true, }, shared_link: { access: ACCESS_NONE, + url: 'https://box.com/shared-link', }, ...overrides, }); - const defaultProps = { + const defaultProps: ShareAccessSelectProps = { canSetShareAccess: true, className: 'test-share-access', item: getDefaultItem(), onChange: jest.fn(), }; - const renderComponent = (props = {}) => { + const renderComponent = (props: Partial = {}) => { return render(); }; @@ -93,7 +96,7 @@ describe('elements/common/share-access-select/ShareAccessSelect', () => { test('should show "Remove" text for ACCESS_NONE option when current access is not ACCESS_NONE', () => { const item = getDefaultItem({ - shared_link: { access: ACCESS_OPEN }, + shared_link: { access: ACCESS_OPEN, url: 'https://box.com/shared-link' }, }); renderComponent({ item }); diff --git a/src/elements/content-explorer/ContentExplorer.tsx b/src/elements/content-explorer/ContentExplorer.tsx index 809df69a95..f1547690a6 100644 --- a/src/elements/content-explorer/ContentExplorer.tsx +++ b/src/elements/content-explorer/ContentExplorer.tsx @@ -324,6 +324,7 @@ class ContentExplorer extends Component { isUploadModalOpen: false, markers: [], metadataFilters: {}, + // @ts-expect-error -- Legacy state uses an empty template until metadata loads. metadataTemplate: {}, rootName: '', selectedItemIds: new Set(), @@ -460,6 +461,7 @@ class ContentExplorer extends Component { * @return {void} */ showMetadataQueryResults() { + // @ts-expect-error -- The optional query intentionally defaults to an empty object before required fields are supplied. const { features, metadataQuery = {} }: ContentExplorerProps = this.props; const { currentPageNumber, markers, metadataFilters, sortBy, sortDirection }: State = this.state; const metadataQueryClone = cloneDeep(metadataQuery); @@ -654,6 +656,7 @@ class ContentExplorer extends Component { // Fire folder navigation event this.setState({ rootName }, this.finishNavigation); if (boxItem) { + // @ts-expect-error -- Collection boxItem retains the legacy flattened shape with an optional id. onNavigate(cloneDeep(boxItem)); } } else { @@ -1080,6 +1083,7 @@ class ContentExplorer extends Component { const { items = [] } = currentCollection; const newCollection = { ...currentCollection } as const; + // @ts-expect-error -- Preserve the legacy mutation of the cloned collection despite its const assertion. newCollection.items = items.map(item => (item.id === newItem.id ? newItem : item)); this.validateSelectedItemIds(newCollection.items); @@ -1108,6 +1112,7 @@ class ContentExplorer extends Component { const selectedItem: BoxItem = { ...item, selected: true }; this.updateCollection(currentCollection, selectedItem, () => { + // @ts-expect-error -- The legacy callback contract receives a single-item array at runtime. onSelect(cloneDeep([selectedItem])); callback(selectedItem); }); @@ -1198,6 +1203,7 @@ class ContentExplorer extends Component { const openUrl = (url: string) => { openUrlInsideIframe(url); + // @ts-expect-error -- The legacy callback contract receives a single-item array at runtime. onDownload(cloneDeep([selected])); }; @@ -1251,6 +1257,7 @@ class ContentExplorer extends Component { this.api.getAPI(type).deleteItem( selected, () => { + // @ts-expect-error -- The legacy callback contract receives a single-item array at runtime. onDelete(cloneDeep([selected])); this.refreshCollection(); }, @@ -1268,6 +1275,7 @@ class ContentExplorer extends Component { * @return {void} */ rename = (item: BoxItem): void => { + // @ts-expect-error -- Legacy selection invokes the rename callback with the selected item before opening the dialog. this.select(item, this.renameCallback); }; diff --git a/src/elements/content-explorer/MetadataQueryAPIHelper.ts b/src/elements/content-explorer/MetadataQueryAPIHelper.ts index eb8716eadf..44cbadbd21 100644 --- a/src/elements/content-explorer/MetadataQueryAPIHelper.ts +++ b/src/elements/content-explorer/MetadataQueryAPIHelper.ts @@ -77,10 +77,12 @@ export default class MetadataQueryAPIHelper { let operation = JSON_PATCH_OP_REPLACE; if (isEmptyValue(oldValue) && !isEmptyValue(newValue)) { + // @ts-expect-error -- Legacy inference narrows operation to "replace" even though runtime also assigns "add". operation = JSON_PATCH_OP_ADD; } if (!isEmptyValue(oldValue) && isEmptyValue(newValue)) { + // @ts-expect-error -- Legacy inference narrows operation to "replace" even though runtime also assigns "remove". operation = JSON_PATCH_OP_REMOVE; } @@ -95,10 +97,12 @@ export default class MetadataQueryAPIHelper { value: newValue, }; + // @ts-expect-error -- The inferred "replace" literal does not reflect the runtime remove branch. if (operation === JSON_PATCH_OP_REMOVE) { delete patchOp.value; } + // @ts-expect-error -- The inferred "replace" literal does not reflect the runtime add branch. return operation === JSON_PATCH_OP_ADD ? [patchOp] : [testOp, patchOp]; }; @@ -155,6 +159,7 @@ export default class MetadataQueryAPIHelper { return { enterprise: { fields, + // @ts-expect-error -- Dynamic metadata instance lookup is inferred as never after the Flow-to-TS migration. id: instance.$id, }, }; @@ -284,6 +289,7 @@ export default class MetadataQueryAPIHelper { const operations: JSONPatchOperations = []; items.forEach(item => { const operation = this.generateOperations(item, templateOldFields, templateNewFields); + // @ts-expect-error -- Bulk metadata API preserves the legacy nested patch-operation array shape. operations.push(operation); }); return this.api @@ -390,9 +396,11 @@ export default class MetadataQueryAPIHelper { const { query: filterQuery, queryParams: filteredQueryParams } = this.buildMetadataQueryParams(fields); const { query: customQuery, query_params: customQueryParams } = clonedQuery; const query = this.mergeQuery(customQuery, filterQuery); + // @ts-expect-error -- Legacy query-param helpers disagree on boolean versus Date support. const queryParams = mergeQueryParams(filteredQueryParams, customQueryParams); if (query) { clonedQuery.query = query; + // @ts-expect-error -- Legacy query-param helpers disagree on boolean versus Date support. clonedQuery.query_params = queryParams; } } diff --git a/src/elements/content-explorer/MetadataSidePanel.tsx b/src/elements/content-explorer/MetadataSidePanel.tsx index b21f3bbf6a..8f33b4d410 100644 --- a/src/elements/content-explorer/MetadataSidePanel.tsx +++ b/src/elements/content-explorer/MetadataSidePanel.tsx @@ -59,6 +59,7 @@ const MetadataSidePanel = ({ selectedItemIds === 'all' ? currentCollection.items : currentCollection.items.filter(item => selectedItemIds.has(item.id)); + // @ts-expect-error -- Local metadata templates omit the editor package's required legacy type field. const templateInstance = useTemplateInstance(metadataTemplate, selectedItems, isEditing); const handleMetadataInstanceEdit = () => { diff --git a/src/elements/content-explorer/MetadataViewContainer.tsx b/src/elements/content-explorer/MetadataViewContainer.tsx index c409d520f3..1d49509c1b 100644 --- a/src/elements/content-explorer/MetadataViewContainer.tsx +++ b/src/elements/content-explorer/MetadataViewContainer.tsx @@ -177,6 +177,7 @@ const MetadataViewContainer = ({ key: ITEM_FILTER_NAME, displayName: formatMessage(messages.name), type: 'string', + // @ts-expect-error -- Local metadata fields retain the legacy shouldRenderChip extension. shouldRenderChip: true, }, ...fields, @@ -263,8 +264,10 @@ const MetadataViewContainer = ({ return ( { }); test('renders MetadataBasedItemList when view is VIEW_METADATA', () => { - const collection = { boxItem: {}, id: '0', items: [{ id: 1 }], name: 'name' }; + const collection: Collection = { boxItem: {}, id: '0', items: [{ id: '1' }], name: 'name' }; renderComponent({ view: VIEW_METADATA, fieldsToShow: ['id'], currentCollection: collection }); expect(screen.getByTestId('metadata-based-item-list')).toBeInTheDocument(); }); test('renders ItemList when viewMode is VIEW_MODE_LIST', () => { - const collection = { + const collection: Collection = { boxItem: {}, id: '0', - items: [{ id: 1, name: 'Item 1', size: 1000, modified_at: '2023-10-10T10:00:00Z', type: 'file' }], + items: [{ id: '1', name: 'Item 1', size: 1000, modified_at: '2023-10-10T10:00:00Z', type: 'file' }], name: 'name', percentLoaded: 100, }; @@ -81,8 +79,13 @@ describe('Content Component', () => { }); test('renders ItemGrid when viewMode is VIEW_MODE_GRID', () => { - const item1 = { id: 1, name: 'Item 1', size: 1000, modified_at: '2023-10-10T10:00:00Z', type: 'file' }; - const collection = { boxItem: {}, id: '0', items: [item1], name: 'name', percentLoaded: 100 }; + const collection: Collection = { + boxItem: {}, + id: '0', + items: [{ id: '1', name: 'Item 1', size: 1000, modified_at: '2023-10-10T10:00:00Z', type: 'file' }], + name: 'name', + percentLoaded: 100, + }; render(); expect(screen.getByText('Item 1')).toBeInTheDocument(); @@ -94,11 +97,11 @@ describe('Content Component', () => { const features = { contentExplorer: { metadataViewV2: true }, }; - const collection = { + const collection: Collection = { percentLoaded: 100, boxItem: {}, id: '0', - items: [{ id: 1 }], + items: [{ id: '1' }], name: 'name', }; diff --git a/src/elements/content-explorer/__tests__/ContentExplorer.test.tsx b/src/elements/content-explorer/__tests__/ContentExplorer.test.tsx index 7973b7ab20..004b34d1bc 100644 --- a/src/elements/content-explorer/__tests__/ContentExplorer.test.tsx +++ b/src/elements/content-explorer/__tests__/ContentExplorer.test.tsx @@ -393,7 +393,7 @@ describe('elements/content-explorer/ContentExplorer', () => { const metadataSourceFieldName = `metadata.${metadataSource}`; const metadataQuery = { from: metadataSource, - ancestor_folder_id: 0, + ancestor_folder_id: '0', fields: [`${metadataSourceFieldName}.industry`, `${metadataSourceFieldName}.last_contacted_at`], }; const fieldsToShow = [ diff --git a/src/elements/content-explorer/__tests__/DeleteConfirmationDialog.test.tsx b/src/elements/content-explorer/__tests__/DeleteConfirmationDialog.test.tsx index 3f9e235112..a443e874e9 100644 --- a/src/elements/content-explorer/__tests__/DeleteConfirmationDialog.test.tsx +++ b/src/elements/content-explorer/__tests__/DeleteConfirmationDialog.test.tsx @@ -4,18 +4,18 @@ import { render, screen } from '../../../test-utils/testing-library'; import DeleteConfirmationDialog, { DeleteConfirmationDialogProps } from '../DeleteConfirmationDialog'; import { TYPE_FOLDER } from '../../../constants'; +import type { BoxItem } from '../../../common/types/core'; -const mockItem = { type: 'pdf', name: 'Test File' }; -const mockFolderItem = { type: TYPE_FOLDER, name: 'Test Folder' }; +const mockItem: BoxItem = { id: '1', name: 'Test File' }; +const mockFolderItem: BoxItem = { id: '2', type: TYPE_FOLDER, name: 'Test Folder' }; jest.mock('react-modal', () => { return jest.fn(({ children }) =>

{children}
); }); describe('elements/content-explorer/DeleteConfirmationDialog', () => { - const defaultProps = { + const defaultProps: DeleteConfirmationDialogProps = { appElement: document.body, - errorCode: '', isLoading: false, isOpen: false, item: mockItem, diff --git a/src/elements/content-explorer/__tests__/MetadataSidePanel.test.tsx b/src/elements/content-explorer/__tests__/MetadataSidePanel.test.tsx index 322a173a67..78dfdc083e 100644 --- a/src/elements/content-explorer/__tests__/MetadataSidePanel.test.tsx +++ b/src/elements/content-explorer/__tests__/MetadataSidePanel.test.tsx @@ -2,6 +2,7 @@ import * as React from 'react'; import userEvent from '@testing-library/user-event'; import { Notification } from '@box/blueprint-web'; import { render, screen, waitFor } from '../../../test-utils/testing-library'; +import type { BoxItem, Collection } from '../../../common/types/core'; import MetadataSidePanel, { type MetadataSidePanelProps } from '../MetadataSidePanel'; // Mock scrollTo method @@ -10,7 +11,14 @@ Object.defineProperty(Element.prototype, 'scrollTo', { writable: true, }); -const mockCollection = { +type MetadataQueryItem = Omit & { + metadata: Record>>; +}; +type MetadataQueryCollection = Omit & { items: MetadataQueryItem[] }; + +const toCollection = (collection: MetadataQueryCollection): Collection => collection as unknown as Collection; + +const mockCollection: MetadataQueryCollection = { items: [ { id: '1', @@ -87,7 +95,7 @@ const TestWrapper = ({ describe('elements/content-explorer/MetadataSidePanel', () => { const defaultProps: Omit = { - currentCollection: mockCollection, + currentCollection: toCollection(mockCollection), metadataTemplate: mockMetadataTemplate, onClose: mockOnClose, onUpdate: jest.fn(), @@ -282,7 +290,7 @@ describe('elements/content-explorer/MetadataSidePanel', () => { }; renderComponent({ - currentCollection: collectionWithDifferentValues, + currentCollection: toCollection(collectionWithDifferentValues), selectedItemIds: new Set(['1', '2']), }); diff --git a/src/elements/content-explorer/__tests__/MetadataViewContainer.test.tsx b/src/elements/content-explorer/__tests__/MetadataViewContainer.test.tsx index dd3c4c1b53..c4dca41aea 100644 --- a/src/elements/content-explorer/__tests__/MetadataViewContainer.test.tsx +++ b/src/elements/content-explorer/__tests__/MetadataViewContainer.test.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; import type { FloatType } from '@box/metadata-filter'; -import type { Collection } from '../../../common/types/core'; +import type { BoxItem, Collection } from '../../../common/types/core'; import type { MetadataTemplate, MetadataTemplateField } from '../../../common/types/metadata'; import { render, screen, userEvent, waitFor, within } from '../../../test-utils/testing-library'; import MetadataViewContainer, { @@ -16,7 +16,7 @@ Object.defineProperty(Element.prototype, 'scrollTo', { }); describe('elements/content-explorer/MetadataViewContainer', () => { - const mockItems = [ + const mockItems: Array = [ { id: '1', name: 'File 1.txt', diff --git a/src/elements/content-explorer/__tests__/RenameDialog.test.tsx b/src/elements/content-explorer/__tests__/RenameDialog.test.tsx index aa397dd4d5..b0741a447c 100644 --- a/src/elements/content-explorer/__tests__/RenameDialog.test.tsx +++ b/src/elements/content-explorer/__tests__/RenameDialog.test.tsx @@ -2,26 +2,27 @@ import * as React from 'react'; import userEvent from '@testing-library/user-event'; import { render, screen } from '../../../test-utils/testing-library'; -import RenameDialog from '../RenameDialog'; +import RenameDialog, { type RenameDialogProps } from '../RenameDialog'; import { ERROR_CODE_ITEM_NAME_TOO_LONG, ERROR_CODE_ITEM_NAME_IN_USE } from '../../../constants'; jest.mock('react-modal', () => { return jest.fn(({ children }) =>
{children}
); }); -const defaultProps = { +const defaultProps: RenameDialogProps = { appElement: document.createElement('div'), errorCode: '', isLoading: false, isOpen: true, - item: { name: 'test.txt', extension: 'txt' }, + item: { id: '1', name: 'test.txt', extension: 'txt' }, onCancel: jest.fn(), onRename: jest.fn(), parentElement: document.createElement('div'), }; describe('elements/content-explorer/RenameDialog', () => { - const renderComponent = (props = {}) => render(); + const renderComponent = (props: Partial = {}) => + render(); test('renders the dialog with the correct initial state', () => { renderComponent(); diff --git a/src/elements/content-explorer/__tests__/ShareDialog.test.tsx b/src/elements/content-explorer/__tests__/ShareDialog.test.tsx index 533aeb1ee1..5297e8bb5e 100644 --- a/src/elements/content-explorer/__tests__/ShareDialog.test.tsx +++ b/src/elements/content-explorer/__tests__/ShareDialog.test.tsx @@ -2,18 +2,19 @@ import * as React from 'react'; import userEvent from '@testing-library/user-event'; import { render, screen } from '../../../test-utils/testing-library'; import ShareDialog, { ShareDialogProps } from '../ShareDialog'; +import type { BoxItem } from '../../../common/types/core'; jest.mock('react-modal', () => { return jest.fn(({ children }) =>
{children}
); }); describe('elements/content-explorer/ShareDialog', () => { - const defaultProps = { + const defaultProps: ShareDialogProps = { appElement: document.createElement('div'), canSetShareAccess: true, isLoading: false, isOpen: true, - item: { shared_link: { url: 'http://example.com' } }, + item: { id: '1', shared_link: { access: 'open', url: 'http://example.com' } }, onCancel: jest.fn(), onShareAccessChange: jest.fn(), parentElement: document.createElement('div'), @@ -47,7 +48,8 @@ describe('elements/content-explorer/ShareDialog', () => { }); test('renders with empty input when item has no shared link', async () => { - renderComponent({ item: { shared_link: null } }); + const itemWithoutSharedLink = { id: '2', shared_link: null } as unknown as BoxItem; + renderComponent({ item: itemWithoutSharedLink }); expect(await screen.findByRole('textbox')).toHaveValue('None'); }); }); diff --git a/src/elements/content-sharing/ContentSharingV2.tsx b/src/elements/content-sharing/ContentSharingV2.tsx index 7957352405..1efe3a2267 100644 --- a/src/elements/content-sharing/ContentSharingV2.tsx +++ b/src/elements/content-sharing/ContentSharingV2.tsx @@ -128,7 +128,9 @@ function ContentSharingV2({ let errorMessage; if (error.status) { errorMessage = messages[CONTENT_SHARING_ERRORS[error.status]]; + // @ts-expect-error -- legacy ElementsXhrError responses may expose a nested response status } else if (error.response && error.response.status) { + // @ts-expect-error -- legacy ElementsXhrError responses may expose a nested response status errorMessage = messages[CONTENT_SHARING_ERRORS[error.response.status]]; } else { errorMessage = messages.loadingError; diff --git a/src/elements/content-sharing/utils/__tests__/convertCollaborators.test.ts b/src/elements/content-sharing/utils/__tests__/convertCollaborators.test.ts index 53808a26a2..c11af5eea4 100644 --- a/src/elements/content-sharing/utils/__tests__/convertCollaborators.test.ts +++ b/src/elements/content-sharing/utils/__tests__/convertCollaborators.test.ts @@ -11,7 +11,22 @@ import { mockOwnerName, } from '../__mocks__/ContentSharingV2Mocks'; -import type { Collaborations } from '../../../../common/types/core'; +import type { Collaboration, Collaborations } from '../../../../common/types/core'; + +type CollaborationFixture = Omit & { + accessible_by: Partial | null; + created_by?: { email: string; id: string; name: string }; + expires_at?: Collaboration['expires_at']; +}; + +type CollaborationsFixture = Omit & { + entries: CollaborationFixture[]; +}; + +const toCollaboration = (collab: CollaborationFixture | null | undefined): Collaboration => + collab as unknown as Collaboration; +const toCollaborations = (collaborations: CollaborationsFixture): Collaborations => + collaborations as unknown as Collaborations; const ownerFromApi = { id: mockOwnerId, @@ -23,7 +38,7 @@ const mockCurrentUser = { email: mockOwnerEmail, emailDomain: mockOwnerEmailDomain, }; -const itemOwner = { +const itemOwner: CollaborationFixture = { id: mockOwnerEmail, status: STATUS_ACCEPTED, role: 'owner', @@ -31,10 +46,11 @@ const itemOwner = { id: mockOwnerId, login: mockOwnerEmail, name: mockOwnerName, + type: 'user', }, }; -const mockCollaborationsFromApi: Collaborations = { +const mockCollaborationsFromApi: CollaborationsFixture = { entries: [ { id: '123', @@ -89,16 +105,17 @@ const mockCollaborationsFromApi: Collaborations = { invite_email: 'rrobot@external.example.com', }, ], + next_marker: null, }; -const mockCollaborations = [itemOwner, ...mockCollaborationsFromApi.entries]; +const mockCollaborations: CollaborationFixture[] = [itemOwner, ...mockCollaborationsFromApi.entries]; describe('convertCollaborators', () => { describe('convertCollab', () => { test('should convert a valid collaboration to Collaborator format', () => { const result = convertCollab({ avatarUrlMap: mockAvatarUrlMap, - collab: mockCollaborations[1], + collab: toCollaboration(mockCollaborations[1]), currentUser: mockCurrentUser, ownerEmailDomain: mockOwnerEmailDomain, }); @@ -121,7 +138,7 @@ describe('convertCollaborators', () => { test('should convert pending collaboration with invite_email to a pending collaborator', () => { const result = convertCollab({ avatarUrlMap: mockAvatarUrlMap, - collab: mockCollaborations[5], + collab: toCollaboration(mockCollaborations[5]), currentUser: mockCurrentUser, ownerEmailDomain: mockOwnerEmailDomain, }); @@ -143,7 +160,7 @@ describe('convertCollaborators', () => { test('should convert pending collaboration with accessible_by', () => { const result = convertCollab({ avatarUrlMap: mockAvatarUrlMap, - collab: mockCollaborations[3], + collab: toCollaboration(mockCollaborations[3]), currentUser: mockCurrentUser, ownerEmailDomain: mockOwnerEmailDomain, }); @@ -166,7 +183,7 @@ describe('convertCollaborators', () => { test.each([undefined, null])('should return null for %s collaboration', collab => { const result = convertCollab({ avatarUrlMap: mockAvatarUrlMap, - collab, + collab: toCollaboration(collab), currentUser: mockCurrentUser, ownerEmailDomain: mockOwnerEmailDomain, }); @@ -177,7 +194,7 @@ describe('convertCollaborators', () => { test('should return null for pending collab with accessible_by without collab name', () => { const result = convertCollab({ avatarUrlMap: mockAvatarUrlMap, - collab: mockCollaborations[4], + collab: toCollaboration(mockCollaborations[4]), currentUser: mockCurrentUser, ownerEmailDomain: mockOwnerEmailDomain, }); @@ -188,7 +205,7 @@ describe('convertCollaborators', () => { test('should return null for rejected collab', () => { const result = convertCollab({ avatarUrlMap: mockAvatarUrlMap, - collab: mockCollaborations[6], + collab: toCollaboration(mockCollaborations[6]), currentUser: mockCurrentUser, ownerEmailDomain: mockOwnerEmailDomain, }); @@ -199,7 +216,7 @@ describe('convertCollaborators', () => { test('should identify current user correctly', () => { const result = convertCollab({ avatarUrlMap: mockAvatarUrlMap, - collab: mockCollaborations[0], + collab: toCollaboration(mockCollaborations[0]), currentUser: mockCurrentUser, ownerEmailDomain: mockOwnerEmailDomain, }); @@ -221,7 +238,7 @@ describe('convertCollaborators', () => { test('should identify external user correctly', () => { const result = convertCollab({ avatarUrlMap: mockAvatarUrlMap, - collab: mockCollaborations[2], + collab: toCollaboration(mockCollaborations[2]), currentUser: mockCurrentUser, ownerEmailDomain: mockOwnerEmailDomain, }); @@ -233,7 +250,7 @@ describe('convertCollaborators', () => { const currentUserWithoutDomain = { id: mockOwnerId, email: mockOwnerEmail }; const result = convertCollab({ avatarUrlMap: mockAvatarUrlMap, - collab: mockCollaborations[2], + collab: toCollaboration(mockCollaborations[2]), currentUser: currentUserWithoutDomain, ownerEmailDomain: mockOwnerEmailDomain, }); @@ -246,7 +263,7 @@ describe('convertCollaborators', () => { avatarUrlMap => { const result = convertCollab({ avatarUrlMap, - collab: mockCollaborations[1], + collab: toCollaboration(mockCollaborations[1]), currentUser: mockCurrentUser, ownerEmailDomain: mockOwnerEmailDomain, }); @@ -264,7 +281,7 @@ describe('convertCollaborators', () => { const result = convertCollab({ avatarUrlMap: mockAvatarUrlMap, - collab: collabWithoutExpiration, + collab: toCollaboration(collabWithoutExpiration), currentUser: mockCurrentUser, ownerEmailDomain: mockOwnerEmailDomain, }); @@ -276,7 +293,7 @@ describe('convertCollaborators', () => { describe('convertCollabsResponse', () => { test('should convert valid collaborations data to Collaborator array', () => { const result = convertCollabsResponse( - mockCollaborationsFromApi, + toCollaborations(mockCollaborationsFromApi), mockCurrentUser, ownerFromApi, mockAvatarUrlMap, @@ -352,14 +369,24 @@ describe('convertCollaborators', () => { }); test('should return empty array for empty entries', () => { - const emptyCollaborations: Collaborations = { entries: [] }; - const result = convertCollabsResponse(emptyCollaborations, mockCurrentUser, ownerFromApi, mockAvatarUrlMap); + const emptyCollaborations: CollaborationsFixture = { entries: [], next_marker: null }; + const result = convertCollabsResponse( + toCollaborations(emptyCollaborations), + mockCurrentUser, + ownerFromApi, + mockAvatarUrlMap, + ); expect(result).toEqual([]); }); test('should handle null avatar URL map', () => { - const collabs = convertCollabsResponse(mockCollaborationsFromApi, mockCurrentUser, ownerFromApi, null); + const collabs = convertCollabsResponse( + toCollaborations(mockCollaborationsFromApi), + mockCurrentUser, + ownerFromApi, + null, + ); collabs.map(collab => { expect(collab.avatarUrl).toBeUndefined(); diff --git a/src/elements/content-sharing/utils/convertCollaborators.ts b/src/elements/content-sharing/utils/convertCollaborators.ts index 8e061dc8c0..99fc98cc94 100644 --- a/src/elements/content-sharing/utils/convertCollaborators.ts +++ b/src/elements/content-sharing/utils/convertCollaborators.ts @@ -94,6 +94,7 @@ export const convertCollabsResponse = ( }; return [itemOwner, ...entries].flatMap(collab => { + // @ts-expect-error -- synthetic owner collaborations omit the API-only expires_at field const converted = convertCollab({ avatarUrlMap, collab, currentUser, ownerEmailDomain }); return converted ? [converted] : []; }); diff --git a/src/elements/content-sidebar/__tests__/DocGenSidebar.test.tsx b/src/elements/content-sidebar/__tests__/DocGenSidebar.test.tsx index 2ca1a22e21..8cf33a4401 100644 --- a/src/elements/content-sidebar/__tests__/DocGenSidebar.test.tsx +++ b/src/elements/content-sidebar/__tests__/DocGenSidebar.test.tsx @@ -6,12 +6,15 @@ import type { DocGenTag } from '../DocGenSidebar/types'; import mockData, { mockPdfTemplateData } from '../__mocks__/DocGenSidebar.mock'; const docGenSidebarProps = { + checkDocGenTemplate: undefined, + enabled: true, getDocGenTags: jest.fn().mockReturnValue( Promise.resolve({ pagination: {}, data: mockData, }), ), + isDocgenTemplate: true, }; const processAndResolveMock = jest @@ -47,7 +50,14 @@ describe('elements/content-sidebar/DocGenSidebar', () => { }); const renderComponent = (props = {}) => - render(); + render( + , + ); test('componentDidMount() should call fetch tags', async () => { renderComponent(); @@ -100,7 +110,7 @@ describe('elements/content-sidebar/DocGenSidebar', () => { expect(await screen.findByText('pathFromUnknown')).toBeInTheDocument(); }); - test('should render DocGen sidebar component correctly with tags list', async () => { + test('should reveal nested tags when a parent tag is clicked', async () => { renderComponent(); const parentTag = await screen.findByText('about'); let nestedTag = await screen.queryByText('name'); diff --git a/src/elements/content-sidebar/__tests__/MetadataSidebarRedesign-taxonomyPicker.test.tsx b/src/elements/content-sidebar/__tests__/MetadataSidebarRedesign-taxonomyPicker.test.tsx index 90ae169c38..8ea676a6c1 100644 --- a/src/elements/content-sidebar/__tests__/MetadataSidebarRedesign-taxonomyPicker.test.tsx +++ b/src/elements/content-sidebar/__tests__/MetadataSidebarRedesign-taxonomyPicker.test.tsx @@ -84,6 +84,7 @@ const renderSidebar = (features: Record = {}) => { filteredTemplateIds: [], getPreview: jest.fn().mockReturnValue({}), isFeatureEnabled: true, + logger: { onPreviewMetric: jest.fn(), onReadyMetric: jest.fn() }, onError: jest.fn(), onSuccess: jest.fn(), ...routerProps, diff --git a/src/elements/content-sidebar/__tests__/MetadataSidebarRedesign.test.tsx b/src/elements/content-sidebar/__tests__/MetadataSidebarRedesign.test.tsx index 2e8a9b7b1f..47af39712b 100644 --- a/src/elements/content-sidebar/__tests__/MetadataSidebarRedesign.test.tsx +++ b/src/elements/content-sidebar/__tests__/MetadataSidebarRedesign.test.tsx @@ -125,6 +125,7 @@ describe('elements/content-sidebar/Metadata/MetadataSidebarRedesign', () => { elementId: 'element-1', filteredTemplateIds: [], isFeatureEnabled: true, + logger: { onPreviewMetric: jest.fn(), onReadyMetric: jest.fn() }, onError: jest.fn(), onSuccess: jest.fn(), ...routeComponentProps, diff --git a/src/elements/content-sidebar/__tests__/convertTemplateToTemplateInstance.test.tsx b/src/elements/content-sidebar/__tests__/convertTemplateToTemplateInstance.test.tsx index 5916f02783..e57fe11fd4 100644 --- a/src/elements/content-sidebar/__tests__/convertTemplateToTemplateInstance.test.tsx +++ b/src/elements/content-sidebar/__tests__/convertTemplateToTemplateInstance.test.tsx @@ -5,6 +5,7 @@ import { type BoxItem } from '../../../common/types/core'; describe('convertTemplateToTemplateInstance', () => { it('should correctly convert template to template instance', () => { const mockFile: BoxItem = { + id: '123', permissions: { can_upload: true, }, diff --git a/src/elements/content-sidebar/__tests__/isFileLargerThan.test.ts b/src/elements/content-sidebar/__tests__/isFileLargerThan.test.ts index fc911b74f7..18c363d20b 100644 --- a/src/elements/content-sidebar/__tests__/isFileLargerThan.test.ts +++ b/src/elements/content-sidebar/__tests__/isFileLargerThan.test.ts @@ -2,10 +2,10 @@ import { isFileLargerThan } from '../utils/isFileLargerThan'; describe('isFileLargerThan', () => { test.each([ - [{ size: 49 }, 50, false], - [{ size: 50 }, 50, false], - [{ size: 51 }, 50, true], - [{}, 50, false], + [{ id: 'file-49', size: 49 }, 50, false], + [{ id: 'file-50', size: 50 }, 50, false], + [{ id: 'file-51', size: 51 }, 50, true], + [{ id: 'file-without-size' }, 50, false], [null, 50, false], ])('Should correctly check if file is larger than benchmark', (file, breakpoint, expected) => { const result = isFileLargerThan(file, breakpoint); diff --git a/src/elements/content-sidebar/activity-feed-v2/__tests__/ActivityFeedV2.test.tsx b/src/elements/content-sidebar/activity-feed-v2/__tests__/ActivityFeedV2.test.tsx index fd9c80160b..b3b840c5fb 100644 --- a/src/elements/content-sidebar/activity-feed-v2/__tests__/ActivityFeedV2.test.tsx +++ b/src/elements/content-sidebar/activity-feed-v2/__tests__/ActivityFeedV2.test.tsx @@ -7,6 +7,8 @@ import ActivityFeedV2 from '..'; import type { ActivityFeedV2Props } from '../ActivityFeedV2'; import type { TaskModalV2Props } from '../task-modal-v2'; import type { CreateTaskCallback } from '../task-modal-v2/types'; +import type { Comment } from '../../../../common/types/feed'; +import type { TaskNew } from '../../../../common/types/tasks'; type EditorProps = React.ComponentProps; @@ -121,7 +123,7 @@ const mockFileWithCommentPermission: NonNullable = permissions: { can_comment: true }, }; -const mockComment = { +const mockComment: Comment = { created_at: '2024-01-01T00:00:00Z', created_by: { id: '2', name: 'Commenter', type: 'user' }, id: 'comment-1', @@ -145,7 +147,7 @@ const mockAnnotation = { type: 'annotation', }; -const mockTask = { +const mockTask: TaskNew = { assigned_to: { entries: [], limit: 20, next_marker: null }, completion_rule: 'ALL_ASSIGNEES', created_at: '2024-03-01T00:00:00Z', @@ -153,7 +155,7 @@ const mockTask = { id: 'tc', role: 'CREATOR', status: 'NOT_STARTED', - target: { id: 'user-1', name: 'Current User' }, + target: { id: 'user-1', name: 'Current User', type: 'user' }, type: 'task_collaborator', }, description: 'Review document', @@ -295,7 +297,10 @@ describe('elements/content-sidebar/activity-feed-v2/ActivityFeedV2', () => { test('should render task feed items', () => { render( - , + , ); expect(screen.getByTestId('task-task-1')).toBeVisible(); @@ -349,7 +354,7 @@ describe('elements/content-sidebar/activity-feed-v2/ActivityFeedV2', () => { render( , ); @@ -403,7 +408,7 @@ describe('elements/content-sidebar/activity-feed-v2/ActivityFeedV2', () => { render( , @@ -431,7 +436,7 @@ describe('elements/content-sidebar/activity-feed-v2/ActivityFeedV2', () => { render( , @@ -459,7 +464,7 @@ describe('elements/content-sidebar/activity-feed-v2/ActivityFeedV2', () => { render( , ); @@ -474,7 +479,7 @@ describe('elements/content-sidebar/activity-feed-v2/ActivityFeedV2', () => { render( , ); diff --git a/src/elements/content-sidebar/fetchers/metadataTaxonomyFetcher.ts b/src/elements/content-sidebar/fetchers/metadataTaxonomyFetcher.ts index ba2d2001b8..bfd35b56b8 100644 --- a/src/elements/content-sidebar/fetchers/metadataTaxonomyFetcher.ts +++ b/src/elements/content-sidebar/fetchers/metadataTaxonomyFetcher.ts @@ -10,6 +10,7 @@ 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'; +// @ts-expect-error -- MetadataOptionEntry exists locally but is not exported from the migrated metadata types import type { MetadataOptionEntry } from '../../../common/types/metadata'; export const metadataTaxonomyFetcher = async ( diff --git a/src/elements/content-sidebar/hooks/useSidebarMetadataFetcher.ts b/src/elements/content-sidebar/hooks/useSidebarMetadataFetcher.ts index bdc64951ef..c49d2da44a 100644 --- a/src/elements/content-sidebar/hooks/useSidebarMetadataFetcher.ts +++ b/src/elements/content-sidebar/hooks/useSidebarMetadataFetcher.ts @@ -86,6 +86,7 @@ function useSidebarMetadataFetcher( const isValidError = isUserCorrectableError(errorStatus); setStatus(STATUS.ERROR); setErrorMessage(message); + // @ts-expect-error -- ErrorContext accepts runtime ElementsXhrError values that need not extend Error onError(error, code, { error, isErrorDisplayed: isValidError, diff --git a/src/elements/content-sidebar/stories/__mocks__/TaskModalV2Mocks.tsx b/src/elements/content-sidebar/stories/__mocks__/TaskModalV2Mocks.tsx index 16869c6c5b..c4684c7379 100644 --- a/src/elements/content-sidebar/stories/__mocks__/TaskModalV2Mocks.tsx +++ b/src/elements/content-sidebar/stories/__mocks__/TaskModalV2Mocks.tsx @@ -50,7 +50,11 @@ export const mockEditingAssignees: TaskAssignee[] = [ ]; export const mockEditingTask: TaskNew = { - assigned_to: { entries: mockEditingAssignees, limit: 25, next_marker: '' }, + assigned_to: { + entries: mockEditingAssignees as unknown as TaskNew['assigned_to']['entries'], + limit: 25, + next_marker: '', + }, completion_rule: TASK_COMPLETION_RULE_ALL, created_at: '2026-06-30T12:00:00Z', created_by: { diff --git a/src/elements/content-uploader/ContentUploader.tsx b/src/elements/content-uploader/ContentUploader.tsx index 90ae3ccbca..8f9d3230a9 100644 --- a/src/elements/content-uploader/ContentUploader.tsx +++ b/src/elements/content-uploader/ContentUploader.tsx @@ -586,6 +586,7 @@ class ContentUploader extends Component { }; }); + // @ts-expect-error -- folder queue entries intentionally have no File object this.addToQueue(folderUploads, itemUpdateCallback); }; @@ -639,6 +640,7 @@ class ContentUploader extends Component { this.addToQueue( [ // $FlowFixMe no file property + // @ts-expect-error -- folder queue entries intentionally have no File object { api: folderUpload, extension: '', @@ -1163,6 +1165,7 @@ class ContentUploader extends Component { if (!useUploadsManager) { if (shouldFireOnComplete) { + // @ts-expect-error -- completion callback historically receives uploaded Box files here onComplete(cloneDeep(filesToBeUploaded.map(item => item.boxFile))); } // Reset item collection after successful upload @@ -1177,6 +1180,7 @@ class ContentUploader extends Component { if (!useUploadsManager) { if (shouldFireOnComplete) { + // @ts-expect-error -- completion callback historically receives uploaded Box files here onComplete(cloneDeep(items.map(item => item.boxFile))); } // Reset item collection after successful upload @@ -1957,6 +1961,7 @@ class ContentUploader extends Component { isResumableUploadsEnabled={isResumableUploadsEnabled} isVisible={isVisible} items={uploadsManagerItems} + // @ts-expect-error -- legacy action callback receives an upload item instead of a button event onItemActionClick={this.onClick} onRemoveActionClick={this.removeFileFromUploadQueue} onUpgradeCTAClick={onUpgradeCTAClick} diff --git a/src/elements/content-uploader/DroppableContent.tsx b/src/elements/content-uploader/DroppableContent.tsx index de5c4a7cda..78ad95ab9b 100644 --- a/src/elements/content-uploader/DroppableContent.tsx +++ b/src/elements/content-uploader/DroppableContent.tsx @@ -57,7 +57,11 @@ const DroppableContentComponent = React.forwardRef - + { return (
- +
); diff --git a/src/elements/content-uploader/__tests__/CellRenderer.test.tsx b/src/elements/content-uploader/__tests__/CellRenderer.test.tsx index 632a42cbd2..5119b43d21 100644 --- a/src/elements/content-uploader/__tests__/CellRenderer.test.tsx +++ b/src/elements/content-uploader/__tests__/CellRenderer.test.tsx @@ -4,6 +4,7 @@ import actionCellRenderer from '../actionCellRenderer'; import progressCellRenderer from '../progressCellRenderer'; import removeCellRenderer from '../removeCellRenderer'; +import PlainUpload from '../../../api/uploads/PlainUpload'; import Browser from '../../../utils/Browser'; import { STATUS_COMPLETE, @@ -23,6 +24,22 @@ import { import type { UploadItem } from '../../../common/types/upload'; +const uploadApi = new PlainUpload({ token: 'token' }); +const createUploadItem = (overrides: Partial = {}): UploadItem => { + const file = new File(['test content'], overrides.name || 'test.txt'); + + return { + api: uploadApi, + extension: 'txt', + file, + name: file.name, + progress: 0, + size: file.size, + status: STATUS_STAGED, + ...overrides, + }; +}; + describe('elements/content-uploader/CellRenderer', () => { describe('actionCellRenderer', () => { const renderComponent = (rowData: UploadItem, onClick: jest.Mock) => { @@ -31,7 +48,7 @@ describe('elements/content-uploader/CellRenderer', () => { }; test('calls onClick with rowData when ItemAction is clicked', () => { - const rowData = { id: '3', status: STATUS_ERROR, isFolder: false }; + const rowData = createUploadItem({ status: STATUS_ERROR, isFolder: false }); const onClick = jest.fn(); renderComponent(rowData, onClick); fireEvent.click(screen.getByRole('button')); @@ -39,9 +56,9 @@ describe('elements/content-uploader/CellRenderer', () => { }); }); describe('progressCellRenderer', () => { - const renderComponent = (rowData: UploadItem, shouldShowUpgradeCTAMessage?: boolean) => { + const renderComponent = (overrides: Partial, shouldShowUpgradeCTAMessage?: boolean) => { const Component = progressCellRenderer(shouldShowUpgradeCTAMessage); - return render(); + return render(); }; test('renders ItemProgress for in-progress status', () => { @@ -125,7 +142,7 @@ describe('elements/content-uploader/CellRenderer', () => { const rowData = { status: STATUS_ERROR, error: { code: ERROR_CODE_UPLOAD_BAD_DIGEST }, - file: { name: 'file.zip' }, + file: new File(['zip content'], 'file.zip'), }; renderComponent(rowData); expect( @@ -140,7 +157,7 @@ describe('elements/content-uploader/CellRenderer', () => { }); test('returns null for folder with non-error status', () => { - const rowData = { status: STATUS_IN_PROGRESS, isFolder: true }; + const rowData = createUploadItem({ status: STATUS_IN_PROGRESS, isFolder: true }); const Component = progressCellRenderer(); const { container } = render(); expect(container.firstChild).toBeNull(); @@ -153,21 +170,21 @@ describe('elements/content-uploader/CellRenderer', () => { }; test('renders ItemRemove for non-folder item', () => { - const rowData = { isFolder: false }; + const rowData = createUploadItem({ isFolder: false }); const onClick = jest.fn(); renderComponent(rowData, onClick); expect(screen.getByRole('button', { name: 'Remove' })).toBeInTheDocument(); }); test('does not render ItemRemove for folder item', () => { - const rowData = { id: '2', status: STATUS_COMPLETE, isFolder: true }; + const rowData = createUploadItem({ status: STATUS_COMPLETE, isFolder: true }); const onClick = jest.fn(); const { container } = renderComponent(rowData, onClick); expect(container.firstChild).toBeNull(); }); test('calls onClick with rowData when ItemRemove is clicked', () => { - const rowData = { id: '3', status: STATUS_ERROR, isFolder: false }; + const rowData = createUploadItem({ status: STATUS_ERROR, isFolder: false }); const onClick = jest.fn(); renderComponent(rowData, onClick); fireEvent.click(screen.getByRole('button', { name: 'Remove' })); diff --git a/src/elements/content-uploader/__tests__/ItemList.test.tsx b/src/elements/content-uploader/__tests__/ItemList.test.tsx index 7bbac463ee..5d87d43ddd 100644 --- a/src/elements/content-uploader/__tests__/ItemList.test.tsx +++ b/src/elements/content-uploader/__tests__/ItemList.test.tsx @@ -2,6 +2,8 @@ import * as React from 'react'; import { render, screen } from '../../../test-utils/testing-library'; import ItemList, { ItemListProps } from '../ItemList'; import { STATUS_ERROR, STATUS_COMPLETE, ERROR_CODE_UPLOAD_FILE_SIZE_LIMIT_EXCEEDED } from '../../../constants'; +import PlainUpload from '../../../api/uploads/PlainUpload'; +import type { UploadItem } from '../../../common/types/upload'; jest.mock( '@box/react-virtualized/dist/es/AutoSizer', @@ -10,6 +12,22 @@ jest.mock( children({ height: 600, width: 600 }), ); +const uploadApi = new PlainUpload({ token: 'token' }); +const createUploadItem = (name: string, overrides: Partial = {}): UploadItem => { + const file = new File(['test content'], name); + + return { + api: uploadApi, + extension: '', + file, + name, + progress: 100, + size: file.size, + status: STATUS_COMPLETE, + ...overrides, + }; +}; + describe('elements/content-uploader/ItemList', () => { const renderComponent = (props?: Partial) => render(); @@ -20,11 +38,7 @@ describe('elements/content-uploader/ItemList', () => { }); test('should render component with correct number of items', () => { - const items = [ - { id: '1', name: 'item1', status: STATUS_COMPLETE }, - { id: '2', name: 'item2', status: STATUS_COMPLETE }, - { id: '3', name: 'item3', status: STATUS_COMPLETE }, - ]; + const items = [createUploadItem('item1'), createUploadItem('item2'), createUploadItem('item3')]; renderComponent({ items }); expect(screen.getAllByRole('row')).toHaveLength(3); @@ -36,7 +50,10 @@ describe('elements/content-uploader/ItemList', () => { test('should render action column with correct width for upgrade cta', () => { const items = [ - { id: '1', name: 'item1', status: STATUS_ERROR, code: ERROR_CODE_UPLOAD_FILE_SIZE_LIMIT_EXCEEDED }, + createUploadItem('item1', { + error: { code: ERROR_CODE_UPLOAD_FILE_SIZE_LIMIT_EXCEEDED }, + status: STATUS_ERROR, + }), ]; renderComponent({ items, onUpgradeCTAClick: jest.fn() }); @@ -48,7 +65,7 @@ describe('elements/content-uploader/ItemList', () => { }); test('should render component with resumable uploads enabled', () => { - const items = [{ id: '1', name: 'item1', status: STATUS_COMPLETE }]; + const items = [createUploadItem('item1')]; renderComponent({ items, isResumableUploadsEnabled: true }); expect(screen.getByRole('grid')).toBeInTheDocument(); expect(screen.getAllByRole('gridcell')).toHaveLength(4); diff --git a/src/elements/content-uploader/actionCellRenderer.tsx b/src/elements/content-uploader/actionCellRenderer.tsx index cdc8660a7a..430fa2df12 100644 --- a/src/elements/content-uploader/actionCellRenderer.tsx +++ b/src/elements/content-uploader/actionCellRenderer.tsx @@ -11,7 +11,10 @@ export default ( onClick(rowData)} + onClick={() => + // @ts-expect-error -- legacy callback receives the upload item instead of the button event + onClick(rowData) + } onUpgradeCTAClick={onUpgradeCTAClick} /> );