diff --git a/packages/threat-composer-app/src/containers/WorkspaceSelector/index.tsx b/packages/threat-composer-app/src/containers/WorkspaceSelector/index.tsx index fc262a9e..ad2c9520 100644 --- a/packages/threat-composer-app/src/containers/WorkspaceSelector/index.tsx +++ b/packages/threat-composer-app/src/containers/WorkspaceSelector/index.tsx @@ -30,7 +30,7 @@ const WorkspaceSelector = () => { const navigate = useNavigateView(); return navigate(ROUTE_VIEW_THREAT_MODEL)} singletonMode={appMode === APP_MODE_BROWSER_EXTENSION || appMode === APP_MODE_IDE_EXTENSION} singletonPrimaryActionButtonConfig={appMode === APP_MODE_IDE_EXTENSION ? { diff --git a/packages/threat-composer/src/configs/content.ts b/packages/threat-composer/src/configs/content.ts index 6aedc057..d9fded79 100644 --- a/packages/threat-composer/src/configs/content.ts +++ b/packages/threat-composer/src/configs/content.ts @@ -15,4 +15,5 @@ ******************************************************************************************************************** */ export const REGEX_CONTENT_NOT_HTML_TAG = /(<.*?>)/i; export const REGEX_CONTENT_IMAGE_URL = /^https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&\/\/=]*)/i; -export const REGEX_CONTENT_IMAGE_BASE64 = /^(?:data:image\/[a-z+]{3,};base64,)(?:[A-Za-z0-9+\/]{4})*(?:[A-Za-z0-9+\/]{2}==|[A-Za-z0-9+\/]{3}=)?$/gi; \ No newline at end of file +export const REGEX_CONTENT_IMAGE_BASE64 = /^(?:data:image\/[a-z+]{3,};base64,)(?:[A-Za-z0-9+\/]{4})*(?:[A-Za-z0-9+\/]{2}==|[A-Za-z0-9+\/]{3}=)?$/gi; +export const REGEX_WORKSPACE_NAME = /^[A-Za-z0-9-_# ]*$/; \ No newline at end of file diff --git a/packages/threat-composer/src/contexts/WorkspacesContext/components/LocalStateContextProvider/index.tsx b/packages/threat-composer/src/contexts/WorkspacesContext/components/LocalStateContextProvider/index.tsx index 6d7a92fa..71aa48f2 100644 --- a/packages/threat-composer/src/contexts/WorkspacesContext/components/LocalStateContextProvider/index.tsx +++ b/packages/threat-composer/src/contexts/WorkspacesContext/components/LocalStateContextProvider/index.tsx @@ -14,11 +14,11 @@ limitations under the License. ******************************************************************************************************************** */ import { FC, useState } from 'react'; -import { DEFAULT_WORKSPACE_ID } from '../../../../configs/constants'; import { Workspace } from '../../../../customTypes'; import { useWorkspaceExamplesContext } from '../../../WorkspaceExamplesContext'; import { WorkspacesContext } from '../../context'; import { WorkspacesContextProviderProps } from '../../types'; +import useCurrentWorkspace from '../../useCurrentWorkspace'; import useWorkspaces from '../../useWorkspaces'; const WorkspacesLocalStateContextProvider: FC = ({ @@ -31,25 +31,9 @@ const WorkspacesLocalStateContextProvider: FC = const [workspaceList, setWorkspaceList] = useState([]); - const [currentWorkspace, setCurrentWorkspace] = useState(() => { - if (workspaceName) { // If the workspaceName is specified by outside scope (e.g. Url), return the workspace specified by the id - if (workspaceName === DEFAULT_WORKSPACE_ID) { - return null; - } + const [lastWorkspace, setCurrentWorkspace] = useState(null); - const foundWorkspace = workspaceList.find(x => x.name === workspaceName); - if (foundWorkspace) { - return foundWorkspace; - } - - const foundWorkspaceExample = workspaceExamples.find(x => x.name === workspaceName); - if (foundWorkspaceExample) { - return foundWorkspaceExample; - } - } - - return null; - }); + const currentWorkspace = useCurrentWorkspace(lastWorkspace, workspaceName, workspaceList, workspaceExamples, onWorkspaceChanged); const { handleSwitchWorkspace, diff --git a/packages/threat-composer/src/contexts/WorkspacesContext/components/LocalStorageContextProvider/index.tsx b/packages/threat-composer/src/contexts/WorkspacesContext/components/LocalStorageContextProvider/index.tsx index a6f6af73..d3099504 100644 --- a/packages/threat-composer/src/contexts/WorkspacesContext/components/LocalStorageContextProvider/index.tsx +++ b/packages/threat-composer/src/contexts/WorkspacesContext/components/LocalStorageContextProvider/index.tsx @@ -13,15 +13,15 @@ See the License for the specific language governing permissions and limitations under the License. ******************************************************************************************************************** */ -import { FC, useMemo } from 'react'; +import { FC } from 'react'; import useLocalStorageState from 'use-local-storage-state'; -import { DEFAULT_WORKSPACE_ID } from '../../../../configs/constants'; import { LOCAL_STORAGE_KEY_CURRENT_WORKSPACE, LOCAL_STORAGE_KEY_WORKSPACE_LIST } from '../../../../configs/localStorageKeys'; import { Workspace } from '../../../../customTypes'; import WorkspacesMigration from '../../../../migrations/WorkspacesMigration'; import { useWorkspaceExamplesContext } from '../../../WorkspaceExamplesContext'; import { WorkspacesContext } from '../../context'; import { WorkspacesContextProviderProps } from '../../types'; +import useCurrentWorkspace from '../../useCurrentWorkspace'; import useWorkspaces from '../../useWorkspaces'; const WorkspacesLocalStorageContextProvider: FC = ({ @@ -40,25 +40,7 @@ const WorkspacesLocalStorageContextProvider: FC const { workspaceExamples } = useWorkspaceExamplesContext(); - const currentWorkspace = useMemo(() => { - if (workspaceName) { // If the workspaceName is specified by outside scope (e.g. Url), return the workspace specified by the id - if (workspaceName === DEFAULT_WORKSPACE_ID) { - return null; - } - - const foundWorkspace = workspaceList.find(x => x.name === workspaceName); - if (foundWorkspace) { - return foundWorkspace; - } - - const foundWorkspaceExample = workspaceExamples.find(x => x.name === workspaceName); - if (foundWorkspaceExample) { - return foundWorkspaceExample; - } - } - - return lastWorkspace; - }, [lastWorkspace, workspaceName, workspaceExamples, workspaceList]); + const currentWorkspace = useCurrentWorkspace(lastWorkspace, workspaceName, workspaceList, workspaceExamples, onWorkspaceChanged); const { handleSwitchWorkspace, diff --git a/packages/threat-composer/src/contexts/WorkspacesContext/useCurrentWorkspace.ts b/packages/threat-composer/src/contexts/WorkspacesContext/useCurrentWorkspace.ts new file mode 100644 index 00000000..c6f4aa72 --- /dev/null +++ b/packages/threat-composer/src/contexts/WorkspacesContext/useCurrentWorkspace.ts @@ -0,0 +1,60 @@ +/** ******************************************************************************************************************* + Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the "License"). + You may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + ******************************************************************************************************************** */ +import { useEffect, useMemo } from 'react'; +import { DEFAULT_WORKSPACE_ID } from '../../configs'; +import { Workspace } from '../../customTypes'; + +const useCurrentWorkspace = ( + lastWorkspace: Workspace | null, + workspaceName: string | undefined, + workspaceList: Workspace[], + workspaceExamples: Workspace[], + onWorkspaceChanged?: (workspaceId: string) => void) => { + const [currentWorkspace, navigateToWorkspace] = useMemo(() => { + if (workspaceName) { // If the workspaceName is specified by outside scope (e.g. Url), return the workspace specified by the id + if (workspaceName === DEFAULT_WORKSPACE_ID) { + return [null, null]; + } + + const foundWorkspace = workspaceList.find(x => x.name === workspaceName); + if (foundWorkspace) { + return [foundWorkspace, null]; + } + + const foundWorkspaceExample = workspaceExamples.find(x => x.name === workspaceName); + if (foundWorkspaceExample) { + return [foundWorkspaceExample, null]; + } + + // Unable to located the workspace from workspace name, redirect to last visited workspace or default workspace. + return [null, lastWorkspace || { + name: DEFAULT_WORKSPACE_ID, + }]; + } + + return [lastWorkspace, null]; + }, [lastWorkspace, workspaceName, workspaceExamples, workspaceList]); + + useEffect(() => { + if (navigateToWorkspace) { + onWorkspaceChanged?.(navigateToWorkspace?.name || DEFAULT_WORKSPACE_ID); + } + }, [navigateToWorkspace, onWorkspaceChanged]); + + return currentWorkspace; +}; + +export default useCurrentWorkspace; \ No newline at end of file diff --git a/packages/threat-composer/src/customTypes/workspaces.ts b/packages/threat-composer/src/customTypes/workspaces.ts index 6cda8788..c3f86c22 100644 --- a/packages/threat-composer/src/customTypes/workspaces.ts +++ b/packages/threat-composer/src/customTypes/workspaces.ts @@ -16,6 +16,7 @@ import { z } from 'zod'; import { MetadataNodeSchema } from './entities'; import { + REGEX_WORKSPACE_NAME, SINGLE_FIELD_INPUT_SMALL_MAX_LENGTH, STORAGE_LOCAL_STATE, STORAGE_LOCAL_STORAGE, @@ -23,7 +24,7 @@ import { export const WorkspaceSchema = z.object({ id: z.string().length(36), - name: z.string().max(SINGLE_FIELD_INPUT_SMALL_MAX_LENGTH), + name: z.string().max(SINGLE_FIELD_INPUT_SMALL_MAX_LENGTH).regex(REGEX_WORKSPACE_NAME, `Invalid. Workspace name pattern ${REGEX_WORKSPACE_NAME}`), storageType: z.enum([STORAGE_LOCAL_STATE, STORAGE_LOCAL_STORAGE]).optional(), metadata: MetadataNodeSchema.optional(), });