From b4fe0dde817459600f718f0d1b5760f34cf39264 Mon Sep 17 00:00:00 2001 From: Flatlogic Bot Date: Wed, 18 Mar 2026 06:35:27 +0000 Subject: [PATCH] Autosave: 20260318-063526 --- backend/src/db/api/assets.js | 20 +++ .../20260318102000-add-type-to-assets.js | 105 ++++++++++++++++ backend/src/db/models/assets.js | 39 +++++- backend/src/services/projects.js | 1 + frontend/src/components/Assets/CardAssets.tsx | 11 +- frontend/src/components/Assets/ListAssets.tsx | 9 +- .../components/Assets/configureAssetsCols.tsx | 17 ++- frontend/src/pages/_app.tsx | 26 ++++ frontend/src/pages/assets/[assetsId].tsx | 17 ++- frontend/src/pages/assets/assets-edit.tsx | 17 ++- frontend/src/pages/assets/assets-list.tsx | 45 ++++--- frontend/src/pages/assets/assets-new.tsx | 17 ++- frontend/src/pages/assets/assets-table.tsx | 3 +- frontend/src/pages/assets/assets-view.tsx | 8 +- frontend/src/pages/constructor.tsx | 119 ++++++++++-------- frontend/src/pages/projects/projects-edit.tsx | 3 +- 16 files changed, 378 insertions(+), 79 deletions(-) create mode 100644 backend/src/db/migrations/20260318102000-add-type-to-assets.js diff --git a/backend/src/db/api/assets.js b/backend/src/db/api/assets.js index d2ff837..14efe2f 100644 --- a/backend/src/db/api/assets.js +++ b/backend/src/db/api/assets.js @@ -28,6 +28,11 @@ module.exports = class AssetsDBApi { || null , + + type: data.type + || + 'general' + , cdn_url: data.cdn_url || @@ -123,6 +128,11 @@ module.exports = class AssetsDBApi { asset_type: item.asset_type || null + , + + type: item.type + || + 'general' , cdn_url: item.cdn_url @@ -215,6 +225,9 @@ module.exports = class AssetsDBApi { if (data.asset_type !== undefined) updatePayload.asset_type = data.asset_type; + if (data.type !== undefined) updatePayload.type = data.type; + + if (data.cdn_url !== undefined) updatePayload.cdn_url = data.cdn_url; @@ -608,6 +621,13 @@ module.exports = class AssetsDBApi { }; } + if (filter.type) { + where = { + ...where, + type: filter.type, + }; + } + if (filter.is_public) { where = { ...where, diff --git a/backend/src/db/migrations/20260318102000-add-type-to-assets.js b/backend/src/db/migrations/20260318102000-add-type-to-assets.js new file mode 100644 index 0000000..717ec2d --- /dev/null +++ b/backend/src/db/migrations/20260318102000-add-type-to-assets.js @@ -0,0 +1,105 @@ +module.exports = { + async up(queryInterface, Sequelize) { + const transaction = await queryInterface.sequelize.transaction(); + + try { + const tableRows = await queryInterface.sequelize.query( + "SELECT to_regclass('public.assets') AS regclass_name;", + { + transaction, + type: Sequelize.QueryTypes.SELECT, + }, + ); + + if (!tableRows[0]?.regclass_name) { + await transaction.commit(); + return; + } + + const tableDefinition = await queryInterface.describeTable('assets', { transaction }); + + if (!tableDefinition.type) { + await queryInterface.addColumn( + 'assets', + 'type', + { + type: Sequelize.DataTypes.ENUM, + values: [ + 'icon', + 'background_image', + 'audio', + 'video', + 'transition', + 'logo', + 'favicon', + 'document', + 'general', + ], + allowNull: false, + defaultValue: 'general', + }, + { transaction }, + ); + + await queryInterface.sequelize.query( + ` + UPDATE assets + SET type = ( + CASE + WHEN name ILIKE '[ICON] %' OR name ILIKE '[IMAGE] %' THEN 'icon' + WHEN name ILIKE '[BACKGROUND] %' THEN 'background_image' + WHEN name ILIKE '[AUDIO] %' THEN 'audio' + WHEN name ILIKE '[VIDEO] %' THEN 'video' + WHEN name ILIKE '[TRANSITION] %' THEN 'transition' + WHEN name ILIKE '[LOGO] %' THEN 'logo' + WHEN asset_type = 'audio' THEN 'audio' + WHEN asset_type = 'video' THEN 'video' + ELSE 'general' + END + )::"enum_assets_type" + WHERE type = 'general'; + `, + { transaction }, + ); + + await queryInterface.addIndex('assets', ['type'], { transaction }); + } + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + }, + + async down(queryInterface, Sequelize) { + const transaction = await queryInterface.sequelize.transaction(); + + try { + const tableRows = await queryInterface.sequelize.query( + "SELECT to_regclass('public.assets') AS regclass_name;", + { + transaction, + type: Sequelize.QueryTypes.SELECT, + }, + ); + + if (!tableRows[0]?.regclass_name) { + await transaction.commit(); + return; + } + + const tableDefinition = await queryInterface.describeTable('assets', { transaction }); + + if (tableDefinition.type) { + await queryInterface.removeIndex('assets', ['type'], { transaction }); + await queryInterface.removeColumn('assets', 'type', { transaction }); + } + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + }, +}; diff --git a/backend/src/db/models/assets.js b/backend/src/db/models/assets.js index 70e8a6f..c92f6a4 100644 --- a/backend/src/db/models/assets.js +++ b/backend/src/db/models/assets.js @@ -36,6 +36,43 @@ asset_type: { }, +type: { + type: DataTypes.ENUM, + allowNull: false, + defaultValue: "general", + + values: [ + +"icon", + + +"background_image", + + +"audio", + + +"video", + + +"transition", + + +"logo", + + +"favicon", + + +"document", + + +"general" + + ], + + }, + cdn_url: { type: DataTypes.TEXT, @@ -132,6 +169,7 @@ deleted_at_time: { indexes: [ { fields: ['projectId'] }, { fields: ['asset_type'] }, + { fields: ['type'] }, { fields: ['is_public'] }, { fields: ['is_deleted'] }, { fields: ['deletedAt'] }, @@ -198,4 +236,3 @@ deleted_at_time: { return assets; }; - diff --git a/backend/src/services/projects.js b/backend/src/services/projects.js index c6df0b7..ffcd977 100644 --- a/backend/src/services/projects.js +++ b/backend/src/services/projects.js @@ -120,6 +120,7 @@ module.exports = class ProjectsService { { name: sourceAsset.name, asset_type: sourceAsset.asset_type, + type: sourceAsset.type || 'general', cdn_url: sourceAsset.cdn_url, storage_key: sourceAsset.storage_key, mime_type: sourceAsset.mime_type, diff --git a/frontend/src/components/Assets/CardAssets.tsx b/frontend/src/components/Assets/CardAssets.tsx index 2f17731..9600324 100644 --- a/frontend/src/components/Assets/CardAssets.tsx +++ b/frontend/src/components/Assets/CardAssets.tsx @@ -102,13 +102,22 @@ const CardAssets = ({
-
Assettype
+
Asset format
{ item.asset_type }
+ +
+
Type
+
+
+ { item.type || 'general' } +
+
+
diff --git a/frontend/src/components/Assets/ListAssets.tsx b/frontend/src/components/Assets/ListAssets.tsx index c44f6e8..f68b646 100644 --- a/frontend/src/components/Assets/ListAssets.tsx +++ b/frontend/src/components/Assets/ListAssets.tsx @@ -64,9 +64,14 @@ const ListAssets = ({ assets, loading, onDelete, currentPage, numPages, onPageCh
-

Assettype

+

Asset format

{ item.asset_type }

+ +
+

Type

+

{ item.type || 'general' }

+
@@ -189,4 +194,4 @@ const ListAssets = ({ assets, loading, onDelete, currentPage, numPages, onPageCh ) }; -export default ListAssets \ No newline at end of file +export default ListAssets diff --git a/frontend/src/components/Assets/configureAssetsCols.tsx b/frontend/src/components/Assets/configureAssetsCols.tsx index def8b33..2a3a4f5 100644 --- a/frontend/src/components/Assets/configureAssetsCols.tsx +++ b/frontend/src/components/Assets/configureAssetsCols.tsx @@ -80,7 +80,22 @@ export const loadColumns = async ( { field: 'asset_type', - headerName: 'Assettype', + headerName: 'Asset format', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + + editable: hasUpdatePermission, + + + }, + + { + field: 'type', + headerName: 'Type', flex: 1, minWidth: 120, filterable: false, diff --git a/frontend/src/pages/_app.tsx b/frontend/src/pages/_app.tsx index bc06c95..72c7b49 100644 --- a/frontend/src/pages/_app.tsx +++ b/frontend/src/pages/_app.tsx @@ -16,6 +16,7 @@ import { appWithTranslation } from 'next-i18next'; import '../i18n'; import IntroGuide from '../components/IntroGuide'; import { appSteps, loginSteps, usersSteps, rolesSteps } from '../stores/introSteps'; +import { logoutUser } from '../stores/authSlice'; // Initialize axios axios.defaults.baseURL = process.env.NEXT_PUBLIC_BACK_API @@ -62,6 +63,31 @@ function MyApp({ Component, pageProps }: AppPropsWithLayout) { }; }, []); + React.useEffect(() => { + const interceptorId = axios.interceptors.response.use( + (response) => response, + (error) => { + const status = error?.response?.status; + const requestUrl = `${error?.config?.url || ''}`; + const isLoginRequest = requestUrl.includes('/auth/signin/local') || requestUrl.includes('auth/signin/local'); + + if (status === 401 && !isLoginRequest) { + store.dispatch(logoutUser()); + + if (router.pathname !== '/login') { + router.replace('/login'); + } + } + + return Promise.reject(error); + }, + ); + + return () => { + axios.interceptors.response.eject(interceptorId); + }; + }, [router]); + // TODO: Remove this code in future releases React.useEffect(() => { const trustedOrigins = new Set([window.location.origin]); diff --git a/frontend/src/pages/assets/[assetsId].tsx b/frontend/src/pages/assets/[assetsId].tsx index 5ec89c3..e37f045 100644 --- a/frontend/src/pages/assets/[assetsId].tsx +++ b/frontend/src/pages/assets/[assetsId].tsx @@ -113,6 +113,7 @@ const EditAssets = () => { asset_type: '', + type: 'general', @@ -612,7 +613,7 @@ const EditAssets = () => { - + @@ -625,6 +626,20 @@ const EditAssets = () => { + + + + + + + + + + + + + + diff --git a/frontend/src/pages/assets/assets-edit.tsx b/frontend/src/pages/assets/assets-edit.tsx index 47180c6..34bed3c 100644 --- a/frontend/src/pages/assets/assets-edit.tsx +++ b/frontend/src/pages/assets/assets-edit.tsx @@ -113,6 +113,7 @@ const EditAssetsPage = () => { asset_type: '', + type: 'general', @@ -609,7 +610,7 @@ const EditAssetsPage = () => { - + @@ -622,6 +623,20 @@ const EditAssetsPage = () => { + + + + + + + + + + + + + + diff --git a/frontend/src/pages/assets/assets-list.tsx b/frontend/src/pages/assets/assets-list.tsx index 8b091ee..5bccaa3 100644 --- a/frontend/src/pages/assets/assets-list.tsx +++ b/frontend/src/pages/assets/assets-list.tsx @@ -23,6 +23,16 @@ type Asset = { id: string name: string asset_type: 'image' | 'video' | 'audio' | 'file' + type?: + | 'icon' + | 'background_image' + | 'audio' + | 'video' + | 'transition' + | 'logo' + | 'favicon' + | 'document' + | 'general' cdn_url?: string | null mime_type?: string | null } @@ -31,8 +41,9 @@ type AssetSection = { key: 'images' | 'backgroundImages' | 'audio' | 'video' | 'transitions' | 'logo' label: string accept: string - assetType: 'image' | 'video' | 'audio' - tag: string + assetFormat: 'image' | 'video' | 'audio' + assetCategory: NonNullable + legacyTag: string } type UploadQueueItem = { @@ -44,28 +55,28 @@ type UploadQueueItem = { } const ASSET_SECTIONS: AssetSection[] = [ - { key: 'images', label: 'Icons', accept: 'image/*', assetType: 'image', tag: 'IMAGE' }, + { key: 'images', label: 'Icons', accept: 'image/*', assetFormat: 'image', assetCategory: 'icon', legacyTag: 'IMAGE' }, { key: 'backgroundImages', label: 'Background Images', accept: 'image/*', - assetType: 'image', - tag: 'BACKGROUND', + assetFormat: 'image', + assetCategory: 'background_image', + legacyTag: 'BACKGROUND', }, - { key: 'audio', label: 'Audio', accept: 'audio/*', assetType: 'audio', tag: 'AUDIO' }, - { key: 'video', label: 'Video', accept: 'video/*', assetType: 'video', tag: 'VIDEO' }, + { key: 'audio', label: 'Audio', accept: 'audio/*', assetFormat: 'audio', assetCategory: 'audio', legacyTag: 'AUDIO' }, + { key: 'video', label: 'Video', accept: 'video/*', assetFormat: 'video', assetCategory: 'video', legacyTag: 'VIDEO' }, { key: 'transitions', label: 'Transitions', accept: 'video/*', - assetType: 'video', - tag: 'TRANSITION', + assetFormat: 'video', + assetCategory: 'transition', + legacyTag: 'TRANSITION', }, - { key: 'logo', label: 'Logo', accept: 'image/*', assetType: 'image', tag: 'LOGO' }, + { key: 'logo', label: 'Logo', accept: 'image/*', assetFormat: 'image', assetCategory: 'logo', legacyTag: 'LOGO' }, ] -const getTaggedName = (tag: string, name: string) => `[${tag}] ${name}` - const AssetsTablesPage = () => { const router = useRouter() const routeProjectId = useMemo(() => { @@ -218,8 +229,9 @@ const AssetsTablesPage = () => { await axios.post('/assets', { data: { project: projectId, - name: getTaggedName(section.tag, file.name), - asset_type: section.assetType, + name: file.name, + asset_type: section.assetFormat, + type: section.assetCategory, cdn_url: remoteFile.publicUrl, storage_key: remoteFile.privateUrl, mime_type: file.type || null, @@ -313,7 +325,10 @@ const AssetsTablesPage = () => { const assetsBySection = useMemo(() => { return ASSET_SECTIONS.reduce>((acc, section) => { - acc[section.key] = assets.filter((asset) => asset.name?.startsWith(`[${section.tag}] `)) + acc[section.key] = assets.filter((asset) => { + if (asset.type) return asset.type === section.assetCategory + return asset.name?.startsWith(`[${section.legacyTag}] `) + }) return acc }, {}) }, [assets]) diff --git a/frontend/src/pages/assets/assets-new.tsx b/frontend/src/pages/assets/assets-new.tsx index cece01a..28a50a5 100644 --- a/frontend/src/pages/assets/assets-new.tsx +++ b/frontend/src/pages/assets/assets-new.tsx @@ -72,6 +72,7 @@ const initialValues = { asset_type: 'image', + type: 'general', @@ -369,7 +370,7 @@ const AssetsNew = () => { - + @@ -383,6 +384,20 @@ const AssetsNew = () => { + + + + + + + + + + + + + + diff --git a/frontend/src/pages/assets/assets-table.tsx b/frontend/src/pages/assets/assets-table.tsx index 15ce3ff..df9735b 100644 --- a/frontend/src/pages/assets/assets-table.tsx +++ b/frontend/src/pages/assets/assets-table.tsx @@ -44,7 +44,8 @@ const AssetsTablesPage = () => { - {label: 'Assettype', title: 'asset_type', type: 'enum', options: ['image','video','audio','file']}, + {label: 'Asset format', title: 'asset_type', type: 'enum', options: ['image','video','audio','file']}, + {label: 'Type', title: 'type', type: 'enum', options: ['icon','background_image','audio','video','transition','logo','favicon','document','general']}, ]); const hasCreatePermission = currentUser && hasPermission(currentUser, 'CREATE_ASSETS'); diff --git a/frontend/src/pages/assets/assets-view.tsx b/frontend/src/pages/assets/assets-view.tsx index 433b818..4f73102 100644 --- a/frontend/src/pages/assets/assets-view.tsx +++ b/frontend/src/pages/assets/assets-view.tsx @@ -170,9 +170,13 @@ const AssetsView = () => {
-

Assettype

+

Asset format

{assets?.asset_type ?? 'No data'}

+
+

Type

+

{assets?.type ?? 'No data'}

+
@@ -676,4 +680,4 @@ AssetsView.getLayout = function getLayout(page: ReactElement) { ) } -export default AssetsView; \ No newline at end of file +export default AssetsView; diff --git a/frontend/src/pages/constructor.tsx b/frontend/src/pages/constructor.tsx index e23a675..7c8f253 100644 --- a/frontend/src/pages/constructor.tsx +++ b/frontend/src/pages/constructor.tsx @@ -35,6 +35,7 @@ type ProjectAsset = { id: string; name?: string; asset_type?: 'image' | 'video' | 'audio' | 'file'; + type?: 'icon' | 'background_image' | 'audio' | 'video' | 'transition' | 'logo' | 'favicon' | 'document' | 'general'; cdn_url?: string | null; storage_key?: string | null; }; @@ -109,7 +110,7 @@ type TransitionPreviewState = { title: string; }; -type EditorMenuItem = 'none' | 'background_image' | 'background_video' | 'background_audio'; +type EditorMenuItem = 'none' | 'background_image' | 'background_video' | 'background_audio' | 'create_transition'; const parseJsonObject = (value?: string, fallback?: T): T => { if (!value) return (fallback || ({} as T)) as T; @@ -211,6 +212,7 @@ const resolveAssetPlaybackUrl = (value?: string) => { }; const isBackgroundImageAsset = (asset: ProjectAsset) => { + if (asset.type) return asset.type === 'background_image'; const normalizedName = String(asset.name || '').toLowerCase(); if (!normalizedName) return false; const hasBackgroundKeyword = /\bbackground\b|\bbg\b|backdrop|wallpaper/.test(normalizedName); @@ -411,13 +413,19 @@ const ConstructorPage = () => { [assets], ); const transitionVideoAssetOptions = useMemo(() => { - const tagged = assets + const typedAssets = assets + .filter((asset) => asset.type === 'transition' && asset.asset_type === 'video' && getAssetSourceValue(asset)) + .map((asset) => ({ value: getAssetSourceValue(asset), label: getAssetLabel(asset) })); + + if (typedAssets.length > 0) return typedAssets; + + const taggedAssets = assets .filter( (asset) => asset.asset_type === 'video' && getAssetSourceValue(asset) && /\[TRANSITION\]/i.test(String(asset.name || '')), ) .map((asset) => ({ value: getAssetSourceValue(asset), label: getAssetLabel(asset) })); - if (tagged.length > 0) return tagged; + if (taggedAssets.length > 0) return taggedAssets; return videoAssetOptions; }, [assets, videoAssetOptions]); @@ -747,7 +755,7 @@ const ConstructorPage = () => { environment: activePage?.environment || 'dev', source_key: '', name: sanitizedName, - slug: `transition-${Date.now().toString().slice(-4)}`, + slug: `transition-${createLocalId()}`, video_url: sanitizedVideoUrl, audio_url: '', supports_reverse: Boolean(newTransitionSupportsReverse), @@ -1070,7 +1078,9 @@ const ConstructorPage = () => { ? 'Background video' : selectedMenuItem === 'background_audio' ? 'Background audio' - : selectedElement?.label || 'Element editor'; + : selectedMenuItem === 'create_transition' + ? 'Create transition' + : selectedElement?.label || 'Element editor'; if (backgroundImageSrc) { canvasBackgroundStyle.backgroundImage = `url("${backgroundImageSrc}")`; @@ -1339,6 +1349,55 @@ const ConstructorPage = () => { )} + {selectedMenuItem === 'create_transition' && ( +
+

Create next page transition

+ setNewTransitionName(event.target.value)} + /> + + + setNewTransitionDurationSec(Number(event.target.value || 0.7))} + /> + +
+ )} + {selectedElement && (
@@ -1447,52 +1506,6 @@ const ConstructorPage = () => { openTransitionPreview('forward')} /> openTransitionPreview('back')} />
-
-

Create next page transition

- setNewTransitionName(event.target.value)} - /> - - - setNewTransitionDurationSec(Number(event.target.value || 0.7))} - /> - -
)} @@ -1729,6 +1742,10 @@ const ConstructorPage = () => { Add Navigation +