Skip to content

Commit

Permalink
Views (#396)
Browse files Browse the repository at this point in the history
* Update next.js

* Move to define views as a string which we'll compile only for the dashboard next

* Compile & use views

* Use fields

* Remove Hint - if we ever revive GraphQL support we'll need this again but it's in the way for now

* Reference nested views

* Fix custom views

* Improve error handling during builds

* Update cito

* Update error reporting

* Update web

* Interim

* Allow inline components as views still

* Restore media root icon
  • Loading branch information
benmerckx authored Sep 27, 2024
1 parent ce7ea99 commit ee01acd
Show file tree
Hide file tree
Showing 123 changed files with 937 additions and 1,116 deletions.
20 changes: 18 additions & 2 deletions apps/dev/content/primary/fields/examples/conditional.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,29 @@
"_i18nId": "2dgfhj4jZy1ouxya8e6BBIoLyoa",
"title": "Conditional fields",
"rootField": "a",
"nestedList": [],
"nestedList": [
{
"_id": "2mWG5FYuj8KxeEbQ3urZDq5NxeR",
"_index": "a0",
"_type": "Row",
"a": "",
"b": ""
},
{
"_id": "2mWG5S9Wdf8yG5DhE3l8UKc3PoC",
"_index": "a1",
"_type": "Row",
"a": "",
"b": ""
}
],
"metadata": {
"title": "",
"description": "",
"openGraph": {
"title": "",
"siteName": "",
"image": {},
"title": "",
"description": ""
}
}
Expand Down
7 changes: 7 additions & 0 deletions apps/dev/content/primary/fields/examples/custom-view.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"_id": "2mWHuPMJwVqOpADbv7Uf9nFl7zj",
"_type": "CustomViewExample",
"_index": "Zy",
"_i18nId": "2mWHuPMJwVqOpADbv7Uf9nFl7zj",
"title": "Custom view"
}
7 changes: 7 additions & 0 deletions apps/dev/src/CustomRootView.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export function CustomRootView() {
return (
<div style={{width: '100%', height: '100%', background: 'yellow'}}>
Custom root view
</div>
)
}
10 changes: 1 addition & 9 deletions apps/dev/src/cms.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,15 +29,7 @@ export const cms = createCMS({
}),
custom: Config.root('Custom', {
contains: ['CustomPage'],
view() {
return (
<div
style={{width: '100%', height: '100%', background: 'yellow'}}
>
Custom root view
</div>
)
}
view: './src/CustomRootView.tsx#CustomRootView'
}),
media: Config.media()
}
Expand Down
20 changes: 20 additions & 0 deletions apps/dev/src/field/PositionField.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import {FieldOptions, ScalarField, WithoutLabel} from 'alinea/core'

export interface PositionOptions extends FieldOptions<Position> {}

export interface Position {
x: number | null
y: number | null
}

class PositionField extends ScalarField<Position, PositionOptions> {}

export function position(
label: string,
options: WithoutLabel<PositionOptions> = {}
) {
return new PositionField({
options: {label, ...options},
view: '@/field/PositionField.view#PositionInput'
})
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {FieldOptions, ScalarField, WithoutLabel} from 'alinea/core'
import {FieldOptions, ScalarField} from 'alinea/core'
import {InputLabel, useField} from 'alinea/dashboard'
import {HStack, VStack} from 'alinea/ui'
import {SVGProps, useState} from 'react'
Expand All @@ -13,17 +13,6 @@ export interface Position {

class PositionField extends ScalarField<Position, PositionOptions> {}

export function position(
label: string,
options: WithoutLabel<PositionOptions> = {}
) {
return new PositionField({
options: {label, ...options},
hint: undefined!,
view: PositionInput
})
}

interface PositionInputProps {
field: PositionField
}
Expand Down
7 changes: 7 additions & 0 deletions apps/dev/src/schema/example/CustomEntryView.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export default function CustomEntryView() {
return (
<div style={{width: '100%', height: '100%', background: 'red'}}>
Custom entry view
</div>
)
}
8 changes: 1 addition & 7 deletions apps/dev/src/schema/example/CustomPageExample.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,6 @@
import {Config} from 'alinea'

export const CustomPage = Config.document('Custom page', {
view() {
return (
<div style={{width: '100%', height: '100%', background: 'red'}}>
Custom entry view
</div>
)
},
view: '@/schema/example/CustomEntryView',
fields: {}
})
7 changes: 7 additions & 0 deletions apps/dev/src/schema/example/CustomViewExample.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import {Config, Field} from 'alinea'

export const CustomViewExample = Config.document('Custom view', {
fields: {
...Field.view('@/schema/example/CustomViewExample.view#CustomViewExample')
}
})
3 changes: 3 additions & 0 deletions apps/dev/src/schema/example/CustomViewExample.view.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export function CustomViewExample() {
return <div>Custom view example</div>
}
1 change: 1 addition & 0 deletions apps/dev/src/schema/example/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ export * from './BasicFields'
export * from './ConditionalExamples'
export * from './CustomFieldExample'
export * from './CustomPageExample'
export * from './CustomViewExample'
export * from './I18nFields'
export * from './InlineFields'
export * from './LayoutFields'
Expand Down
5 changes: 4 additions & 1 deletion apps/dev/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,10 @@
"resolveJsonModule": true,
"isolatedModules": true,
"strictNullChecks": true,
"jsx": "react-jsx"
"jsx": "react-jsx",
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["src", "next-env.d.ts"]
}
6 changes: 3 additions & 3 deletions apps/web/content/pages/docs/fields.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
"_type": "Docs",
"_index": "Zn",
"_i18nId": "24yE2mu6Xq959jrP135Sdb4e3lG",
"_root": "pages",
"title": "Fields",
"navigationTitle": "",
"body": [
Expand Down Expand Up @@ -37,7 +36,7 @@
{
"_type": "ExampleBlock",
"_id": "2YklqLFpIIeyPugEBoAD6iZSvtr",
"code": "import {Config, Field} from 'alinea'\nimport {FieldOptions, ScalarField, Hint, WithoutLabel} from 'alinea/core'\nimport {InputLabel, useField} from 'alinea/dashboard'\n\ninterface RangeFieldOptions extends FieldOptions<number> {\n min?: number\n max?: number\n}\n\nclass RangeField extends ScalarField<number, RangeFieldOptions> {\n}\n\n// The constructor function is used to create fields in our schema\n// later on. It is usually passed a label and options.\nexport function range(label: string, options: WithoutLabel<RangeFieldOptions> = {}): RangeField {\n return new RangeField({\n hint: Hint.Number(),\n options: {label, ...options},\n view: RangeInput\n })\n}\n\ninterface RangeInputProps {\n field: RangeField\n}\n\n// To view our field we can create a React component. \n// This component can call the useInput hook to receive the\n// current value and a method to update it.\nfunction RangeInput({field}: RangeInputProps) {\n const {value, mutator, options} = useField(field)\n const {min = 0, max = 10} = options\n return (\n <InputLabel {...options}>\n <input \n type=\"range\" \n min={min} max={max} \n value={value} \n onChange={e => mutator(Number(e.target.value))} \n />\n </InputLabel>\n )\n}\n\nexport default Config.type('Kitchen sink', {\n fields: {\n ...Field.tabs(\n Field.tab('Basic fields', {\n title: Field.text('Text field'),\n path: Field.path('Path field', {\n help: 'Creates a slug of the value of another field'\n }),\n richText: Field.richText('Rich text field'),\n select: Field.select('Select field', {\n a: 'Option a',\n b: 'Option b'\n }),\n number: Field.number('Number field', {\n minValue: 0,\n maxValue: 10\n }),\n check: Field.check('Check field', {label: 'Check me please'}),\n date: Field.date('Date field'),\n code: Field.code('Code field')\n }),\n Field.tab('Link fields', {\n externalLink: Field.url('External link'),\n entry: Field.entry('Internal link'),\n linkMultiple: Field.link.multiple('Mixed links, multiple'),\n image: Field.entry('Image link'),\n file: Field.entry('File link')\n }),\n\n Field.tab('List fields', {\n list: Field.list('My list field', {\n schema: {\n Text: Config.type('Text', {\n fields: {\n title: Field.text('Item title'),\n text: Field.richText('Item body text')\n }\n }),\n Image: Config.type('Image', {\n fields: {\n image: Field.image('Image')\n }\n })\n }\n }) \n }),\n Field.tab('Inline fields', {\n street: Field.text('Street', {width: 0.6, inline: true, multiline: true}),\n streetNr: Field.text('Number', {width: 0.2, inline: true}),\n box: Field.text('Box', {width: 0.2, inline: true}),\n zip: Field.text('Zipcode', {width: 0.2, inline: true}),\n city: Field.text('City', {width: 0.4, inline: true}),\n country: Field.text('Country', {\n width: 0.4,\n inline: true\n })\n }),\n Field.tab('Custom fields', {\n range: range('Range field') \n })\n )\n }\n})\n"
"code": "import {Config, Field} from 'alinea'\nimport {FieldOptions, ScalarField, WithoutLabel} from 'alinea/core'\nimport {InputLabel, useField} from 'alinea/dashboard'\n\ninterface RangeFieldOptions extends FieldOptions<number> {\n min?: number\n max?: number\n}\n\nclass RangeField extends ScalarField<number, RangeFieldOptions> {\n}\n\n// The constructor function is used to create fields in our schema\n// later on. It is usually passed a label and options.\nexport function range(label: string, options: WithoutLabel<RangeFieldOptions> = {}): RangeField {\n return new RangeField({\n options: {label, ...options},\n view: RangeInput\n })\n}\n\ninterface RangeInputProps {\n field: RangeField\n}\n\n// To view our field we can create a React component. \n// This component can call the useInput hook to receive the\n// current value and a method to update it.\nfunction RangeInput({field}: RangeInputProps) {\n const {value, mutator, options} = useField(field)\n const {min = 0, max = 10} = options\n return (\n <InputLabel {...options}>\n <input \n type=\"range\" \n min={min} max={max} \n value={value} \n onChange={e => mutator(Number(e.target.value))} \n />\n </InputLabel>\n )\n}\n\nexport default Config.type('Kitchen sink', {\n fields: {\n ...Field.tabs(\n Field.tab('Basic fields', {\n title: Field.text('Text field'),\n path: Field.path('Path field', {\n help: 'Creates a slug of the value of another field'\n }),\n richText: Field.richText('Rich text field'),\n select: Field.select('Select field', {\n a: 'Option a',\n b: 'Option b'\n }),\n number: Field.number('Number field', {\n minValue: 0,\n maxValue: 10\n }),\n check: Field.check('Check field', {label: 'Check me please'}),\n date: Field.date('Date field'),\n code: Field.code('Code field')\n }),\n Field.tab('Link fields', {\n externalLink: Field.url('External link'),\n entry: Field.entry('Internal link'),\n linkMultiple: Field.link.multiple('Mixed links, multiple'),\n image: Field.entry('Image link'),\n file: Field.entry('File link')\n }),\n\n Field.tab('List fields', {\n list: Field.list('My list field', {\n schema: {\n Text: Config.type('Text', {\n fields: {\n title: Field.text('Item title'),\n text: Field.richText('Item body text')\n }\n }),\n Image: Config.type('Image', {\n fields: {\n image: Field.image('Image')\n }\n })\n }\n }) \n }),\n Field.tab('Inline fields', {\n street: Field.text('Street', {width: 0.6, inline: true, multiline: true}),\n streetNr: Field.text('Number', {width: 0.2, inline: true}),\n box: Field.text('Box', {width: 0.2, inline: true}),\n zip: Field.text('Zipcode', {width: 0.2, inline: true}),\n city: Field.text('City', {width: 0.4, inline: true}),\n country: Field.text('Country', {\n width: 0.4,\n inline: true\n })\n }),\n Field.tab('Custom fields', {\n range: range('Range field') \n })\n )\n }\n})\n"
},
{
"_type": "heading",
Expand Down Expand Up @@ -332,8 +331,9 @@
"title": "",
"description": "",
"openGraph": {
"title": "",
"siteName": "",
"image": {},
"title": "",
"description": ""
}
}
Expand Down
8 changes: 4 additions & 4 deletions apps/web/content/pages/docs/fields/custom-fields.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
"_type": "Doc",
"_index": "a6",
"_i18nId": "286iEJsjhd8R9NxN6EVg59fELyd",
"_root": "pages",
"title": "Custom fields",
"navigationTitle": "",
"body": [
Expand Down Expand Up @@ -40,7 +39,7 @@
{
"_type": "CodeBlock",
"_id": "tjXTNIX1E2w3fts1zXG9Z",
"code": "import {FieldOptions, ScalarField, Hint, WithoutLabel} from 'alinea/core'\nimport {InputLabel, useField} from 'alinea/dashboard'\n\ninterface RangeFieldOptions extends FieldOptions<number> {\n min?: number\n max?: number\n}\n\nclass RangeField extends ScalarField<number, RangeFieldOptions> {\n}\n\n// The constructor function is used to create fields in our schema\n// later on. It is usually passed a label and options.\nexport function range(label: string, options: WithoutLabel<RangeFieldOptions> = {}): RangeField {\n return new RangeField({\n hint: Hint.Number(),\n options: {label, ...options},\n view: RangeInput\n })\n}\n\ninterface RangeInputProps {\n field: RangeField\n}\n\n// To view our field we can create a React component. \n// This component can call the useInput hook to receive the\n// current value and a method to update it.\nfunction RangeInput({field}: RangeInputProps) {\n const {value, mutator, options} = useField(field)\n const {min = 0, max = 10} = options\n return (\n <InputLabel {...options}>\n <input \n type=\"range\" \n min={min} max={max} \n value={value} \n onChange={e => mutator(Number(e.target.value))} \n />\n </InputLabel>\n )\n}",
"code": "import {FieldOptions, ScalarField, WithoutLabel} from 'alinea/core'\nimport {InputLabel, useField} from 'alinea/dashboard'\n\ninterface RangeFieldOptions extends FieldOptions<number> {\n min?: number\n max?: number\n}\n\nclass RangeField extends ScalarField<number, RangeFieldOptions> {\n}\n\n// The constructor function is used to create fields in our schema\n// later on. It is usually passed a label and options.\nexport function range(label: string, options: WithoutLabel<RangeFieldOptions> = {}): RangeField {\n return new RangeField({\n options: {label, ...options},\n view: RangeInput\n })\n}\n\ninterface RangeInputProps {\n field: RangeField\n}\n\n// To view our field we can create a React component. \n// This component can call the useInput hook to receive the\n// current value and a method to update it.\nfunction RangeInput({field}: RangeInputProps) {\n const {value, mutator, options} = useField(field)\n const {min = 0, max = 10} = options\n return (\n <InputLabel {...options}>\n <input \n type=\"range\" \n min={min} max={max} \n value={value} \n onChange={e => mutator(Number(e.target.value))} \n />\n </InputLabel>\n )\n}",
"fileName": "",
"language": "",
"compact": false
Expand All @@ -66,7 +65,7 @@
{
"_type": "ExampleBlock",
"_id": "yhfKjT9ITmHHD0N5JRU4G",
"code": "import {Config} from 'alinea'\nimport {FieldOptions, ScalarField, Hint, WithoutLabel} from 'alinea/core'\nimport {InputLabel, useField} from 'alinea/dashboard'\n\ninterface RangeFieldOptions extends FieldOptions<number> {\n min?: number\n max?: number\n}\n\nclass RangeField extends ScalarField<number, RangeFieldOptions> {\n}\n\n// The constructor function is used to create fields in our schema\n// later on. It is usually passed a label and options.\nexport function range(label: string, options: WithoutLabel<RangeFieldOptions> = {}): RangeField {\n return new RangeField({\n hint: Hint.Number(),\n options: {label, ...options},\n view: RangeInput\n })\n}\n\ninterface RangeInputProps {\n field: RangeField\n}\n\n// To view our field we can create a React component. \n// This component can call the useInput hook to receive the\n// current value and a method to update it.\nfunction RangeInput({field}: RangeInputProps) {\n const {value, mutator, options} = useField(field)\n const {min = 0, max = 10} = options\n return (\n <InputLabel {...options}>\n <input \n type=\"range\" \n min={min} max={max} \n value={value} \n onChange={e => mutator(Number(e.target.value))} \n />\n </InputLabel>\n )\n}\n\nexport default Config.type('Custom fields', {\n range: range('A range field', {min: 0, max: 20})\n})"
"code": "import {Config} from 'alinea'\nimport {FieldOptions, ScalarField, WithoutLabel} from 'alinea/core'\nimport {InputLabel, useField} from 'alinea/dashboard'\n\ninterface RangeFieldOptions extends FieldOptions<number> {\n min?: number\n max?: number\n}\n\nclass RangeField extends ScalarField<number, RangeFieldOptions> {\n}\n\n// The constructor function is used to create fields in our schema\n// later on. It is usually passed a label and options.\nexport function range(label: string, options: WithoutLabel<RangeFieldOptions> = {}): RangeField {\n return new RangeField({\n options: {label, ...options},\n view: RangeInput\n })\n}\n\ninterface RangeInputProps {\n field: RangeField\n}\n\n// To view our field we can create a React component. \n// This component can call the useInput hook to receive the\n// current value and a method to update it.\nfunction RangeInput({field}: RangeInputProps) {\n const {value, mutator, options} = useField(field)\n const {min = 0, max = 10} = options\n return (\n <InputLabel {...options}>\n <input \n type=\"range\" \n min={min} max={max} \n value={value} \n onChange={e => mutator(Number(e.target.value))} \n />\n </InputLabel>\n )\n}\n\nexport default Config.type('Custom fields', {\n range: range('A range field', {min: 0, max: 20})\n})"
},
{
"_type": "heading",
Expand Down Expand Up @@ -189,8 +188,9 @@
"title": "",
"description": "",
"openGraph": {
"title": "",
"siteName": "",
"image": {},
"title": "",
"description": ""
}
}
Expand Down
2 changes: 1 addition & 1 deletion apps/web/next-env.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
/// <reference types="next/image-types/global" />

// NOTE: This file should not be edited
// see https://nextjs.org/docs/basic-features/typescript for more information.
// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information.
2 changes: 1 addition & 1 deletion apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
"@vercel/postgres": "^0.10.0",
"alinea": "0.0.0",
"lz-string": "^1.4.4",
"next": "^14.2.6",
"next": "^14.2.13",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-string-replace": "^1.0.0",
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/app/demo/preview/[slug]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import {cms} from '@/cms'
import {DemoRecipePage} from '@/page/demo/DemoRecipePage'

export default async ({params}) => {
export default async function DemoSlug({params}) {
const props = await cms.get(DemoRecipePage.fragment.wherePath(params.slug))
return <DemoRecipePage {...props} />
}
2 changes: 1 addition & 1 deletion apps/web/src/app/demo/preview/page.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import {cms} from '@/cms'
import {DemoHomePage} from '@/page/demo/DemoHomePage'

export default async () => {
export default async function DemoPage() {
const props = await cms.get(DemoHomePage.fragment)
return <DemoHomePage {...props} />
}
2 changes: 1 addition & 1 deletion apps/web/src/layout/Header.client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ function SearchModal({onClose}: SearchModalProps) {
}
window.addEventListener('keydown', handleEsc)
return () => window.removeEventListener('keydown', handleEsc)
}, [])
}, [onClose])
return createPortal(
<div className={styles.searchmodal()}>
<div className={styles.searchmodal.backdrop()} onClick={onClose} />
Expand Down
1 change: 1 addition & 0 deletions apps/web/src/layout/RootLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ const favicon = btoa(
export default async function RootLayout({children}: PropsWithChildren) {
return (
<html lang="en">
{/* eslint-disable-next-line @next/next/no-head-element */}
<head>
<meta name="theme-color" content={color} />
<style>
Expand Down
14 changes: 8 additions & 6 deletions apps/web/src/layout/WebTypo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,14 @@ function Link({href, ...props}: LinkProps) {
}

function withPermaLink(Tag: string) {
return (props: HTMLAttributes<HTMLHeadingElement>) => (
<Tag {...props}>
{props.id && <a href={`#${props.id}`} className={styles.permaLink()} />}
{props.children}
</Tag>
)
return function Perma(props: HTMLAttributes<HTMLHeadingElement>) {
return (
<Tag {...props}>
{props.id && <a href={`#${props.id}`} className={styles.permaLink()} />}
{props.children}
</Tag>
)
}
}

export const WebTypo = createTypo(styles, {
Expand Down
1 change: 1 addition & 0 deletions apps/web/src/page/blog/BlogPostMeta.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export function BlogPostMeta({publishDate, author}: BlogPostMetaProps) {
<a href={author.url._url} className={styles.root.author.url()}>
<HStack center gap={8}>
{author.avatar && (
// eslint-disable-next-line @next/next/no-img-element
<img
alt="Author avatar"
className={styles.root.author.avatar()}
Expand Down
Loading

0 comments on commit ee01acd

Please sign in to comment.