✨ Supports release assets & installers
This commit is contained in:
		| @@ -13,6 +13,7 @@ | |||||||
|     "@emotion/react": "^11.14.0", |     "@emotion/react": "^11.14.0", | ||||||
|     "@emotion/server": "^11.11.0", |     "@emotion/server": "^11.11.0", | ||||||
|     "@emotion/styled": "^11.14.0", |     "@emotion/styled": "^11.14.0", | ||||||
|  |     "@monaco-editor/react": "^4.6.0", | ||||||
|     "@mui/icons-material": "^6.3.1", |     "@mui/icons-material": "^6.3.1", | ||||||
|     "@mui/material": "^6.3.1", |     "@mui/material": "^6.3.1", | ||||||
|     "@mui/material-nextjs": "^6.3.1", |     "@mui/material-nextjs": "^6.3.1", | ||||||
| @@ -37,7 +38,7 @@ | |||||||
|     "remark-parse": "^11.0.0", |     "remark-parse": "^11.0.0", | ||||||
|     "remark-rehype": "^11.1.1", |     "remark-rehype": "^11.1.1", | ||||||
|     "sitemap": "^8.0.0", |     "sitemap": "^8.0.0", | ||||||
|     "solar-js-sdk": "^0.0.2", |     "solar-js-sdk": "0.0.8", | ||||||
|     "unified": "^11.0.5", |     "unified": "^11.0.5", | ||||||
|     "zustand": "^5.0.3" |     "zustand": "^5.0.3" | ||||||
|   }, |   }, | ||||||
|   | |||||||
| @@ -8,7 +8,7 @@ | |||||||
|     "name": "LittleSheep", |     "name": "LittleSheep", | ||||||
|     "email": "littlesheep.code@hotmail.com" |     "email": "littlesheep.code@hotmail.com" | ||||||
|   }, |   }, | ||||||
|   "version": "0.0.5", |   "version": "0.0.8", | ||||||
|   "tsup": { |   "tsup": { | ||||||
|     "entry": [ |     "entry": [ | ||||||
|       "src/index.ts" |       "src/index.ts" | ||||||
|   | |||||||
| @@ -6,7 +6,8 @@ export interface MaRelease { | |||||||
|   version: string |   version: string | ||||||
|   type: number |   type: number | ||||||
|   channel: string |   channel: string | ||||||
|   assets: Record<string, any> |   assets: Record<string, MaReleaseAsset> | ||||||
|  |   installers: Record<string, MaReleaseInstaller> | ||||||
|   product_id: number |   product_id: number | ||||||
|   meta: MaReleaseMeta |   meta: MaReleaseMeta | ||||||
| } | } | ||||||
| @@ -22,3 +23,19 @@ export interface MaReleaseMeta { | |||||||
|   attachments: string[] |   attachments: string[] | ||||||
|   release_id: number |   release_id: number | ||||||
| } | } | ||||||
|  |  | ||||||
|  | export interface MaReleaseAsset { | ||||||
|  |   uri: string | ||||||
|  |   contentType: string | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export interface MaReleaseInstallerPatch { | ||||||
|  |   action: string | ||||||
|  |   glob: string | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export interface MaReleaseInstaller { | ||||||
|  |   workdir?: string | ||||||
|  |   script?: string | ||||||
|  |   patches: MaReleaseInstallerPatch[] | ||||||
|  | } | ||||||
|   | |||||||
| @@ -11,15 +11,16 @@ import { | |||||||
|   Typography, |   Typography, | ||||||
|   Grid2 as Grid, |   Grid2 as Grid, | ||||||
|   IconButton, |   IconButton, | ||||||
|  |   Card, | ||||||
| } from '@mui/material' | } from '@mui/material' | ||||||
| import { useRouter } from 'next-nprogress-bar' | import { useRouter } from 'next-nprogress-bar' | ||||||
| import { useEffect, useState } from 'react' | import { useEffect, useState } from 'react' | ||||||
| import { useForm } from 'react-hook-form' | import { useForm } from 'react-hook-form' | ||||||
|  | import { MaProduct, MaRelease, MaReleaseAsset, MaReleaseInstaller, MaReleaseInstallerPatch } from 'solar-js-sdk' | ||||||
|  | import MonacoEditor from '@monaco-editor/react' | ||||||
|  |  | ||||||
| import ErrorIcon from '@mui/icons-material/Error' | import ErrorIcon from '@mui/icons-material/Error' | ||||||
| import CloseIcon from '@mui/icons-material/Close' | import CloseIcon from '@mui/icons-material/Close' | ||||||
| import { MaProduct } from 'solar-js-sdk' |  | ||||||
| import { version } from 'node:os' |  | ||||||
|  |  | ||||||
| export interface MatrixReleaseForm { | export interface MatrixReleaseForm { | ||||||
|   version: string |   version: string | ||||||
| @@ -28,7 +29,8 @@ export interface MatrixReleaseForm { | |||||||
|   title: string |   title: string | ||||||
|   description: string |   description: string | ||||||
|   content: string |   content: string | ||||||
|   assets: Record<string, any> |   assets: Record<string, MaReleaseAsset> | ||||||
|  |   installers: Record<string, MaReleaseInstaller> | ||||||
|   attachments: string[] |   attachments: string[] | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -41,7 +43,7 @@ export default function MaReleaseForm({ | |||||||
|   onSubmit: (data: MatrixReleaseForm) => Promise<any> |   onSubmit: (data: MatrixReleaseForm) => Promise<any> | ||||||
|   onSuccess?: () => void |   onSuccess?: () => void | ||||||
|   parent: Partial<MaProduct> |   parent: Partial<MaProduct> | ||||||
|   defaultValue?: any |   defaultValue?: MaRelease | ||||||
| }) { | }) { | ||||||
|   const { handleSubmit, register } = useForm<MatrixReleaseForm>({ |   const { handleSubmit, register } = useForm<MatrixReleaseForm>({ | ||||||
|     defaultValues: { |     defaultValues: { | ||||||
| @@ -56,17 +58,25 @@ export default function MaReleaseForm({ | |||||||
|   }) |   }) | ||||||
|  |  | ||||||
|   useEffect(() => { |   useEffect(() => { | ||||||
|     if (defaultValue) { |     if (defaultValue?.assets) { | ||||||
|       setAssets(Object.keys(defaultValue.assets).map((k) => ({ k, v: defaultValue.assets[k] }))) |       setAssets(Object.keys(defaultValue.assets).map((k) => ({ k, v: defaultValue.assets[k] }))) | ||||||
|     } |     } | ||||||
|  |     if (defaultValue?.installers) { | ||||||
|  |       setInstallers(Object.keys(defaultValue.installers).map((k) => ({ k, v: defaultValue.installers[k] }))) | ||||||
|  |     } | ||||||
|   }, []) |   }, []) | ||||||
|  |  | ||||||
|   const router = useRouter() |   const router = useRouter() | ||||||
|  |  | ||||||
|   const [assets, setAssets] = useState<{ k: string; v: string }[]>([]) |   const [assets, setAssets] = useState<{ k: string; v: MaReleaseAsset }[]>([]) | ||||||
|  |   const [installers, setInstallers] = useState<{ k: string; v: MaReleaseInstaller }[]>([]) | ||||||
|  |  | ||||||
|   function addAsset() { |   function addAsset() { | ||||||
|     setAssets((val) => [...val, { k: '', v: '' }]) |     setAssets((val) => [...val, { k: '', v: { uri: '', contentType: '' } }]) | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   function addInstaller() { | ||||||
|  |     setInstallers((val) => [...val, { k: '', v: { workdir: '', script: '', patches: [] } }]) | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   const [error, setError] = useState<string | null>(null) |   const [error, setError] = useState<string | null>(null) | ||||||
| @@ -83,7 +93,11 @@ export default function MaReleaseForm({ | |||||||
|   async function submit(data: MatrixReleaseForm) { |   async function submit(data: MatrixReleaseForm) { | ||||||
|     try { |     try { | ||||||
|       setBusy(true) |       setBusy(true) | ||||||
|       await onSubmit({ ...data, assets: assets.reduce((a, { k, v }) => ({ ...a, [k]: v }), {}) }) |       await onSubmit({ | ||||||
|  |         ...data, | ||||||
|  |         assets: assets.reduce((a, { k, v }) => ({ ...a, [k]: v }), {}), | ||||||
|  |         installers: installers.reduce((a, { k, v }) => ({ ...a, [k]: v }), {}), | ||||||
|  |       }) | ||||||
|       callback() |       callback() | ||||||
|     } catch (err: any) { |     } catch (err: any) { | ||||||
|       setError(err.toString()) |       setError(err.toString()) | ||||||
| @@ -105,7 +119,12 @@ export default function MaReleaseForm({ | |||||||
|  |  | ||||||
|         <FormControl fullWidth> |         <FormControl fullWidth> | ||||||
|           <InputLabel id="release-type">Type</InputLabel> |           <InputLabel id="release-type">Type</InputLabel> | ||||||
|           <Select labelId="release-type" label="Type" {...register('type', { required: true })}> |           <Select | ||||||
|  |             labelId="release-type" | ||||||
|  |             label="Type" | ||||||
|  |             defaultValue={defaultValue?.type} | ||||||
|  |             {...register('type', { required: true })} | ||||||
|  |           > | ||||||
|             <MenuItem value={0}>Full Release</MenuItem> |             <MenuItem value={0}>Full Release</MenuItem> | ||||||
|             <MenuItem value={1}>Patch Release</MenuItem> |             <MenuItem value={1}>Patch Release</MenuItem> | ||||||
|           </Select> |           </Select> | ||||||
| @@ -120,11 +139,13 @@ export default function MaReleaseForm({ | |||||||
|         <TextField minRows={5} multiline label="Content" {...register('content')} /> |         <TextField minRows={5} multiline label="Content" {...register('content')} /> | ||||||
|  |  | ||||||
|         <Box sx={{ mt: 3, display: 'flex', flexDirection: 'column', gap: 2 }}> |         <Box sx={{ mt: 3, display: 'flex', flexDirection: 'column', gap: 2 }}> | ||||||
|           <Typography variant="subtitle1">Assets</Typography> |           <Typography variant="h5">Assets</Typography> | ||||||
|  |  | ||||||
|           {assets.map(({ k, v }, idx) => ( |           {assets.map(({ k, v }, idx) => ( | ||||||
|             <Grid container key={idx} spacing={2}> |             <Card variant="outlined" key={idx}> | ||||||
|               <Grid size={4}> |               <Box sx={{ pl: 2, pr: 4, py: 2 }}> | ||||||
|  |                 <Grid container spacing={2}> | ||||||
|  |                   <Grid size={11}> | ||||||
|                     <TextField |                     <TextField | ||||||
|                       label="Platform" |                       label="Platform" | ||||||
|                       sx={{ width: '100%' }} |                       sx={{ width: '100%' }} | ||||||
| @@ -136,18 +157,6 @@ export default function MaReleaseForm({ | |||||||
|                       }} |                       }} | ||||||
|                     /> |                     /> | ||||||
|                   </Grid> |                   </Grid> | ||||||
|               <Grid size={7}> |  | ||||||
|                 <TextField |  | ||||||
|                   label="URL" |  | ||||||
|                   sx={{ width: '100%' }} |  | ||||||
|                   value={v} |  | ||||||
|                   onChange={(val) => { |  | ||||||
|                     setAssets((data) => |  | ||||||
|                       data.map((ele, index) => (index == idx ? { v: val.target.value, k: ele.k } : ele)), |  | ||||||
|                     ) |  | ||||||
|                   }} |  | ||||||
|                 /> |  | ||||||
|               </Grid> |  | ||||||
|                   <Grid size={1} sx={{ display: 'grid', placeItems: 'center' }}> |                   <Grid size={1} sx={{ display: 'grid', placeItems: 'center' }}> | ||||||
|                     <IconButton |                     <IconButton | ||||||
|                       onClick={() => { |                       onClick={() => { | ||||||
| @@ -157,7 +166,37 @@ export default function MaReleaseForm({ | |||||||
|                       <CloseIcon /> |                       <CloseIcon /> | ||||||
|                     </IconButton> |                     </IconButton> | ||||||
|                   </Grid> |                   </Grid> | ||||||
|  |                   <Grid size={8}> | ||||||
|  |                     <TextField | ||||||
|  |                       label="URI" | ||||||
|  |                       sx={{ width: '100%' }} | ||||||
|  |                       value={v.uri} | ||||||
|  |                       onChange={(val) => { | ||||||
|  |                         setAssets((data) => | ||||||
|  |                           data.map((ele, index) => | ||||||
|  |                             index == idx ? { v: { ...ele.v, uri: val.target.value }, k: ele.k } : ele, | ||||||
|  |                           ), | ||||||
|  |                         ) | ||||||
|  |                       }} | ||||||
|  |                     /> | ||||||
|                   </Grid> |                   </Grid> | ||||||
|  |                   <Grid size={4}> | ||||||
|  |                     <TextField | ||||||
|  |                       label="Content Type" | ||||||
|  |                       sx={{ width: '100%' }} | ||||||
|  |                       value={v.contentType} | ||||||
|  |                       onChange={(val) => { | ||||||
|  |                         setAssets((data) => | ||||||
|  |                           data.map((ele, index) => | ||||||
|  |                             index == idx ? { v: { ...ele.v, contentType: val.target.value }, k: ele.k } : ele, | ||||||
|  |                           ), | ||||||
|  |                         ) | ||||||
|  |                       }} | ||||||
|  |                     /> | ||||||
|  |                   </Grid> | ||||||
|  |                 </Grid> | ||||||
|  |               </Box> | ||||||
|  |             </Card> | ||||||
|           ))} |           ))} | ||||||
|  |  | ||||||
|           <Box> |           <Box> | ||||||
| @@ -167,6 +206,109 @@ export default function MaReleaseForm({ | |||||||
|           </Box> |           </Box> | ||||||
|         </Box> |         </Box> | ||||||
|  |  | ||||||
|  |         <Box sx={{ mt: 3, display: 'flex', flexDirection: 'column', gap: 2 }}> | ||||||
|  |           <Typography variant="h5">Installers</Typography> | ||||||
|  |  | ||||||
|  |           {installers.map(({ k, v }, idx) => ( | ||||||
|  |             <Card variant="outlined" key={idx}> | ||||||
|  |               <Box sx={{ pl: 2, pr: 4, py: 2 }}> | ||||||
|  |                 <Grid container spacing={2}> | ||||||
|  |                   <Grid size={4}> | ||||||
|  |                     <TextField | ||||||
|  |                       label="Platform" | ||||||
|  |                       sx={{ width: '100%' }} | ||||||
|  |                       value={k} | ||||||
|  |                       onChange={(val) => { | ||||||
|  |                         setInstallers((data) => | ||||||
|  |                           data.map((ele, index) => (index == idx ? { k: val.target.value, v: ele.v } : ele)), | ||||||
|  |                         ) | ||||||
|  |                       }} | ||||||
|  |                     /> | ||||||
|  |                   </Grid> | ||||||
|  |                   <Grid size={7}> | ||||||
|  |                     <TextField | ||||||
|  |                       label="Working Directory" | ||||||
|  |                       sx={{ width: '100%' }} | ||||||
|  |                       value={v.workdir} | ||||||
|  |                       onChange={(val) => { | ||||||
|  |                         setInstallers((data) => | ||||||
|  |                           data.map((ele, index) => | ||||||
|  |                             index == idx ? { k: ele.k, v: { ...ele.v, workdir: val.target.value } } : ele, | ||||||
|  |                           ), | ||||||
|  |                         ) | ||||||
|  |                       }} | ||||||
|  |                     /> | ||||||
|  |                   </Grid> | ||||||
|  |                   <Grid size={1} sx={{ display: 'grid', placeItems: 'center' }}> | ||||||
|  |                     <IconButton | ||||||
|  |                       onClick={() => { | ||||||
|  |                         setInstallers((data) => data.filter((_, index) => index != idx)) | ||||||
|  |                       }} | ||||||
|  |                     > | ||||||
|  |                       <CloseIcon /> | ||||||
|  |                     </IconButton> | ||||||
|  |                   </Grid> | ||||||
|  |                   <Grid size={12}> | ||||||
|  |                     <Typography variant="subtitle1" sx={{ mx: 1 }}> | ||||||
|  |                       Script | ||||||
|  |                     </Typography> | ||||||
|  |                     <Card variant="outlined"> | ||||||
|  |                       <MonacoEditor | ||||||
|  |                         height="280px" | ||||||
|  |                         width="100%" | ||||||
|  |                         options={{ minimap: { enabled: false } }} | ||||||
|  |                         defaultValue={v.script} | ||||||
|  |                         onChange={(val) => | ||||||
|  |                           setInstallers((data) => | ||||||
|  |                             data.map((ele, index) => (index == idx ? { v: { ...ele.v, script: val }, k: ele.k } : ele)), | ||||||
|  |                           ) | ||||||
|  |                         } | ||||||
|  |                       /> | ||||||
|  |                     </Card> | ||||||
|  |                   </Grid> | ||||||
|  |                   <Grid size={12}> | ||||||
|  |                     <Typography variant="subtitle1" sx={{ mx: 1 }}> | ||||||
|  |                       Patches | ||||||
|  |                     </Typography> | ||||||
|  |                     <Card variant="outlined"> | ||||||
|  |                       <MonacoEditor | ||||||
|  |                         height="280px" | ||||||
|  |                         width="100%" | ||||||
|  |                         options={{ minimap: { enabled: false } }} | ||||||
|  |                         defaultValue={v.patches.map((p) => `${p.action}:${p.glob}`).join('\n')} | ||||||
|  |                         onChange={(val) => | ||||||
|  |                           setInstallers((data) => | ||||||
|  |                             data.map((ele, index) => | ||||||
|  |                               index == idx | ||||||
|  |                                 ? { | ||||||
|  |                                     v: { | ||||||
|  |                                       ...ele.v, | ||||||
|  |                                       patches: val?.split('\n')?.map((p) => ({ | ||||||
|  |                                         action: p.split(':')[0], | ||||||
|  |                                         glob: p.split(':')[1], | ||||||
|  |                                       })) as MaReleaseInstallerPatch[], | ||||||
|  |                                     }, | ||||||
|  |                                     k: ele.k, | ||||||
|  |                                   } | ||||||
|  |                                 : ele, | ||||||
|  |                             ), | ||||||
|  |                           ) | ||||||
|  |                         } | ||||||
|  |                       /> | ||||||
|  |                     </Card> | ||||||
|  |                   </Grid> | ||||||
|  |                 </Grid> | ||||||
|  |               </Box> | ||||||
|  |             </Card> | ||||||
|  |           ))} | ||||||
|  |  | ||||||
|  |           <Box> | ||||||
|  |             <Button variant="outlined" onClick={addInstaller}> | ||||||
|  |               Add | ||||||
|  |             </Button> | ||||||
|  |           </Box> | ||||||
|  |         </Box> | ||||||
|  |  | ||||||
|         <Box sx={{ mt: 5 }} display="flex" gap={2}> |         <Box sx={{ mt: 5 }} display="flex" gap={2}> | ||||||
|           <Button variant="contained" type="submit" disabled={busy}> |           <Button variant="contained" type="submit" disabled={busy}> | ||||||
|             Submit |             Submit | ||||||
|   | |||||||
| @@ -34,15 +34,27 @@ export default function MatrixMarketplace() { | |||||||
|     if (!yes) return |     if (!yes) return | ||||||
|  |  | ||||||
|     await sni.delete('/cgi/ma/products/' + id) |     await sni.delete('/cgi/ma/products/' + id) | ||||||
|     window.location.reload() |     await fetchProducts() | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   return ( |   return ( | ||||||
|     <ConsoleLayout> |     <ConsoleLayout> | ||||||
|       <Container sx={{ py: 16, display: 'flex', flexDirection: 'column', gap: 4 }}> |       <Container sx={{ py: 16, display: 'flex', flexDirection: 'column', gap: 4 }}> | ||||||
|  |         <Box> | ||||||
|           <Typography variant="h3" component="h1"> |           <Typography variant="h3" component="h1"> | ||||||
|             Matrix Marketplace |             Matrix Marketplace | ||||||
|           </Typography> |           </Typography> | ||||||
|  |           <Typography variant="body1"> | ||||||
|  |             The new way to release your app, implement version check and auto updating. | ||||||
|  |           </Typography> | ||||||
|  |         </Box> | ||||||
|  |  | ||||||
|  |         <Box display="flex" flexDirection="column" gap={2}> | ||||||
|  |           <Box> | ||||||
|  |             <NextLink passHref href="/console/matrix/products/new"> | ||||||
|  |               <Button variant="contained">Create a product</Button> | ||||||
|  |             </NextLink> | ||||||
|  |           </Box> | ||||||
|  |  | ||||||
|           <Grid container columns={{ xs: 1, sm: 2, md: 3 }} spacing={4}> |           <Grid container columns={{ xs: 1, sm: 2, md: 3 }} spacing={4}> | ||||||
|             {products.map((p) => ( |             {products.map((p) => ( | ||||||
| @@ -69,11 +81,6 @@ export default function MatrixMarketplace() { | |||||||
|               </Grid> |               </Grid> | ||||||
|             ))} |             ))} | ||||||
|           </Grid> |           </Grid> | ||||||
|  |  | ||||||
|         <Box> |  | ||||||
|           <NextLink passHref href="/console/matrix/products/new"> |  | ||||||
|             <Button variant="contained">Create a product</Button> |  | ||||||
|           </NextLink> |  | ||||||
|         </Box> |         </Box> | ||||||
|       </Container> |       </Container> | ||||||
|     </ConsoleLayout> |     </ConsoleLayout> | ||||||
|   | |||||||
| @@ -1,7 +1,8 @@ | |||||||
| import { ConsoleLayout, getConsoleStaticProps } from '@/components/layouts/ConsoleLayout' | import { ConsoleLayout, getConsoleStaticProps } from '@/components/layouts/ConsoleLayout' | ||||||
| import { Box, Button, Container, Typography, Grid2 as Grid, Card, CardContent, CardActions } from '@mui/material' | import { Box, Button, Container, Typography, Grid2 as Grid, Card, CardContent, CardActions } from '@mui/material' | ||||||
| import { GetServerSideProps, InferGetServerSidePropsType } from 'next' | import { GetServerSideProps, InferGetServerSidePropsType } from 'next' | ||||||
| import { sni, MaProduct } from 'solar-js-sdk' | import { sni, MaProduct, MaRelease } from 'solar-js-sdk' | ||||||
|  | import { useEffect, useState } from 'react' | ||||||
| import NextLink from 'next/link' | import NextLink from 'next/link' | ||||||
|  |  | ||||||
| export const getServerSideProps: GetServerSideProps = (async (context) => { | export const getServerSideProps: GetServerSideProps = (async (context) => { | ||||||
| @@ -9,28 +10,37 @@ export const getServerSideProps: GetServerSideProps = (async (context) => { | |||||||
|  |  | ||||||
|   const { data } = await sni.get<MaProduct>('/cgi/ma/products/' + id) |   const { data } = await sni.get<MaProduct>('/cgi/ma/products/' + id) | ||||||
|  |  | ||||||
|   const { data: resp } = await sni.get<{ data: any[] }>('/cgi/ma/products/' + id + '/releases', { |   return getConsoleStaticProps({ | ||||||
|  |     props: { | ||||||
|  |       title: `Product "${data.name}"`, | ||||||
|  |       product: data, | ||||||
|  |     }, | ||||||
|  |   }) | ||||||
|  | }) satisfies GetServerSideProps<{ product: MaProduct }> | ||||||
|  |  | ||||||
|  | export default function ProductDetails({ product }: InferGetServerSidePropsType<typeof getServerSideProps>) { | ||||||
|  |   const [releases, setReleases] = useState<MaRelease[]>([]) | ||||||
|  |  | ||||||
|  |   async function fetchReleases() { | ||||||
|  |     const { data: resp } = await sni.get<{ data: MaRelease[] }>('/cgi/ma/products/' + product.id + '/releases', { | ||||||
|       params: { |       params: { | ||||||
|         take: 10, |         take: 10, | ||||||
|       }, |       }, | ||||||
|     }) |     }) | ||||||
|  |  | ||||||
|   return getConsoleStaticProps({ |     setReleases(resp.data) | ||||||
|     props: { |   } | ||||||
|       title: `Product "${data.name}"`, |  | ||||||
|       product: data, |   useEffect(() => { | ||||||
|       releases: resp.data, |     fetchReleases() | ||||||
|     }, |   }, []) | ||||||
|   }) |  | ||||||
| }) satisfies GetServerSideProps<{ product: MaProduct; releases: any[] }> |  | ||||||
|  |  | ||||||
| export default function ProductDetails({ product, releases }: InferGetServerSidePropsType<typeof getServerSideProps>) { |  | ||||||
|   async function deleteRelease(id: number) { |   async function deleteRelease(id: number) { | ||||||
|     const yes = confirm(`Are you sure you want to delete this release #${id}?`) |     const yes = confirm(`Are you sure you want to delete this release #${id}?`) | ||||||
|     if (!yes) return |     if (!yes) return | ||||||
|  |  | ||||||
|     await sni.delete('/cgi/ma/products/' + product.id + '/releases/' + id) |     await sni.delete('/cgi/ma/products/' + product.id + '/releases/' + id) | ||||||
|     window.location.reload() |     await fetchReleases() | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   return ( |   return ( | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user