fixed assets isolation issue

This commit is contained in:
Dmitri 2026-03-31 12:13:06 +04:00
parent fcc3d9e868
commit 34770304c5
10 changed files with 84 additions and 86 deletions

View File

@ -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() {

View File

@ -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];
}

View File

@ -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) {

View File

@ -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<Project[]>([]);
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,
};

View File

@ -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<string>('');
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'),

View File

@ -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`,

View File

@ -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,

View File

@ -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)

View File

@ -19,6 +19,7 @@ export const {
deleteItemsByIds,
uploadCsv,
setRefetch,
clearState,
} = actions;
export const assetsSlice = slice;

View File

@ -227,6 +227,10 @@ export function createEntitySlice<T extends BaseEntity>(
setRefetch: (state, action: PayloadAction<boolean>) => {
state.refetch = action.payload;
},
clearState: (state) => {
(state as Record<string, unknown>)[name] = [];
state.count = 0;
},
},
extraReducers: (builder) => {
// Fetch handlers