Supports release assets & installers

This commit is contained in:
LittleSheep 2025-01-11 13:58:27 +08:00
parent 847151afe8
commit bdcbb18592
7 changed files with 270 additions and 93 deletions

BIN
bun.lockb

Binary file not shown.

View File

@ -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"
}, },

View File

@ -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"

View File

@ -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[]
}

View File

@ -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

View File

@ -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>

View File

@ -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 (