From 77e8aa2b2e74e71f337fa0bb54edc006c84068d6 Mon Sep 17 00:00:00 2001 From: andreastanderen <71079896+standeren@users.noreply.github.com> Date: Fri, 22 Nov 2024 10:43:52 +0100 Subject: [PATCH] fix: move `filenameUtils` from `shared` to `studio-pure-functions` (#14134) --- .../SchemaEditorWithToolbar.tsx | 4 +- .../SelectedSchemaEditor.tsx | 6 +- .../TopToolbar/XSDUpload/XSDUpload.tsx | 4 +- .../TopToolbar/XSDUpload/validationUtils.ts | 4 +- .../useDeleteDataModelMutation.test.ts | 51 +++++++--- .../mutations/useDeleteDataModelMutation.ts | 8 +- .../app-development/utils/metadataUtils.ts | 5 +- .../src/FileNameUtils/FilenameUtils.test.ts | 93 +++++++++++++++++++ .../src/FileNameUtils/FilenameUtils.ts | 42 +++++++++ .../src/FileNameUtils/index.ts | 1 + .../libs/studio-pure-functions/src/index.ts | 1 + .../FilePath/FilePath.tsx | 8 +- .../shared/src/utils/filenameUtils.test.ts | 84 ----------------- .../shared/src/utils/filenameUtils.ts | 40 -------- .../src/hooks/useLayoutNamesQuery.ts | 6 +- .../ChooseFromLibrary/ImageLibraryPreview.tsx | 11 +-- .../PreviewImageSummary/PreviewFileInfo.tsx | 4 +- .../EditOptionList/EditOptionList.tsx | 4 +- .../EditOptionList/utils/findFileNameError.ts | 4 +- 19 files changed, 209 insertions(+), 171 deletions(-) create mode 100644 frontend/libs/studio-pure-functions/src/FileNameUtils/FilenameUtils.test.ts create mode 100644 frontend/libs/studio-pure-functions/src/FileNameUtils/FilenameUtils.ts create mode 100644 frontend/libs/studio-pure-functions/src/FileNameUtils/index.ts delete mode 100644 frontend/packages/shared/src/utils/filenameUtils.test.ts delete mode 100644 frontend/packages/shared/src/utils/filenameUtils.ts diff --git a/frontend/app-development/features/dataModelling/SchemaEditorWithToolbar/SchemaEditorWithToolbar.tsx b/frontend/app-development/features/dataModelling/SchemaEditorWithToolbar/SchemaEditorWithToolbar.tsx index 7c6cba15a82..3a68e7c8ce1 100644 --- a/frontend/app-development/features/dataModelling/SchemaEditorWithToolbar/SchemaEditorWithToolbar.tsx +++ b/frontend/app-development/features/dataModelling/SchemaEditorWithToolbar/SchemaEditorWithToolbar.tsx @@ -7,7 +7,7 @@ import { SelectedSchemaEditor } from './SelectedSchemaEditor'; import type { DataModelMetadata } from 'app-shared/types/DataModelMetadata'; import { SchemaGenerationErrorsPanel } from './SchemaGenerationErrorsPanel'; import { useAddXsdMutation } from '../../../hooks/mutations/useAddXsdMutation'; -import { isXsdFile } from 'app-shared/utils/filenameUtils'; +import { FileNameUtils } from '@studio/pure-functions'; export interface SchemaEditorWithToolbarProps { createPathOption?: boolean; @@ -32,7 +32,7 @@ export const SchemaEditorWithToolbar = ({ useEffect(() => { dataModels.forEach((model) => { - if (model.repositoryRelativeUrl && isXsdFile(model.repositoryRelativeUrl)) { + if (model.repositoryRelativeUrl && FileNameUtils.isXsdFile(model.repositoryRelativeUrl)) { addXsdFromRepo(model.repositoryRelativeUrl); } }); diff --git a/frontend/app-development/features/dataModelling/SchemaEditorWithToolbar/SelectedSchemaEditor.tsx b/frontend/app-development/features/dataModelling/SchemaEditorWithToolbar/SelectedSchemaEditor.tsx index ae817cd8fde..9b1d7fb985b 100644 --- a/frontend/app-development/features/dataModelling/SchemaEditorWithToolbar/SelectedSchemaEditor.tsx +++ b/frontend/app-development/features/dataModelling/SchemaEditorWithToolbar/SelectedSchemaEditor.tsx @@ -16,7 +16,7 @@ import { useQueryClient } from '@tanstack/react-query'; import { QueryKey } from 'app-shared/types/QueryKey'; import { useStudioEnvironmentParams } from 'app-shared/hooks/useStudioEnvironmentParams'; import { mergeJsonAndXsdData } from 'app-development/utils/metadataUtils'; -import { extractFilename, removeSchemaExtension } from 'app-shared/utils/filenameUtils'; +import { FileNameUtils } from '@studio/pure-functions'; export interface SelectedSchemaEditorProps { modelPath: string; } @@ -111,6 +111,6 @@ const SchemaEditorWithDebounce = ({ jsonSchema, modelPath }: SchemaEditorWithDeb }; const extractModelNameFromPath = (path: string): string => { - const filename = extractFilename(path); - return removeSchemaExtension(filename); + const filename = FileNameUtils.extractFilename(path); + return FileNameUtils.removeSchemaExtension(filename); }; diff --git a/frontend/app-development/features/dataModelling/SchemaEditorWithToolbar/TopToolbar/XSDUpload/XSDUpload.tsx b/frontend/app-development/features/dataModelling/SchemaEditorWithToolbar/TopToolbar/XSDUpload/XSDUpload.tsx index 5919dfbd758..0ccdf8a4d62 100644 --- a/frontend/app-development/features/dataModelling/SchemaEditorWithToolbar/TopToolbar/XSDUpload/XSDUpload.tsx +++ b/frontend/app-development/features/dataModelling/SchemaEditorWithToolbar/TopToolbar/XSDUpload/XSDUpload.tsx @@ -1,6 +1,7 @@ import React from 'react'; import type { StudioButtonProps } from '@studio/components'; import { StudioFileUploader, StudioSpinner } from '@studio/components'; +import { FileNameUtils } from '@studio/pure-functions'; import { useTranslation } from 'react-i18next'; import { useUploadDataModelMutation } from '../../../../../hooks/mutations/useUploadDataModelMutation'; import type { AxiosError } from 'axios'; @@ -10,7 +11,6 @@ import type { MetadataOption } from '../../../../../types/MetadataOption'; import { useStudioEnvironmentParams } from 'app-shared/hooks/useStudioEnvironmentParams'; import { useAppMetadataQuery } from 'app-shared/hooks/queries'; import { useValidationAlert } from './useValidationAlert'; -import { removeExtension } from 'app-shared/utils/filenameUtils'; import { doesFileExistInMetadataWithClassRef, doesFileExistInMetadataWithoutClassRef, @@ -65,7 +65,7 @@ export const XSDUpload = ({ if (fileNameError) { validationAlert(fileNameError); } - const fileNameWithoutExtension = removeExtension(file.name); + const fileNameWithoutExtension = FileNameUtils.removeExtension(file.name); if (doesFileExistInMetadataWithClassRef(appMetadata, fileNameWithoutExtension)) { const userConfirmed = window.confirm( t('schema_editor.error_upload_data_model_id_exists_override_option'), diff --git a/frontend/app-development/features/dataModelling/SchemaEditorWithToolbar/TopToolbar/XSDUpload/validationUtils.ts b/frontend/app-development/features/dataModelling/SchemaEditorWithToolbar/TopToolbar/XSDUpload/validationUtils.ts index 768ef1ee4d6..7367c51abd6 100644 --- a/frontend/app-development/features/dataModelling/SchemaEditorWithToolbar/TopToolbar/XSDUpload/validationUtils.ts +++ b/frontend/app-development/features/dataModelling/SchemaEditorWithToolbar/TopToolbar/XSDUpload/validationUtils.ts @@ -1,5 +1,5 @@ import type { ApplicationMetadata } from 'app-shared/types/ApplicationMetadata'; -import { removeExtension } from 'app-shared/utils/filenameUtils'; +import { FileNameUtils } from '@studio/pure-functions'; import type { FileNameError } from './FileNameError'; export const doesFileExistInMetadataWithClassRef = ( @@ -28,7 +28,7 @@ export const findFileNameError = ( fileName: string, appMetadata: ApplicationMetadata, ): FileNameError | null => { - const fileNameWithoutExtension = removeExtension(fileName); + const fileNameWithoutExtension = FileNameUtils.removeExtension(fileName); if (!isNameFormatValid(fileNameWithoutExtension)) { return 'invalidFileName'; } else if (doesFileExistInMetadata(appMetadata, fileNameWithoutExtension)) { diff --git a/frontend/app-development/hooks/mutations/useDeleteDataModelMutation.test.ts b/frontend/app-development/hooks/mutations/useDeleteDataModelMutation.test.ts index 9154efb9771..a07cd5301a5 100644 --- a/frontend/app-development/hooks/mutations/useDeleteDataModelMutation.test.ts +++ b/frontend/app-development/hooks/mutations/useDeleteDataModelMutation.test.ts @@ -6,7 +6,7 @@ import { createQueryClientMock } from 'app-shared/mocks/queryClientMock'; import { waitFor } from '@testing-library/react'; import { QueryKey } from 'app-shared/types/QueryKey'; import { queriesMock } from 'app-shared/mocks/queriesMock'; -import { createJsonModelPathMock } from 'app-shared/mocks/modelPathMocks'; +import { createJsonModelPathMock, createXsdModelPathMock } from 'app-shared/mocks/modelPathMocks'; import { createJsonMetadataMock, createXsdMetadataMock, @@ -14,7 +14,8 @@ import { import { app, org } from '@studio/testing/testids'; const modelName = 'modelName'; -const modelPath = createJsonModelPathMock(modelName); +const modelJsonPath = createJsonModelPathMock(modelName); +const modelXsdPath = createXsdModelPathMock(modelName); const modelMetadataJson = createJsonMetadataMock(modelName); const modelMetadataXsd = createXsdMetadataMock(modelName); @@ -29,38 +30,62 @@ describe('useDeleteDataModelMutation', () => { renderHookResult: { result }, } = render({}, client); expect(result.current).toBeDefined(); - result.current.mutate(modelPath); + result.current.mutate(modelJsonPath); await waitFor(() => result.current.isSuccess); expect(queriesMock.deleteDataModel).toHaveBeenCalledTimes(1); - expect(queriesMock.deleteDataModel).toHaveBeenCalledWith(org, app, modelPath); + expect(queriesMock.deleteDataModel).toHaveBeenCalledWith(org, app, modelJsonPath); }); - it('Removes the metadata instances from the query cache', async () => { + it('Removes the metadata instances from the query cache when model is json', async () => { const client = createQueryClientMock(); client.setQueryData([QueryKey.DataModelsJson, org, app], [modelMetadataJson]); client.setQueryData([QueryKey.DataModelsXsd, org, app], [modelMetadataXsd]); const { renderHookResult: { result }, } = render({}, client); - result.current.mutate(modelPath); + result.current.mutate(modelJsonPath); await waitFor(() => result.current.isSuccess); expect(client.getQueryData([QueryKey.DataModelsJson, org, app])).toEqual([]); expect(client.getQueryData([QueryKey.DataModelsXsd, org, app])).toEqual([]); }); - it('Removes the schema queries from the query cache', async () => { + it('Removes the metadata instances from the query cache when model is xsd', async () => { const client = createQueryClientMock(); client.setQueryData([QueryKey.DataModelsJson, org, app], [modelMetadataJson]); client.setQueryData([QueryKey.DataModelsXsd, org, app], [modelMetadataXsd]); const { renderHookResult: { result }, } = render({}, client); - result.current.mutate(modelPath); + result.current.mutate(modelXsdPath); await waitFor(() => result.current.isSuccess); - expect(client.getQueryData([QueryKey.JsonSchema, org, app, modelPath])).toBeUndefined(); - expect( - client.getQueryData([QueryKey.JsonSchema, org, app, modelMetadataXsd.repositoryRelativeUrl]), - ).toBeUndefined(); + expect(client.getQueryData([QueryKey.DataModelsJson, org, app])).toEqual([]); + expect(client.getQueryData([QueryKey.DataModelsXsd, org, app])).toEqual([]); + }); + + it('Removes the schema queries from the query cache when model is json', async () => { + const client = createQueryClientMock(); + client.setQueryData([QueryKey.DataModelsJson, org, app], [modelMetadataJson]); + client.setQueryData([QueryKey.DataModelsXsd, org, app], [modelMetadataXsd]); + const { + renderHookResult: { result }, + } = render({}, client); + result.current.mutate(modelJsonPath); + await waitFor(() => result.current.isSuccess); + expect(client.getQueryData([QueryKey.JsonSchema, org, app, modelJsonPath])).toBeUndefined(); + expect(client.getQueryData([QueryKey.JsonSchema, org, app, modelXsdPath])).toBeUndefined(); + }); + + it('Removes the schema queries from the query cache when model is xsd', async () => { + const client = createQueryClientMock(); + client.setQueryData([QueryKey.DataModelsJson, org, app], [modelMetadataJson]); + client.setQueryData([QueryKey.DataModelsXsd, org, app], [modelMetadataXsd]); + const { + renderHookResult: { result }, + } = render({}, client); + result.current.mutate(modelXsdPath); + await waitFor(() => result.current.isSuccess); + expect(client.getQueryData([QueryKey.JsonSchema, org, app, modelJsonPath])).toBeUndefined(); + expect(client.getQueryData([QueryKey.JsonSchema, org, app, modelXsdPath])).toBeUndefined(); }); it('Invalidates the appMetadataModelIds and appMetadata from the cache', async () => { @@ -71,7 +96,7 @@ describe('useDeleteDataModelMutation', () => { const { renderHookResult: { result }, } = render({}, client); - result.current.mutate(modelPath); + result.current.mutate(modelJsonPath); await waitFor(() => result.current.isSuccess); expect(invalidateQueriesSpy).toHaveBeenCalledTimes(2); expect(invalidateQueriesSpy).toHaveBeenCalledWith({ diff --git a/frontend/app-development/hooks/mutations/useDeleteDataModelMutation.ts b/frontend/app-development/hooks/mutations/useDeleteDataModelMutation.ts index 06f78743c46..aa549aab7b0 100644 --- a/frontend/app-development/hooks/mutations/useDeleteDataModelMutation.ts +++ b/frontend/app-development/hooks/mutations/useDeleteDataModelMutation.ts @@ -2,7 +2,7 @@ import { useMutation, useQueryClient } from '@tanstack/react-query'; import { useServicesContext } from 'app-shared/contexts/ServicesContext'; import { QueryKey } from 'app-shared/types/QueryKey'; import { useStudioEnvironmentParams } from 'app-shared/hooks/useStudioEnvironmentParams'; -import { isXsdFile } from 'app-shared/utils/filenameUtils'; +import { FileNameUtils } from '@studio/pure-functions'; import type { DataModelMetadata } from 'app-shared/types/DataModelMetadata'; export const useDeleteDataModelMutation = () => { @@ -11,10 +11,12 @@ export const useDeleteDataModelMutation = () => { const queryClient = useQueryClient(); return useMutation({ mutationFn: async (modelPath: string) => { - const jsonSchemaPath = isXsdFile(modelPath) + const jsonSchemaPath = FileNameUtils.isXsdFile(modelPath) ? modelPath.replace('.xsd', '.schema.json') : modelPath; - const xsdPath = isXsdFile(modelPath) ? modelPath : modelPath.replace('.schema.json', '.xsd'); + const xsdPath = FileNameUtils.isXsdFile(modelPath) + ? modelPath + : modelPath.replace('.schema.json', '.xsd'); queryClient.setQueryData( [QueryKey.DataModelsJson, org, app], (oldData: DataModelMetadata[]) => removeDataModelFromList(oldData, jsonSchemaPath), diff --git a/frontend/app-development/utils/metadataUtils.ts b/frontend/app-development/utils/metadataUtils.ts index 6e31c6ab731..9b0e6fce88d 100644 --- a/frontend/app-development/utils/metadataUtils.ts +++ b/frontend/app-development/utils/metadataUtils.ts @@ -3,10 +3,9 @@ import type { DataModelMetadataJson, DataModelMetadataXsd, } from 'app-shared/types/DataModelMetadata'; -import { ArrayUtils, StringUtils } from '@studio/pure-functions'; +import { ArrayUtils, StringUtils, FileNameUtils } from '@studio/pure-functions'; import type { MetadataOption } from '../types/MetadataOption'; import type { MetadataOptionsGroup } from '../types/MetadataOptionsGroup'; -import { removeSchemaExtension } from 'app-shared/utils/filenameUtils'; /** * Filters out items from the Xsd data list if there are items in the Json data list with the same name. @@ -43,7 +42,7 @@ export const mergeJsonAndXsdData = ( * @returns The MetadataOption object. */ export const convertMetadataToOption = (metadata: DataModelMetadata): MetadataOption => { - let label = removeSchemaExtension(metadata.fileName); + let label = FileNameUtils.removeSchemaExtension(metadata.fileName); if (metadata.fileType === '.xsd') { label += ' (XSD)'; } diff --git a/frontend/libs/studio-pure-functions/src/FileNameUtils/FilenameUtils.test.ts b/frontend/libs/studio-pure-functions/src/FileNameUtils/FilenameUtils.test.ts new file mode 100644 index 00000000000..ae51eb22ec8 --- /dev/null +++ b/frontend/libs/studio-pure-functions/src/FileNameUtils/FilenameUtils.test.ts @@ -0,0 +1,93 @@ +import { FileNameUtils } from './FilenameUtils'; + +describe('FileNameUtils', () => { + describe('removeExtension', () => { + it('Removes extension from filename if it exists', () => { + expect(FileNameUtils.removeExtension('filename.txt')).toEqual('filename'); + expect(FileNameUtils.removeExtension('filename.xsd')).toEqual('filename'); + expect(FileNameUtils.removeExtension('.abc')).toEqual(''); + expect(FileNameUtils.removeExtension('filename.schema.json')).toEqual('filename.schema'); + }); + + it('Removes the extension for filenames with special characters', () => { + expect(FileNameUtils.removeExtension('file name.txt')).toBe('file name'); + expect(FileNameUtils.removeExtension('my-file.name$!.txt')).toBe('my-file.name$!'); + expect(FileNameUtils.removeExtension('.hiddenfile.txt')).toBe('.hiddenfile'); + expect(FileNameUtils.removeExtension('file123.456.txt')).toBe('file123.456'); + }); + + it('Returns same input string if there is no extension', () => { + expect(FileNameUtils.removeExtension('filename')).toEqual('filename'); + expect(FileNameUtils.removeExtension('')).toEqual(''); + }); + + it('returns an empty string if the filename starts with dot', () => { + expect(FileNameUtils.removeExtension('.hiddenfile')).toBe(''); + expect(FileNameUtils.removeExtension('.')).toBe(''); + }); + }); + + describe('removeSchemaExtension', () => { + it('Removes .schema.json extension from filename if it exists', () => { + expect(FileNameUtils.removeSchemaExtension('filename.schema.json')).toEqual('filename'); + expect(FileNameUtils.removeSchemaExtension('filename.SCHEMA.JSON')).toEqual('filename'); + }); + + it('Removes .xsd extension from filename if it exists', () => { + expect(FileNameUtils.removeSchemaExtension('filename.xsd')).toEqual('filename'); + expect(FileNameUtils.removeSchemaExtension('filename.XSD')).toEqual('filename'); + }); + + it('Returns entire input string if there is no .schema.json or .xsd extension', () => { + expect(FileNameUtils.removeSchemaExtension('filename.xml')).toEqual('filename.xml'); + }); + }); + + describe('isXsdFile', () => { + it('Returns true if filename has an XSD extension', () => { + expect(FileNameUtils.isXsdFile('filename.xsd')).toBe(true); + expect(FileNameUtils.isXsdFile('filename.XSD')).toBe(true); + }); + + it('Returns false if filename does not have an XSD extension', () => { + expect(FileNameUtils.isXsdFile('filename.schema.json')).toBe(false); + expect(FileNameUtils.isXsdFile('filename')).toBe(false); + }); + }); + + describe('extractFilename', () => { + it('Returns filename if path contains a slash', () => { + expect(FileNameUtils.extractFilename('/path/to/filename')).toEqual('filename'); + expect(FileNameUtils.extractFilename('/path/to/filename.json')).toEqual('filename.json'); + }); + + it('Returns path if path does not contain a slash', () => { + expect(FileNameUtils.extractFilename('filename')).toEqual('filename'); + expect(FileNameUtils.extractFilename('filename.json')).toEqual('filename.json'); + }); + }); + + describe('removeFileNameFromPath', () => { + it('Returns file path without file name', () => { + expect(FileNameUtils.removeFileNameFromPath('/path/to/filename')).toEqual('/path/to/'); + expect(FileNameUtils.removeFileNameFromPath('/path/to/filename.json')).toEqual('/path/to/'); + }); + + it('Returns file path without file name and last slash if "excludeLastSlash" is true', () => { + expect(FileNameUtils.removeFileNameFromPath('/path/to/filename', true)).toEqual('/path/to'); + expect(FileNameUtils.removeFileNameFromPath('/path/to/filename.json', true)).toEqual( + '/path/to', + ); + }); + + it('Returns empty string if path is only fileName', () => { + expect(FileNameUtils.removeFileNameFromPath('filename')).toEqual(''); + expect(FileNameUtils.removeFileNameFromPath('filename.json')).toEqual(''); + }); + + it('Returns empty string if path is only fileName and "excludeLastSlash" is true', () => { + expect(FileNameUtils.removeFileNameFromPath('filename', true)).toEqual(''); + expect(FileNameUtils.removeFileNameFromPath('filename.json', true)).toEqual(''); + }); + }); +}); diff --git a/frontend/libs/studio-pure-functions/src/FileNameUtils/FilenameUtils.ts b/frontend/libs/studio-pure-functions/src/FileNameUtils/FilenameUtils.ts new file mode 100644 index 00000000000..fdd8b63c166 --- /dev/null +++ b/frontend/libs/studio-pure-functions/src/FileNameUtils/FilenameUtils.ts @@ -0,0 +1,42 @@ +import { StringUtils } from '@studio/pure-functions'; + +export class FileNameUtils { + /** + * Remove extension from filename. + * @param filename + * @returns filename without extension + */ + static removeExtension = (filename: string): string => { + const indexOfLastDot = filename.lastIndexOf('.'); + return indexOfLastDot < 0 ? filename : filename.substring(0, indexOfLastDot); + }; + + /** + * Remove json.schema or .xsd extension from filename. + * @param filename + * @returns filename without extension if the extension is ".schema.json" or ".xsd", otherwise the filename is returned unchanged. + */ + static removeSchemaExtension = (filename: string): string => + StringUtils.removeEnd(filename, '.schema.json', '.xsd'); + + /** + * Remove json.schema or .xsd extension from filename. + * @param filename + * @returns filename without extension if the extension is ".schema.json" or ".xsd", otherwise the filename is returned unchanged. + */ + static isXsdFile = (filename: string): boolean => filename.toLowerCase().endsWith('.xsd'); + + static extractFilename = (path: string): string => { + const indexOfLastSlash = path.lastIndexOf('/'); + return indexOfLastSlash < 0 ? path : path.substring(indexOfLastSlash + 1); + }; + + static removeFileNameFromPath = (path: string, excludeLastSlash: boolean = false): string => { + const fileName = this.extractFilename(path); + const indexOfLastSlash = path.lastIndexOf('/'); + return path.slice( + 0, + path.lastIndexOf(excludeLastSlash && indexOfLastSlash > 0 ? '/' + fileName : fileName), + ); + }; +} diff --git a/frontend/libs/studio-pure-functions/src/FileNameUtils/index.ts b/frontend/libs/studio-pure-functions/src/FileNameUtils/index.ts new file mode 100644 index 00000000000..179352dfda5 --- /dev/null +++ b/frontend/libs/studio-pure-functions/src/FileNameUtils/index.ts @@ -0,0 +1 @@ +export { FileNameUtils } from './FilenameUtils'; diff --git a/frontend/libs/studio-pure-functions/src/index.ts b/frontend/libs/studio-pure-functions/src/index.ts index a9414d7bece..566087a5f25 100644 --- a/frontend/libs/studio-pure-functions/src/index.ts +++ b/frontend/libs/studio-pure-functions/src/index.ts @@ -1,6 +1,7 @@ export * from './ArrayUtils'; export * from './BlobDownloader'; export * from './DateUtils'; +export * from './FileNameUtils'; export * from './NumberUtils'; export * from './ObjectUtils'; export * from './ScopedStorage'; diff --git a/frontend/packages/shared/src/components/GiteaHeader/VersionControlButtons/components/ShareChangesPopover/CommitAndPushContent/FileChangesInfoModal/FilePath/FilePath.tsx b/frontend/packages/shared/src/components/GiteaHeader/VersionControlButtons/components/ShareChangesPopover/CommitAndPushContent/FileChangesInfoModal/FilePath/FilePath.tsx index 586600945b2..b8a53179474 100644 --- a/frontend/packages/shared/src/components/GiteaHeader/VersionControlButtons/components/ShareChangesPopover/CommitAndPushContent/FileChangesInfoModal/FilePath/FilePath.tsx +++ b/frontend/packages/shared/src/components/GiteaHeader/VersionControlButtons/components/ShareChangesPopover/CommitAndPushContent/FileChangesInfoModal/FilePath/FilePath.tsx @@ -4,7 +4,7 @@ import cn from 'classnames'; import { convertPureGitDiffToUserFriendlyDiff } from './FilePathUtils'; import { ChevronRightIcon } from '@studio/icons'; import { useTranslation } from 'react-i18next'; -import { extractFilename, removeFileNameFromPath } from 'app-shared/utils/filenameUtils'; +import { FileNameUtils } from '@studio/pure-functions'; import type { QueryStatus } from '@tanstack/react-query'; export interface FilePathProps { @@ -20,7 +20,7 @@ export const FilePath = ({ filePath, diff, repoDiffStatus }: FilePathProps) => { return ; } - const fileName = extractFilename(filePath); + const fileName = FileNameUtils.extractFilename(filePath); const linesToRender = convertPureGitDiffToUserFriendlyDiff(diff); return ( @@ -56,8 +56,8 @@ type FormattedFilePathProps = { }; const FormattedFilePath = ({ filePath }: FormattedFilePathProps) => { - const fileName = extractFilename(filePath); - const filePathWithoutName = removeFileNameFromPath(filePath, true); + const fileName = FileNameUtils.extractFilename(filePath); + const filePathWithoutName = FileNameUtils.removeFileNameFromPath(filePath, true); return ( <> diff --git a/frontend/packages/shared/src/utils/filenameUtils.test.ts b/frontend/packages/shared/src/utils/filenameUtils.test.ts deleted file mode 100644 index 4756e8a6d1a..00000000000 --- a/frontend/packages/shared/src/utils/filenameUtils.test.ts +++ /dev/null @@ -1,84 +0,0 @@ -import { - extractFilename, - isXsdFile, - removeExtension, - removeFileNameFromPath, - removeSchemaExtension, -} from 'app-shared/utils/filenameUtils'; - -describe('filenameUtils', () => { - describe('removeExtension', () => { - it('Removes extension from filename if it exists', () => { - expect(removeExtension('filename.txt')).toEqual('filename'); - expect(removeExtension('filename.xsd')).toEqual('filename'); - expect(removeExtension('.abc')).toEqual(''); - expect(removeExtension('filename.schema.json')).toEqual('filename.schema'); - }); - - it('Returns entire input string if there is no extension', () => { - expect(removeExtension('filename')).toEqual('filename'); - }); - }); - - describe('removeSchemaExtension', () => { - it('Removes .schema.json extension from filename if it exists', () => { - expect(removeSchemaExtension('filename.schema.json')).toEqual('filename'); - expect(removeSchemaExtension('filename.SCHEMA.JSON')).toEqual('filename'); - }); - - it('Removes .xsd extension from filename if it exists', () => { - expect(removeSchemaExtension('filename.xsd')).toEqual('filename'); - expect(removeSchemaExtension('filename.XSD')).toEqual('filename'); - }); - - it('Returns entire input string if there is no .schema.json or .xsd extension', () => { - expect(removeSchemaExtension('filename.xml')).toEqual('filename.xml'); - }); - }); - - describe('isXsdFile', () => { - it('Returns true if filename has an XSD extension', () => { - expect(isXsdFile('filename.xsd')).toBe(true); - expect(isXsdFile('filename.XSD')).toBe(true); - }); - - it('Returns false if filename does not have an XSD extension', () => { - expect(isXsdFile('filename.schema.json')).toBe(false); - expect(isXsdFile('filename')).toBe(false); - }); - }); - - describe('extractFilename', () => { - it('Returns filename if path contains a slash', () => { - expect(extractFilename('/path/to/filename')).toEqual('filename'); - expect(extractFilename('/path/to/filename.json')).toEqual('filename.json'); - }); - - it('Returns path if path does not contain a slash', () => { - expect(extractFilename('filename')).toEqual('filename'); - expect(extractFilename('filename.json')).toEqual('filename.json'); - }); - }); - - describe('removeFileNameFromPath', () => { - it('Returns file path without file name', () => { - expect(removeFileNameFromPath('/path/to/filename')).toEqual('/path/to/'); - expect(removeFileNameFromPath('/path/to/filename.json')).toEqual('/path/to/'); - }); - - it('Returns file path without file name and last slash if "excludeLastSlash" is true', () => { - expect(removeFileNameFromPath('/path/to/filename', true)).toEqual('/path/to'); - expect(removeFileNameFromPath('/path/to/filename.json', true)).toEqual('/path/to'); - }); - - it('Returns empty string if path is only fileName', () => { - expect(removeFileNameFromPath('filename')).toEqual(''); - expect(removeFileNameFromPath('filename.json')).toEqual(''); - }); - - it('Returns empty string if path is only fileName and "excludeLastSlash" is true', () => { - expect(removeFileNameFromPath('filename', true)).toEqual(''); - expect(removeFileNameFromPath('filename.json', true)).toEqual(''); - }); - }); -}); diff --git a/frontend/packages/shared/src/utils/filenameUtils.ts b/frontend/packages/shared/src/utils/filenameUtils.ts deleted file mode 100644 index 36eefe53a8c..00000000000 --- a/frontend/packages/shared/src/utils/filenameUtils.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { StringUtils } from '@studio/pure-functions'; - -/** - * Remove extension from filename. - * @param filename - * @returns filename without extension - */ -export const removeExtension = (filename: string): string => { - const indexOfLastDot = filename.lastIndexOf('.'); - return indexOfLastDot < 0 ? filename : filename.substring(0, indexOfLastDot); -}; - -/** - * Remove json.schema or .xsd extension from filename. - * @param filename - * @returns filename without extension if the extension is ".schema.json" or ".xsd", otherwise the filename is returned unchanged. - */ -export const removeSchemaExtension = (filename: string): string => - StringUtils.removeEnd(filename, '.schema.json', '.xsd'); - -/** - * Check if filename has an XSD extension. - * @param filename - * @returns true if filename has an XSD extension, otherwise false. - */ -export const isXsdFile = (filename: string): boolean => filename.toLowerCase().endsWith('.xsd'); - -export const extractFilename = (path: string): string => { - const indexOfLastSlash = path.lastIndexOf('/'); - return indexOfLastSlash < 0 ? path : path.substring(indexOfLastSlash + 1); -}; - -export const removeFileNameFromPath = (path: string, excludeLastSlash: boolean = false): string => { - const fileName = extractFilename(path); - const indexOfLastSlash = path.lastIndexOf('/'); - return path.slice( - 0, - path.lastIndexOf(excludeLastSlash && indexOfLastSlash > 0 ? '/' + fileName : fileName), - ); -}; diff --git a/frontend/packages/text-editor/src/hooks/useLayoutNamesQuery.ts b/frontend/packages/text-editor/src/hooks/useLayoutNamesQuery.ts index df114bbb7b3..4bd2c47aa1e 100644 --- a/frontend/packages/text-editor/src/hooks/useLayoutNamesQuery.ts +++ b/frontend/packages/text-editor/src/hooks/useLayoutNamesQuery.ts @@ -2,13 +2,15 @@ import type { UseQueryResult } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query'; import { useServicesContext } from 'app-shared/contexts/ServicesContext'; import { QueryKey } from 'app-shared/types/QueryKey'; -import { removeExtension } from 'app-shared/utils/filenameUtils'; +import { FileNameUtils } from '@studio/pure-functions'; export const useLayoutNamesQuery = (owner, app): UseQueryResult => { const { getLayoutNames } = useServicesContext(); return useQuery({ queryKey: [QueryKey.LayoutNames, owner, app], queryFn: () => - getLayoutNames(owner, app).then((layoutNames) => layoutNames.map(removeExtension)), + getLayoutNames(owner, app).then((layoutNames) => + layoutNames.map(FileNameUtils.removeExtension), + ), }); }; diff --git a/frontend/packages/ux-editor/src/components/config/editModal/EditImage/LocalImage/ImportImage/AddImageFromLibrary/ChooseFromLibrary/ImageLibraryPreview.tsx b/frontend/packages/ux-editor/src/components/config/editModal/EditImage/LocalImage/ImportImage/AddImageFromLibrary/ChooseFromLibrary/ImageLibraryPreview.tsx index 128e770e336..9f6e6391824 100644 --- a/frontend/packages/ux-editor/src/components/config/editModal/EditImage/LocalImage/ImportImage/AddImageFromLibrary/ChooseFromLibrary/ImageLibraryPreview.tsx +++ b/frontend/packages/ux-editor/src/components/config/editModal/EditImage/LocalImage/ImportImage/AddImageFromLibrary/ChooseFromLibrary/ImageLibraryPreview.tsx @@ -1,7 +1,7 @@ import React from 'react'; import classes from './ChooseFromLibrary.module.css'; import { imagePath } from 'app-shared/api/paths'; -import { extractFilename } from 'app-shared/utils/filenameUtils'; +import { FileNameUtils } from '@studio/pure-functions'; import { useStudioEnvironmentParams } from 'app-shared/hooks/useStudioEnvironmentParams'; import { WWWROOT_FILE_PATH } from '../../../../../EditImage/constants'; import { StudioCard, StudioHeading } from '@studio/components'; @@ -42,6 +42,7 @@ const ImageFromLibrary = ({ onAddImageReference, imageSource, }: ImageFromLibraryProps) => { + const fileName = FileNameUtils.extractFilename(imageFilePath); // The img component requires an alt which we can set to be the descriptions from the metadata in the library when this is available. // TODO: Add description when we know how to store them. See analysis issue: https://github.com/Altinn/altinn-studio/issues/13346 return ( @@ -51,12 +52,8 @@ const ImageFromLibrary = ({ {imageFilePath} - - {extractFilename(imageFilePath)} + + {fileName} diff --git a/frontend/packages/ux-editor/src/components/config/editModal/EditImage/LocalImage/PreviewImageSummary/PreviewFileInfo.tsx b/frontend/packages/ux-editor/src/components/config/editModal/EditImage/LocalImage/PreviewImageSummary/PreviewFileInfo.tsx index 36be4f85699..5b7bf2ae8ca 100644 --- a/frontend/packages/ux-editor/src/components/config/editModal/EditImage/LocalImage/PreviewImageSummary/PreviewFileInfo.tsx +++ b/frontend/packages/ux-editor/src/components/config/editModal/EditImage/LocalImage/PreviewImageSummary/PreviewFileInfo.tsx @@ -1,7 +1,7 @@ import React from 'react'; import classes from './PreviewFileInfo.module.css'; import { StudioParagraph } from '@studio/components'; -import { extractFilename } from 'app-shared/utils/filenameUtils'; +import { FileNameUtils } from '@studio/pure-functions'; interface PreviewFileInfoProps { existingImageUrl: string; @@ -11,7 +11,7 @@ export const PreviewFileInfo = ({ existingImageUrl }: PreviewFileInfoProps) => { return (
- {extractFilename(existingImageUrl)} + {FileNameUtils.extractFilename(existingImageUrl)}
); diff --git a/frontend/packages/ux-editor/src/components/config/editModal/EditOptions/OptionTabs/EditOptionList/EditOptionList.tsx b/frontend/packages/ux-editor/src/components/config/editModal/EditOptions/OptionTabs/EditOptionList/EditOptionList.tsx index 79dbde88e34..101659260be 100644 --- a/frontend/packages/ux-editor/src/components/config/editModal/EditOptions/OptionTabs/EditOptionList/EditOptionList.tsx +++ b/frontend/packages/ux-editor/src/components/config/editModal/EditOptions/OptionTabs/EditOptionList/EditOptionList.tsx @@ -9,7 +9,7 @@ import { altinnDocsUrl } from 'app-shared/ext-urls'; import { FormField } from '../../../../../FormField'; import { useStudioEnvironmentParams } from 'app-shared/hooks/useStudioEnvironmentParams'; import type { SelectionComponentType } from '../../../../../../types/FormComponent'; -import { removeExtension } from 'app-shared/utils/filenameUtils'; +import { FileNameUtils } from '@studio/pure-functions'; import { findFileNameError } from './utils/findFileNameError'; import type { FileNameError } from './utils/findFileNameError'; import type { AxiosError } from 'axios'; @@ -53,7 +53,7 @@ export function EditOptionList({ const handleUpload = (file: File) => { uploadOptionList(file, { onSuccess: () => { - handleOptionsIdChange(removeExtension(file.name)); + handleOptionsIdChange(FileNameUtils.removeExtension(file.name)); toast.success(t('ux_editor.modal_properties_code_list_upload_success')); }, onError: (error: AxiosError) => { diff --git a/frontend/packages/ux-editor/src/components/config/editModal/EditOptions/OptionTabs/EditOptionList/utils/findFileNameError.ts b/frontend/packages/ux-editor/src/components/config/editModal/EditOptions/OptionTabs/EditOptionList/utils/findFileNameError.ts index 307307991a0..f13182f7336 100644 --- a/frontend/packages/ux-editor/src/components/config/editModal/EditOptions/OptionTabs/EditOptionList/utils/findFileNameError.ts +++ b/frontend/packages/ux-editor/src/components/config/editModal/EditOptions/OptionTabs/EditOptionList/utils/findFileNameError.ts @@ -1,4 +1,4 @@ -import { removeExtension } from 'app-shared/utils/filenameUtils'; +import { FileNameUtils } from '@studio/pure-functions'; export type FileNameError = 'invalidFileName' | 'fileExists'; @@ -6,7 +6,7 @@ export const findFileNameError = ( optionListIds: string[], fileName: string, ): FileNameError | null => { - const fileNameWithoutExtension = removeExtension(fileName); + const fileNameWithoutExtension = FileNameUtils.removeExtension(fileName); if (!isFilenameValid(fileNameWithoutExtension)) { return 'invalidFileName';