From f892feaaffed3cffac7bb24b272d8cefe088ef93 Mon Sep 17 00:00:00 2001 From: Matthew Yarmolinsky Date: Tue, 23 Sep 2025 11:56:02 -0400 Subject: [PATCH 1/2] Replace some usage of lodash with native JS alternatives Change-type: patch --- src/components/DownloadImageDialog/index.tsx | 13 ++++--- src/components/DownloadImageDialog/version.ts | 2 +- src/components/DropDownButton/index.tsx | 21 ++++++----- src/components/Form/Widgets/FileWidget.tsx | 2 +- src/components/RJST/DataTypes/object.tsx | 37 ++++++++++++++----- src/components/RJST/Filters/utils.ts | 5 +-- src/components/RJST/Lenses/index.tsx | 3 +- .../RJST/components/Table/useTagActions.tsx | 2 +- .../components/Widget/Formats/TxtWidget.tsx | 3 +- .../RJST/components/Widget/index.tsx | 4 +- src/components/RJST/models/helpers.ts | 3 +- src/components/RJST/utils.ts | 4 +- .../TagManagementDialog/AddTagForm.tsx | 13 +++---- src/components/TagManagementDialog/index.tsx | 3 +- .../tag-management-service.ts | 25 ++++++------- src/index.tsx | 1 + src/utils/arrays.ts | 7 ++++ 17 files changed, 84 insertions(+), 64 deletions(-) create mode 100644 src/utils/arrays.ts diff --git a/src/components/DownloadImageDialog/index.tsx b/src/components/DownloadImageDialog/index.tsx index c31ba9b7..a39e282d 100644 --- a/src/components/DownloadImageDialog/index.tsx +++ b/src/components/DownloadImageDialog/index.tsx @@ -24,10 +24,9 @@ import { DropDownButton } from '../DropDownButton'; import pickBy from 'lodash/pickBy'; import debounce from 'lodash/debounce'; -import isEmpty from 'lodash/isEmpty'; import type { DeviceType, Dictionary, OsVersionsByDeviceType } from './models'; import { OsTypesEnum } from './models'; -import uniq from 'lodash/uniq'; +import { uniq } from '../../utils/arrays'; import { enqueueSnackbar } from 'notistack'; import { DialogWithCloseButton } from '../DialogWithCloseButton'; import type { CalloutProps } from '../Callout'; @@ -68,9 +67,9 @@ const getUniqueOsTypes = ( deviceTypeSlug: string | undefined, ) => { if ( - isEmpty(osVersions) || + Object.keys(osVersions).length === 0 || !deviceTypeSlug || - isEmpty(osVersions[deviceTypeSlug]) + osVersions[deviceTypeSlug]?.length === 0 ) { return []; } @@ -237,7 +236,9 @@ export const DownloadImageDialog = ({ } : {}, ); - const [isFetching, setIsFetching] = useState(isEmpty(osVersions)); + const [isFetching, setIsFetching] = useState( + Object.keys(osVersions).length === 0, + ); const [downloadSize, setDownloadSize] = useState(null); const [isValidatingUrl, setIsValidatingUrl] = useState(false); @@ -488,7 +489,7 @@ export const DownloadImageDialog = ({ ) : ( <> - {isEmpty(osVersions) && ( + {Object.keys(osVersions).length === 0 && ( No OS versions available for download diff --git a/src/components/DownloadImageDialog/version.ts b/src/components/DownloadImageDialog/version.ts index 0238f2a2..c6c2f2e7 100644 --- a/src/components/DownloadImageDialog/version.ts +++ b/src/components/DownloadImageDialog/version.ts @@ -1,4 +1,4 @@ -import uniq from 'lodash/uniq'; +import { uniq } from '../../utils/arrays'; import partition from 'lodash/partition'; import type { Dictionary, OsVersion } from './models'; diff --git a/src/components/DropDownButton/index.tsx b/src/components/DropDownButton/index.tsx index 1442091a..de3a44c4 100644 --- a/src/components/DropDownButton/index.tsx +++ b/src/components/DropDownButton/index.tsx @@ -9,7 +9,6 @@ import { Button, ButtonGroup, MenuItem, Menu } from '@mui/material'; import { ButtonWithTracking } from '../ButtonWithTracking'; import { useAnalyticsContext } from '../../contexts/AnalyticsContext'; import groupBy from 'lodash/groupBy'; -import flatMap from 'lodash/flatMap'; import { Tooltip } from '../Tooltip'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { @@ -59,16 +58,18 @@ export const DropDownButton = ({ return items; } const grouped = groupBy(items, (item) => item[groupByProp]); - const keys = Object.keys(grouped); - const lastKey = keys[keys.length - 1]; + const entries = Object.entries(grouped); + const lastKey = entries.at(-1)?.[0]; - return flatMap(grouped, (value, key) => [ - ...value.map((v, index) => - key !== lastKey && index === value.length - 1 - ? { ...v, divider: true } - : v, - ), - ]).filter((item) => item); + return entries + .flatMap(([key, value]) => + value.map((v, index) => + key !== lastKey && index === value.length - 1 + ? { ...v, divider: true } + : v, + ), + ) + .filter(Boolean); }, [items, groupByProp]); const handleClick = ( diff --git a/src/components/Form/Widgets/FileWidget.tsx b/src/components/Form/Widgets/FileWidget.tsx index 50999aa3..fab7a278 100644 --- a/src/components/Form/Widgets/FileWidget.tsx +++ b/src/components/Form/Widgets/FileWidget.tsx @@ -29,7 +29,7 @@ import { faUpload, } from '@fortawesome/free-solid-svg-icons'; import { useRandomUUID } from '../../../hooks/useRandomUUID'; -import uniq from 'lodash/uniq'; +import { uniq } from '../../../utils/arrays'; import { token } from '../../../utils/token'; const restingStyle: SxProps = { diff --git a/src/components/RJST/DataTypes/object.tsx b/src/components/RJST/DataTypes/object.tsx index 8963ec0d..dbdbe191 100644 --- a/src/components/RJST/DataTypes/object.tsx +++ b/src/components/RJST/DataTypes/object.tsx @@ -1,5 +1,4 @@ import type { JSONSchema7 as JSONSchema } from 'json-schema'; -import find from 'lodash/find'; import type { CreateFilter, KeysOfUnion } from './utils'; import { getDataTypeSchema, regexEscape } from './utils'; import type { FormData } from '../components/Filters/SchemaSieve'; @@ -8,23 +7,43 @@ import { createModelFilter, } from '../components/Filters/SchemaSieve'; import { isJSONSchema, getRefSchema } from '../schemaOps'; -import findKey from 'lodash/findKey'; import pick from 'lodash/pick'; import mapValues from 'lodash/mapValues'; +const findValueByDescription = ( + obj: object | undefined, + description: string, +) => { + return Object.values(obj ?? {}).find( + (property) => + typeof property === 'object' && + 'description' in property && + property.description === description, + ) as JSONSchema | undefined; +}; + +const findKeyByDescription = (obj: object | undefined, description: string) => { + return Object.entries(obj ?? {}).find( + ([, value]) => + typeof value === 'object' && + 'description' in value && + value.description === description, + )?.[0]; +}; + const getKeyLabel = (schema: JSONSchema) => { - const s = find(schema.properties, { description: 'key' }) as JSONSchema; + const s = findValueByDescription(schema.properties, 'key'); return s?.title ?? 'key'; }; const getValueLabel = (schema: JSONSchema) => { - const s = find(schema.properties, { description: 'value' }) as JSONSchema; + const s = findValueByDescription(schema.properties, 'value'); return s?.title ?? 'value'; }; export const isKeyValueObj = (schema: JSONSchema) => - !!find(schema.properties, { description: 'key' }) || - !!find(schema.properties, { description: 'value' }); + !!findValueByDescription(schema.properties, 'key') || + !!findValueByDescription(schema.properties, 'value'); export const operators = (s: JSONSchema) => { return { @@ -94,7 +113,7 @@ const getValueForOperation = ( ? 'value' : null; const schemaProperty = schemaField - ? findKey(schema.properties, { description: schemaField }) + ? findKeyByDescription(schema.properties, schemaField) : null; // Return the appropriate value format based on the operation type @@ -191,8 +210,8 @@ export const createFilter: CreateFilter = ( // TODO: this case does not cover complex objects for FULL_TEXT_SLUG if (operator === FULL_TEXT_SLUG && schema.properties) { - const schemaKey = findKey(schema.properties, { description: 'key' }); - const schemaValue = findKey(schema.properties, { description: 'value' }); + const schemaKey = findKeyByDescription(schema.properties, 'key'); + const schemaValue = findKeyByDescription(schema.properties, 'value'); const properties = [schemaKey, schemaValue] .map((key) => key diff --git a/src/components/RJST/Filters/utils.ts b/src/components/RJST/Filters/utils.ts index 6e59cc45..36e596cd 100644 --- a/src/components/RJST/Filters/utils.ts +++ b/src/components/RJST/Filters/utils.ts @@ -10,7 +10,6 @@ import { parseDescription, parseDescriptionProperty, } from '../schemaOps'; -import isEmpty from 'lodash/isEmpty'; import get from 'lodash/get'; const X_FOREIGN_KEY_SCHEMA_SEPARATOR = '___ref_scheme_separator_'; @@ -69,12 +68,12 @@ export const removeFieldsWithNoFilter = (schema: JSONSchema): JSONSchema => { } const hasEmptyProperties = - newValue.properties && isEmpty(newValue.properties); + newValue.properties && Object.keys(newValue.properties).length === 0; const hasEmptyItemsProperties = isJSONSchema(newValue.items) && 'properties' in newValue.items && - isEmpty(newValue.items.properties); + Object.keys(newValue.items.properties ?? {}).length === 0; if (hasEmptyProperties || hasEmptyItemsProperties) { continue; diff --git a/src/components/RJST/Lenses/index.tsx b/src/components/RJST/Lenses/index.tsx index 08b5a662..e4736d05 100644 --- a/src/components/RJST/Lenses/index.tsx +++ b/src/components/RJST/Lenses/index.tsx @@ -1,6 +1,5 @@ import * as types from './types'; import type { IconProp } from '@fortawesome/fontawesome-svg-core'; -import uniq from 'lodash/uniq'; export interface LensTemplate { slug: string; @@ -32,7 +31,7 @@ export const getLenses = ( ); const slugs = concatenatedLenses.map((lens) => lens.slug); - if (slugs.length > uniq(slugs).length) { + if (slugs.length > new Set(slugs).size) { throw new Error('Lenses must have unique slugs'); } diff --git a/src/components/RJST/components/Table/useTagActions.tsx b/src/components/RJST/components/Table/useTagActions.tsx index 4fc951e9..c1a3c029 100644 --- a/src/components/RJST/components/Table/useTagActions.tsx +++ b/src/components/RJST/components/Table/useTagActions.tsx @@ -1,7 +1,7 @@ import { useMemo, useState } from 'react'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faPlus } from '@fortawesome/free-solid-svg-icons/faPlus'; -import uniq from 'lodash/uniq'; +import { uniq } from '../../../../utils/arrays'; import type { RJSTContext } from '../../schemaOps'; import { useQuery } from '@tanstack/react-query'; import { Stack } from '@mui/material'; diff --git a/src/components/RJST/components/Widget/Formats/TxtWidget.tsx b/src/components/RJST/components/Widget/Formats/TxtWidget.tsx index dcef5ae2..de4116e3 100644 --- a/src/components/RJST/components/Widget/Formats/TxtWidget.tsx +++ b/src/components/RJST/components/Widget/Formats/TxtWidget.tsx @@ -1,6 +1,5 @@ import get from 'lodash/get'; import invokeMap from 'lodash/invokeMap'; -import isArray from 'lodash/isArray'; import type { UiSchema, Value } from '../utils'; import { UiOption, JsonTypes, widgetFactory, formatTimestamp } from '../utils'; import { Truncate } from '../../../../Truncate'; @@ -45,7 +44,7 @@ const TxtWidget = widgetFactory( variant: UiOption.string, }, )(({ value, schema, uiSchema }) => { - let displayValue = isArray(value) + let displayValue = Array.isArray(value) ? getArrayValue(value as Array>, uiSchema) : value?.toString(); if (DATE_TIME_FORMATS.includes(schema?.format ?? '')) { diff --git a/src/components/RJST/components/Widget/index.tsx b/src/components/RJST/components/Widget/index.tsx index db7fa269..3ba80af7 100644 --- a/src/components/RJST/components/Widget/index.tsx +++ b/src/components/RJST/components/Widget/index.tsx @@ -1,4 +1,4 @@ -import castArray from 'lodash/castArray'; +import { toArray } from '../../../../utils/arrays'; import type { Format, UiSchema, Value, WidgetProps } from './utils'; import { JsonTypes } from './utils'; import { typeWidgets } from './Formats'; @@ -56,7 +56,7 @@ export const Widget = ({ const processedValue = getValue(value, schema, uiSchema); const subSchema = getSubSchemaFromRefScheme(schema); - const types = subSchema?.type != null ? castArray(subSchema.type) : []; + const types = subSchema?.type != null ? toArray(subSchema.type) : []; if (processedValue == null && !types.includes(JsonTypes.null)) { return null; diff --git a/src/components/RJST/models/helpers.ts b/src/components/RJST/models/helpers.ts index 93a554a2..97d52761 100644 --- a/src/components/RJST/models/helpers.ts +++ b/src/components/RJST/models/helpers.ts @@ -1,4 +1,3 @@ -import isEmpty from 'lodash/isEmpty'; import type { Permissions, RJSTModel, RJSTRawModel } from '../schemaOps'; import { rjstJsonSchemaPick } from '../schemaOps'; @@ -33,7 +32,7 @@ export const rjstRunTransformers = < return data; } - if (!transformers || isEmpty(transformers)) { + if (!transformers || Object.keys(transformers).length === 0) { return data as TResult; } diff --git a/src/components/RJST/utils.ts b/src/components/RJST/utils.ts index 9ea45716..cd9b9f93 100644 --- a/src/components/RJST/utils.ts +++ b/src/components/RJST/utils.ts @@ -1,6 +1,6 @@ import type { RJSTBaseResource, Permissions, RJSTTagsSdk } from './schemaOps'; import { getPropertyScheme } from './schemaOps'; -import castArray from 'lodash/castArray'; +import { toArray } from '../../utils/arrays'; import get from 'lodash/get'; import type { JSONSchema7 as JSONSchema } from 'json-schema'; import type { CheckedState } from './components/Table/utils'; @@ -163,7 +163,7 @@ export const getSortingFunction = ( schemaKey: keyof T, schemaValue: JSONSchema, ) => { - const types = castArray(schemaValue.type); + const types = toArray(schemaValue.type); const refScheme = getPropertyScheme(schemaValue); const splitRefScheme = refScheme?.[0] ? splitPath(refScheme[0]) : null; if (types.includes(JsonTypes.string)) { diff --git a/src/components/TagManagementDialog/AddTagForm.tsx b/src/components/TagManagementDialog/AddTagForm.tsx index c9f0086e..bd9b68d1 100644 --- a/src/components/TagManagementDialog/AddTagForm.tsx +++ b/src/components/TagManagementDialog/AddTagForm.tsx @@ -1,8 +1,5 @@ import * as React from 'react'; import type { ResourceTagInfo } from './models'; -import find from 'lodash/find'; -import startsWith from 'lodash/startsWith'; -import isEmpty from 'lodash/isEmpty'; import { Button, Stack, TextField, Typography } from '@mui/material'; import { Callout } from '../Callout'; import type { TFunction } from '../../hooks/useTranslations'; @@ -26,7 +23,7 @@ const newTagValidationRules = ( ) => { return [ { - test: () => !key || isEmpty(key), + test: () => !key, message: t('fields_errors.tag_name_cannot_be_empty'), }, { @@ -35,7 +32,7 @@ const newTagValidationRules = ( }, { test: () => - RESERVED_NAMESPACES.some((reserved) => startsWith(key, reserved)), + RESERVED_NAMESPACES.some((reserved) => key.startsWith(reserved)), message: t(`fields_errors.some_tag_keys_are_reserved`, { namespace: RESERVED_NAMESPACES.join(', '), }), @@ -92,9 +89,9 @@ export const AddTagForm = ({ }; const checkTagOverwrites = async () => { - const overridableTag = find(overridableTags, { - tag_key: tagKey, - }); + const overridableTag = overridableTags.find( + (tag) => tag.tag_key === tagKey, + ); if (!overridableTag) { return true; diff --git a/src/components/TagManagementDialog/index.tsx b/src/components/TagManagementDialog/index.tsx index 4ed867f7..ddb43fa2 100644 --- a/src/components/TagManagementDialog/index.tsx +++ b/src/components/TagManagementDialog/index.tsx @@ -11,7 +11,6 @@ import type { TaggedResource, } from './models'; import sortBy from 'lodash/sortBy'; -import toString from 'lodash/toString'; import { faPencilAlt } from '@fortawesome/free-solid-svg-icons/faPencilAlt'; import { getResourceTagSubmitInfo, @@ -271,7 +270,7 @@ export const TagManagementDialog = ({ const title = typeof titleField === 'function' ? titleField(item) - : toString(item[titleField]); + : `${item[titleField] ?? ''}`; return title || `(${t('no_data.no_name_set')})`; }; diff --git a/src/components/TagManagementDialog/tag-management-service.ts b/src/components/TagManagementDialog/tag-management-service.ts index 2e418b99..e8fa1c7a 100644 --- a/src/components/TagManagementDialog/tag-management-service.ts +++ b/src/components/TagManagementDialog/tag-management-service.ts @@ -5,9 +5,6 @@ import type { SubmitInfo, TaggedResource, } from './models'; -import keys from 'lodash/keys'; -import flatMap from 'lodash/flatMap'; -import map from 'lodash/map'; import groupBy from 'lodash/groupBy'; export const TAGS_COLUMN_KEY = 'Tags'; @@ -27,7 +24,7 @@ export const groupResourcesByTags = < items: T[], tagField: P, ) => { - const resourceTagInfos = flatMap(items, (item) => { + const resourceTagInfos = items.flatMap((item) => { const tags = getResourceTags(item, tagField); return tags.map((tag) => ({ tag_key_value: getTagKeyValueComposite(tag.tag_key, tag.value), @@ -38,15 +35,17 @@ export const groupResourcesByTags = < }); const tagsByTagKeyValue = groupBy(resourceTagInfos, 'tag_key_value'); - const tagsWithItems = map(keys(tagsByTagKeyValue).sort(), (tagKeyValue) => { - const tags = tagsByTagKeyValue[tagKeyValue]; - const firstTag = tags[0]; - return { - tag_key: firstTag.tag_key, - value: firstTag.value, - items: map(tags, 'item'), - } as ResourceTagInfo; - }); + const tagsWithItems = Object.keys(tagsByTagKeyValue) + .sort() + .map((tagKeyValue) => { + const tags = tagsByTagKeyValue[tagKeyValue]; + const firstTag = tags[0]; + return { + tag_key: firstTag.tag_key, + value: firstTag.value, + items: tags.map((tag) => tag.item), + } as ResourceTagInfo; + }); return tagsWithItems; }; diff --git a/src/index.tsx b/src/index.tsx index 8b05a22e..0550efe9 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -186,5 +186,6 @@ export * as MaterialDataGrid from '@mui/x-data-grid'; export { enqueueSnackbar, closeSnackbar } from 'notistack'; export { useRandomUUID } from './hooks/useRandomUUID'; export { token } from './utils/token'; +export { uniq, toArray } from './utils/arrays'; export * as designTokens from '@balena/design-tokens'; export * as ReactQuery from '@tanstack/react-query'; diff --git a/src/utils/arrays.ts b/src/utils/arrays.ts new file mode 100644 index 00000000..b61ab5d8 --- /dev/null +++ b/src/utils/arrays.ts @@ -0,0 +1,7 @@ +export const uniq = (arr: T[]): T[] => { + return Array.from(new Set(arr)); +}; + +export const toArray = (v: T | T[] | null | undefined): T[] => { + return v == null ? [] : Array.isArray(v) ? v : [v]; +}; From f5c2dd6b7054f6167a376db7c0d4d2da8291a9eb Mon Sep 17 00:00:00 2001 From: Matthew Yarmolinsky Date: Fri, 14 Nov 2025 08:36:31 -0500 Subject: [PATCH 2/2] isObjectEmpty Change-type: patch --- src/components/DownloadImageDialog/index.tsx | 9 ++++----- src/components/RJST/DataTypes/array.tsx | 7 ++++--- src/components/RJST/Filters/utils.ts | 5 +++-- src/components/RJST/components/Filters/SchemaSieve.ts | 7 ++----- src/components/RJST/models/helpers.ts | 3 ++- src/components/RJST/schemaOps.ts | 3 ++- src/utils/objects.ts | 5 +++++ 7 files changed, 22 insertions(+), 17 deletions(-) create mode 100644 src/utils/objects.ts diff --git a/src/components/DownloadImageDialog/index.tsx b/src/components/DownloadImageDialog/index.tsx index a39e282d..aaf21ba4 100644 --- a/src/components/DownloadImageDialog/index.tsx +++ b/src/components/DownloadImageDialog/index.tsx @@ -35,6 +35,7 @@ import { Spinner } from '../Spinner'; import { faDownload } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import * as semver from 'balena-semver'; +import { isObjectEmpty } from '../../utils/objects'; const etcherLogoBase64 = 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iODAwIiBoZWlnaHQ9IjYwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KIDxnPgogIDx0aXRsZT5FdGNoZXI8L3RpdGxlPgogIDxnIGlkPSJzdmdfMSIgc3Ryb2tlPSJudWxsIj4KICAgPHBhdGggaWQ9InN2Z18yIiBjbGFzcz0ic3QxIiBkPSJtNDEyLjkwMzgzLDM1OC4wNjcxM2wwLDE3MS40OTU4M2M3LjQ5MjU0LC0xLjY2NTAxIDE0LjE1MjU3LC0zLjMzMDAyIDIwLjgxMjYsLTcuNDkyNTRsMTQyLjM1ODE5LC04MS41ODUzOWMyMC44MTI2LC0xMS42NTUwNiAzMy4zMDAxNiwtMzQuMTMyNjYgMzMuMzAwMTYsLTU4LjI3NTI4bDAsLTE2Mi4zMzgyOGMwLC02LjY2MDAzIC0wLjgzMjUsLTEzLjMyMDA2IC0zLjMzMDAyLC0xOS4xNDc1OWwtMTU0LjAxMzI0LDg5LjA3NzkzYy0zMi40Njc2NiwyMi40Nzc2MSAtMzkuMTI3NjksNDMuMjkwMjEgLTM5LjEyNzY5LDY4LjI2NTMzbDAsLTAuMDAwMDF6IiBmaWxsPSIjQTVERTM3IiBzdHJva2U9Im51bGwiLz4KICAgPHBhdGggaWQ9InN2Z18zIiBjbGFzcz0ic3QyIiBkPSJtNjYyLjY1NTAzLDE2Ny40MjM3MWwtNTYuNjEwMjcsMzIuNDY3NjZjMS42NjUwMSw1LjgyNzUzIDMuMzMwMDIsMTIuNDg3NTYgMy4zMzAwMiwxOS4xNDc1OWwwLDE2My4xNzA3OWMwLDI0LjE0MjYyIC0xMy4zMjAwNiw0Ni42MjAyMiAtMzMuMzAwMTYsNTguMjc1MjhsLTE0Mi4zNTgxOSw4MS41ODUzOWMtNi42NjAwMywzLjMzMDAyIC0xMy4zMjAwNiw1LjgyNzUzIC0yMC44MTI2LDcuNDkyNTRsMCw2NC45MzUzMWM5Ljk5MDA1LC0xLjY2NTAxIDE5Ljk4MDEsLTQuOTk1MDIgMjguMzA1MTQsLTkuOTkwMDVsMTg0LjgxNTg5LC0xMDUuNzI4MDFjMjUuODA3NjIsLTE0Ljk4NTA3IDQxLjYyNTIsLTQyLjQ1NzcgNDEuNjI1MiwtNzIuNDI3ODVsMCwtMjExLjQ1NjAyYzAsLTkuMTU3NTQgLTEuNjY1MDEsLTE4LjMxNTA5IC00Ljk5NTAyLC0yNy40NzI2M2wtMC4wMDAwMSwweiIgZmlsbD0iI0M4RjE3OCIgc3Ryb2tlPSJudWxsIi8+CiAgIDxwYXRoIGlkPSJzdmdfNCIgY2xhc3M9InN0MSIgZD0ibTM5OS41ODM3NiwzMDMuOTU0MzZjOC4zMjUwNCwtMTMuMzIwMDYgMjAuODEyNiwtMjUuODA3NjIgMzkuMTI3NjksLTM2LjYzMDE4bDE1NS42NzgyNSwtODkuOTEwNDNjLTQuOTk1MDIsLTYuNjYwMDMgLTExLjY1NTA2LC0xMi40ODc1NiAtMTguMzE1MDksLTE2LjY1MDA4bC0xNDIuMzU4MTksLTgxLjU4NTM5Yy0yMC44MTI2LC0xMS42NTUwNiAtNDYuNjIwMjIsLTExLjY1NTA2IC02Ny40MzI4MiwwbC0xNDEuNTI1NjgsODEuNTg1MzljLTcuNDkyNTQsNC4xNjI1MiAtMTMuMzIwMDYsOS45OTAwNSAtMTkuMTQ3NTksMTYuNjUwMDhsMTU0Ljg0NTc1LDg5LjkxMDQzYzE4LjMxNTA5LDExLjY1NTA2IDMwLjgwMjY1LDIzLjMxMDExIDM5LjEyNzY5LDM2LjYzMDE4bC0wLjAwMDAxLDB6IiBmaWxsPSIjQTVERTM3IiBzdHJva2U9Im51bGwiLz4KICAgPHBhdGggaWQ9InN2Z181IiBjbGFzcz0ic3QyIiBkPSJtMjI0Ljc1NzkyLDE2MS41OTYxOGwxNDEuNTI1NjgsLTgxLjU4NTM5YzIwLjgxMjYsLTExLjY1NTA2IDQ2LjYyMDIyLC0xMS42NTUwNiA2Ny40MzI4MiwwbDE0Mi4zNTgxOSw4MS41ODUzOWM3LjQ5MjU0LDQuMTYyNTIgMTMuMzIwMDYsOS45OTAwNSAxOC4zMTUwOSwxNi42NTAwOGw1Ni42MTAyNywtMzIuNDY3NjZjLTYuNjYwMDMsLTkuMTU3NTQgLTE0Ljk4NTA3LC0xNi42NTAwOCAtMjQuOTc1MTIsLTIxLjY0NTFsLTE4NC44MTU4OSwtMTA3LjM5MzAyYy0yNS44MDc2MiwtMTQuOTg1MDcgLTU3LjQ0Mjc4LC0xNC45ODUwNyAtODMuMjUwNCwwbC0xODMuMTUwODgsMTA2LjU2MDUxYy05Ljk5MDA1LDUuODI3NTMgLTE4LjMxNTA5LDEzLjMyMDA2IC0yNC45NzUxMiwyMi40Nzc2MWw1Ni42MTAyNywzMi40Njc2NmM0LjE2MjUyLC02LjY2MDAzIDEwLjgyMjU1LC0xMi40ODc1NiAxOC4zMTUwOSwtMTYuNjUwMDh6IiBmaWxsPSIjQzhGMTc4IiBzdHJva2U9Im51bGwiLz4KICAgPHBhdGggaWQ9InN2Z182IiBjbGFzcz0ic3QyIiBkPSJtMzY2LjI4MzYsNTIyLjA3MDQxbC0xNDEuNTI1NjgsLTgxLjU4NTM5Yy0yMC44MTI2LC0xMS42NTUwNiAtMzMuMzAwMTYsLTM0LjEzMjY2IC0zMy4zMDAxNiwtNTguMjc1MjhsMCwtMTYzLjE3MDc5YzAsLTYuNjYwMDMgMC44MzI1LC0xMi40ODc1NiAyLjQ5NzUxLC0xOC4zMTUwOWwtNTYuNjEwMjcsLTMyLjQ2NzY2Yy0zLjMzMDAyLDkuMTU3NTQgLTQuOTk1MDIsMTcuNDgyNTggLTQuOTk1MDIsMjYuNjQwMTNsMCwyMTIuMjg4NTJjMCwyOS45NzAxNCAxNS44MTc1OCw1Ny40NDI3OCA0MS42MjUyLDcxLjU5NTM0bDE4My45ODMzOSwxMDUuNzI4MDFjOC4zMjUwNCw0Ljk5NTAyIDE4LjMxNTA5LDguMzI1MDQgMjguMzA1MTQsOS45OTAwNWwwLC02NC45MzUzMWMtNi42NjAwMywtMC44MzI1IC0xMy4zMjAwNiwtMy4zMzAwMiAtMTkuOTgwMSwtNy40OTI1NGwtMC4wMDAwMSwwLjAwMDAxeiIgZmlsbD0iI0M4RjE3OCIgc3Ryb2tlPSJudWxsIi8+CiAgIDxwYXRoIGlkPSJzdmdfNyIgY2xhc3M9InN0MSIgZD0ibTM0Ny4xMzYwMSwyODguOTY5MjlsLTE1My4xODA3NCwtODguMjQ1NDJjLTEuNjY1MDEsNS44Mjc1MyAtMi40OTc1MSwxMi40ODc1NiAtMi40OTc1MSwxOC4zMTUwOWwwLDE2My4xNzA3OWMwLDI0LjE0MjYyIDEyLjQ4NzU2LDQ2LjYyMDIyIDMzLjMwMDE2LDU4LjI3NTI4bDE0MS41MjU2OCw4MS41ODUzOWM2LjY2MDAzLDMuMzMwMDIgMTMuMzIwMDYsNS44Mjc1MyAyMC44MTI2LDcuNDkyNTRsMCwtMTcxLjQ5NTgzYy0wLjgzMjUsLTI0Ljk3NTEyIC03LjQ5MjU0LC00NS43ODc3MiAtMzkuOTYwMTksLTY5LjA5NzgzbDAsLTAuMDAwMDF6IiBmaWxsPSIjQTVERTM3IiBzdHJva2U9Im51bGwiLz4KICA8L2c+CiA8L2c+Cgo8L3N2Zz4='; @@ -67,7 +68,7 @@ const getUniqueOsTypes = ( deviceTypeSlug: string | undefined, ) => { if ( - Object.keys(osVersions).length === 0 || + isObjectEmpty(osVersions) || !deviceTypeSlug || osVersions[deviceTypeSlug]?.length === 0 ) { @@ -236,9 +237,7 @@ export const DownloadImageDialog = ({ } : {}, ); - const [isFetching, setIsFetching] = useState( - Object.keys(osVersions).length === 0, - ); + const [isFetching, setIsFetching] = useState(isObjectEmpty(osVersions)); const [downloadSize, setDownloadSize] = useState(null); const [isValidatingUrl, setIsValidatingUrl] = useState(false); @@ -489,7 +488,7 @@ export const DownloadImageDialog = ({ ) : ( <> - {Object.keys(osVersions).length === 0 && ( + {isObjectEmpty(osVersions) && ( No OS versions available for download diff --git a/src/components/RJST/DataTypes/array.tsx b/src/components/RJST/DataTypes/array.tsx index 5bb68364..cb0699e0 100644 --- a/src/components/RJST/DataTypes/array.tsx +++ b/src/components/RJST/DataTypes/array.tsx @@ -8,6 +8,7 @@ import type { CreateFilter } from './utils'; import { getDataTypeSchema } from './utils'; import { getDataModel } from '.'; import { getRefSchema, isJSONSchema } from '../schemaOps'; +import { isObjectEmpty } from '../../../utils/objects'; export const operators = () => ({ contains: 'contains', @@ -42,7 +43,7 @@ const buildFilterForPropertySchema = ( ): JSONSchema => { const filter = getFilter(field, schema, value, operator); - if (!Object.keys(filter).length) { + if (isObjectEmpty(filter)) { return {}; } return { @@ -93,7 +94,7 @@ const getFilter = ( operator: effectiveOperator, value, }); - if (!filter || typeof filter !== 'object' || !Object.keys(filter).length) { + if (!filter || typeof filter !== 'object' || isObjectEmpty(filter)) { return {}; } @@ -103,7 +104,7 @@ const getFilter = ( return recursiveFilter && isJSONSchema(recursiveFilter) && - Object.keys(recursiveFilter).length + !isObjectEmpty(recursiveFilter) ? wrapFilter(recursiveFilter) : {}; }; diff --git a/src/components/RJST/Filters/utils.ts b/src/components/RJST/Filters/utils.ts index 36e596cd..f86776ff 100644 --- a/src/components/RJST/Filters/utils.ts +++ b/src/components/RJST/Filters/utils.ts @@ -11,6 +11,7 @@ import { parseDescriptionProperty, } from '../schemaOps'; import get from 'lodash/get'; +import { isObjectEmpty } from '../../../utils/objects'; const X_FOREIGN_KEY_SCHEMA_SEPARATOR = '___ref_scheme_separator_'; @@ -68,12 +69,12 @@ export const removeFieldsWithNoFilter = (schema: JSONSchema): JSONSchema => { } const hasEmptyProperties = - newValue.properties && Object.keys(newValue.properties).length === 0; + newValue.properties && isObjectEmpty(newValue.properties); const hasEmptyItemsProperties = isJSONSchema(newValue.items) && 'properties' in newValue.items && - Object.keys(newValue.items.properties ?? {}).length === 0; + isObjectEmpty(newValue.items.properties ?? {}); if (hasEmptyProperties || hasEmptyItemsProperties) { continue; diff --git a/src/components/RJST/components/Filters/SchemaSieve.ts b/src/components/RJST/components/Filters/SchemaSieve.ts index 121f00f2..3bfc46e3 100644 --- a/src/components/RJST/components/Filters/SchemaSieve.ts +++ b/src/components/RJST/components/Filters/SchemaSieve.ts @@ -12,6 +12,7 @@ import addFormats from 'ajv-formats'; import pickBy from 'lodash/pickBy'; import Ajv from 'ajv'; import { enqueueSnackbar } from 'notistack'; +import { isObjectEmpty } from '../../../../utils/objects'; const ajv = new Ajv(); ajvKeywords(ajv, ['regexp']); @@ -130,11 +131,7 @@ export const createFilter = ( operator, value, }); - if ( - !filter || - typeof filter !== 'object' || - !Object.keys(filter).length - ) { + if (!filter || typeof filter !== 'object' || isObjectEmpty(filter)) { return {}; } return { diff --git a/src/components/RJST/models/helpers.ts b/src/components/RJST/models/helpers.ts index 97d52761..5600411d 100644 --- a/src/components/RJST/models/helpers.ts +++ b/src/components/RJST/models/helpers.ts @@ -1,3 +1,4 @@ +import { isObjectEmpty } from '../../../utils/objects'; import type { Permissions, RJSTModel, RJSTRawModel } from '../schemaOps'; import { rjstJsonSchemaPick } from '../schemaOps'; @@ -32,7 +33,7 @@ export const rjstRunTransformers = < return data; } - if (!transformers || Object.keys(transformers).length === 0) { + if (!transformers || isObjectEmpty(transformers)) { return data as TResult; } diff --git a/src/components/RJST/schemaOps.ts b/src/components/RJST/schemaOps.ts index ecd05d88..e0a228db 100644 --- a/src/components/RJST/schemaOps.ts +++ b/src/components/RJST/schemaOps.ts @@ -8,6 +8,7 @@ import { findInObject } from './utils'; import type { ResourceTagModelService } from '../TagManagementDialog/tag-management-service'; import type { CheckedState } from './components/Table/utils'; import type { PineFilterObject } from './oData/jsonToOData'; +import { isObjectEmpty } from '../../utils/objects'; export interface RJSTBaseResource { id: number; @@ -222,7 +223,7 @@ export const isJSONSchema = ( typeof value === 'object' && value !== null && typeof value !== 'boolean' && - !!Object.keys(value).length + !isObjectEmpty(value) ); }; diff --git a/src/utils/objects.ts b/src/utils/objects.ts new file mode 100644 index 00000000..1f4adf49 --- /dev/null +++ b/src/utils/objects.ts @@ -0,0 +1,5 @@ +export const isObjectEmpty = ( + obj: Record | object, +): boolean => { + return Object.keys(obj).length === 0; +};