diff --git a/azure.yaml b/azure.yaml index 8440175d..94b5098d 100644 --- a/azure.yaml +++ b/azure.yaml @@ -15,4 +15,11 @@ hooks: windows: shell: pwsh run: $timestamp = Get-Date -Format "yyyyMMdd-HHmmss"; $logFile = "azd_preprovision_$timestamp.log"; ./infra/scripts/docker-build.ps1 $env:AZURE_SUBSCRIPTION_ID $env:AZURE_ENV_NAME $env:AZURE_LOCATION $env:AZURE_RESOURCE_GROUP $env:USE_LOCAL_BUILD *>&1 | Tee-Object -FilePath $logFile + postprovision: + posix: + shell: sh + run: sed -i 's/\r$//' ./infra/scripts/post_deployment.sh; ./infra/scripts/post_deployment.sh + windows: + shell: pwsh + run: ./infra/scripts/post_deployment.ps1 diff --git a/docs/ConfigureAppAuthentication.md b/docs/ConfigureAppAuthentication.md index a8fe2936..835fe422 100644 --- a/docs/ConfigureAppAuthentication.md +++ b/docs/ConfigureAppAuthentication.md @@ -73,13 +73,13 @@ We will add Microsoft Entra ID as an authentication provider to API and Web Appl 3. Grab Scope Name for Impersonation - Select **Expose an API** in the left menu. Copy the Scope name, then paste it in some temporary place. - The copied text will be used for Web Application Environment variable - **APP_MSAL_AUTH_SCOPE**. + The copied text will be used for Web Application Environment variable - **APP_WEB_SCOPE**. ![configure_app_registration_web_9](./Images/configure_app_registration_web_9.png) 4. Grab Client Id for Web App - Select **Overview** in the left menu. Copy the Client Id, then paste it in some temporary place. - The copied text will be used for Web Application Environment variable - **APP_MSAL_AUTH_CLIENT_ID**. + The copied text will be used for Web Application Environment variable - **APP_WEB_CLIENT_ID**. ![configure_app_registration_web_10](./Images/configure_app_registration_web_10.png) ## Step 3: Configure Application Registration - API Application @@ -90,7 +90,7 @@ We will add Microsoft Entra ID as an authentication provider to API and Web Appl ![configure_app_registration_api_1](./Images/configure_app_registration_api_1.png) - Select **Expose an API** in the left menu. Copy the Scope name, then paste it in some temporary place. - The copied text will be used for Web Application Environment variable - **APP_MSAL_TOKEN_SCOPE**. + The copied text will be used for Web Application Environment variable - **APP_API_SCOPE**. ![configure_app_registration_api_2](./Images/configure_app_registration_api_2.png) ## Step 4: Add Web Application's Client Id to Allowed Client Applications List in API Application Registration @@ -112,7 +112,7 @@ Now, we will edit and deploy the Web Application Container with updated Environm 1. Select **Containers** menu under **Application**. Then click **Environment variables** tab. ![update_env_app_1_1](./Images/update_env_app_1_1.png) -2. Update 3 values which were taken in previous steps for **APP_MSAL_AUTH_CLIENT_ID**, **APP_MSAL_AUTH_SCOPE**, **APP_MSAL_TOKEN_SCOPE**. +2. Update 3 values which were taken in previous steps for **APP_WEB_CLIENT_ID**, **APP_WEB_SCOPE**, **APP_API_SCOPE**. Click on **Save as a new revision**. The updated revision will be activated soon. diff --git a/docs/Images/update_env_app_1_1.png b/docs/Images/update_env_app_1_1.png index eca1ac11..4ab91d33 100644 Binary files a/docs/Images/update_env_app_1_1.png and b/docs/Images/update_env_app_1_1.png differ diff --git a/infra/container_app/deploy_container_app_api_web.bicep b/infra/container_app/deploy_container_app_api_web.bicep index 6a377b5f..9c4c23de 100644 --- a/infra/container_app/deploy_container_app_api_web.bicep +++ b/infra/container_app/deploy_container_app_api_web.bicep @@ -131,23 +131,23 @@ module containerAppWeb 'deploy_container_app.bicep' = { value: containerAppApiEndpoint } { - name: 'APP_MSAL_AUTH_CLIENT_ID' + name: 'APP_WEB_CLIENT_ID' value: '' } { - name: 'APP_MSAL_AUTH_AUTHORITY' + name: 'APP_WEB_AUTHORITY' value: '${environment().authentication.loginEndpoint}/${tenant().tenantId}' } { - name: 'APP_MSAL_AUTH_SCOPE' + name: 'APP_WEB_SCOPE' value: '' } { - name: 'APP_MSAL_TOKEN_SCOPE' + name: 'APP_API_SCOPE' value: '' } { - name: 'APP_ISLOGS_ENABLED' + name: 'APP_CONSOLE_LOG_ENABLED' value: 'false' } ] diff --git a/infra/deploy_container_registry.bicep b/infra/deploy_container_registry.bicep index 895e24fe..021fb7b4 100644 --- a/infra/deploy_container_registry.bicep +++ b/infra/deploy_container_registry.bicep @@ -3,18 +3,18 @@ targetScope = 'resourceGroup' param environmentName string - + var uniqueId = toLower(uniqueString(subscription().id, environmentName, resourceGroup().location)) var solutionName = 'cps-${padLeft(take(uniqueId, 12), 12, '0')}' - + var containerNameCleaned = replace('cr${solutionName }', '-', '') - + @description('Provide a location for the registry.') param location string = resourceGroup().location - + @description('Provide a tier of your Azure Container Registry.') -param acrSku string = 'Premium' - +param acrSku string = 'Basic' + resource containerRegistry 'Microsoft.ContainerRegistry/registries@2021-09-01' = { name: containerNameCleaned location: location @@ -22,30 +22,12 @@ resource containerRegistry 'Microsoft.ContainerRegistry/registries@2021-09-01' = name: acrSku } properties: { - adminUserEnabled: true - dataEndpointEnabled: false - networkRuleBypassOptions: 'AzureServices' - networkRuleSet: { - defaultAction: 'Allow' - } - policies: { - quarantinePolicy: { - status: 'disabled' - } - retentionPolicy: { - status: 'enabled' - days: 7 - } - trustPolicy: { - status: 'disabled' - type: 'Notary' - } - } publicNetworkAccess: 'Enabled' zoneRedundancy: 'Disabled' } } - + output createdAcrName string = containerNameCleaned output createdAcrId string = containerRegistry.id output acrEndpoint string = containerRegistry.properties.loginServer + \ No newline at end of file diff --git a/infra/deploy_keyvault.bicep b/infra/deploy_keyvault.bicep index 4cfbf2e0..bf339354 100644 --- a/infra/deploy_keyvault.bicep +++ b/infra/deploy_keyvault.bicep @@ -32,7 +32,6 @@ resource keyVault 'Microsoft.KeyVault/vaults@2022-07-01' = { enabledForDiskEncryption: true enabledForTemplateDeployment: true enableRbacAuthorization: true - enablePurgeProtection: true publicNetworkAccess: 'enabled' // networkAcls: { // bypass: 'AzureServices' diff --git a/infra/main.bicep b/infra/main.bicep index af99bd02..fcddcf6f 100644 --- a/infra/main.bicep +++ b/infra/main.bicep @@ -41,6 +41,9 @@ param gptModelName string = 'gpt-4o' @minLength(1) @description('Version of the GPT model to deploy:') +@allowed([ + '2024-08-06' +]) param gptModelVersion string = '2024-08-06' //var gptModelVersion = '2024-02-15-preview' diff --git a/infra/main.json b/infra/main.json index 38822f7d..4debab16 100644 --- a/infra/main.json +++ b/infra/main.json @@ -5,7 +5,7 @@ "_generator": { "name": "bicep", "version": "0.34.44.8038", - "templateHash": "17985360808056860425" + "templateHash": "2631796362162534903" } }, "parameters": { @@ -67,6 +67,9 @@ "gptModelVersion": { "type": "string", "defaultValue": "2024-08-06", + "allowedValues": [ + "2024-08-06" + ], "minLength": 1, "metadata": { "description": "Version of the GPT model to deploy:" @@ -480,7 +483,7 @@ "_generator": { "name": "bicep", "version": "0.34.44.8038", - "templateHash": "7568462549649877267" + "templateHash": "17770758516688495068" } }, "parameters": { @@ -526,7 +529,6 @@ "enabledForDiskEncryption": true, "enabledForTemplateDeployment": true, "enableRbacAuthorization": true, - "enablePurgeProtection": true, "publicNetworkAccess": "enabled", "sku": { "family": "A", @@ -672,7 +674,7 @@ "_generator": { "name": "bicep", "version": "0.34.44.8038", - "templateHash": "10286514074548439078" + "templateHash": "18372681746235366113" } }, "parameters": { @@ -688,7 +690,7 @@ }, "acrSku": { "type": "string", - "defaultValue": "Premium", + "defaultValue": "Basic", "metadata": { "description": "Provide a tier of your Azure Container Registry." } @@ -709,25 +711,6 @@ "name": "[parameters('acrSku')]" }, "properties": { - "adminUserEnabled": true, - "dataEndpointEnabled": false, - "networkRuleBypassOptions": "AzureServices", - "networkRuleSet": { - "defaultAction": "Allow" - }, - "policies": { - "quarantinePolicy": { - "status": "disabled" - }, - "retentionPolicy": { - "status": "enabled", - "days": 7 - }, - "trustPolicy": { - "status": "disabled", - "type": "Notary" - } - }, "publicNetworkAccess": "Enabled", "zoneRedundancy": "Disabled" } @@ -1655,7 +1638,7 @@ "_generator": { "name": "bicep", "version": "0.34.44.8038", - "templateHash": "15815884747026956332" + "templateHash": "1111747132207169107" } }, "parameters": { @@ -2392,23 +2375,23 @@ "value": "[parameters('containerAppApiEndpoint')]" }, { - "name": "APP_MSAL_AUTH_CLIENT_ID", + "name": "APP_WEB_CLIENT_ID", "value": "" }, { - "name": "APP_MSAL_AUTH_AUTHORITY", + "name": "APP_WEB_AUTHORITY", "value": "[format('{0}/{1}', environment().authentication.loginEndpoint, tenant().tenantId)]" }, { - "name": "APP_MSAL_AUTH_SCOPE", + "name": "APP_WEB_SCOPE", "value": "" }, { - "name": "APP_MSAL_TOKEN_SCOPE", + "name": "APP_API_SCOPE", "value": "" }, { - "name": "APP_ISLOGS_ENABLED", + "name": "APP_CONSOLE_LOG_ENABLED", "value": "false" } ] @@ -3178,7 +3161,7 @@ "_generator": { "name": "bicep", "version": "0.34.44.8038", - "templateHash": "15815884747026956332" + "templateHash": "1111747132207169107" } }, "parameters": { @@ -3915,23 +3898,23 @@ "value": "[parameters('containerAppApiEndpoint')]" }, { - "name": "APP_MSAL_AUTH_CLIENT_ID", + "name": "APP_WEB_CLIENT_ID", "value": "" }, { - "name": "APP_MSAL_AUTH_AUTHORITY", + "name": "APP_WEB_AUTHORITY", "value": "[format('{0}/{1}', environment().authentication.loginEndpoint, tenant().tenantId)]" }, { - "name": "APP_MSAL_AUTH_SCOPE", + "name": "APP_WEB_SCOPE", "value": "" }, { - "name": "APP_MSAL_TOKEN_SCOPE", + "name": "APP_API_SCOPE", "value": "" }, { - "name": "APP_ISLOGS_ENABLED", + "name": "APP_CONSOLE_LOG_ENABLED", "value": "false" } ] diff --git a/infra/scripts/post_deployment.ps1 b/infra/scripts/post_deployment.ps1 new file mode 100644 index 00000000..7d678b75 --- /dev/null +++ b/infra/scripts/post_deployment.ps1 @@ -0,0 +1,49 @@ +# Stop script on any error +$ErrorActionPreference = "Stop" + +Write-Host "🔍 Fetching container app info from azd environment..." + +# Load values from azd env +$CONTAINER_WEB_APP_NAME = azd env get-value CONTAINER_WEB_APP_NAME +$CONTAINER_WEB_APP_FQDN = azd env get-value CONTAINER_WEB_APP_FQDN + +$CONTAINER_API_APP_NAME = azd env get-value CONTAINER_API_APP_NAME +$CONTAINER_API_APP_FQDN = azd env get-value CONTAINER_API_APP_FQDN + +# Get subscription and resource group (assuming same for both) +$SUBSCRIPTION_ID = azd env get-value AZURE_SUBSCRIPTION_ID +$RESOURCE_GROUP = azd env get-value AZURE_RESOURCE_GROUP + +# Construct Azure Portal URLs +$WEB_APP_PORTAL_URL = "https://portal.azure.com/#resource/subscriptions/$SUBSCRIPTION_ID/resourceGroups/$RESOURCE_GROUP/providers/Microsoft.App/containerApps/$CONTAINER_WEB_APP_NAME" +$API_APP_PORTAL_URL = "https://portal.azure.com/#resource/subscriptions/$SUBSCRIPTION_ID/resourceGroups/$RESOURCE_GROUP/providers/Microsoft.App/containerApps/$CONTAINER_API_APP_NAME" + +# Get the current script's directory +$ScriptDir = $PSScriptRoot + +# Navigate from infra/scripts → root → src/api/data/data.sh +$DataScriptPath = Join-Path $ScriptDir "..\..\src\ContentProcessorAPI\samples\schemas" + +# Resolve to an absolute path +$FullPath = Resolve-Path $DataScriptPath + +# Output +Write-Host "" +Write-Host "🧭 Web App Details:" +Write-Host " ✅ Name: $CONTAINER_WEB_APP_NAME" +Write-Host " 🌐 Endpoint: https://$CONTAINER_WEB_APP_FQDN" +Write-Host " 🔗 Portal URL: $WEB_APP_PORTAL_URL" + +Write-Host "" +Write-Host "🧭 API App Details:" +Write-Host " ✅ Name: $CONTAINER_API_APP_NAME" +Write-Host " 🌐 Endpoint: https://$CONTAINER_API_APP_FQDN" +Write-Host " 🔗 Portal URL: $API_APP_PORTAL_URL" + +Write-Host "" +Write-Host "📦 Follow Next steps to import Schemas:" +Write-Host "👉 Run the following commands in your terminal:" +$CurrentPath = Get-Location +Write-Host "" +Write-Host " cd $FullPath" +Write-Host " ./register_schema.ps1 https://$CONTAINER_API_APP_FQDN/schemavault/ schema_info_ps1.json" diff --git a/infra/scripts/post_deployment.sh b/infra/scripts/post_deployment.sh new file mode 100644 index 00000000..7b6a9171 --- /dev/null +++ b/infra/scripts/post_deployment.sh @@ -0,0 +1,51 @@ +#!/bin/bash + +# Stop script on any error +set -e + +echo "🔍 Fetching container app info from azd environment..." + +# Load values from azd env +CONTAINER_WEB_APP_NAME=$(azd env get-value CONTAINER_WEB_APP_NAME) +CONTAINER_WEB_APP_FQDN=$(azd env get-value CONTAINER_WEB_APP_FQDN) + +CONTAINER_API_APP_NAME=$(azd env get-value CONTAINER_API_APP_NAME) +CONTAINER_API_APP_FQDN=$(azd env get-value CONTAINER_API_APP_FQDN) + +# Get subscription and resource group (assuming same for both) +SUBSCRIPTION_ID=$(azd env get-value AZURE_SUBSCRIPTION_ID) +RESOURCE_GROUP=$(azd env get-value AZURE_RESOURCE_GROUP) + +# Construct Azure Portal URLs +WEB_APP_PORTAL_URL="https://portal.azure.com/#resource/subscriptions/$SUBSCRIPTION_ID/resourceGroups/$RESOURCE_GROUP/providers/Microsoft.App/containerApps/$CONTAINER_WEB_APP_NAME" +API_APP_PORTAL_URL="https://portal.azure.com/#resource/subscriptions/$SUBSCRIPTION_ID/resourceGroups/$RESOURCE_GROUP/providers/Microsoft.App/containerApps/$CONTAINER_API_APP_NAME" + +# Get the directory where this script is located +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +# Go from infra/scripts → root → src +DATA_SCRIPT_PATH="$SCRIPT_DIR/../../src/ContentProcessorAPI/samples/schemas" + +# Normalize the path (optional, in case of ../..) +DATA_SCRIPT_PATH="$(realpath "$DATA_SCRIPT_PATH")" + +# Output +echo "" +echo "🧭 Web App Details:" +echo " ✅ Name: $CONTAINER_WEB_APP_NAME" +echo " 🌐 Endpoint: https://$CONTAINER_WEB_APP_FQDN" +echo " 🔗 Portal URL: $WEB_APP_PORTAL_URL" + +echo "" +echo "🧭 API App Details:" +echo " ✅ Name: $CONTAINER_API_APP_NAME" +echo " 🌐 Endpoint: https://$CONTAINER_API_APP_FQDN" +echo " 🔗 Portal URL: $API_APP_PORTAL_URL" + +echo "" +echo "📦 Follow Next steps to import Schemas:" +echo "👉 Run the following commands in your terminal:" +echo "" + +echo " cd \"$DATA_SCRIPT_PATH\"" +echo " ./register_schema.sh https://$CONTAINER_API_APP_FQDN/schemavault/ schema_info_sh.json" diff --git a/src/ContentProcessorAPI/app/routers/contentprocessor.py b/src/ContentProcessorAPI/app/routers/contentprocessor.py index e9f197bd..115a32f0 100644 --- a/src/ContentProcessorAPI/app/routers/contentprocessor.py +++ b/src/ContentProcessorAPI/app/routers/contentprocessor.py @@ -517,5 +517,5 @@ async def delete_processed_file( return ContentResultDelete( status="Success" if deleted_file else "Failed", process_id=deleted_file.process_id if deleted_file else "", - message="" if deleted_file else "This record no longer exists. Please refresh the page." + message="" if deleted_file else "This record no longer exists. Please refresh." ) \ No newline at end of file diff --git a/src/ContentProcessorWeb/.env b/src/ContentProcessorWeb/.env index d9bf079d..ecf8f866 100644 --- a/src/ContentProcessorWeb/.env +++ b/src/ContentProcessorWeb/.env @@ -1,13 +1,13 @@ REACT_APP_API_BASE_URL=APP_API_BASE_URL -REACT_APP_MSAL_AUTH_CLIENT_ID = APP_MSAL_AUTH_CLIENT_ID -REACT_APP_MSAL_AUTH_AUTHORITY = APP_MSAL_AUTH_AUTHORITY +REACT_APP_WEB_CLIENT_ID = APP_WEB_CLIENT_ID +REACT_APP_WEB_AUTHORITY = APP_WEB_AUTHORITY -REACT_APP_MSAL_REDIRECT_URL = "/" +REACT_APP_REDIRECT_URL = "/" -REACT_APP_MSAL_POST_REDIRECT_URL = "/" +REACT_APP_POST_REDIRECT_URL = "/" -REACT_APP_MSAL_AUTH_SCOPE = APP_MSAL_AUTH_SCOPE +REACT_APP_WEB_SCOPE = APP_WEB_SCOPE -REACT_APP_MSAL_TOKEN_SCOPE = APP_MSAL_TOKEN_SCOPE -REACT_APP_ISLOGS_ENABLED = APP_ISLOGS_ENABLED \ No newline at end of file +REACT_APP_API_SCOPE = APP_API_SCOPE +REACT_APP_CONSOLE_LOG_ENABLED = APP_CONSOLE_LOG_ENABLED \ No newline at end of file diff --git a/src/ContentProcessorWeb/config-overrides.js b/src/ContentProcessorWeb/config-overrides.js index 3f39c5ad..ccd6c3d9 100644 --- a/src/ContentProcessorWeb/config-overrides.js +++ b/src/ContentProcessorWeb/config-overrides.js @@ -1,7 +1,5 @@ const { override, addWebpackModuleRule, addWebpackResolve } = require('customize-cra'); -console.log('Applying config-overrides.js...'); - module.exports = override( addWebpackModuleRule({ test: /\.md$/, diff --git a/src/ContentProcessorWeb/src/Components/UploadContent/UploadFilesModal.tsx b/src/ContentProcessorWeb/src/Components/UploadContent/UploadFilesModal.tsx index b2872a4b..80f7f0a6 100644 --- a/src/ContentProcessorWeb/src/Components/UploadContent/UploadFilesModal.tsx +++ b/src/ContentProcessorWeb/src/Components/UploadContent/UploadFilesModal.tsx @@ -9,12 +9,20 @@ import { import { Button } from "@fluentui/react-button"; import { Field, ProgressBar, makeStyles } from "@fluentui/react-components"; import { useDispatch, useSelector, shallowEqual } from "react-redux"; -import { fetchContentTableData, uploadFile } from "../../store/slices/leftPanelSlice"; +import { fetchContentTableData, setRefreshGrid, uploadFile } from "../../store/slices/leftPanelSlice"; import { AppDispatch, RootState } from "../../store"; import "./UploadFilesModal.styles.scss"; import { CheckmarkCircle16Filled, DismissCircle16Filled } from "@fluentui/react-icons"; +import { + MessageBar, + MessageBarTitle, + MessageBarBody, + MessageBarIntent, + Link, +} from "@fluentui/react-components"; + const useStyles = makeStyles({ container: { margin: "10px 0px", @@ -29,6 +37,15 @@ const useStyles = makeStyles({ } }); +const useClasses = makeStyles({ + messageContainer: { + display: "flex", + flexDirection: "column", + gap: "10px", + marginBottom: "10px" + }, +}); + interface UploadFilesModalProps { open: boolean; @@ -48,6 +65,7 @@ interface FileErrors { const UploadFilesModal: React.FC = ({ open, onClose }) => { const styles = useStyles(); + const classes = useClasses(); const [files, setFiles] = useState([]); const [startUpload, setStartUpload] = useState(false); @@ -61,6 +79,7 @@ const UploadFilesModal: React.FC = ({ open, onClose }) => const [uploadCompleted, setUploadCompleted] = useState(false); + const intents: MessageBarIntent[] = ["warning"]; const store = useSelector((state: RootState) => ({ schemaSelectedOption: state.leftPanel.schemaSelectedOption, page_size: state.leftPanel.gridData.page_size, @@ -145,6 +164,7 @@ const UploadFilesModal: React.FC = ({ open, onClose }) => // Upload files const handleUpload = async () => { setUploading(true); + let uploadCount = 0; try { const schema = store.schemaSelectedOption?.optionValue ?? "defaultSchema"; @@ -153,12 +173,13 @@ const UploadFilesModal: React.FC = ({ open, onClose }) => try { await dispatch(uploadFile({ file, schema })).unwrap(); + uploadCount++; setUploadProgress((prev) => ({ ...prev, [file.name]: 100 })); // Set progress to 100% after upload } catch (error: any) { // Capture and log the error specific to the file setFileErrors((prev) => ({ ...prev, - [file.name]: { message: error.message } + [file.name]: { message: error } })); setUploadProgress((prev) => ({ ...prev, [file.name]: -1 })); // Optional: Indicate failure with -1 or another value } @@ -167,17 +188,16 @@ const UploadFilesModal: React.FC = ({ open, onClose }) => //console.error("Overall upload failed:", error); } finally { setUploading(false); - // setFiles([]) // If you want to clear the files after upload setStartUpload(false); setUploadCompleted(true); if (fileInputRef.current) { fileInputRef.current.value = ''; // Reset the file input } - dispatch(fetchContentTableData({ pageSize: store.pageSize, pageNumber: 1 })).unwrap(); + if (uploadCount > 0) + dispatch(setRefreshGrid(true)); } }; - const handleButtonClick = () => { fileInputRef.current?.click(); // Open file selector }; @@ -202,6 +222,16 @@ const UploadFilesModal: React.FC = ({ open, onClose }) => Import Content
+
+ {intents.map((intent) => ( + + + Selected Schema : {store.schemaSelectedOption.optionText} +
Please upload files specific to "{store.schemaSelectedOption.optionText}" +
+
+ ))} +
{/* Drag & Drop Area with Centered Button & Message */}
{ + + function toBoolean(value: unknown): boolean { + return (window.location.hostname === "localhost") ? true : String(value).toLowerCase() === "true"; + } + useEffect(() => { - if (window.location.hostname !== "localhost" && process.env.REACT_APP_ISLOGS_ENABLED?.toLocaleLowerCase() != "true" ) { + const isConsoleFlag = toBoolean(process.env.REACT_APP_CONSOLE_LOG_ENABLED); + if (isConsoleFlag !== true) { // Save the original console methods const originalConsoleError = console.error; const originalConsoleWarn = console.warn; @@ -11,10 +17,10 @@ const useConsoleSuppression = () => { const originalConsoleInfo = console.info; // Suppress console methods - console.error = () => {}; - console.warn = () => {}; - console.log = () => {}; - console.info = () => {}; + console.error = () => { }; + console.warn = () => { }; + console.log = () => { }; + console.info = () => { }; // Clean up when component unmounts return () => { diff --git a/src/ContentProcessorWeb/src/Pages/DefaultPage/Components/ProcessQueueGrid/ProcessQueueGrid.tsx b/src/ContentProcessorWeb/src/Pages/DefaultPage/Components/ProcessQueueGrid/ProcessQueueGrid.tsx index a9ed6cfa..05858ec9 100644 --- a/src/ContentProcessorWeb/src/Pages/DefaultPage/Components/ProcessQueueGrid/ProcessQueueGrid.tsx +++ b/src/ContentProcessorWeb/src/Pages/DefaultPage/Components/ProcessQueueGrid/ProcessQueueGrid.tsx @@ -13,7 +13,7 @@ import AutoSizer from "react-virtualized-auto-sizer"; import CustomCellRender from "./CustomCellRender"; import { useDispatch, useSelector, shallowEqual } from 'react-redux'; import { RootState, AppDispatch } from '../../../../store'; -import { setSelectedGridRow, deleteProcessedFile, fetchContentTableData } from '../../../../store/slices/leftPanelSlice'; +import { setSelectedGridRow, deleteProcessedFile } from '../../../../store/slices/leftPanelSlice'; import useFileType from "../../../../Hooks/useFileType"; import { Confirmation } from "../../../../Components/DialogComponent/DialogComponent.tsx"; import { Item, TableRowData, ReactWindowRenderFnProps, GridComponentProps } from './ProcessQueueGridTypes.ts'; @@ -140,7 +140,6 @@ const ProcessQueueGrid: React.FC = () => { }, [store.gridData]) useEffect(() => { - console.log("selectedRows", selectedRows, items); if (items.length > 0 && selectedRows.size > 0) { const selectedRow = [...selectedRows][0]; if (typeof selectedRow === 'number') { @@ -148,7 +147,6 @@ const ProcessQueueGrid: React.FC = () => { if (!selectedItem) { setSelectedRows(new Set([0])); } else { - console.log("selectedItem", selectedItem); const findItem = getSelectedItem(selectedItem?.processId.label ?? ''); dispatch(setSelectedGridRow({ processId: selectedItem?.processId.label, item: findItem })); } @@ -289,9 +287,7 @@ const ProcessQueueGrid: React.FC = () => { toggleDialog(); await dispatch(deleteProcessedFile({ processId: selectedDeleteItem.processId.label ?? null })); } catch (error: any) { - - } finally { - dispatch(fetchContentTableData({ pageSize: store.pageSize, pageNumber: 1 })).unwrap(); + console.log("error : ", error) } } }; @@ -315,7 +311,7 @@ const ProcessQueueGrid: React.FC = () => { size="medium" aria-label="Table with selection" aria-rowcount={rows.length} - className="gridTable" + className="gridTable" > diff --git a/src/ContentProcessorWeb/src/Pages/DefaultPage/Components/ProcessSteps/ProcessSteps.tsx b/src/ContentProcessorWeb/src/Pages/DefaultPage/Components/ProcessSteps/ProcessSteps.tsx index 5cff258c..5b5513d8 100644 --- a/src/ContentProcessorWeb/src/Pages/DefaultPage/Components/ProcessSteps/ProcessSteps.tsx +++ b/src/ContentProcessorWeb/src/Pages/DefaultPage/Components/ProcessSteps/ProcessSteps.tsx @@ -84,7 +84,7 @@ const ProcessSteps = () => { restrictEdit={true} restrictDelete={true} restrictAdd={true} - rootName="" + rootName={step.step_name.toLowerCase()} collapseAnimationTime={300} theme={[{ styles: { diff --git a/src/ContentProcessorWeb/src/Pages/DefaultPage/PanelCenter.tsx b/src/ContentProcessorWeb/src/Pages/DefaultPage/PanelCenter.tsx index 5cc18d1a..13b57a99 100644 --- a/src/ContentProcessorWeb/src/Pages/DefaultPage/PanelCenter.tsx +++ b/src/ContentProcessorWeb/src/Pages/DefaultPage/PanelCenter.tsx @@ -12,8 +12,11 @@ import { fetchContentJsonData, setActiveProcessId } from '../../store/slices/cen import ProcessSteps from './Components/ProcessSteps/ProcessSteps'; import { setRefreshGrid } from "../../store/slices/leftPanelSlice.ts"; +import { bundleIcon, ChevronDoubleLeft20Filled, ChevronDoubleLeft20Regular } from "@fluentui/react-icons"; +const ChevronDoubleLeft = bundleIcon(ChevronDoubleLeft20Regular, ChevronDoubleLeft20Filled); + interface PanelCenterProps { - + togglePanel: (panel: string) => void; } const useStyles = makeStyles({ @@ -52,7 +55,7 @@ const useStyles = makeStyles({ overflow: 'auto', background: '#f6f6f6', padding: '10px 5px', - boxSizing:'border-box' + boxSizing: 'border-box' }, processTabItemCotnent: { @@ -61,7 +64,7 @@ const useStyles = makeStyles({ overflow: 'auto', background: '#f6f6f6', padding: '5px', - boxSizing:'border-box' + boxSizing: 'border-box' }, fieldLabel: { fontWeight: 'bold', @@ -86,7 +89,7 @@ const useStyles = makeStyles({ } }) -const PanelCenter: React.FC = () => { +const PanelCenter: React.FC = ({ togglePanel }) => { const styles = useStyles(); const dispatch = useDispatch(); @@ -164,11 +167,13 @@ const PanelCenter: React.FC = () => { try { dispatch(startLoader("1")); dispatch(setUpdateComments(comment)) - await dispatch(saveContentJson({ 'processId': store.activeProcessId, 'contentJson': store.modified_result.extracted_result, 'comments': comment, 'savedComments': store.comments })) + const result = await dispatch(saveContentJson({ 'processId': store.activeProcessId, 'contentJson': store.modified_result.extracted_result, 'comments': comment, 'savedComments': store.comments })) + if (result?.type === 'SaveContentJSON-Comments/fulfilled') { + dispatch(setRefreshGrid(true)); + } } catch (error) { console.error('API Error:', error); } finally { - dispatch(setRefreshGrid(true)); dispatch(stopLoader("1")); } } @@ -182,8 +187,10 @@ const PanelCenter: React.FC = () => { } return ( -
- +
+ + +
+
+
-
- +
+
+ +
+
- -
- + +
+
+ +
+
); diff --git a/src/ContentProcessorWeb/src/Services/httpUtility.ts b/src/ContentProcessorWeb/src/Services/httpUtility.ts index f35c5bfc..8a4c2495 100644 --- a/src/ContentProcessorWeb/src/Services/httpUtility.ts +++ b/src/ContentProcessorWeb/src/Services/httpUtility.ts @@ -3,8 +3,9 @@ const api: string = process.env.REACT_APP_API_BASE_URL as string; // base API URL // Define the types for the response -interface ApiResponse { - data: T; +interface FetchResponse { + data: T | null; + status: number; } interface FetchOptions { @@ -13,47 +14,81 @@ interface FetchOptions { body?: string | FormData | null; } +export const handleApiThunk = async ( + apiCall: Promise<{ data: T | null; status: number }>, + rejectWithValue: (reason: any) => any, + errorMessage = 'Request failed' +): Promise => { + try { + const response = await apiCall; + console.log("API Response : ", response); + if (response.status === 200 || response.status === 202) { + return response.data; + } else { + return rejectWithValue(`${errorMessage}. Status: ${response.status}`); + } + } catch (error: any) { + if (error.status == 415 || error.status == 404) + return rejectWithValue(error.data.message || `Unexpected error: ${errorMessage}`); + return rejectWithValue(error.message || `Unexpected error: ${errorMessage}`); + } +}; + + // Fetch with JWT Authentication -const fetchWithAuth = async (url: string, method: string = 'GET', body: any = null): Promise => { - const token = localStorage.getItem('token'); // Get the token from localStorage + +const fetchWithAuth = async ( + url: string, + method: string = 'GET', + body: any = null +): Promise> => { + const token = localStorage.getItem('token'); const headers: HeadersInit = { - 'Authorization': `Bearer ${token}`, // Add the token to the Authorization header - 'Content-Type': 'application/json', + 'Authorization': `Bearer ${token}`, 'Accept': 'application/json', 'Cache-Control': 'no-cache', }; - // If body is FormData, do not set Content-Type header - if (body instanceof FormData) { - delete headers['Content-Type']; - } else { + if (!(body instanceof FormData)) { headers['Content-Type'] = 'application/json'; body = body ? JSON.stringify(body) : null; } - const options: FetchOptions = { + const options: RequestInit = { method, headers, }; if (body) { - options.body = body; // Add the body only if it exists (for POST, PUT) + options.body = body; } try { const response = await fetch(`${api}${url}`, options); + const status = response.status; + const isJson = response.headers.get('content-type')?.includes('application/json'); + + const data = isJson ? await response.json() : null; + if (!response.ok) { - const errorText = await response.text(); - throw new Error(errorText || 'Something went wrong'); + throw { data, status }; } - const isJson = response.headers.get('content-type')?.includes('application/json'); - return isJson ? (await response.json()) as T : null; + return { data, status }; } catch (error: any) { - //console.error('API Error:', error.message); - throw error; + if (error?.status !== undefined) { + throw error; + } + const isNetworkError = error instanceof TypeError && error.message === 'Failed to fetch'; + const isOffline = !navigator.onLine; //isNetworkError || + + const message = isOffline + ? 'No internet connection. Please check your network and try again.' + : 'Unable to connect to the server. Please try again later.'; + + throw { data: null, status: 0, message }; } }; @@ -133,13 +168,13 @@ const fetchWithoutAuth = async (url: string, method: string = 'POST', body: a // Authenticated requests (with token) and login (without token) export const httpUtility = { - get: (url: string): Promise => fetchWithAuth(url, 'GET'), - post: (url: string, body: any): Promise => fetchWithAuth(url, 'POST', body), - put: (url: string, body: any): Promise => fetchWithAuth(url, 'PUT', body), - delete: (url: string): Promise => fetchWithAuth(url, 'DELETE'), - upload: (url: string, formData: FormData): Promise => fetchWithAuth(url, 'POST', formData), + get: (url: string): Promise<{ data: T | null; status: number }> => fetchWithAuth(url, 'GET'), + post: (url: string, body: any): Promise<{ data: T | null; status: number }> => fetchWithAuth(url, 'POST', body), + put: (url: string, body: any): Promise<{ data: T | null; status: number }> => fetchWithAuth(url, 'PUT', body), + delete: (url: string): Promise<{ data: T | null; status: number }> => fetchWithAuth(url, 'DELETE'), + upload: (url: string, formData: FormData): Promise<{ data: T | null; status: number }> => fetchWithAuth(url, 'POST', formData), login: (url: string, body: any): Promise => fetchWithoutAuth(url, 'POST', body), // For login without auth - headers : (url: string): Promise => fetchHeadersWithAuth(url, 'GET'), + headers: (url: string): Promise => fetchHeadersWithAuth(url, 'GET'), }; export default httpUtility; diff --git a/src/ContentProcessorWeb/src/msal-auth/msaConfig.ts b/src/ContentProcessorWeb/src/msal-auth/msaConfig.ts index f6c9ae79..1ed4f4e2 100644 --- a/src/ContentProcessorWeb/src/msal-auth/msaConfig.ts +++ b/src/ContentProcessorWeb/src/msal-auth/msaConfig.ts @@ -3,10 +3,10 @@ import { Configuration, LogLevel } from '@azure/msal-browser'; export const msalConfig: Configuration = { auth: { - clientId: process.env.REACT_APP_MSAL_AUTH_CLIENT_ID as string, - authority: process.env.REACT_APP_MSAL_AUTH_AUTHORITY, - redirectUri: process.env.REACT_APP_MSAL_REDIRECT_URL as string, - postLogoutRedirectUri: process.env.REACT_APP_MSAL_POST_REDIRECT_URL as string, + clientId: process.env.REACT_APP_WEB_CLIENT_ID as string, + authority: process.env.REACT_APP_WEB_AUTHORITY, + redirectUri: process.env.REACT_APP_REDIRECT_URL as string, + postLogoutRedirectUri: process.env.REACT_APP_POST_REDIRECT_URL as string, }, cache: { cacheLocation: 'localStorage', // Use localStorage for persistent cache @@ -25,8 +25,8 @@ export const msalConfig: Configuration = { }, }; -const loginScope = process.env.REACT_APP_MSAL_AUTH_SCOPE as string; -const tokenScope = process.env.REACT_APP_MSAL_TOKEN_SCOPE as string; +const loginScope = process.env.REACT_APP_WEB_SCOPE as string; +const tokenScope = process.env.REACT_APP_API_SCOPE as string; // console.log("loginScope", loginScope); // console.log("tokenScope", tokenScope); diff --git a/src/ContentProcessorWeb/src/store/rootReducer.ts b/src/ContentProcessorWeb/src/store/rootReducer.ts index afee0870..d6912b46 100644 --- a/src/ContentProcessorWeb/src/store/rootReducer.ts +++ b/src/ContentProcessorWeb/src/store/rootReducer.ts @@ -4,13 +4,15 @@ import loaderSlice from './slices/loaderSlice'; import leftPanelSlice from './slices/leftPanelSlice'; import centerPanelSlice from './slices/centerPanelSlice'; import rightPanelSlice from './slices/rightPanelSlice' +import defaultPageSlice from './slices/defaultPageSlice'; // Combine all reducers here const rootReducer = combineReducers({ loader : loaderSlice, leftPanel: leftPanelSlice, centerPanel : centerPanelSlice, - rightPanel : rightPanelSlice + rightPanel : rightPanelSlice, + defaultPage : defaultPageSlice, }); export default rootReducer; diff --git a/src/ContentProcessorWeb/src/store/slices/centerPanelSlice.ts b/src/ContentProcessorWeb/src/store/slices/centerPanelSlice.ts index 0b97ae05..f3710d27 100644 --- a/src/ContentProcessorWeb/src/store/slices/centerPanelSlice.ts +++ b/src/ContentProcessorWeb/src/store/slices/centerPanelSlice.ts @@ -1,6 +1,6 @@ import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit'; -import httpUtility from '../../Services/httpUtility'; +import httpUtility, { handleApiThunk } from '../../Services/httpUtility'; import { toast } from "react-toastify"; interface CenterPanelState { @@ -10,65 +10,97 @@ interface CenterPanelState { modified_result: any; comments: string; isSavingInProgress: boolean; - activeProcessId : string, + activeProcessId: string, processStepsData: any[]; isJSONEditorSearchEnabled: boolean; } +const getDisplayMessage = (text: string) => { + if ( + text.startsWith('Processing of file with Process') && + text.endsWith('not found.') + ) { + return 'This record no longer exists. Please refresh.'; + } + return text; +}; // Create the async thunk with the argument and return types -export const fetchContentJsonData = createAsyncThunk('/contentprocessor/processed/', async ({ processId }, {rejectWithValue}) => { +export const fetchContentJsonData = createAsyncThunk('/contentprocessor/processed/', async ({ processId }, { rejectWithValue }) => { if (!processId) { return rejectWithValue("Reset store"); } const url = '/contentprocessor/processed/' + processId; - const response = await httpUtility.get(url); - //console.log("response", response); - return response; + + return handleApiThunk( + httpUtility.get(url), + rejectWithValue, + 'Failed to fetch content JSON data' + ); }); export const fetchProcessSteps = createAsyncThunk('/contentprocessor/processed/processId/steps', async ({ processId }, { rejectWithValue }) => { if (!processId) { return rejectWithValue("Reset store"); } - const url = '/contentprocessor/processed/' + processId + "/steps"; - const response = await httpUtility.get(url); - //console.log("response", response); - return response; + const url = `/contentprocessor/processed/${processId}/steps`; + + return handleApiThunk( + httpUtility.get(url), + rejectWithValue, + 'Failed to fetch process steps' + ); }); -export const saveContentJson = createAsyncThunk('SaveContentJSON-Comments', async ({ processId, contentJson, comments ,savedComments }) => { +export const saveContentJson = createAsyncThunk('SaveContentJSON-Comments', async ({ processId, contentJson, comments, savedComments }, { rejectWithValue }) => { try { - if (!processId) throw new Error("Process ID is required"); + if (!processId) { + return rejectWithValue('Process ID is required'); + } const url = `/contentprocessor/processed/${processId}`; const requests: Promise[] = []; + // Add contentJson update if valid if (contentJson && Object.keys(contentJson).length > 0) { requests.push( - httpUtility.put(url, { - process_id: processId, - modified_result: contentJson, - }) + handleApiThunk( + httpUtility.put(url, { + process_id: processId, + modified_result: contentJson, + }), + rejectWithValue, + 'Failed to save content JSON' + ) ); } - if (comments.trim() !== '' || (savedComments!='' && comments.trim()=='') ) { + + // Add comments update if applicable + if (comments.trim() !== '' || (savedComments !== '' && comments.trim() === '')) { requests.push( - httpUtility.put(url, { - process_id: processId, - comment: comments, - }) + handleApiThunk( + httpUtility.put(url, { + process_id: processId, + comment: comments, + }), + rejectWithValue, + 'Failed to save comments' + ) ); } + + // If no changes, short-circuit if (requests.length === 0) { - return { message: "No updates were made" }; + return { message: 'No updates were made' }; } - const responses = await Promise.all(requests); - return responses; + // Wait for all updates to complete + const responses = await Promise.all(requests); + return responses[0]; } catch (error) { return Promise.reject(error); } + }); @@ -79,9 +111,9 @@ const initialState: CenterPanelState = { modified_result: {}, comments: '', isSavingInProgress: false, - activeProcessId : '', + activeProcessId: '', processStepsData: [], - isJSONEditorSearchEnabled : true, + isJSONEditorSearchEnabled: true, }; const centerPanelSlice = createSlice({ @@ -91,12 +123,12 @@ const centerPanelSlice = createSlice({ setModifiedResult: (state, action) => { state.modified_result = action.payload; }, - setUpdateComments : (state, action) => { - state.comments = action.payload + setUpdateComments: (state, action) => { + state.comments = action.payload }, - setActiveProcessId :(state,action) =>{ - state.activeProcessId = action.payload - } + setActiveProcessId: (state, action) => { + state.activeProcessId = action.payload + } }, extraReducers: (builder) => { //Fetch Dropdown values @@ -108,22 +140,22 @@ const centerPanelSlice = createSlice({ state.comments = ''; }) .addCase(fetchContentJsonData.fulfilled, (state, action) => { // Adjust `any` to the response data type - if(state.activeProcessId == action.payload.process_id){ + if (state.activeProcessId == action.payload.process_id) { state.contentData = action.payload; - state.comments = action.payload.comment?? "" ; + state.comments = action.payload.comment ?? ""; state.cLoader = false; } }) - .addCase(fetchContentJsonData.rejected, (state, action) => { + .addCase(fetchContentJsonData.rejected, (state, action: any) => { state.cError = action.error.message || 'An error occurred'; state.cLoader = false; state.contentData = []; state.comments = ""; - //console.error("Error fetching JSON data:", action.error.message || 'An error occurred'); + toast.error(getDisplayMessage(action.payload)) }); builder - .addCase(saveContentJson.pending, (state,action) => { + .addCase(saveContentJson.pending, (state, action) => { state.modified_result = {}; state.isSavingInProgress = true; }) @@ -131,9 +163,8 @@ const centerPanelSlice = createSlice({ toast.success("Data saved successfully!"); // Success toast state.isSavingInProgress = false; }) - .addCase(saveContentJson.rejected, (state, action : any) => { - console.log("action", action) - toast.error(JSON.parse(action.error.message).message); + .addCase(saveContentJson.rejected, (state, action: any) => { + toast.error(getDisplayMessage(action.payload)) state.isSavingInProgress = false; }); @@ -156,5 +187,5 @@ const centerPanelSlice = createSlice({ }, }); -export const { setModifiedResult ,setUpdateComments, setActiveProcessId} = centerPanelSlice.actions; +export const { setModifiedResult, setUpdateComments, setActiveProcessId } = centerPanelSlice.actions; export default centerPanelSlice.reducer; diff --git a/src/ContentProcessorWeb/src/store/slices/defaultPageSlice.ts b/src/ContentProcessorWeb/src/store/slices/defaultPageSlice.ts new file mode 100644 index 00000000..f1f9dabb --- /dev/null +++ b/src/ContentProcessorWeb/src/store/slices/defaultPageSlice.ts @@ -0,0 +1,50 @@ +import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit'; + +interface defaultPageState { + isLeftPanelCollapse: boolean; + isCenterPanelCollapse: boolean; + isRightPanelCollapse: boolean; +} + +const initialState: defaultPageState = { + isLeftPanelCollapse: false, + isCenterPanelCollapse: false, + isRightPanelCollapse: false +}; + +const defaultPageSlice = createSlice({ + name: 'Default Page', + initialState, + reducers: { + updatePanelCollapse: (state, action) => { + switch (action.payload) { + case 'Left': + state.isLeftPanelCollapse = !state.isLeftPanelCollapse; + break; + case 'Right': + state.isRightPanelCollapse = !state.isRightPanelCollapse; + break; + case 'Center': + state.isCenterPanelCollapse = !state.isCenterPanelCollapse; + break; + case 'All': + state.isLeftPanelCollapse = true; + state.isRightPanelCollapse = true; + state.isLeftPanelCollapse = true; + break; + default: + state.isLeftPanelCollapse = false; + state.isRightPanelCollapse = false; + state.isLeftPanelCollapse = false; + break; + } + }, + + }, + extraReducers: (builder) => { + + }, +}); + +export const { updatePanelCollapse } = defaultPageSlice.actions; +export default defaultPageSlice.reducer; diff --git a/src/ContentProcessorWeb/src/store/slices/leftPanelSlice.ts b/src/ContentProcessorWeb/src/store/slices/leftPanelSlice.ts index 72272a35..fa94dff1 100644 --- a/src/ContentProcessorWeb/src/store/slices/leftPanelSlice.ts +++ b/src/ContentProcessorWeb/src/store/slices/leftPanelSlice.ts @@ -1,6 +1,6 @@ import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit'; -import httpUtility from '../../Services/httpUtility'; +import httpUtility, { handleApiThunk } from '../../Services/httpUtility'; import { toast } from "react-toastify"; @@ -30,47 +30,74 @@ interface UploadFileResponse { data?: any; // You can specify a more precise type for the response data if needed } -export const fetchSwaggerData = createAsyncThunk('/openapi', async (): Promise => { - const url = '/openapi.json'; - const response = await httpUtility.get(url); - return response; -}); + +export const fetchSwaggerData = createAsyncThunk( + '/openapi', + async (_, { rejectWithValue }) => { + return handleApiThunk( + httpUtility.get('/openapi.json'), + rejectWithValue, + 'Failed to fetch Swagger data' + ); + } +); + // Async thunk for fetching data -export const fetchSchemaData = createAsyncThunk('/schemavault', async (): Promise => { - const url = '/schemavault/'; - const response = await httpUtility.get(url); - return response; -}); +export const fetchSchemaData = createAsyncThunk( + '/schemavault', + async (_, { rejectWithValue }) => { + return handleApiThunk( + httpUtility.get('/schemavault/'), + rejectWithValue, + 'Failed to fetch schema' + ); + } +); + +export const fetchContentTableData = createAsyncThunk< + any, + { pageSize: number; pageNumber: number } +>( + '/contentprocessor/processed', + async ({ pageSize, pageNumber }, { rejectWithValue }) => { + return handleApiThunk( + httpUtility.post('/contentprocessor/processed', { + page_size: pageSize, + page_number: pageNumber, + }), + rejectWithValue, + 'Failed to fetch content data.' + ); + } +); -export const fetchContentTableData = createAsyncThunk('/contentprocessor/processed', async ({ pageSize, pageNumber }): Promise => { - const url = '/contentprocessor/processed'; - const response = await httpUtility.post(url, { - page_size: pageSize, - page_number: pageNumber, - }); - return response; -}); interface DeleteApiResponse { process_id: string; status: string; message: string; } -export const deleteProcessedFile = createAsyncThunk('/contentprocessor/deleteProcessedFile/', async ({ processId }, { rejectWithValue }) => { - if (!processId) { - return rejectWithValue("Reset store"); +export const deleteProcessedFile = createAsyncThunk( + '/contentprocessor/deleteProcessedFile/', + async ({ processId }, { rejectWithValue }) => { + if (!processId) { + return rejectWithValue('Reset store'); + } + + const url = '/contentprocessor/processed/' + processId; + return handleApiThunk( + httpUtility.delete(url), + rejectWithValue, + 'Failed to delete processed file' + ); } - const url = '/contentprocessor/processed/' + processId; - const response = await httpUtility.delete(url); - console.log("response", response); - return response as DeleteApiResponse;; -}); +); + export const uploadFile = createAsyncThunk< - any, // Type for fulfilled response - { file: File; schema: string } // Type for the input payload -// Type for rejected value (error payload) + any, // Type for fulfilled response + { file: File; schema: string } // Type for the input payload >( '/contentprocessor/submit', async ({ file, schema }, { rejectWithValue }): Promise => { @@ -82,21 +109,14 @@ export const uploadFile = createAsyncThunk< }; const formData = new FormData(); - formData.append('file', file); // Attach the file - formData.append('data', JSON.stringify(metadata)); // Attach JSON metadata - - try { - // Assuming httpUtility.upload returns a Response object, cast it explicitly - const response = await httpUtility.upload(url, formData) as Response; - - return response; - } catch (error: any) { - // Handle any unexpected errors (e.g., network issues) - return rejectWithValue({ - success: false, - message: JSON.parse(error?.message)?.message || 'An unexpected error occurred', - }); - } + formData.append('file', file); + formData.append('data', JSON.stringify(metadata)); + + return handleApiThunk( + httpUtility.upload(url, formData), // Cast to expected response type + rejectWithValue, + 'Failed to upload file' + ); } ); @@ -172,16 +192,16 @@ const leftPanelSlice = createSlice({ .addCase(fetchContentTableData.pending, (state) => { //state.schemaError = null; state.gridLoader = true; - state.gridData = { ...gridDefaultVal }; + //state.gridData = { ...gridDefaultVal }; }) .addCase(fetchContentTableData.fulfilled, (state, action: PayloadAction) => { // Adjust `any` to the response data type //state.schemaLoader = false; state.gridData = action.payload; state.gridLoader = false; }) - .addCase(fetchContentTableData.rejected, (state, action) => { + .addCase(fetchContentTableData.rejected, (state, action: PayloadAction) => { state.gridLoader = false; - toast.error('Something went wrong!') + toast.error(action.payload) }); @@ -192,7 +212,6 @@ const leftPanelSlice = createSlice({ }) .addCase(uploadFile.fulfilled, (state, action: PayloadAction) => { // Adjust `any` to the response data type //state.schemaLoader = false; - //console.log("file upload Success !") }) .addCase(uploadFile.rejected, (state, action) => { // state.schemaError = action.error.message || 'An error occurred'; @@ -214,8 +233,10 @@ const leftPanelSlice = createSlice({ if (processId) { state.deleteFilesLoader = state.deleteFilesLoader.filter(id => id !== processId); } - if (action.payload.status === 'Success') + if (action.payload.status === 'Success') { toast.success("File deleted successfully.") + state.isGridRefresh = true; + } else toast.error(action.payload.message) }) diff --git a/src/ContentProcessorWeb/src/store/slices/rightPanelSlice.ts b/src/ContentProcessorWeb/src/store/slices/rightPanelSlice.ts index fb47ad44..e7b49f85 100644 --- a/src/ContentProcessorWeb/src/store/slices/rightPanelSlice.ts +++ b/src/ContentProcessorWeb/src/store/slices/rightPanelSlice.ts @@ -4,9 +4,9 @@ import httpUtility from '../../Services/httpUtility'; import { toast } from "react-toastify"; interface T { - headers:any, - blobURL:string, - processId : string + headers: any, + blobURL: string, + processId: string } interface RightPanelState { @@ -14,7 +14,7 @@ interface RightPanelState { rLoader: boolean; blobURL: string; rError: string | null; - fileResponse : Array + fileResponse: Array } export const fetchContentFileData = createAsyncThunk('/contentprocessor/processed/files/', async ({ processId }, { rejectWithValue }): Promise => { @@ -30,7 +30,7 @@ export const fetchContentFileData = createAsyncThunki.processId === action.payload.processId) - if(!isItemExists ) + const isItemExists = state.fileResponse.find(i => i.processId === action.payload.processId) + if (!isItemExists) state.fileResponse.push(action.payload) }) .addCase(fetchContentFileData.rejected, (state, action) => { - //console.log('Error : ', action.error.message) state.rLoader = false; state.rError = action.error.message || 'An error occurred'; - toast.error(action.error.message || 'Failed to fetch file'); + //toast.error(action.error.message || 'Failed to fetch file'); }); },