✨ Supports release assets & installers
This commit is contained in:
parent
847151afe8
commit
bdcbb18592
@ -13,6 +13,7 @@
|
||||
"@emotion/react": "^11.14.0",
|
||||
"@emotion/server": "^11.11.0",
|
||||
"@emotion/styled": "^11.14.0",
|
||||
"@monaco-editor/react": "^4.6.0",
|
||||
"@mui/icons-material": "^6.3.1",
|
||||
"@mui/material": "^6.3.1",
|
||||
"@mui/material-nextjs": "^6.3.1",
|
||||
@ -37,7 +38,7 @@
|
||||
"remark-parse": "^11.0.0",
|
||||
"remark-rehype": "^11.1.1",
|
||||
"sitemap": "^8.0.0",
|
||||
"solar-js-sdk": "^0.0.2",
|
||||
"solar-js-sdk": "0.0.8",
|
||||
"unified": "^11.0.5",
|
||||
"zustand": "^5.0.3"
|
||||
},
|
||||
|
@ -8,7 +8,7 @@
|
||||
"name": "LittleSheep",
|
||||
"email": "littlesheep.code@hotmail.com"
|
||||
},
|
||||
"version": "0.0.5",
|
||||
"version": "0.0.8",
|
||||
"tsup": {
|
||||
"entry": [
|
||||
"src/index.ts"
|
||||
|
@ -6,7 +6,8 @@ export interface MaRelease {
|
||||
version: string
|
||||
type: number
|
||||
channel: string
|
||||
assets: Record<string, any>
|
||||
assets: Record<string, MaReleaseAsset>
|
||||
installers: Record<string, MaReleaseInstaller>
|
||||
product_id: number
|
||||
meta: MaReleaseMeta
|
||||
}
|
||||
@ -22,3 +23,19 @@ export interface MaReleaseMeta {
|
||||
attachments: string[]
|
||||
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,
|
||||
Grid2 as Grid,
|
||||
IconButton,
|
||||
Card,
|
||||
} from '@mui/material'
|
||||
import { useRouter } from 'next-nprogress-bar'
|
||||
import { useEffect, useState } from 'react'
|
||||
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 CloseIcon from '@mui/icons-material/Close'
|
||||
import { MaProduct } from 'solar-js-sdk'
|
||||
import { version } from 'node:os'
|
||||
|
||||
export interface MatrixReleaseForm {
|
||||
version: string
|
||||
@ -28,7 +29,8 @@ export interface MatrixReleaseForm {
|
||||
title: string
|
||||
description: string
|
||||
content: string
|
||||
assets: Record<string, any>
|
||||
assets: Record<string, MaReleaseAsset>
|
||||
installers: Record<string, MaReleaseInstaller>
|
||||
attachments: string[]
|
||||
}
|
||||
|
||||
@ -41,7 +43,7 @@ export default function MaReleaseForm({
|
||||
onSubmit: (data: MatrixReleaseForm) => Promise<any>
|
||||
onSuccess?: () => void
|
||||
parent: Partial<MaProduct>
|
||||
defaultValue?: any
|
||||
defaultValue?: MaRelease
|
||||
}) {
|
||||
const { handleSubmit, register } = useForm<MatrixReleaseForm>({
|
||||
defaultValues: {
|
||||
@ -56,17 +58,25 @@ export default function MaReleaseForm({
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (defaultValue) {
|
||||
if (defaultValue?.assets) {
|
||||
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 [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() {
|
||||
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)
|
||||
@ -83,7 +93,11 @@ export default function MaReleaseForm({
|
||||
async function submit(data: MatrixReleaseForm) {
|
||||
try {
|
||||
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()
|
||||
} catch (err: any) {
|
||||
setError(err.toString())
|
||||
@ -105,7 +119,12 @@ export default function MaReleaseForm({
|
||||
|
||||
<FormControl fullWidth>
|
||||
<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={1}>Patch Release</MenuItem>
|
||||
</Select>
|
||||
@ -120,44 +139,64 @@ export default function MaReleaseForm({
|
||||
<TextField minRows={5} multiline label="Content" {...register('content')} />
|
||||
|
||||
<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) => (
|
||||
<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>
|
||||
<Card variant="outlined" key={idx}>
|
||||
<Box sx={{ pl: 2, pr: 4, py: 2 }}>
|
||||
<Grid container spacing={2}>
|
||||
<Grid size={11}>
|
||||
<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={1} sx={{ display: 'grid', placeItems: 'center' }}>
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
setAssets((data) => data.filter((_, index) => index != idx))
|
||||
}}
|
||||
>
|
||||
<CloseIcon />
|
||||
</IconButton>
|
||||
</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 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>
|
||||
@ -167,6 +206,109 @@ export default function MaReleaseForm({
|
||||
</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}>
|
||||
<Button variant="contained" type="submit" disabled={busy}>
|
||||
Submit
|
||||
|
@ -34,46 +34,53 @@ export default function MatrixMarketplace() {
|
||||
if (!yes) return
|
||||
|
||||
await sni.delete('/cgi/ma/products/' + id)
|
||||
window.location.reload()
|
||||
await fetchProducts()
|
||||
}
|
||||
|
||||
return (
|
||||
<ConsoleLayout>
|
||||
<Container sx={{ py: 16, display: 'flex', flexDirection: 'column', gap: 4 }}>
|
||||
<Typography variant="h3" component="h1">
|
||||
Matrix Marketplace
|
||||
</Typography>
|
||||
|
||||
<Grid container columns={{ xs: 1, sm: 2, md: 3 }} spacing={4}>
|
||||
{products.map((p) => (
|
||||
<Grid size={1} key={p.id}>
|
||||
<Card sx={{ width: '100%' }}>
|
||||
<CardContent>
|
||||
<Typography variant="h5" gutterBottom>
|
||||
{p.name}
|
||||
</Typography>
|
||||
<Typography variant="body1">{p.description}</Typography>
|
||||
</CardContent>
|
||||
<CardActions>
|
||||
<NextLink passHref href={`/console/matrix/products/${p.id}`}>
|
||||
<Button size="small">Details</Button>
|
||||
</NextLink>
|
||||
<NextLink passHref href={`/console/matrix/products/${p.id}/edit`}>
|
||||
<Button size="small">Edit</Button>
|
||||
</NextLink>
|
||||
<Button size="small" color="error" onClick={() => deleteProduct(p.id)}>
|
||||
Delete
|
||||
</Button>
|
||||
</CardActions>
|
||||
</Card>
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
|
||||
<Box>
|
||||
<NextLink passHref href="/console/matrix/products/new">
|
||||
<Button variant="contained">Create a product</Button>
|
||||
</NextLink>
|
||||
<Typography variant="h3" component="h1">
|
||||
Matrix Marketplace
|
||||
</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}>
|
||||
{products.map((p) => (
|
||||
<Grid size={1} key={p.id}>
|
||||
<Card sx={{ width: '100%' }}>
|
||||
<CardContent>
|
||||
<Typography variant="h5" gutterBottom>
|
||||
{p.name}
|
||||
</Typography>
|
||||
<Typography variant="body1">{p.description}</Typography>
|
||||
</CardContent>
|
||||
<CardActions>
|
||||
<NextLink passHref href={`/console/matrix/products/${p.id}`}>
|
||||
<Button size="small">Details</Button>
|
||||
</NextLink>
|
||||
<NextLink passHref href={`/console/matrix/products/${p.id}/edit`}>
|
||||
<Button size="small">Edit</Button>
|
||||
</NextLink>
|
||||
<Button size="small" color="error" onClick={() => deleteProduct(p.id)}>
|
||||
Delete
|
||||
</Button>
|
||||
</CardActions>
|
||||
</Card>
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
</Box>
|
||||
</Container>
|
||||
</ConsoleLayout>
|
||||
|
@ -1,7 +1,8 @@
|
||||
import { ConsoleLayout, getConsoleStaticProps } from '@/components/layouts/ConsoleLayout'
|
||||
import { Box, Button, Container, Typography, Grid2 as Grid, Card, CardContent, CardActions } from '@mui/material'
|
||||
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'
|
||||
|
||||
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: resp } = await sni.get<{ data: any[] }>('/cgi/ma/products/' + id + '/releases', {
|
||||
params: {
|
||||
take: 10,
|
||||
},
|
||||
})
|
||||
|
||||
return getConsoleStaticProps({
|
||||
props: {
|
||||
title: `Product "${data.name}"`,
|
||||
product: data,
|
||||
releases: resp.data,
|
||||
},
|
||||
})
|
||||
}) satisfies GetServerSideProps<{ product: MaProduct; releases: any[] }>
|
||||
}) 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: {
|
||||
take: 10,
|
||||
},
|
||||
})
|
||||
|
||||
setReleases(resp.data)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
fetchReleases()
|
||||
}, [])
|
||||
|
||||
export default function ProductDetails({ product, releases }: InferGetServerSidePropsType<typeof getServerSideProps>) {
|
||||
async function deleteRelease(id: number) {
|
||||
const yes = confirm(`Are you sure you want to delete this release #${id}?`)
|
||||
if (!yes) return
|
||||
|
||||
await sni.delete('/cgi/ma/products/' + product.id + '/releases/' + id)
|
||||
window.location.reload()
|
||||
await fetchReleases()
|
||||
}
|
||||
|
||||
return (
|
||||
|
Loading…
x
Reference in New Issue
Block a user