Compare commits
No commits in common. "a69362470fea35d23f431de3d338afabd3aeef55" and "d8f51e305b5ab40ee25fd2158b6e89730d67de2f" have entirely different histories.
a69362470f
...
d8f51e305b
@ -37,7 +37,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": "./packages/sn",
|
"solar-js-sdk": "^0.0.1",
|
||||||
"unified": "^11.0.5",
|
"unified": "^11.0.5",
|
||||||
"zustand": "^5.0.3"
|
"zustand": "^5.0.3"
|
||||||
},
|
},
|
||||||
|
@ -7,7 +7,7 @@
|
|||||||
"name": "LittleSheep",
|
"name": "LittleSheep",
|
||||||
"email": "littlesheep.code@hotmail.com"
|
"email": "littlesheep.code@hotmail.com"
|
||||||
},
|
},
|
||||||
"version": "0.0.2",
|
"version": "0.0.1",
|
||||||
"tsup": {
|
"tsup": {
|
||||||
"entry": [
|
"entry": [
|
||||||
"src/index.ts"
|
"src/index.ts"
|
||||||
|
@ -46,141 +46,3 @@ export async function listAttachment(id: string[]): Promise<SnAttachment[]> {
|
|||||||
})
|
})
|
||||||
return resp.data.data
|
return resp.data.data
|
||||||
}
|
}
|
||||||
|
|
||||||
export type MultipartProgress = {
|
|
||||||
value: number | null
|
|
||||||
current: number
|
|
||||||
total: number
|
|
||||||
}
|
|
||||||
|
|
||||||
export type MultipartInfo = {
|
|
||||||
rid: string
|
|
||||||
fileChunks: Record<string, number>
|
|
||||||
isUploaded: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
export class UploadAttachmentTask {
|
|
||||||
private content: File
|
|
||||||
private pool: string
|
|
||||||
private multipartSize: number = 0
|
|
||||||
private multipartInfo: MultipartInfo | null = null
|
|
||||||
private multipartProgress: MultipartProgress = { value: null, current: 0, total: 0 }
|
|
||||||
|
|
||||||
loading: boolean = false
|
|
||||||
success: boolean = false
|
|
||||||
error: string | null = null
|
|
||||||
|
|
||||||
constructor(content: File, pool: string) {
|
|
||||||
if (!content || !pool) {
|
|
||||||
throw new Error('Content and pool are required.')
|
|
||||||
}
|
|
||||||
this.content = content
|
|
||||||
this.pool = pool
|
|
||||||
}
|
|
||||||
|
|
||||||
public async submit(): Promise<void> {
|
|
||||||
this.loading = true
|
|
||||||
const limit = 3
|
|
||||||
|
|
||||||
try {
|
|
||||||
await this.createFragment()
|
|
||||||
console.log(`[Paperclip] Multipart placeholder has been created with rid ${this.multipartInfo?.rid}`)
|
|
||||||
|
|
||||||
this.multipartProgress.value = 0
|
|
||||||
this.multipartProgress.current = 0
|
|
||||||
|
|
||||||
const chunks = Object.keys(this.multipartInfo?.fileChunks || {})
|
|
||||||
this.multipartProgress.total = chunks.length
|
|
||||||
|
|
||||||
const uploadChunks = async (chunk: string): Promise<void> => {
|
|
||||||
try {
|
|
||||||
await this.uploadSingleMultipart(chunk)
|
|
||||||
this.multipartProgress.current++
|
|
||||||
console.log(
|
|
||||||
`[Paperclip] Uploaded multipart ${this.multipartProgress.current}/${this.multipartProgress.total}`,
|
|
||||||
)
|
|
||||||
this.multipartProgress.value = this.multipartProgress.current / this.multipartProgress.total
|
|
||||||
} catch (err) {
|
|
||||||
console.log(`[Paperclip] Upload multipart ${chunk} failed, retrying in 3 seconds...`)
|
|
||||||
await this.delay(3000)
|
|
||||||
await uploadChunks(chunk)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let i = 0; i < chunks.length; i += limit) {
|
|
||||||
const chunkSlice = chunks.slice(i, i + limit)
|
|
||||||
await Promise.all(chunkSlice.map(uploadChunks))
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.multipartInfo?.isUploaded) {
|
|
||||||
console.log(`[Paperclip] Entire file has been uploaded in ${this.multipartProgress.total} chunk(s)`)
|
|
||||||
this.success = true
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e)
|
|
||||||
this.error = e instanceof Error ? e.message : String(e)
|
|
||||||
} finally {
|
|
||||||
this.loading = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async createFragment(): Promise<void> {
|
|
||||||
const mimetypeMap: Record<string, string> = {
|
|
||||||
mp4: 'video/mp4',
|
|
||||||
mov: 'video/quicktime',
|
|
||||||
mp3: 'audio/mp3',
|
|
||||||
wav: 'audio/wav',
|
|
||||||
m4a: 'audio/m4a',
|
|
||||||
}
|
|
||||||
|
|
||||||
const fileExtension = this.content.name.split('.').pop() || ''
|
|
||||||
const mimetype = mimetypeMap[fileExtension]
|
|
||||||
|
|
||||||
const nameArray = this.content.name.split('.')
|
|
||||||
nameArray.pop()
|
|
||||||
|
|
||||||
const resp = await sni.post('/cgi/uc/attachments/fragments', {
|
|
||||||
pool: this.pool,
|
|
||||||
size: this.content.size,
|
|
||||||
name: this.content.name,
|
|
||||||
alt: nameArray.join('.'),
|
|
||||||
mimetype,
|
|
||||||
})
|
|
||||||
|
|
||||||
const data = await resp.data
|
|
||||||
|
|
||||||
this.multipartSize = data.chunkSize
|
|
||||||
this.multipartInfo = data.meta
|
|
||||||
}
|
|
||||||
|
|
||||||
private async uploadSingleMultipart(chunkId: string): Promise<void> {
|
|
||||||
if (!this.multipartInfo) return
|
|
||||||
|
|
||||||
const chunkIdx = this.multipartInfo.fileChunks[chunkId]
|
|
||||||
const chunk = this.content.slice(chunkIdx * this.multipartSize, (chunkIdx + 1) * this.multipartSize)
|
|
||||||
|
|
||||||
const data = new FormData()
|
|
||||||
data.set('file', chunk)
|
|
||||||
|
|
||||||
const resp = await sni.post(`/cgi/uc/attachments/fragments/${this.multipartInfo.rid}/${chunkId}`, data, {
|
|
||||||
timeout: 3 * 60 * 1000,
|
|
||||||
})
|
|
||||||
|
|
||||||
this.multipartInfo = resp.data
|
|
||||||
}
|
|
||||||
|
|
||||||
private delay(ms: number): Promise<void> {
|
|
||||||
return new Promise((resolve) => setTimeout(resolve, ms))
|
|
||||||
}
|
|
||||||
|
|
||||||
public static formatBytes(bytes: number, decimals = 2): string {
|
|
||||||
if (!+bytes) return '0 Bytes'
|
|
||||||
|
|
||||||
const k = 1024
|
|
||||||
const dm = decimals < 0 ? 0 : decimals
|
|
||||||
const sizes = ['Bytes', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB']
|
|
||||||
|
|
||||||
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
|
||||||
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(dm))} ${sizes[i]}`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -1,24 +0,0 @@
|
|||||||
export interface MaRelease {
|
|
||||||
id: number
|
|
||||||
created_at: Date
|
|
||||||
updated_at: Date
|
|
||||||
deleted_at?: Date
|
|
||||||
version: string
|
|
||||||
type: number
|
|
||||||
channel: string
|
|
||||||
assets: Record<string, any>
|
|
||||||
product_id: number
|
|
||||||
meta: MaReleaseMeta
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface MaReleaseMeta {
|
|
||||||
id: number
|
|
||||||
created_at: Date
|
|
||||||
updated_at: Date
|
|
||||||
deleted_at?: Date
|
|
||||||
title: string
|
|
||||||
description: string
|
|
||||||
content: string
|
|
||||||
attachments: string[]
|
|
||||||
release_id: number
|
|
||||||
}
|
|
@ -1,166 +0,0 @@
|
|||||||
import {
|
|
||||||
Collapse,
|
|
||||||
Alert,
|
|
||||||
TextField,
|
|
||||||
Button,
|
|
||||||
Box,
|
|
||||||
FormControl,
|
|
||||||
InputLabel,
|
|
||||||
MenuItem,
|
|
||||||
Select,
|
|
||||||
Typography,
|
|
||||||
Grid2 as Grid,
|
|
||||||
IconButton,
|
|
||||||
} from '@mui/material'
|
|
||||||
import { useRouter } from 'next-nprogress-bar'
|
|
||||||
import { useState } from 'react'
|
|
||||||
import { useForm } from 'react-hook-form'
|
|
||||||
|
|
||||||
import ErrorIcon from '@mui/icons-material/Error'
|
|
||||||
import CloseIcon from '@mui/icons-material/Close'
|
|
||||||
import { MaProduct } from 'solar-js-sdk'
|
|
||||||
|
|
||||||
export interface MatrixReleaseForm {
|
|
||||||
version: string
|
|
||||||
type: number
|
|
||||||
channel: string
|
|
||||||
title: string
|
|
||||||
description: string
|
|
||||||
content: string
|
|
||||||
assets: Record<string, any>
|
|
||||||
attachments: string[]
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function MaReleaseForm({
|
|
||||||
onSubmit,
|
|
||||||
onSuccess,
|
|
||||||
parent,
|
|
||||||
defaultValue,
|
|
||||||
}: {
|
|
||||||
onSubmit: (data: MatrixReleaseForm) => Promise<any>
|
|
||||||
onSuccess?: () => void
|
|
||||||
parent: MaProduct
|
|
||||||
defaultValue?: unknown
|
|
||||||
}) {
|
|
||||||
const { handleSubmit, register } = useForm<MatrixReleaseForm>({
|
|
||||||
defaultValues: {},
|
|
||||||
})
|
|
||||||
|
|
||||||
const router = useRouter()
|
|
||||||
|
|
||||||
const [assets, setAssets] = useState<{ k: string; v: string }[]>([])
|
|
||||||
|
|
||||||
function addAsset() {
|
|
||||||
setAssets((val) => [...val, { k: '', v: '' }])
|
|
||||||
}
|
|
||||||
|
|
||||||
const [error, setError] = useState<string | null>(null)
|
|
||||||
const [busy, setBusy] = useState<boolean>(false)
|
|
||||||
|
|
||||||
function callback() {
|
|
||||||
if (onSuccess) {
|
|
||||||
onSuccess()
|
|
||||||
} else {
|
|
||||||
router.push(`/console/matrix/products/${parent.id}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function submit(data: MatrixReleaseForm) {
|
|
||||||
try {
|
|
||||||
setBusy(true)
|
|
||||||
await onSubmit({ ...data, assets: assets.reduce((a, { k, v }) => ({ ...a, [k]: v }), {}) })
|
|
||||||
callback()
|
|
||||||
} catch (err: any) {
|
|
||||||
setError(err.toString())
|
|
||||||
} finally {
|
|
||||||
setBusy(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<form onSubmit={handleSubmit(submit)}>
|
|
||||||
<Box display="flex" flexDirection="column" maxWidth="sm" gap={2.5}>
|
|
||||||
<Collapse in={!!error} sx={{ width: '100%' }}>
|
|
||||||
<Alert icon={<ErrorIcon fontSize="inherit" />} severity="error">
|
|
||||||
{error}
|
|
||||||
</Alert>
|
|
||||||
</Collapse>
|
|
||||||
|
|
||||||
<TextField label="Version" placeholder="Major.Minor.Patch" {...register('version', { required: true })} />
|
|
||||||
|
|
||||||
<FormControl fullWidth>
|
|
||||||
<InputLabel id="release-type">Type</InputLabel>
|
|
||||||
<Select labelId="release-type" label="Type" {...register('type', { required: true })}>
|
|
||||||
<MenuItem value={0}>Full Release</MenuItem>
|
|
||||||
<MenuItem value={1}>Patch Release</MenuItem>
|
|
||||||
</Select>
|
|
||||||
</FormControl>
|
|
||||||
|
|
||||||
<TextField label="Title" {...register('title')} />
|
|
||||||
|
|
||||||
<TextField label="Alias" {...register('channel')} />
|
|
||||||
|
|
||||||
<TextField minRows={3} maxRows={3} multiline label="Description" {...register('description')} />
|
|
||||||
|
|
||||||
<TextField minRows={5} multiline label="Content" {...register('content')} />
|
|
||||||
|
|
||||||
<Box sx={{ mt: 3, display: 'flex', flexDirection: 'column', gap: 2 }}>
|
|
||||||
<Typography variant="subtitle1">Assets</Typography>
|
|
||||||
|
|
||||||
{assets.map(({ k, v }, idx) => (
|
|
||||||
<Grid container key={idx} spacing={2}>
|
|
||||||
<Grid size={4}>
|
|
||||||
<TextField
|
|
||||||
label="Platform"
|
|
||||||
sx={{ width: '100%' }}
|
|
||||||
value={k}
|
|
||||||
onChange={(val) => {
|
|
||||||
setAssets((data) =>
|
|
||||||
data.map((ele, index) => (index == idx ? { k: val.target.value, v: ele.v } : ele)),
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</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' }}>
|
|
||||||
<IconButton
|
|
||||||
onClick={() => {
|
|
||||||
setAssets((data) => data.filter((_, index) => index != idx))
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<CloseIcon />
|
|
||||||
</IconButton>
|
|
||||||
</Grid>
|
|
||||||
</Grid>
|
|
||||||
))}
|
|
||||||
|
|
||||||
<Box>
|
|
||||||
<Button variant="outlined" onClick={addAsset}>
|
|
||||||
Add
|
|
||||||
</Button>
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
<Box sx={{ mt: 5 }} display="flex" gap={2}>
|
|
||||||
<Button variant="contained" type="submit" disabled={busy}>
|
|
||||||
Submit
|
|
||||||
</Button>
|
|
||||||
<Button onClick={callback} variant="outlined" disabled={busy}>
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
</form>
|
|
||||||
)
|
|
||||||
}
|
|
@ -1,92 +0,0 @@
|
|||||||
import { Alert, Box, Button, CircularProgress, Collapse, Container, styled, Typography } from '@mui/material'
|
|
||||||
import { useEffect, useState } from 'react'
|
|
||||||
import { checkAuthenticatedClient, redirectToLogin, UploadAttachmentTask } from 'solar-js-sdk'
|
|
||||||
|
|
||||||
import ErrorIcon from '@mui/icons-material/Error'
|
|
||||||
import CloudUploadIcon from '@mui/icons-material/CloudUpload'
|
|
||||||
|
|
||||||
export function getStaticProps() {
|
|
||||||
return {
|
|
||||||
props: {
|
|
||||||
title: 'New Attachment',
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const VisuallyHiddenInput = styled('input')({
|
|
||||||
clip: 'rect(0 0 0 0)',
|
|
||||||
clipPath: 'inset(50%)',
|
|
||||||
height: 1,
|
|
||||||
overflow: 'hidden',
|
|
||||||
position: 'absolute',
|
|
||||||
bottom: 0,
|
|
||||||
left: 0,
|
|
||||||
whiteSpace: 'nowrap',
|
|
||||||
width: 1,
|
|
||||||
})
|
|
||||||
|
|
||||||
export default function AttachmentNew() {
|
|
||||||
useEffect(() => {
|
|
||||||
if (!checkAuthenticatedClient()) redirectToLogin()
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const [file, setFile] = useState<File>()
|
|
||||||
const [busy, setBusy] = useState(false)
|
|
||||||
const [error, setError] = useState<string | null>(null)
|
|
||||||
|
|
||||||
async function submit() {
|
|
||||||
if (!file) return
|
|
||||||
|
|
||||||
try {
|
|
||||||
setBusy(true)
|
|
||||||
const task = new UploadAttachmentTask(file, 'interactive')
|
|
||||||
await task.submit()
|
|
||||||
} catch (err: any) {
|
|
||||||
setError(err.toString())
|
|
||||||
} finally {
|
|
||||||
setBusy(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Container
|
|
||||||
sx={{
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column',
|
|
||||||
justifyContent: 'center',
|
|
||||||
alignItems: 'center',
|
|
||||||
height: 'calc(100vh - 64px)',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Box sx={{ textAlign: 'center' }}>
|
|
||||||
<Collapse in={!!error} sx={{ width: '100%' }}>
|
|
||||||
<Alert sx={{ mb: 4 }} icon={<ErrorIcon fontSize="inherit" />} severity="error">
|
|
||||||
{error}
|
|
||||||
</Alert>
|
|
||||||
</Collapse>
|
|
||||||
|
|
||||||
{busy ? (
|
|
||||||
<CircularProgress />
|
|
||||||
) : (
|
|
||||||
<Button component="label" variant="contained" tabIndex={-1} startIcon={<CloudUploadIcon />}>
|
|
||||||
Upload files
|
|
||||||
<VisuallyHiddenInput
|
|
||||||
type="file"
|
|
||||||
onChange={(event) => {
|
|
||||||
if (event.target.files) setFile(event.target.files[0])
|
|
||||||
submit()
|
|
||||||
}}
|
|
||||||
multiple
|
|
||||||
/>
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Box sx={{ mt: 2 }}>
|
|
||||||
<Typography variant="subtitle1" fontFamily="monospace" fontSize={13}>
|
|
||||||
Pool: Interactive
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
</Container>
|
|
||||||
)
|
|
||||||
}
|
|
@ -47,9 +47,7 @@ export default function MatrixMarketplace() {
|
|||||||
<Typography variant="body1">{p.description}</Typography>
|
<Typography variant="body1">{p.description}</Typography>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
<CardActions>
|
<CardActions>
|
||||||
<NextLink passHref href={`/console/matrix/products/${p.id}`}>
|
|
||||||
<Button size="small">Details</Button>
|
<Button size="small">Details</Button>
|
||||||
</NextLink>
|
|
||||||
<NextLink passHref href={`/console/matrix/products/${p.id}/edit`}>
|
<NextLink passHref href={`/console/matrix/products/${p.id}/edit`}>
|
||||||
<Button size="small">Edit</Button>
|
<Button size="small">Edit</Button>
|
||||||
</NextLink>
|
</NextLink>
|
||||||
|
@ -1,43 +0,0 @@
|
|||||||
import { ConsoleLayout, getConsoleStaticProps } from '@/components/layouts/ConsoleLayout'
|
|
||||||
import { Box, Button, Container, Typography } from '@mui/material'
|
|
||||||
import { GetServerSideProps, InferGetServerSidePropsType } from 'next'
|
|
||||||
import { sni, MaProduct } from 'solar-js-sdk'
|
|
||||||
import NextLink from 'next/link'
|
|
||||||
|
|
||||||
export const getServerSideProps: GetServerSideProps = (async (context) => {
|
|
||||||
const id = context.params!.id
|
|
||||||
|
|
||||||
const { data } = await sni.get<MaProduct>('/cgi/ma/products/' + id)
|
|
||||||
|
|
||||||
return getConsoleStaticProps({
|
|
||||||
props: {
|
|
||||||
title: `Product "${data.name}"`,
|
|
||||||
product: data,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}) satisfies GetServerSideProps<{ product: MaProduct }>
|
|
||||||
|
|
||||||
export default function ProductDetails({ product }: InferGetServerSidePropsType<typeof getServerSideProps>) {
|
|
||||||
return (
|
|
||||||
<ConsoleLayout>
|
|
||||||
<Container sx={{ py: 16, display: 'flex', flexDirection: 'column', gap: 8 }}>
|
|
||||||
<Box maxWidth="sm">
|
|
||||||
<Typography variant="h3" component="h1">
|
|
||||||
{product.name}
|
|
||||||
</Typography>
|
|
||||||
<Typography variant="subtitle1">{product.description}</Typography>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
<Box display="flex" flexDirection="column" gap={2}>
|
|
||||||
<Typography variant="h4" component="h2">
|
|
||||||
Releases
|
|
||||||
</Typography>
|
|
||||||
|
|
||||||
<NextLink passHref href={`/console/matrix/products/${product.id}/releases/new`}>
|
|
||||||
<Button variant="contained">Create a release</Button>
|
|
||||||
</NextLink>
|
|
||||||
</Box>
|
|
||||||
</Container>
|
|
||||||
</ConsoleLayout>
|
|
||||||
)
|
|
||||||
}
|
|
@ -1,40 +0,0 @@
|
|||||||
import { ConsoleLayout, getConsoleStaticProps } from '@/components/layouts/ConsoleLayout'
|
|
||||||
import { Typography, Container, Box } from '@mui/material'
|
|
||||||
import { MaProduct, sni } from 'solar-js-sdk'
|
|
||||||
|
|
||||||
import MaReleaseForm, { MatrixReleaseForm } from '@/components/matrix/MaReleaseForm'
|
|
||||||
import { GetServerSideProps, InferGetServerSidePropsType } from 'next'
|
|
||||||
|
|
||||||
export const getServerSideProps: GetServerSideProps = (async (context) => {
|
|
||||||
const id = context.params!.id
|
|
||||||
|
|
||||||
const { data } = await sni.get<MaProduct>('/cgi/ma/products/' + id)
|
|
||||||
|
|
||||||
return getConsoleStaticProps({
|
|
||||||
props: {
|
|
||||||
title: `New Release for "${data.name}"`,
|
|
||||||
product: data,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}) satisfies GetServerSideProps<{ product: MaProduct }>
|
|
||||||
|
|
||||||
export default function ReleaseNew({ product }: InferGetServerSidePropsType<typeof getServerSideProps>) {
|
|
||||||
async function onSubmit(data: MatrixReleaseForm) {
|
|
||||||
await sni.post(`/cgi/ma/products/${product.id}/releases`, data)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ConsoleLayout>
|
|
||||||
<Container sx={{ py: 16, display: 'flex', flexDirection: 'column', gap: 4 }}>
|
|
||||||
<Box>
|
|
||||||
<Typography variant="h3" component="h1">
|
|
||||||
Create a release
|
|
||||||
</Typography>
|
|
||||||
<Typography variant="subtitle1">for {product.name}</Typography>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
<MaReleaseForm onSubmit={onSubmit} parent={product} />
|
|
||||||
</Container>
|
|
||||||
</ConsoleLayout>
|
|
||||||
)
|
|
||||||
}
|
|
@ -7,7 +7,7 @@ import MaProductForm, { MatrixProductForm } from '@/components/matrix/MaProductF
|
|||||||
export async function getStaticProps() {
|
export async function getStaticProps() {
|
||||||
return getConsoleStaticProps({
|
return getConsoleStaticProps({
|
||||||
props: {
|
props: {
|
||||||
title: 'New Product',
|
title: 'Matrix',
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user