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' && (
+
+ )}
+
{selectedElement && (
@@ -1447,52 +1506,6 @@ const ConstructorPage = () => {
openTransitionPreview('forward')} />
openTransitionPreview('back')} />
-
)}
@@ -1729,6 +1742,10 @@ const ConstructorPage = () => {
Add Navigation
+