Autosave: 20260318-063526
This commit is contained in:
parent
06dd524cd0
commit
b4fe0dde81
@ -28,6 +28,11 @@ module.exports = class AssetsDBApi {
|
|||||||
||
|
||
|
||||||
null
|
null
|
||||||
,
|
,
|
||||||
|
|
||||||
|
type: data.type
|
||||||
|
||
|
||||||
|
'general'
|
||||||
|
,
|
||||||
|
|
||||||
cdn_url: data.cdn_url
|
cdn_url: data.cdn_url
|
||||||
||
|
||
|
||||||
@ -123,6 +128,11 @@ module.exports = class AssetsDBApi {
|
|||||||
asset_type: item.asset_type
|
asset_type: item.asset_type
|
||||||
||
|
||
|
||||||
null
|
null
|
||||||
|
,
|
||||||
|
|
||||||
|
type: item.type
|
||||||
|
||
|
||||||
|
'general'
|
||||||
,
|
,
|
||||||
|
|
||||||
cdn_url: item.cdn_url
|
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.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;
|
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) {
|
if (filter.is_public) {
|
||||||
where = {
|
where = {
|
||||||
...where,
|
...where,
|
||||||
|
|||||||
105
backend/src/db/migrations/20260318102000-add-type-to-assets.js
Normal file
105
backend/src/db/migrations/20260318102000-add-type-to-assets.js
Normal file
@ -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;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
@ -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: {
|
cdn_url: {
|
||||||
type: DataTypes.TEXT,
|
type: DataTypes.TEXT,
|
||||||
|
|
||||||
@ -132,6 +169,7 @@ deleted_at_time: {
|
|||||||
indexes: [
|
indexes: [
|
||||||
{ fields: ['projectId'] },
|
{ fields: ['projectId'] },
|
||||||
{ fields: ['asset_type'] },
|
{ fields: ['asset_type'] },
|
||||||
|
{ fields: ['type'] },
|
||||||
{ fields: ['is_public'] },
|
{ fields: ['is_public'] },
|
||||||
{ fields: ['is_deleted'] },
|
{ fields: ['is_deleted'] },
|
||||||
{ fields: ['deletedAt'] },
|
{ fields: ['deletedAt'] },
|
||||||
@ -198,4 +236,3 @@ deleted_at_time: {
|
|||||||
|
|
||||||
return assets;
|
return assets;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -120,6 +120,7 @@ module.exports = class ProjectsService {
|
|||||||
{
|
{
|
||||||
name: sourceAsset.name,
|
name: sourceAsset.name,
|
||||||
asset_type: sourceAsset.asset_type,
|
asset_type: sourceAsset.asset_type,
|
||||||
|
type: sourceAsset.type || 'general',
|
||||||
cdn_url: sourceAsset.cdn_url,
|
cdn_url: sourceAsset.cdn_url,
|
||||||
storage_key: sourceAsset.storage_key,
|
storage_key: sourceAsset.storage_key,
|
||||||
mime_type: sourceAsset.mime_type,
|
mime_type: sourceAsset.mime_type,
|
||||||
|
|||||||
@ -102,13 +102,22 @@ const CardAssets = ({
|
|||||||
|
|
||||||
|
|
||||||
<div className='flex justify-between gap-x-4 py-3'>
|
<div className='flex justify-between gap-x-4 py-3'>
|
||||||
<dt className=' text-gray-500 dark:text-dark-600'>Assettype</dt>
|
<dt className=' text-gray-500 dark:text-dark-600'>Asset format</dt>
|
||||||
<dd className='flex items-start gap-x-2'>
|
<dd className='flex items-start gap-x-2'>
|
||||||
<div className='font-medium line-clamp-4'>
|
<div className='font-medium line-clamp-4'>
|
||||||
{ item.asset_type }
|
{ item.asset_type }
|
||||||
</div>
|
</div>
|
||||||
</dd>
|
</dd>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className='flex justify-between gap-x-4 py-3'>
|
||||||
|
<dt className=' text-gray-500 dark:text-dark-600'>Type</dt>
|
||||||
|
<dd className='flex items-start gap-x-2'>
|
||||||
|
<div className='font-medium line-clamp-4'>
|
||||||
|
{ item.type || 'general' }
|
||||||
|
</div>
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -64,9 +64,14 @@ const ListAssets = ({ assets, loading, onDelete, currentPage, numPages, onPageCh
|
|||||||
|
|
||||||
|
|
||||||
<div className={'flex-1 px-3'}>
|
<div className={'flex-1 px-3'}>
|
||||||
<p className={'text-xs text-gray-500 '}>Assettype</p>
|
<p className={'text-xs text-gray-500 '}>Asset format</p>
|
||||||
<p className={'line-clamp-2'}>{ item.asset_type }</p>
|
<p className={'line-clamp-2'}>{ item.asset_type }</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className={'flex-1 px-3'}>
|
||||||
|
<p className={'text-xs text-gray-500 '}>Type</p>
|
||||||
|
<p className={'line-clamp-2'}>{ item.type || 'general' }</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -189,4 +194,4 @@ const ListAssets = ({ assets, loading, onDelete, currentPage, numPages, onPageCh
|
|||||||
)
|
)
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ListAssets
|
export default ListAssets
|
||||||
|
|||||||
@ -80,7 +80,22 @@ export const loadColumns = async (
|
|||||||
|
|
||||||
{
|
{
|
||||||
field: 'asset_type',
|
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,
|
flex: 1,
|
||||||
minWidth: 120,
|
minWidth: 120,
|
||||||
filterable: false,
|
filterable: false,
|
||||||
|
|||||||
@ -16,6 +16,7 @@ import { appWithTranslation } from 'next-i18next';
|
|||||||
import '../i18n';
|
import '../i18n';
|
||||||
import IntroGuide from '../components/IntroGuide';
|
import IntroGuide from '../components/IntroGuide';
|
||||||
import { appSteps, loginSteps, usersSteps, rolesSteps } from '../stores/introSteps';
|
import { appSteps, loginSteps, usersSteps, rolesSteps } from '../stores/introSteps';
|
||||||
|
import { logoutUser } from '../stores/authSlice';
|
||||||
|
|
||||||
// Initialize axios
|
// Initialize axios
|
||||||
axios.defaults.baseURL = process.env.NEXT_PUBLIC_BACK_API
|
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
|
// TODO: Remove this code in future releases
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
const trustedOrigins = new Set<string>([window.location.origin]);
|
const trustedOrigins = new Set<string>([window.location.origin]);
|
||||||
|
|||||||
@ -113,6 +113,7 @@ const EditAssets = () => {
|
|||||||
|
|
||||||
|
|
||||||
asset_type: '',
|
asset_type: '',
|
||||||
|
type: 'general',
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -612,7 +613,7 @@ const EditAssets = () => {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
<FormField label="Assettype" labelFor="asset_type">
|
<FormField label="Asset Format" labelFor="asset_type">
|
||||||
<Field name="asset_type" id="asset_type" component="select">
|
<Field name="asset_type" id="asset_type" component="select">
|
||||||
|
|
||||||
<option value="image">image</option>
|
<option value="image">image</option>
|
||||||
@ -625,6 +626,20 @@ const EditAssets = () => {
|
|||||||
|
|
||||||
</Field>
|
</Field>
|
||||||
</FormField>
|
</FormField>
|
||||||
|
|
||||||
|
<FormField label="Type" labelFor="type">
|
||||||
|
<Field name="type" id="type" component="select">
|
||||||
|
<option value="general">general</option>
|
||||||
|
<option value="icon">icon</option>
|
||||||
|
<option value="background_image">background_image</option>
|
||||||
|
<option value="audio">audio</option>
|
||||||
|
<option value="video">video</option>
|
||||||
|
<option value="transition">transition</option>
|
||||||
|
<option value="logo">logo</option>
|
||||||
|
<option value="favicon">favicon</option>
|
||||||
|
<option value="document">document</option>
|
||||||
|
</Field>
|
||||||
|
</FormField>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -113,6 +113,7 @@ const EditAssetsPage = () => {
|
|||||||
|
|
||||||
|
|
||||||
asset_type: '',
|
asset_type: '',
|
||||||
|
type: 'general',
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -609,7 +610,7 @@ const EditAssetsPage = () => {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
<FormField label="Assettype" labelFor="asset_type">
|
<FormField label="Asset Format" labelFor="asset_type">
|
||||||
<Field name="asset_type" id="asset_type" component="select">
|
<Field name="asset_type" id="asset_type" component="select">
|
||||||
|
|
||||||
<option value="image">image</option>
|
<option value="image">image</option>
|
||||||
@ -622,6 +623,20 @@ const EditAssetsPage = () => {
|
|||||||
|
|
||||||
</Field>
|
</Field>
|
||||||
</FormField>
|
</FormField>
|
||||||
|
|
||||||
|
<FormField label="Type" labelFor="type">
|
||||||
|
<Field name="type" id="type" component="select">
|
||||||
|
<option value="general">general</option>
|
||||||
|
<option value="icon">icon</option>
|
||||||
|
<option value="background_image">background_image</option>
|
||||||
|
<option value="audio">audio</option>
|
||||||
|
<option value="video">video</option>
|
||||||
|
<option value="transition">transition</option>
|
||||||
|
<option value="logo">logo</option>
|
||||||
|
<option value="favicon">favicon</option>
|
||||||
|
<option value="document">document</option>
|
||||||
|
</Field>
|
||||||
|
</FormField>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -23,6 +23,16 @@ type Asset = {
|
|||||||
id: string
|
id: string
|
||||||
name: string
|
name: string
|
||||||
asset_type: 'image' | 'video' | 'audio' | 'file'
|
asset_type: 'image' | 'video' | 'audio' | 'file'
|
||||||
|
type?:
|
||||||
|
| 'icon'
|
||||||
|
| 'background_image'
|
||||||
|
| 'audio'
|
||||||
|
| 'video'
|
||||||
|
| 'transition'
|
||||||
|
| 'logo'
|
||||||
|
| 'favicon'
|
||||||
|
| 'document'
|
||||||
|
| 'general'
|
||||||
cdn_url?: string | null
|
cdn_url?: string | null
|
||||||
mime_type?: string | null
|
mime_type?: string | null
|
||||||
}
|
}
|
||||||
@ -31,8 +41,9 @@ type AssetSection = {
|
|||||||
key: 'images' | 'backgroundImages' | 'audio' | 'video' | 'transitions' | 'logo'
|
key: 'images' | 'backgroundImages' | 'audio' | 'video' | 'transitions' | 'logo'
|
||||||
label: string
|
label: string
|
||||||
accept: string
|
accept: string
|
||||||
assetType: 'image' | 'video' | 'audio'
|
assetFormat: 'image' | 'video' | 'audio'
|
||||||
tag: string
|
assetCategory: NonNullable<Asset['type']>
|
||||||
|
legacyTag: string
|
||||||
}
|
}
|
||||||
|
|
||||||
type UploadQueueItem = {
|
type UploadQueueItem = {
|
||||||
@ -44,28 +55,28 @@ type UploadQueueItem = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const ASSET_SECTIONS: AssetSection[] = [
|
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',
|
key: 'backgroundImages',
|
||||||
label: 'Background Images',
|
label: 'Background Images',
|
||||||
accept: 'image/*',
|
accept: 'image/*',
|
||||||
assetType: 'image',
|
assetFormat: 'image',
|
||||||
tag: 'BACKGROUND',
|
assetCategory: 'background_image',
|
||||||
|
legacyTag: 'BACKGROUND',
|
||||||
},
|
},
|
||||||
{ key: 'audio', label: 'Audio', accept: 'audio/*', assetType: 'audio', tag: 'AUDIO' },
|
{ key: 'audio', label: 'Audio', accept: 'audio/*', assetFormat: 'audio', assetCategory: 'audio', legacyTag: 'AUDIO' },
|
||||||
{ key: 'video', label: 'Video', accept: 'video/*', assetType: 'video', tag: 'VIDEO' },
|
{ key: 'video', label: 'Video', accept: 'video/*', assetFormat: 'video', assetCategory: 'video', legacyTag: 'VIDEO' },
|
||||||
{
|
{
|
||||||
key: 'transitions',
|
key: 'transitions',
|
||||||
label: 'Transitions',
|
label: 'Transitions',
|
||||||
accept: 'video/*',
|
accept: 'video/*',
|
||||||
assetType: 'video',
|
assetFormat: 'video',
|
||||||
tag: 'TRANSITION',
|
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 AssetsTablesPage = () => {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const routeProjectId = useMemo(() => {
|
const routeProjectId = useMemo(() => {
|
||||||
@ -218,8 +229,9 @@ const AssetsTablesPage = () => {
|
|||||||
await axios.post('/assets', {
|
await axios.post('/assets', {
|
||||||
data: {
|
data: {
|
||||||
project: projectId,
|
project: projectId,
|
||||||
name: getTaggedName(section.tag, file.name),
|
name: file.name,
|
||||||
asset_type: section.assetType,
|
asset_type: section.assetFormat,
|
||||||
|
type: section.assetCategory,
|
||||||
cdn_url: remoteFile.publicUrl,
|
cdn_url: remoteFile.publicUrl,
|
||||||
storage_key: remoteFile.privateUrl,
|
storage_key: remoteFile.privateUrl,
|
||||||
mime_type: file.type || null,
|
mime_type: file.type || null,
|
||||||
@ -313,7 +325,10 @@ const AssetsTablesPage = () => {
|
|||||||
|
|
||||||
const assetsBySection = useMemo(() => {
|
const assetsBySection = useMemo(() => {
|
||||||
return ASSET_SECTIONS.reduce<Record<string, Asset[]>>((acc, section) => {
|
return ASSET_SECTIONS.reduce<Record<string, Asset[]>>((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
|
return acc
|
||||||
}, {})
|
}, {})
|
||||||
}, [assets])
|
}, [assets])
|
||||||
|
|||||||
@ -72,6 +72,7 @@ const initialValues = {
|
|||||||
|
|
||||||
|
|
||||||
asset_type: 'image',
|
asset_type: 'image',
|
||||||
|
type: 'general',
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -369,7 +370,7 @@ const AssetsNew = () => {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
<FormField label="Assettype" labelFor="asset_type">
|
<FormField label="Asset Format" labelFor="asset_type">
|
||||||
<Field name="asset_type" id="asset_type" component="select">
|
<Field name="asset_type" id="asset_type" component="select">
|
||||||
|
|
||||||
<option value="image">image</option>
|
<option value="image">image</option>
|
||||||
@ -383,6 +384,20 @@ const AssetsNew = () => {
|
|||||||
</Field>
|
</Field>
|
||||||
</FormField>
|
</FormField>
|
||||||
|
|
||||||
|
<FormField label="Type" labelFor="type">
|
||||||
|
<Field name="type" id="type" component="select">
|
||||||
|
<option value="general">general</option>
|
||||||
|
<option value="icon">icon</option>
|
||||||
|
<option value="background_image">background_image</option>
|
||||||
|
<option value="audio">audio</option>
|
||||||
|
<option value="video">video</option>
|
||||||
|
<option value="transition">transition</option>
|
||||||
|
<option value="logo">logo</option>
|
||||||
|
<option value="favicon">favicon</option>
|
||||||
|
<option value="document">document</option>
|
||||||
|
</Field>
|
||||||
|
</FormField>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -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');
|
const hasCreatePermission = currentUser && hasPermission(currentUser, 'CREATE_ASSETS');
|
||||||
|
|||||||
@ -170,9 +170,13 @@ const AssetsView = () => {
|
|||||||
|
|
||||||
|
|
||||||
<div className={'mb-4'}>
|
<div className={'mb-4'}>
|
||||||
<p className={'block font-bold mb-2'}>Assettype</p>
|
<p className={'block font-bold mb-2'}>Asset format</p>
|
||||||
<p>{assets?.asset_type ?? 'No data'}</p>
|
<p>{assets?.asset_type ?? 'No data'}</p>
|
||||||
</div>
|
</div>
|
||||||
|
<div className={'mb-4'}>
|
||||||
|
<p className={'block font-bold mb-2'}>Type</p>
|
||||||
|
<p>{assets?.type ?? 'No data'}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -676,4 +680,4 @@ AssetsView.getLayout = function getLayout(page: ReactElement) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default AssetsView;
|
export default AssetsView;
|
||||||
|
|||||||
@ -35,6 +35,7 @@ type ProjectAsset = {
|
|||||||
id: string;
|
id: string;
|
||||||
name?: string;
|
name?: string;
|
||||||
asset_type?: 'image' | 'video' | 'audio' | 'file';
|
asset_type?: 'image' | 'video' | 'audio' | 'file';
|
||||||
|
type?: 'icon' | 'background_image' | 'audio' | 'video' | 'transition' | 'logo' | 'favicon' | 'document' | 'general';
|
||||||
cdn_url?: string | null;
|
cdn_url?: string | null;
|
||||||
storage_key?: string | null;
|
storage_key?: string | null;
|
||||||
};
|
};
|
||||||
@ -109,7 +110,7 @@ type TransitionPreviewState = {
|
|||||||
title: string;
|
title: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type EditorMenuItem = 'none' | 'background_image' | 'background_video' | 'background_audio';
|
type EditorMenuItem = 'none' | 'background_image' | 'background_video' | 'background_audio' | 'create_transition';
|
||||||
|
|
||||||
const parseJsonObject = <T,>(value?: string, fallback?: T): T => {
|
const parseJsonObject = <T,>(value?: string, fallback?: T): T => {
|
||||||
if (!value) return (fallback || ({} as T)) as T;
|
if (!value) return (fallback || ({} as T)) as T;
|
||||||
@ -211,6 +212,7 @@ const resolveAssetPlaybackUrl = (value?: string) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const isBackgroundImageAsset = (asset: ProjectAsset) => {
|
const isBackgroundImageAsset = (asset: ProjectAsset) => {
|
||||||
|
if (asset.type) return asset.type === 'background_image';
|
||||||
const normalizedName = String(asset.name || '').toLowerCase();
|
const normalizedName = String(asset.name || '').toLowerCase();
|
||||||
if (!normalizedName) return false;
|
if (!normalizedName) return false;
|
||||||
const hasBackgroundKeyword = /\bbackground\b|\bbg\b|backdrop|wallpaper/.test(normalizedName);
|
const hasBackgroundKeyword = /\bbackground\b|\bbg\b|backdrop|wallpaper/.test(normalizedName);
|
||||||
@ -411,13 +413,19 @@ const ConstructorPage = () => {
|
|||||||
[assets],
|
[assets],
|
||||||
);
|
);
|
||||||
const transitionVideoAssetOptions = useMemo(() => {
|
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(
|
.filter(
|
||||||
(asset) => asset.asset_type === 'video' && getAssetSourceValue(asset) && /\[TRANSITION\]/i.test(String(asset.name || '')),
|
(asset) => asset.asset_type === 'video' && getAssetSourceValue(asset) && /\[TRANSITION\]/i.test(String(asset.name || '')),
|
||||||
)
|
)
|
||||||
.map((asset) => ({ value: getAssetSourceValue(asset), label: getAssetLabel(asset) }));
|
.map((asset) => ({ value: getAssetSourceValue(asset), label: getAssetLabel(asset) }));
|
||||||
|
|
||||||
if (tagged.length > 0) return tagged;
|
if (taggedAssets.length > 0) return taggedAssets;
|
||||||
|
|
||||||
return videoAssetOptions;
|
return videoAssetOptions;
|
||||||
}, [assets, videoAssetOptions]);
|
}, [assets, videoAssetOptions]);
|
||||||
@ -747,7 +755,7 @@ const ConstructorPage = () => {
|
|||||||
environment: activePage?.environment || 'dev',
|
environment: activePage?.environment || 'dev',
|
||||||
source_key: '',
|
source_key: '',
|
||||||
name: sanitizedName,
|
name: sanitizedName,
|
||||||
slug: `transition-${Date.now().toString().slice(-4)}`,
|
slug: `transition-${createLocalId()}`,
|
||||||
video_url: sanitizedVideoUrl,
|
video_url: sanitizedVideoUrl,
|
||||||
audio_url: '',
|
audio_url: '',
|
||||||
supports_reverse: Boolean(newTransitionSupportsReverse),
|
supports_reverse: Boolean(newTransitionSupportsReverse),
|
||||||
@ -1070,7 +1078,9 @@ const ConstructorPage = () => {
|
|||||||
? 'Background video'
|
? 'Background video'
|
||||||
: selectedMenuItem === 'background_audio'
|
: selectedMenuItem === 'background_audio'
|
||||||
? 'Background audio'
|
? 'Background audio'
|
||||||
: selectedElement?.label || 'Element editor';
|
: selectedMenuItem === 'create_transition'
|
||||||
|
? 'Create transition'
|
||||||
|
: selectedElement?.label || 'Element editor';
|
||||||
|
|
||||||
if (backgroundImageSrc) {
|
if (backgroundImageSrc) {
|
||||||
canvasBackgroundStyle.backgroundImage = `url("${backgroundImageSrc}")`;
|
canvasBackgroundStyle.backgroundImage = `url("${backgroundImageSrc}")`;
|
||||||
@ -1339,6 +1349,55 @@ const ConstructorPage = () => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{selectedMenuItem === 'create_transition' && (
|
||||||
|
<div className='rounded border border-gray-200 p-2 space-y-2'>
|
||||||
|
<p className='text-[11px] font-semibold text-gray-600'>Create next page transition</p>
|
||||||
|
<input
|
||||||
|
className='w-full rounded border border-gray-300 px-2 py-1 text-xs'
|
||||||
|
placeholder='Name'
|
||||||
|
value={newTransitionName}
|
||||||
|
onChange={(event) => setNewTransitionName(event.target.value)}
|
||||||
|
/>
|
||||||
|
<select
|
||||||
|
className='w-full rounded border border-gray-300 px-2 py-1 text-xs'
|
||||||
|
value={newTransitionVideoUrl}
|
||||||
|
onChange={(event) => setNewTransitionVideoUrl(event.target.value)}
|
||||||
|
>
|
||||||
|
<option value=''>Transition video asset</option>
|
||||||
|
{transitionVideoAssetOptions.map((option) => (
|
||||||
|
<option key={option.value} value={option.value}>
|
||||||
|
{option.label}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
<label className='flex items-center gap-2 text-[11px] text-gray-700'>
|
||||||
|
<input
|
||||||
|
type='checkbox'
|
||||||
|
checked={newTransitionSupportsReverse}
|
||||||
|
onChange={(event) => setNewTransitionSupportsReverse(event.target.checked)}
|
||||||
|
/>
|
||||||
|
Supports reverse playback
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
className='w-full rounded border border-gray-300 px-2 py-1 text-xs'
|
||||||
|
type='number'
|
||||||
|
min='0.2'
|
||||||
|
step='0.1'
|
||||||
|
value={newTransitionDurationSec}
|
||||||
|
onChange={(event) => setNewTransitionDurationSec(Number(event.target.value || 0.7))}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type='button'
|
||||||
|
className='menu-action-btn'
|
||||||
|
onClick={createTransition}
|
||||||
|
disabled={isCreatingTransition}
|
||||||
|
>
|
||||||
|
<BaseIcon path={mdiSwapHorizontal} size={16} />
|
||||||
|
<span>{isCreatingTransition ? 'Creating Transition...' : 'Create Transition'}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{selectedElement && (
|
{selectedElement && (
|
||||||
<div className='mb-2'>
|
<div className='mb-2'>
|
||||||
<label className='mb-1 block text-[11px] font-semibold text-gray-600'>Label</label>
|
<label className='mb-1 block text-[11px] font-semibold text-gray-600'>Label</label>
|
||||||
@ -1447,52 +1506,6 @@ const ConstructorPage = () => {
|
|||||||
<BaseButton small color='lightDark' label='Preview Forward' onClick={() => openTransitionPreview('forward')} />
|
<BaseButton small color='lightDark' label='Preview Forward' onClick={() => openTransitionPreview('forward')} />
|
||||||
<BaseButton small color='lightDark' label='Preview Back' onClick={() => openTransitionPreview('back')} />
|
<BaseButton small color='lightDark' label='Preview Back' onClick={() => openTransitionPreview('back')} />
|
||||||
</div>
|
</div>
|
||||||
<div className='rounded border border-gray-200 p-2 space-y-2'>
|
|
||||||
<p className='text-[11px] font-semibold text-gray-600'>Create next page transition</p>
|
|
||||||
<input
|
|
||||||
className='w-full rounded border border-gray-300 px-2 py-1 text-xs'
|
|
||||||
placeholder='Name'
|
|
||||||
value={newTransitionName}
|
|
||||||
onChange={(event) => setNewTransitionName(event.target.value)}
|
|
||||||
/>
|
|
||||||
<select
|
|
||||||
className='w-full rounded border border-gray-300 px-2 py-1 text-xs'
|
|
||||||
value={newTransitionVideoUrl}
|
|
||||||
onChange={(event) => setNewTransitionVideoUrl(event.target.value)}
|
|
||||||
>
|
|
||||||
<option value=''>Transition video asset</option>
|
|
||||||
{transitionVideoAssetOptions.map((option) => (
|
|
||||||
<option key={option.value} value={option.value}>
|
|
||||||
{option.label}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
<label className='flex items-center gap-2 text-[11px] text-gray-700'>
|
|
||||||
<input
|
|
||||||
type='checkbox'
|
|
||||||
checked={newTransitionSupportsReverse}
|
|
||||||
onChange={(event) => setNewTransitionSupportsReverse(event.target.checked)}
|
|
||||||
/>
|
|
||||||
Supports reverse playback
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
className='w-full rounded border border-gray-300 px-2 py-1 text-xs'
|
|
||||||
type='number'
|
|
||||||
min='0.2'
|
|
||||||
step='0.1'
|
|
||||||
value={newTransitionDurationSec}
|
|
||||||
onChange={(event) => setNewTransitionDurationSec(Number(event.target.value || 0.7))}
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
type='button'
|
|
||||||
className='menu-action-btn'
|
|
||||||
onClick={createTransition}
|
|
||||||
disabled={isCreatingTransition}
|
|
||||||
>
|
|
||||||
<BaseIcon path={mdiSwapHorizontal} size={16} />
|
|
||||||
<span>{isCreatingTransition ? 'Creating Transition...' : 'Create Transition'}</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@ -1729,6 +1742,10 @@ const ConstructorPage = () => {
|
|||||||
<BaseIcon path={mdiSwapHorizontal} size={16} />
|
<BaseIcon path={mdiSwapHorizontal} size={16} />
|
||||||
<span>Add Navigation</span>
|
<span>Add Navigation</span>
|
||||||
</button>
|
</button>
|
||||||
|
<button type='button' className='menu-action-btn' onClick={() => selectMenuItemForEdit('create_transition')}>
|
||||||
|
<BaseIcon path={mdiSwapHorizontal} size={16} />
|
||||||
|
<span>Add Transition</span>
|
||||||
|
</button>
|
||||||
<button type='button' className='menu-action-btn' onClick={() => addElement('gallery')}>
|
<button type='button' className='menu-action-btn' onClick={() => addElement('gallery')}>
|
||||||
<BaseIcon path={mdiImageMultiple} size={16} />
|
<BaseIcon path={mdiImageMultiple} size={16} />
|
||||||
<span>Add Gallery</span>
|
<span>Add Gallery</span>
|
||||||
|
|||||||
@ -432,8 +432,7 @@ const EditProjectsPage = () => {
|
|||||||
typeof asset?.cdn_url === 'string' &&
|
typeof asset?.cdn_url === 'string' &&
|
||||||
asset.cdn_url &&
|
asset.cdn_url &&
|
||||||
asset?.asset_type === 'image' &&
|
asset?.asset_type === 'image' &&
|
||||||
typeof asset?.name === 'string' &&
|
(asset?.type === 'logo' || (typeof asset?.name === 'string' && asset.name.startsWith('[LOGO] '))),
|
||||||
asset.name.startsWith('[LOGO] '),
|
|
||||||
)
|
)
|
||||||
setLogoAssets(taggedLogoAssets)
|
setLogoAssets(taggedLogoAssets)
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user