39948-vm/frontend/src/pages/projects/projects-edit.tsx
2026-03-31 12:13:06 +04:00

339 lines
11 KiB
TypeScript

/**
* Edit Projects Page
* Cleaned up version with consolidated useEffect hooks
* Note: Preserves special logo loading logic for logo_url, favicon_url, og_image_url fields
*/
import { mdiChartTimelineVariant } from '@mdi/js';
import axios from 'axios';
import Head from 'next/head';
import React, { ReactElement, useEffect, useState } from 'react';
import CardBox from '../../components/CardBox';
import LayoutAuthenticated from '../../layouts/Authenticated';
import SectionMain from '../../components/SectionMain';
import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton';
import { getPageTitle } from '../../config';
import { Field, Form, Formik } from 'formik';
import FormField from '../../components/FormField';
import BaseDivider from '../../components/BaseDivider';
import BaseButtons from '../../components/BaseButtons';
import BaseButton from '../../components/BaseButton';
import { deleteItem, update, fetch } from '../../stores/projects/projectsSlice';
import { useAppDispatch, useAppSelector } from '../../stores/hooks';
import { useRouter } from 'next/router';
import type { Project } from '../../types/entities';
import { logger } from '../../lib/logger';
const initVals = {
name: '',
slug: '',
description: '',
logo_url: '',
favicon_url: '',
og_image_url: '',
is_deleted: false,
deleted_at_time: new Date(),
};
const EditProjectsPage = () => {
const router = useRouter();
const dispatch = useAppDispatch();
const [initialValues, setInitialValues] = useState(initVals);
const [logoAssets, setLogoAssets] = useState<
{ id: string; cdn_url: string; storage_key?: string; name: string }[]
>([]);
const [isLoadingLogoAssets, setIsLoadingLogoAssets] = useState(false);
const projectsState = useAppSelector((state) => state.projects);
const projects = projectsState.projects as Project | Project[] | undefined;
const project = Array.isArray(projects) ? projects[0] : projects;
const { id } = router.query as { id?: string };
// Fetch entity data
useEffect(() => {
if (id) {
dispatch(fetch({ id: id as string }));
}
}, [id, dispatch]);
// Load logo assets for the project
const loadLogoAssets = async (projectId: string) => {
if (!projectId) {
setLogoAssets([]);
return;
}
setIsLoadingLogoAssets(true);
try {
const response = await axios.get(
`/assets?limit=500&page=0&sort=desc&field=createdAt&projectId=${projectId}`,
);
const rows = Array.isArray(response?.data?.rows)
? response.data.rows
: [];
const taggedLogoAssets = rows.filter(
(asset: {
cdn_url?: string;
asset_type?: string;
type?: string;
name?: string;
}) =>
typeof asset?.cdn_url === 'string' &&
asset.cdn_url &&
asset?.asset_type === 'image' &&
(asset?.type === 'logo' ||
(typeof asset?.name === 'string' &&
asset.name.startsWith('[LOGO] '))),
);
setLogoAssets(taggedLogoAssets);
} catch (error: unknown) {
const errorMessage =
error instanceof Error ? error.message : String(error);
logger.error(
'Failed to load logo assets for project edit:',
error instanceof Error ? error : { error: errorMessage },
);
setLogoAssets([]);
} finally {
setIsLoadingLogoAssets(false);
}
};
useEffect(() => {
setLogoAssets([]);
setIsLoadingLogoAssets(true);
if (typeof id === 'string' && id) {
void loadLogoAssets(id);
} else {
setIsLoadingLogoAssets(false);
}
}, [id]);
// Sync form values with fetched data (consolidated from redundant useEffects)
useEffect(() => {
if (typeof project === 'object' && project !== null) {
const projectData = project as unknown as Record<string, unknown>;
setInitialValues({
name: String(projectData.name || ''),
slug: String(projectData.slug || ''),
description: String(projectData.description || ''),
logo_url: String(projectData.logo_url || ''),
favicon_url: String(projectData.favicon_url || ''),
og_image_url: String(projectData.og_image_url || ''),
is_deleted: Boolean(projectData.is_deleted),
deleted_at_time: projectData.deleted_at_time
? new Date(projectData.deleted_at_time as string)
: new Date(),
});
}
}, [project]);
const handleSubmit = async (data: typeof initVals) => {
const apiData: Partial<Project> = {
name: data.name,
slug: data.slug,
description: data.description,
logo_url: data.logo_url,
favicon_url: data.favicon_url,
og_image_url: data.og_image_url,
};
await dispatch(update({ id: id as string, data: apiData }));
await router.push('/projects/projects-list');
};
const handleDelete = async () => {
const projectId = typeof id === 'string' ? id : null;
if (!projectId) {
return;
}
if (!window.confirm('Are you sure you want to delete this project?')) {
return;
}
await dispatch(deleteItem(projectId));
await router.push('/projects/projects-list');
};
return (
<>
<Head>
<title>{getPageTitle('Edit projects')}</title>
</Head>
<SectionMain>
<SectionTitleLineWithButton
icon={mdiChartTimelineVariant}
title='Edit projects'
main
>
{''}
</SectionTitleLineWithButton>
<CardBox>
<Formik
enableReinitialize
initialValues={initialValues}
onSubmit={(values) => handleSubmit(values)}
>
{({ values }) => (
<Form>
<FormField label='Name'>
<Field name='name' placeholder='Name' />
</FormField>
<FormField label='Slug'>
<Field name='slug' placeholder='Slug' />
</FormField>
<FormField label='Description' hasTextareaHeight>
<Field
name='description'
id='description'
as='textarea'
rows={4}
placeholder='Project description'
/>
</FormField>
<FormField label='Logo'>
<Field name='logo_url' as='select'>
<option value=''>
{isLoadingLogoAssets
? 'Loading logo options...'
: 'Select logo from Assets'}
</option>
{logoAssets.map((asset) => (
<option key={asset.id} value={asset.storage_key || asset.cdn_url}>
{(asset.name || '').replace(/^\[[^\]]+\]\s*/, '')}
</option>
))}
{values.logo_url &&
!logoAssets.some(
(asset) => (asset.storage_key || asset.cdn_url) === values.logo_url,
) && (
<option value={values.logo_url}>
{values.logo_url}
</option>
)}
</Field>
</FormField>
{values.logo_url && (
<a
className='text-xs underline mt-2 inline-block break-all'
href={values.logo_url}
target='_blank'
rel='noreferrer'
>
Preview selected logo
</a>
)}
<FormField label='Favicon'>
<Field name='favicon_url' as='select'>
<option value=''>
{isLoadingLogoAssets
? 'Loading logo options...'
: 'Select favicon from Assets logos'}
</option>
{logoAssets.map((asset) => (
<option key={asset.id} value={asset.storage_key || asset.cdn_url}>
{(asset.name || '').replace(/^\[[^\]]+\]\s*/, '')}
</option>
))}
{values.favicon_url &&
!logoAssets.some(
(asset) => (asset.storage_key || asset.cdn_url) === values.favicon_url,
) && (
<option value={values.favicon_url}>
{values.favicon_url}
</option>
)}
</Field>
</FormField>
{values.favicon_url && (
<a
className='text-xs underline mt-2 inline-block break-all'
href={values.favicon_url}
target='_blank'
rel='noreferrer'
>
Preview selected favicon
</a>
)}
<FormField label='OG Image'>
<Field name='og_image_url' as='select'>
<option value=''>
{isLoadingLogoAssets
? 'Loading logo options...'
: 'Select OG image from Assets logos'}
</option>
{logoAssets.map((asset) => (
<option key={asset.id} value={asset.storage_key || asset.cdn_url}>
{(asset.name || '').replace(/^\[[^\]]+\]\s*/, '')}
</option>
))}
{values.og_image_url &&
!logoAssets.some(
(asset) => (asset.storage_key || asset.cdn_url) === values.og_image_url,
) && (
<option value={values.og_image_url}>
{values.og_image_url}
</option>
)}
</Field>
</FormField>
{values.og_image_url && (
<a
className='text-xs underline mt-2 inline-block break-all'
href={values.og_image_url}
target='_blank'
rel='noreferrer'
>
Preview selected OG image
</a>
)}
<BaseDivider />
<BaseButtons>
<BaseButton type='submit' color='info' label='Submit' />
<BaseButton type='reset' color='info' outline label='Reset' />
<BaseButton
type='button'
color='danger'
label='Delete'
onClick={handleDelete}
/>
<BaseButton
type='reset'
color='danger'
outline
label='Cancel'
onClick={() => router.push('/projects/projects-list')}
/>
</BaseButtons>
</Form>
)}
</Formik>
</CardBox>
</SectionMain>
</>
);
};
EditProjectsPage.getLayout = function getLayout(page: ReactElement) {
return (
<LayoutAuthenticated permission='UPDATE_PROJECTS'>
{page}
</LayoutAuthenticated>
);
};
export default EditProjectsPage;