Skip to content

Commit

Permalink
Merge pull request #337 from alineacms/sync
Browse files Browse the repository at this point in the history
Sync
  • Loading branch information
benmerckx authored Nov 14, 2023
2 parents 97635ff + 6473d1d commit 513fa1b
Show file tree
Hide file tree
Showing 129 changed files with 3,033 additions and 1,549 deletions.
18 changes: 9 additions & 9 deletions .prettierrc.json
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
{
"printWidth": 80,
"tabWidth": 2,
"singleQuote": true,
"bracketSpacing": false,
"semi": false,
"useTabs": false,
"endOfLine": "lf",
"trailingComma": "none",
"arrowParens": "avoid"
"printWidth": 80,
"tabWidth": 2,
"singleQuote": true,
"bracketSpacing": false,
"semi": false,
"useTabs": false,
"endOfLine": "lf",
"trailingComma": "none",
"arrowParens": "avoid"
}
4 changes: 2 additions & 2 deletions apps/web/content/pages/docs/fields/custom-fields.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@
{
"id": "tjXTNIX1E2w3fts1zXG9Z",
"type": "CodeBlock",
"code": "import {alinea} from 'alinea'\nimport {Field, Hint, Label, Shape} from 'alinea/core'\nimport {InputLabel, InputState, useInput} from 'alinea/editor'\n\nexport interface RangeFieldOptions {\n min?: number\n max?: number\n}\n\nexport interface RangeField extends Field.Scalar<number> {\n label: Label\n options?: 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: Label, options?: RangeFieldOptions): RangeField {\n return {\n shape: Shape.Scalar(label),\n label,\n options,\n view: RangeInput,\n hint: Hint.Number()\n }\n}\n\ninterface RangeInputProps {\n state: InputState<InputState.Scalar<number>>\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({state, field}: RangeInputProps) {\n const [value = 5, setValue] = useInput(state)\n const {min = 0, max = 10} = field.options || {}\n return (\n <InputLabel label={field.label}>\n <input \n type=\"range\" \n min={min} max={max} \n value={value} \n onChange={e => setValue(Number(e.target.value))} \n />\n </InputLabel>\n )\n}"
"code": "import {alinea} from 'alinea'\nimport {Field, Hint, Label, Shape} from 'alinea/core'\nimport {InputLabel, InputState, useInput} from 'alinea/editor'\n\nexport interface RangeFieldOptions {\n min?: number\n max?: number\n}\n\nexport interface RangeField extends Field.Scalar<number> {\n label: Label\n options?: 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: Label, options?: RangeFieldOptions): RangeField {\n return {\n shape: new ScalarShape(label),\n label,\n options,\n view: RangeInput,\n hint: Hint.Number()\n }\n}\n\ninterface RangeInputProps {\n state: InputState<InputState.Scalar<number>>\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({state, field}: RangeInputProps) {\n const [value = 5, setValue] = useInput(state)\n const {min = 0, max = 10} = field.options || {}\n return (\n <InputLabel label={field.label}>\n <input \n type=\"range\" \n min={min} max={max} \n value={value} \n onChange={e => setValue(Number(e.target.value))} \n />\n </InputLabel>\n )\n}"
},
{
"type": "paragraph",
Expand All @@ -68,7 +68,7 @@
{
"id": "yhfKjT9ITmHHD0N5JRU4G",
"type": "ExampleBlock",
"code": "import {alinea} from 'alinea'\nimport {Field, Hint, Label, Shape} from 'alinea/core'\nimport {InputLabel, InputState, useInput} from 'alinea/editor'\n\nexport interface RangeFieldOptions {\n min?: number\n max?: number\n}\n\nexport interface RangeField extends Field.Scalar<number> {\n label: Label\n options?: 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: Label, options?: RangeFieldOptions): RangeField {\n return {\n shape: Shape.Scalar(label),\n label,\n options,\n view: RangeInput,\n hint: Hint.Number()\n }\n}\n\ninterface RangeInputProps {\n state: InputState<InputState.Scalar<number>>\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({state, field}: RangeInputProps) {\n const [value = 5, setValue] = useInput(state)\n const {min = 0, max = 10} = field.options || {}\n return (\n <InputLabel label={field.label}>\n <input \n type=\"range\" \n min={min} max={max} \n value={value} \n onChange={e => setValue(Number(e.target.value))} \n />\n </InputLabel>\n )\n}\n\nexport default alinea.type('My type', {\n range: range('A range field', {min: 0, max: 20})\n})"
"code": "import {alinea} from 'alinea'\nimport {Field, Hint, Label, Shape} from 'alinea/core'\nimport {InputLabel, InputState, useInput} from 'alinea/editor'\n\nexport interface RangeFieldOptions {\n min?: number\n max?: number\n}\n\nexport interface RangeField extends Field.Scalar<number> {\n label: Label\n options?: 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: Label, options?: RangeFieldOptions): RangeField {\n return {\n shape: new ScalarShape(label),\n label,\n options,\n view: RangeInput,\n hint: Hint.Number()\n }\n}\n\ninterface RangeInputProps {\n state: InputState<InputState.Scalar<number>>\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({state, field}: RangeInputProps) {\n const [value = 5, setValue] = useInput(state)\n const {min = 0, max = 10} = field.options || {}\n return (\n <InputLabel label={field.label}>\n <input \n type=\"range\" \n min={min} max={max} \n value={value} \n onChange={e => setValue(Number(e.target.value))} \n />\n </InputLabel>\n )\n}\n\nexport default alinea.type('My type', {\n range: range('A range field', {min: 0, max: 20})\n})"
},
{
"type": "heading",
Expand Down
2 changes: 1 addition & 1 deletion apps/web/content/pages/docs/fields/intro.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
{
"id": "HeTPIvCg4LofpD5C23eYi",
"type": "ExampleBlock",
"code": "import {alinea} from 'alinea'\nimport {Field, Hint, Label, Shape} from 'alinea/core'\nimport {InputLabel, InputState, useInput} from 'alinea/editor'\n\nexport interface RangeFieldOptions {\n min?: number\n max?: number\n help?: Label\n}\n\nexport interface RangeField extends Field.Scalar<number> {\n label: Label\n options?: 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: Label, options?: RangeFieldOptions): RangeField {\n return {\n shape: Shape.Scalar(label),\n label,\n options,\n view: RangeInput,\n hint: Hint.Number()\n }\n}\n\ninterface RangeInputProps {\n state: InputState<InputState.Scalar<number>>\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({state, field}: RangeInputProps) {\n const [value = 5, setValue] = useInput(state)\n const {min = 0, max = 10, help} = field.options || {}\n return (\n <InputLabel label={field.label} help={help}>\n <input \n type=\"range\" \n min={min} max={max} \n value={value} \n onChange={e => setValue(Number(e.target.value))} \n />\n </InputLabel>\n )\n}\n\nexport default alinea.type('Kitchen sink',\n alinea.tabs(\n alinea.tab('Basic fields', {\n title: alinea.text('Text field'),\n path: alinea.path('Path field', {\n help: 'Creates a slug of the value of another field'\n }),\n richText: alinea.richText('Rich text field'),\n select: alinea.select('Select field', {\n a: 'Option a',\n b: 'Option b'\n }),\n number: alinea.number('Number field', {\n minValue: 0,\n maxValue: 10\n }),\n check: alinea.check('Check field', {label: 'Check me please'}),\n date: alinea.date('Date field'),\n code: alinea.code('Code field')\n }),\n alinea.tab('Link fields', {\n externalLink: alinea.url('External link'),\n entry: alinea.entry('Internal link'),\n linkMultiple: alinea.link.multiple('Mixed links, multiple'),\n image: alinea.entry('Image link'),\n file: alinea.entry('File link')\n }),\n alinea.tab('List fields', {\n list: alinea.list('My list field', {\n schema: alinea.schema({\n Text: alinea.type('Text', {\n title: alinea.text('Item title'),\n text: alinea.richText('Item body text')\n }),\n Image: alinea.type('Image', {\n image: alinea.image('Image')\n })\n })\n }) \n }),\n alinea.tab('Inline fields', {\n street: alinea.text('Street', {width: 0.6, inline: true, multiline: true}),\n number: alinea.text('Number', {width: 0.2, inline: true}),\n box: alinea.text('Box', {width: 0.2, inline: true}),\n zip: alinea.text('Zipcode', {width: 0.2, inline: true}),\n city: alinea.text('City', {width: 0.4, inline: true}),\n country: alinea.text('Country', {\n width: 0.4,\n inline: true\n })\n }),\n alinea.tab('Custom fields', {\n range: range('Range field', {\n help: 'See the custom field guide'\n }) \n })\n )\n)"
"code": "import {alinea} from 'alinea'\nimport {Field, Hint, Label, Shape} from 'alinea/core'\nimport {InputLabel, InputState, useInput} from 'alinea/editor'\n\nexport interface RangeFieldOptions {\n min?: number\n max?: number\n help?: Label\n}\n\nexport interface RangeField extends Field.Scalar<number> {\n label: Label\n options?: 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: Label, options?: RangeFieldOptions): RangeField {\n return {\n shape: new ScalarShape(label),\n label,\n options,\n view: RangeInput,\n hint: Hint.Number()\n }\n}\n\ninterface RangeInputProps {\n state: InputState<InputState.Scalar<number>>\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({state, field}: RangeInputProps) {\n const [value = 5, setValue] = useInput(state)\n const {min = 0, max = 10, help} = field.options || {}\n return (\n <InputLabel label={field.label} help={help}>\n <input \n type=\"range\" \n min={min} max={max} \n value={value} \n onChange={e => setValue(Number(e.target.value))} \n />\n </InputLabel>\n )\n}\n\nexport default alinea.type('Kitchen sink',\n alinea.tabs(\n alinea.tab('Basic fields', {\n title: alinea.text('Text field'),\n path: alinea.path('Path field', {\n help: 'Creates a slug of the value of another field'\n }),\n richText: alinea.richText('Rich text field'),\n select: alinea.select('Select field', {\n a: 'Option a',\n b: 'Option b'\n }),\n number: alinea.number('Number field', {\n minValue: 0,\n maxValue: 10\n }),\n check: alinea.check('Check field', {label: 'Check me please'}),\n date: alinea.date('Date field'),\n code: alinea.code('Code field')\n }),\n alinea.tab('Link fields', {\n externalLink: alinea.url('External link'),\n entry: alinea.entry('Internal link'),\n linkMultiple: alinea.link.multiple('Mixed links, multiple'),\n image: alinea.entry('Image link'),\n file: alinea.entry('File link')\n }),\n alinea.tab('List fields', {\n list: alinea.list('My list field', {\n schema: alinea.schema({\n Text: alinea.type('Text', {\n title: alinea.text('Item title'),\n text: alinea.richText('Item body text')\n }),\n Image: alinea.type('Image', {\n image: alinea.image('Image')\n })\n })\n }) \n }),\n alinea.tab('Inline fields', {\n street: alinea.text('Street', {width: 0.6, inline: true, multiline: true}),\n number: alinea.text('Number', {width: 0.2, inline: true}),\n box: alinea.text('Box', {width: 0.2, inline: true}),\n zip: alinea.text('Zipcode', {width: 0.2, inline: true}),\n city: alinea.text('City', {width: 0.4, inline: true}),\n country: alinea.text('Country', {\n width: 0.4,\n inline: true\n })\n }),\n alinea.tab('Custom fields', {\n range: range('Range field', {\n help: 'See the custom field guide'\n }) \n })\n )\n)"
},
{
"type": "heading",
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/cms.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export const main = alinea.workspace('Alinea', {

export const cms = createNextCMS({
dashboard: {
dashboardUrl: process.env.NODE_ENV === 'development' ? '/' : '/admin.html',
dashboardUrl: '/admin.html',
handlerUrl: '/api/cms',
staticFile: 'public/admin.html'
},
Expand Down
4 changes: 3 additions & 1 deletion dev.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@ import path from 'node:path'
import sade from 'sade'

async function run({production, dir, config}) {
const forceProduction = process.env.ALINEA_CLOUD_URL
dotenv.config({path: findConfig('.env')})
process.env.NODE_ENV = production ? 'production' : 'development'
process.env.NODE_ENV =
forceProduction || production ? 'production' : 'development'
const {serve} = await import('alinea/cli/Serve')
return serve({
alineaDev: true,
Expand Down
6 changes: 3 additions & 3 deletions src/auth/passwordless/PasswordLessAuth.server.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {Handler, router} from 'alinea/backend/router/Router'
import {Route, router} from 'alinea/backend/router/Router'
import {Auth, Connection, HttpError, Outcome, User} from 'alinea/core'
import {sign, verify} from 'alinea/core/util/JWT'
import type {Transporter} from 'nodemailer'
Expand All @@ -23,12 +23,12 @@ const LoginBody = object({
// provided in the options to keep state.

export class PasswordLessAuth implements Auth.Server {
handler: Handler<Request, Response | undefined>
router: Route<Request, Response | undefined>
users = new WeakMap<Request, User>()

constructor(protected options: PasswordLessAuthOptions) {
const matcher = router.startAt(Connection.routes.base)
this.handler = router(
this.router = router(
matcher
.post(Connection.routes.base + '/auth.passwordless')
.map(router.parseJson)
Expand Down
1 change: 0 additions & 1 deletion src/backend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ export * from './backend/Database.js'
export * from './backend/FS.js'
export * from './backend/Handler.js'
export * from './backend/Media.js'
export * from './backend/Server.js'
export * from './backend/Target.js'
export * from './backend/loader/JsonLoader.js'
export * from './backend/util/JWTPreviews.js'
163 changes: 163 additions & 0 deletions src/backend/Database.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
import {
CMS,
Entry,
EntryPhase,
EntryRow,
Schema,
Type,
createId,
slugify
} from 'alinea/core'
import {entryChildrenDir, entryFilepath} from 'alinea/core/EntryFilenames'
import {Mutation, MutationType} from 'alinea/core/Mutation'
import {createEntryRow} from 'alinea/core/util/EntryRows'
import {test} from 'uvu'
import * as assert from 'uvu/assert'
import {createExample} from './test/Example.js'

async function entry(
cms: CMS,
type: Type,
data: Partial<EntryRow> = {title: 'Entry'},
parent?: EntryRow
): Promise<EntryRow> {
const typeNames = Schema.typeNames(cms.schema)
const title = data.title ?? 'Entry'
const details = {
entryId: createId(),
phase: EntryPhase.Published,
type: typeNames.get(type)!,
title,
path: data.path ?? slugify(title),
seeded: false,
workspace: 'main',
root: 'pages',
level: 0,
parent: parent?.entryId ?? null,
locale: null,
index: 'a0',
i18nId: createId(),
modifiedAt: 0,
active: true,
main: true,
data: data.data ?? {},
searchableText: ''
}
const parentPaths = parent?.childrenDir.split('/').filter(Boolean) ?? []
const filePath = entryFilepath(cms, details, parentPaths)
const childrenDir = entryChildrenDir(cms, details, parentPaths)
const row = {
...details,
filePath,
childrenDir,
parentDir: childrenDir.split('/').slice(0, -1).join('/'),
url: childrenDir
}
return createEntryRow(cms, row)
}

function create(entry: EntryRow): Mutation {
return {
type: MutationType.Create,
entry: entry,
entryId: entry.entryId,
file: entry.filePath
}
}

function remove(entry: EntryRow): Mutation {
return {
type: MutationType.Remove,
entryId: entry.entryId,
file: entry.filePath
}
}

function edit(entry: EntryRow): Mutation {
return {
type: MutationType.Edit,
entryId: entry.entryId,
file: entry.filePath,
entry: entry
}
}

function publish(entry: EntryRow): Mutation {
return {
type: MutationType.Publish,
entryId: entry.entryId,
file: entry.filePath,
phase: entry.phase
}
}

test('create', async () => {
const example = createExample()
const db = await example.db
const entry1 = await entry(example, example.schema.Page, {
title: 'Test title'
})
await db.applyMutations([create(entry1)], '')
const result = await example.get(Entry({entryId: entry1.entryId}))
assert.is(result.entryId, entry1.entryId)
assert.is(result.title, 'Test title')
})

test('remove child entries', async () => {
const example = createExample()
const db = await example.db
const parent = await entry(example, example.schema.Container)
const sub = await entry(example, example.schema.Container, {}, parent)
const subSub = await entry(example, example.schema.Page, {}, sub)

await db.applyMutations([create(parent), create(sub), create(subSub)], '')

const res1 = await example.get(Entry({entryId: subSub.entryId}))
assert.ok(res1)
assert.is(res1.parent, sub.entryId)

await db.applyMutations([remove(parent)], '')

const res2 = await example.get(Entry({entryId: subSub.entryId}))
assert.not.ok(res2)
})

test('change draft path', async () => {
const example = createExample()
const db = await example.db
const parent = await entry(example, example.schema.Container, {
path: 'parent'
})
const sub = await entry(
example,
example.schema.Container,
{path: 'sub'},
parent
)
await db.applyMutations([create(parent), create(sub)], '')
const resParent0 = await example.get(Entry({entryId: parent.entryId}))
assert.is(resParent0.url, '/parent')

const draft = {
...parent,
phase: EntryPhase.Draft,
data: {path: 'new-path'}
}

// Changing entry paths in draft should not have an influence on
// computed properties such as url, filePath etc. until we publish.
await db.applyMutations([edit(draft)], '')
const resParent1 = await example.drafts.get(Entry({entryId: parent.entryId}))
assert.is(resParent1.url, '/parent')
const res1 = await example.get(Entry({entryId: sub.entryId}))
assert.is(res1.url, '/parent/sub')

// Once we publish, the computed properties should be updated.
await db.applyMutations([publish(draft)], '')
const resParent2 = await example.get(Entry({entryId: parent.entryId}))
assert.is(resParent2.url, '/new-path')
const res2 = await example.get(Entry({entryId: sub.entryId}))
assert.is(res2.url, '/new-path/sub')
})

test.run()
Loading

0 comments on commit 513fa1b

Please sign in to comment.