Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/components/Root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ function DocumentTitle() {
const matches = useMatches()

useEffect(() => {
let found = false
for (let i = matches.length - 1; i >= 0; i--) {
const { handle, params, data } = matches[i] as {
handle?: { title?: string | ((p: Record<string, string>, d: unknown) => string) }
Expand All @@ -16,9 +17,11 @@ function DocumentTitle() {
if (handle?.title) {
document.title =
typeof handle.title === 'function' ? handle.title(params, data) : handle.title
found = true
break
}
}
if (!found) document.title = 'Underlay'
}, [matches])

return null
Expand Down
22 changes: 22 additions & 0 deletions src/routes/[owner]/[collection]/diff.data.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import type { LoaderFunctionArgs } from 'react-router'

export const handle = {
title: (params: Record<string, string>) =>
`Diff — ${params.owner}/${params.collection} · Underlay`,
}

export async function loader({ params, request }: LoaderFunctionArgs) {
const base = new URL(request.url).origin
const headers = { Cookie: request.headers.get('Cookie') ?? '' }
const prefix = `/api/collections/${params.owner}/${params.collection}`

const [data, versions] = await Promise.all([
fetch(new URL(prefix, base), { headers }).then((r) => (r.ok ? r.json() : null)),
fetch(new URL(`${prefix}/versions?limit=100`, base), { headers }).then((r) =>
r.ok ? r.json() : [],
),
])

if (!data) throw new Response('Not Found', { status: 404 })
return { data, versions }
}
70 changes: 11 additions & 59 deletions src/routes/[owner]/[collection]/diff.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import { useEffect, useState } from 'react'
import { useParams, useSearchParams } from 'react-router'
import { useLoaderData, useParams, useSearchParams } from 'react-router'

import BaseLayout from '~/components/BaseLayout'
import { NotFoundError } from '~/components/NotFound'
import { useAppContext } from '~/lib/app-context'

import { CollectionNav } from '.'
Expand All @@ -20,59 +19,21 @@ export default function CollectionDiffPage() {
const { owner, collection } = useParams()
const [searchParams, setSearchParams] = useSearchParams()
const { currentUser } = useAppContext()
const { data, versions } = useLoaderData() as { data: any; versions: any[] }

const isOwner =
currentUser?.slug === owner || currentUser?.orgs?.some((o: any) => o.slug === owner)

const [data, setData] = useState<any>(null)
const [versions, setVersions] = useState<any[]>([])
const [isOwner, setIsOwner] = useState(false)
const [diff, setDiff] = useState<any>(null)
const [diffError, setDiffError] = useState<string | null>(null)
const [loading, setLoading] = useState(true)
const [diffLoading, setDiffLoading] = useState(false)

// Version selectors (semver strings, empty string = none/empty)
const [fromVer, setFromVer] = useState<string>('')
const [toVer, setToVer] = useState<string>('')

useEffect(() => {
if (!owner || !collection) return

Promise.all([
fetch(`/api/collections/${owner}/${collection}`, { credentials: 'include' }).then((r) =>
r.ok ? r.json() : null,
),
fetch(`/api/collections/${owner}/${collection}/versions?limit=100`, {
credentials: 'include',
}).then((r) => (r.ok ? r.json() : [])),
]).then(([col, vers]) => {
if (!col) {
setLoading(false)
return
}
setData(col)
setVersions(vers)

if (currentUser) {
setIsOwner(
currentUser.slug === owner || currentUser.orgs?.some((o: any) => o.slug === owner),
)
}
const latestSemver = versions.length > 0 ? versions[0].semver : ''
const [fromVer, setFromVer] = useState<string>(searchParams.get('from') ?? '')
const [toVer, setToVer] = useState<string>(searchParams.get('to') || latestSemver)

// Determine from/to from URL
const latestSemver = vers.length > 0 ? vers[0].semver : ''
const urlTo = searchParams.get('to')
const urlFrom = searchParams.get('from')
const target = urlTo || latestSemver
const base = urlFrom ?? ''
setToVer(target)
setFromVer(base)

setLoading(false)
})
}, [owner, collection, currentUser])

// Fetch diff when from/to change
useEffect(() => {
if (!toVer || loading) return
if (!toVer) return
setDiffLoading(true)
setDiff(null)
setDiffError(null)
Expand All @@ -90,7 +51,7 @@ export default function CollectionDiffPage() {
}
})
.finally(() => setDiffLoading(false))
}, [fromVer, toVer, loading, owner, collection])
}, [fromVer, toVer, owner, collection])

function handleCompare(e: React.FormEvent) {
e.preventDefault()
Expand All @@ -105,15 +66,6 @@ export default function CollectionDiffPage() {
setSearchParams(params)
}

if (loading) {
return (
<BaseLayout>
<div className="text-ink-muted mx-auto max-w-5xl px-4 py-8 text-sm">Loading…</div>
</BaseLayout>
)
}
if (!data) throw new NotFoundError()

const targetVersion = versions.find((v: any) => v.semver === toVer)
const baseVersion = versions.find((v: any) => v.semver === fromVer)

Expand All @@ -137,7 +89,7 @@ export default function CollectionDiffPage() {
owner={owner!}
collection={collection!}
isPublic={data.public}
isOwner={isOwner}
isOwner={!!isOwner}
active="versions"
/>

Expand Down
2 changes: 1 addition & 1 deletion src/routes/[owner]/[collection]/index.data.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { LoaderFunctionArgs } from 'react-router'

export const handle = {
title: (params: Record<string, string>) => `${params.owner}/${params.collection} Underlay`,
title: (params: Record<string, string>) => `${params.owner}/${params.collection} · Underlay`,
}

export async function loader({ params, request }: LoaderFunctionArgs) {
Expand Down
20 changes: 20 additions & 0 deletions src/routes/[owner]/[collection]/schemas.data.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import type { LoaderFunctionArgs } from 'react-router'

export const handle = {
title: (params: Record<string, string>) =>
`Schemas — ${params.owner}/${params.collection} · Underlay`,
}

export async function loader({ params, request }: LoaderFunctionArgs) {
const base = new URL(request.url).origin
const headers = { Cookie: request.headers.get('Cookie') ?? '' }
const prefix = `/api/collections/${params.owner}/${params.collection}`

const [data, schemas] = await Promise.all([
fetch(new URL(prefix, base), { headers }).then((r) => (r.ok ? r.json() : null)),
fetch(new URL(`${prefix}/schemas`, base), { headers }).then((r) => (r.ok ? r.json() : null)),
])

if (!data) throw new Response('Not Found', { status: 404 })
return { data, schemas }
}
75 changes: 21 additions & 54 deletions src/routes/[owner]/[collection]/schemas.tsx
Original file line number Diff line number Diff line change
@@ -1,63 +1,39 @@
import { type FormEvent, useEffect, useState } from 'react'
import { Link, useParams } from 'react-router'
import { Link, useLoaderData, useParams } from 'react-router'

import BaseLayout from '~/components/BaseLayout'
import { NotFoundError } from '~/components/NotFound'
import { useAppContext } from '~/lib/app-context'

import { CollectionNav } from '.'

export default function CollectionSchemasPage() {
const { owner, collection } = useParams()
const { currentUser } = useAppContext()
const { data, schemas: schemasData } = useLoaderData() as { data: any; schemas: any }

const isOwner =
currentUser?.slug === owner || currentUser?.orgs?.some((o: any) => o.slug === owner)

const schemas: any[] = schemasData?.schemas ?? []

const [data, setData] = useState<any>(null)
const [isOwner, setIsOwner] = useState(false)
const [schemas, setSchemas] = useState<any[]>([])
const [schemasData, setSchemasData] = useState<any>({})
const [arkRecordTypes, setArkRecordTypes] = useState<Record<string, string>>({})
const [loading, setLoading] = useState(true)
const [arkSuccess, setArkSuccess] = useState('')
const [arkError, setArkError] = useState('')

useEffect(() => {
if (!owner || !collection) return

const ownerFlag =
currentUser &&
(currentUser.slug === owner || currentUser.orgs?.some((o: any) => o.slug === owner))
setIsOwner(!!ownerFlag)

Promise.all([
fetch(`/api/collections/${owner}/${collection}`, { credentials: 'include' }).then((r) =>
r.ok ? r.json() : null,
),
fetch(`/api/collections/${owner}/${collection}/schemas`, { credentials: 'include' }).then(
(r) => (r.ok ? r.json() : { schemas: [], version: null, semver: null }),
),
ownerFlag
? fetch(`/api/collections/${owner}/${collection}/ark/record-types`, {
credentials: 'include',
}).then((r) => (r.ok ? r.json() : []))
: Promise.resolve([]),
]).then(([col, sd, arkTypes]) => {
if (!col) {
setLoading(false)
return
}
setData(col)
setSchemasData(sd)
setSchemas(sd.schemas ?? [])

const types: Record<string, string> = {}
for (const entry of arkTypes) {
types[entry.recordType] = entry.redirectUrlField
}
setArkRecordTypes(types)

setLoading(false)
if (!isOwner || !owner || !collection) return
fetch(`/api/collections/${owner}/${collection}/ark/record-types`, {
credentials: 'include',
})
}, [owner, collection, currentUser])
.then((r) => (r.ok ? r.json() : []))
.then((arkTypes: any[]) => {
const types: Record<string, string> = {}
for (const entry of arkTypes) {
types[entry.recordType] = entry.redirectUrlField
}
setArkRecordTypes(types)
})
}, [isOwner, owner, collection])

async function handleUpdateArkType(e: FormEvent, slug: string) {
e.preventDefault()
Expand Down Expand Up @@ -90,23 +66,14 @@ export default function CollectionSchemasPage() {
}
}

if (loading) {
return (
<BaseLayout>
<div className="text-ink-muted mx-auto max-w-5xl px-4 py-8 text-sm">Loading…</div>
</BaseLayout>
)
}
if (!data) throw new NotFoundError()

return (
<BaseLayout>
<div className="mx-auto max-w-5xl px-4 py-8">
<CollectionNav
owner={owner!}
collection={collection!}
isPublic={data.public}
isOwner={isOwner}
isOwner={!!isOwner}
active="schemas"
/>

Expand All @@ -124,7 +91,7 @@ export default function CollectionSchemasPage() {
<div className="mb-4 flex items-center justify-between">
<h2 className="text-ink-muted text-sm font-semibold">
{schemas.length} type{schemas.length !== 1 ? 's' : ''}
{schemasData.semver && (
{schemasData?.semver && (
<span className="ml-1 font-normal">in {schemasData.semver}</span>
)}
</h2>
Expand Down
22 changes: 22 additions & 0 deletions src/routes/[owner]/[collection]/settings.data.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import type { LoaderFunctionArgs } from 'react-router'

export const handle = {
title: (params: Record<string, string>) =>
`Settings — ${params.owner}/${params.collection} · Underlay`,
}

export async function loader({ params, request }: LoaderFunctionArgs) {
const base = new URL(request.url).origin
const headers = { Cookie: request.headers.get('Cookie') ?? '' }
const prefix = `/api/collections/${params.owner}/${params.collection}`

const [data, arkSettings] = await Promise.all([
fetch(new URL(prefix, base), { headers }).then((r) => (r.ok ? r.json() : null)),
fetch(new URL(`${prefix}/ark`, base), { headers }).then((r) =>
r.ok ? r.json() : { enabled: false, customUrl: null, arkUrl: null },
),
])

if (!data) throw new Response('Not Found', { status: 404 })
return { data, arkSettings }
}
Loading
Loading