From 34770304c515e9e86250483e98643edca476b39c Mon Sep 17 00:00:00 2001 From: Dmitri Date: Tue, 31 Mar 2026 12:13:06 +0400 Subject: [PATCH] fixed assets isolation issue --- backend/src/db/api/assets.js | 2 +- backend/src/db/api/base.api.js | 56 ++++++++++------- backend/src/db/utils.js | 15 ++--- .../src/components/Assets/ProjectSelector.tsx | 63 +++++-------------- frontend/src/pages/assets/assets-list.tsx | 16 ++++- frontend/src/pages/constructor.tsx | 2 +- .../pages/project-element-defaults/[id].tsx | 2 +- frontend/src/pages/projects/projects-edit.tsx | 9 ++- frontend/src/stores/assets/assetsSlice.ts | 1 + frontend/src/stores/createEntitySlice.ts | 4 ++ 10 files changed, 84 insertions(+), 86 deletions(-) diff --git a/backend/src/db/api/assets.js b/backend/src/db/api/assets.js index be21d27..8f9cac8 100644 --- a/backend/src/db/api/assets.js +++ b/backend/src/db/api/assets.js @@ -19,7 +19,7 @@ class AssetsDBApi extends GenericDBApi { } static get ENUM_FIELDS() { - return ['asset_type', 'type', 'is_public']; + return ['asset_type', 'type', 'is_public', 'projectId']; } static get CSV_FIELDS() { diff --git a/backend/src/db/api/base.api.js b/backend/src/db/api/base.api.js index d81adff..1ccd562 100644 --- a/backend/src/db/api/base.api.js +++ b/backend/src/db/api/base.api.js @@ -265,7 +265,13 @@ class GenericDBApi { let include = [...this.FIND_ALL_INCLUDES]; if (filter.id) { - where.id = Utils.uuid(filter.id); + const validId = Utils.uuid(filter.id); + if (validId) { + where.id = validId; + } else { + // Invalid UUID provided - return empty results immediately + return { rows: [], count: 0 }; + } } for (const field of this.SEARCHABLE_FIELDS) { @@ -310,31 +316,37 @@ class GenericDBApi { for (const rel of this.RELATION_FILTERS) { if (filter[rel.filterKey]) { const searchTerms = filter[rel.filterKey].split('|'); + + // Filter out null UUIDs - only keep valid ones + const validUuids = searchTerms + .map((term) => Utils.uuid(term)) + .filter((id) => id !== null); + + // Build OR conditions array + const orConditions = []; + + // Add UUID condition only if there are valid UUIDs + if (validUuids.length > 0) { + orConditions.push({ id: { [Op.in]: validUuids } }); + } + + // Add text search condition if searchField is defined + if (rel.searchField) { + orConditions.push({ + [rel.searchField]: { + [Op.or]: searchTerms.map((term) => ({ + [Op.iLike]: `%${term}%`, + })), + }, + }); + } + const relInclude = { model: rel.model, as: rel.as, - required: searchTerms.length > 0, + required: orConditions.length > 0, where: - searchTerms.length > 0 - ? { - [Op.or]: [ - { - id: { - [Op.in]: searchTerms.map((term) => Utils.uuid(term)), - }, - }, - rel.searchField - ? { - [rel.searchField]: { - [Op.or]: searchTerms.map((term) => ({ - [Op.iLike]: `%${term}%`, - })), - }, - } - : {}, - ], - } - : undefined, + orConditions.length > 0 ? { [Op.or]: orConditions } : undefined, }; include = [relInclude, ...include]; } diff --git a/backend/src/db/utils.js b/backend/src/db/utils.js index c257f8d..6a36d5c 100644 --- a/backend/src/db/utils.js +++ b/backend/src/db/utils.js @@ -1,16 +1,17 @@ const validator = require('validator'); -const { v4: uuid } = require('uuid'); const Sequelize = require('./models').Sequelize; module.exports = class Utils { + /** + * Validates a UUID string. + * @param {*} value - The value to validate as UUID + * @returns {string|null} - The valid UUID string, or null if invalid + */ static uuid(value) { - let id = value; - - if (!validator.isUUID(id)) { - id = uuid(); + if (value && validator.isUUID(String(value))) { + return value; } - - return id; + return null; } static ilike(model, column, value) { diff --git a/frontend/src/components/Assets/ProjectSelector.tsx b/frontend/src/components/Assets/ProjectSelector.tsx index a7e268b..58f0c15 100644 --- a/frontend/src/components/Assets/ProjectSelector.tsx +++ b/frontend/src/components/Assets/ProjectSelector.tsx @@ -1,7 +1,6 @@ import axios from 'axios'; import { useRouter } from 'next/router'; import { useCallback, useEffect, useMemo, useState } from 'react'; -import { toast } from 'react-toastify'; import { logger } from '../../lib/logger'; export type Project = { @@ -32,81 +31,47 @@ export function useProjectSelector({ }, [router.query.projectId]); const [projects, setProjects] = useState([]); - const [selectedProjectId, setSelectedProjectId] = useState(''); const [isLoadingProjects, setIsLoadingProjects] = useState(false); + // Redirect if no projectId in URL + useEffect(() => { + if (!router.isReady) return; + if (!routeProjectId) { + router.replace('/projects/projects-list'); + } + }, [router.isReady, routeProjectId, router]); + const loadProjects = useCallback(async () => { setIsLoadingProjects(true); try { - let rows: Project[] = []; const response = await axios.get( '/projects?limit=100&page=0&sort=desc&field=updatedAt', ); - rows = Array.isArray(response?.data?.rows) ? response.data.rows : []; - - if (rows.length === 0) { - const autocompleteResponse = await axios.get( - '/projects/autocomplete?limit=100', - ); - const autocompleteItems = Array.isArray(autocompleteResponse?.data) - ? autocompleteResponse.data - : []; - rows = autocompleteItems.map((item: { id: string; label: string }) => ({ - id: item.id, - name: item.label, - })); - } - - if (rows.length === 0) { - toast('Please create a project first', { - type: 'info', - position: 'bottom-center', - }); - router.replace('/projects/projects-new'); - return; - } - + const rows = Array.isArray(response?.data?.rows) ? response.data.rows : []; setProjects(rows); - - if ( - routeProjectId && - rows.some((project) => project.id === routeProjectId) - ) { - setSelectedProjectId(routeProjectId); - } else { - setSelectedProjectId((prev) => { - if (rows.some((project) => project.id === prev)) return prev; - return rows[0]?.id || ''; - }); - } } catch (error: unknown) { const errorMessage = error instanceof Error ? error.message : String(error); logger.error('Failed to load projects:', { error: errorMessage }); setProjects([]); - setSelectedProjectId(''); - toast('Failed to load projects', { - type: 'error', - position: 'bottom-center', - }); } finally { setIsLoadingProjects(false); } - }, [routeProjectId, router]); + }, []); useEffect(() => { if (!currentUser) return; loadProjects(); - }, [routeProjectId, currentUser, loadProjects]); + }, [currentUser, loadProjects]); const selectedProjectName = useMemo(() => { - return projects.find((p) => p.id === selectedProjectId)?.name || ''; - }, [projects, selectedProjectId]); + return projects.find((p) => p.id === routeProjectId)?.name || ''; + }, [projects, routeProjectId]); return { projects, - selectedProjectId, + selectedProjectId: routeProjectId, // Direct from URL - no fallback isLoadingProjects, selectedProjectName, }; diff --git a/frontend/src/pages/assets/assets-list.tsx b/frontend/src/pages/assets/assets-list.tsx index c5490d8..0411784 100644 --- a/frontend/src/pages/assets/assets-list.tsx +++ b/frontend/src/pages/assets/assets-list.tsx @@ -5,6 +5,7 @@ import React, { useCallback, useEffect, useMemo, + useRef, useState, } from 'react'; import { toast, ToastContainer } from 'react-toastify'; @@ -17,6 +18,7 @@ import { useAppDispatch, useAppSelector } from '../../stores/hooks'; import { fetch as fetchAssets, deleteItem as deleteAsset, + clearState as clearAssets, } from '../../stores/assets/assetsSlice'; import AssetSectionCard, { Asset, @@ -88,12 +90,14 @@ const AssetsTablesPage = () => { const { selectedProjectId, isLoadingProjects, selectedProjectName } = useProjectSelector({ currentUser }); + const prevProjectIdRef = useRef(''); + const loadAssets = useCallback( (projectId: string) => { if (!projectId) return; dispatch( fetchAssets({ - query: `?limit=500&page=0&sort=desc&field=createdAt&project=${projectId}`, + query: `?limit=500&page=0&sort=desc&field=createdAt&projectId=${projectId}`, }), ); }, @@ -106,8 +110,16 @@ const AssetsTablesPage = () => { }); useEffect(() => { + if (!selectedProjectId) return; + + // Clear on project change + if (prevProjectIdRef.current !== selectedProjectId) { + dispatch(clearAssets()); + } + prevProjectIdRef.current = selectedProjectId; + loadAssets(selectedProjectId); - }, [selectedProjectId, loadAssets]); + }, [selectedProjectId, dispatch, loadAssets]); const hasCreatePermission = Boolean( currentUser && hasPermission(currentUser, 'CREATE_ASSETS'), diff --git a/frontend/src/pages/constructor.tsx b/frontend/src/pages/constructor.tsx index c7efa4f..c44405b 100644 --- a/frontend/src/pages/constructor.tsx +++ b/frontend/src/pages/constructor.tsx @@ -625,7 +625,7 @@ const ConstructorPage = ({ mode = 'constructor' }: ConstructorPageProps) => { `/tour_pages?limit=500&sort=asc&field=sort_order&project=${projectId}&environment=dev`, ), axios.get( - `/assets?limit=500&page=0&sort=desc&field=createdAt&project=${projectId}`, + `/assets?limit=500&page=0&sort=desc&field=createdAt&projectId=${projectId}`, ), axios.get( `/project-element-defaults?projectId=${projectId}&limit=200&page=0&sort=asc&field=sort_order`, diff --git a/frontend/src/pages/project-element-defaults/[id].tsx b/frontend/src/pages/project-element-defaults/[id].tsx index a693917..e1c2979 100644 --- a/frontend/src/pages/project-element-defaults/[id].tsx +++ b/frontend/src/pages/project-element-defaults/[id].tsx @@ -157,7 +157,7 @@ const ProjectElementDefaultDetailsPage = () => { if (nextItem.projectId) { try { const assetsResponse = await axios.get( - `/assets?limit=500&page=0&sort=desc&field=createdAt&project=${nextItem.projectId}`, + `/assets?limit=500&page=0&sort=desc&field=createdAt&projectId=${nextItem.projectId}`, ); const assetRows: ConstructorAsset[] = Array.isArray( assetsResponse?.data?.rows, diff --git a/frontend/src/pages/projects/projects-edit.tsx b/frontend/src/pages/projects/projects-edit.tsx index a1c37fe..25ac2db 100644 --- a/frontend/src/pages/projects/projects-edit.tsx +++ b/frontend/src/pages/projects/projects-edit.tsx @@ -69,7 +69,7 @@ const EditProjectsPage = () => { setIsLoadingLogoAssets(true); try { const response = await axios.get( - `/assets?limit=500&page=0&sort=desc&field=createdAt&project=${projectId}`, + `/assets?limit=500&page=0&sort=desc&field=createdAt&projectId=${projectId}`, ); const rows = Array.isArray(response?.data?.rows) ? response.data.rows @@ -103,11 +103,14 @@ const EditProjectsPage = () => { }; useEffect(() => { + setLogoAssets([]); + setIsLoadingLogoAssets(true); + if (typeof id === 'string' && id) { void loadLogoAssets(id); - return; + } else { + setIsLoadingLogoAssets(false); } - setLogoAssets([]); }, [id]); // Sync form values with fetched data (consolidated from redundant useEffects) diff --git a/frontend/src/stores/assets/assetsSlice.ts b/frontend/src/stores/assets/assetsSlice.ts index 62534dc..dfdfaab 100644 --- a/frontend/src/stores/assets/assetsSlice.ts +++ b/frontend/src/stores/assets/assetsSlice.ts @@ -19,6 +19,7 @@ export const { deleteItemsByIds, uploadCsv, setRefetch, + clearState, } = actions; export const assetsSlice = slice; diff --git a/frontend/src/stores/createEntitySlice.ts b/frontend/src/stores/createEntitySlice.ts index 8a6a88e..78906ba 100644 --- a/frontend/src/stores/createEntitySlice.ts +++ b/frontend/src/stores/createEntitySlice.ts @@ -227,6 +227,10 @@ export function createEntitySlice( setRefetch: (state, action: PayloadAction) => { state.refetch = action.payload; }, + clearState: (state) => { + (state as Record)[name] = []; + state.count = 0; + }, }, extraReducers: (builder) => { // Fetch handlers