339 lines
11 KiB
TypeScript
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;
|