diff --git a/.changeset/blue-balloons-turn.md b/.changeset/blue-balloons-turn.md new file mode 100644 index 00000000000..cbe28da7286 --- /dev/null +++ b/.changeset/blue-balloons-turn.md @@ -0,0 +1,6 @@ +--- +"@keystone-6/core": major +--- + +Renamed `__ResolvedKeystoneConfig` to `ResolvedKeystoneConfig`. +Changed `config` function to return config with defaults instead of just type casting. This now returns `ResolvedKeystoneConfig` instead of `KeystoneConfig` diff --git a/.changeset/chilled-moons-walk.md b/.changeset/chilled-moons-walk.md new file mode 100644 index 00000000000..6a5464f1977 --- /dev/null +++ b/.changeset/chilled-moons-walk.md @@ -0,0 +1,6 @@ +--- +"@keystone-6/core": major +--- + +Removed deprecated list and field hooks. +Fixed field hooks of all the in built types diff --git a/.changeset/rotten-vans-vanish.md b/.changeset/rotten-vans-vanish.md new file mode 100644 index 00000000000..1a9924fef75 --- /dev/null +++ b/.changeset/rotten-vans-vanish.md @@ -0,0 +1,5 @@ +--- +"@keystone-6/core": major +--- + +Removed deprecated `fix` option from `postinstall` script diff --git a/.changeset/shaggy-beds-know.md b/.changeset/shaggy-beds-know.md new file mode 100644 index 00000000000..c7d8536ea0b --- /dev/null +++ b/.changeset/shaggy-beds-know.md @@ -0,0 +1,5 @@ +--- +"@keystone-6/auth": major +--- + +Changed type of `withAuth` input to `ResolvedKeystoneConfig` instead of `KeystoneConfig` diff --git a/design-system/website/components/Navigation.tsx b/design-system/website/components/Navigation.tsx index f93a7e9ea14..c8e52158799 100644 --- a/design-system/website/components/Navigation.tsx +++ b/design-system/website/components/Navigation.tsx @@ -4,7 +4,7 @@ import { Fragment, type ReactNode } from 'react' import { jsx, useTheme } from '@keystone-ui/core' import Link from 'next/link' -import { useRouter } from 'next/router' +import { usePathname } from 'next/navigation' const Brand = () => { const { palette } = useTheme() @@ -37,8 +37,8 @@ const Section = ({ label, children }: SectionProps) => { type NavItemProps = { href: string, children: ReactNode } const NavItem = ({ href, children }: NavItemProps) => { const { palette, radii, spacing } = useTheme() - const router = useRouter() - const isSelected = router.pathname === href + const pathname = usePathname() + const isSelected = pathname === href return (
  • + {children} + + ) +} \ No newline at end of file diff --git a/examples/auth/app/(admin)/no-access/page.tsx b/examples/auth/app/(admin)/no-access/page.tsx new file mode 100644 index 00000000000..00571f2f9b3 --- /dev/null +++ b/examples/auth/app/(admin)/no-access/page.tsx @@ -0,0 +1,4 @@ +'use client' +import { getNoAccessPage } from '@keystone-6/core/___internal-do-not-use-will-break-in-patch/admin-ui/pages/NoAccessPage' + +export default getNoAccessPage({ sessionsEnabled: true }) diff --git a/examples/auth/app/(admin)/page.tsx b/examples/auth/app/(admin)/page.tsx new file mode 100644 index 00000000000..5c268390b0f --- /dev/null +++ b/examples/auth/app/(admin)/page.tsx @@ -0,0 +1,2 @@ +'use client' +export { HomePage as default } from '@keystone-6/core/___internal-do-not-use-will-break-in-patch/admin-ui/pages/HomePage' diff --git a/examples/auth/app/(admin)/signin/page.tsx b/examples/auth/app/(admin)/signin/page.tsx new file mode 100644 index 00000000000..ef30e38dcac --- /dev/null +++ b/examples/auth/app/(admin)/signin/page.tsx @@ -0,0 +1,5 @@ +'use client' +/* eslint-disable */ +import { getSigninPage } from '@keystone-6/auth/pages/SigninPage' + +export default getSigninPage({"identityField":"name","secretField":"password","mutationName":"authenticateUserWithPassword","successTypename":"UserAuthenticationWithPasswordSuccess","failureTypename":"UserAuthenticationWithPasswordFailure"}) diff --git a/examples/auth/app/layout.tsx b/examples/auth/app/layout.tsx new file mode 100644 index 00000000000..38a4853e3a5 --- /dev/null +++ b/examples/auth/app/layout.tsx @@ -0,0 +1,11 @@ +export default function RootLayout ({ + children, +}: { + children: React.ReactNode +}) { + return ( + + {children} + + ) +} diff --git a/examples/auth/keystone.ts b/examples/auth/keystone.ts index ee96e919f1e..5d2b780a3f5 100644 --- a/examples/auth/keystone.ts +++ b/examples/auth/keystone.ts @@ -70,5 +70,5 @@ export default withAuth>( // the session secret is used to encrypt cookie data secret: sessionSecret, }), - }) + }) as any ) diff --git a/examples/auth/next-env.d.ts b/examples/auth/next-env.d.ts new file mode 100644 index 00000000000..4f11a03dc6c --- /dev/null +++ b/examples/auth/next-env.d.ts @@ -0,0 +1,5 @@ +/// +/// + +// NOTE: This file should not be edited +// see https://nextjs.org/docs/basic-features/typescript for more information. diff --git a/examples/auth/next.config.mjs b/examples/auth/next.config.mjs new file mode 100644 index 00000000000..8f06b183bdf --- /dev/null +++ b/examples/auth/next.config.mjs @@ -0,0 +1,10 @@ +// you don't need this if you're building something outside of the Keystone repo + +export default { + eslint: { + ignoreDuringBuilds: true, + }, + typescript: { + ignoreBuildErrors: true, + }, +} diff --git a/examples/auth/package.json b/examples/auth/package.json index 0e3f415f09d..4f733d35ec2 100644 --- a/examples/auth/package.json +++ b/examples/auth/package.json @@ -12,7 +12,8 @@ "dependencies": { "@keystone-6/auth": "^8.0.0", "@keystone-6/core": "^6.2.0", - "@prisma/client": "5.17.0" + "@prisma/client": "5.17.0", + "next": "^14.2.0" }, "devDependencies": { "prisma": "5.17.0", diff --git a/examples/auth/schema.graphql b/examples/auth/schema.graphql index c2ded0eb201..d6ddcb6aa87 100644 --- a/examples/auth/schema.graphql +++ b/examples/auth/schema.graphql @@ -67,6 +67,380 @@ input UserCreateInput { isAdmin: Boolean } +type Project { + id: ID! + slug: String + name: String + createdAt: DateTime + updatedAt: DateTime + deletedAt: DateTime + taskRuns(where: TaskRunWhereInput! = {}, orderBy: [TaskRunOrderByInput!]! = [], take: Int, skip: Int! = 0, cursor: TaskRunWhereUniqueInput): [TaskRun!] + taskRunsCount(where: TaskRunWhereInput! = {}): Int +} + +scalar DateTime @specifiedBy(url: "https://datatracker.ietf.org/doc/html/rfc3339#section-5.6") + +input ProjectWhereUniqueInput { + id: ID + slug: String +} + +input ProjectWhereInput { + AND: [ProjectWhereInput!] + OR: [ProjectWhereInput!] + NOT: [ProjectWhereInput!] + id: IDFilter + slug: StringFilter + name: StringFilter + createdAt: DateTimeNullableFilter + updatedAt: DateTimeNullableFilter + deletedAt: DateTimeNullableFilter + taskRuns: TaskRunManyRelationFilter +} + +input StringFilter { + equals: String + in: [String!] + notIn: [String!] + lt: String + lte: String + gt: String + gte: String + contains: String + startsWith: String + endsWith: String + not: NestedStringFilter +} + +input NestedStringFilter { + equals: String + in: [String!] + notIn: [String!] + lt: String + lte: String + gt: String + gte: String + contains: String + startsWith: String + endsWith: String + not: NestedStringFilter +} + +input DateTimeNullableFilter { + equals: DateTime + in: [DateTime!] + notIn: [DateTime!] + lt: DateTime + lte: DateTime + gt: DateTime + gte: DateTime + not: DateTimeNullableFilter +} + +input TaskRunManyRelationFilter { + every: TaskRunWhereInput + some: TaskRunWhereInput + none: TaskRunWhereInput +} + +input ProjectOrderByInput { + id: OrderDirection + slug: OrderDirection + name: OrderDirection + createdAt: OrderDirection + updatedAt: OrderDirection + deletedAt: OrderDirection +} + +input ProjectUpdateInput { + slug: String + name: String + createdAt: DateTime + updatedAt: DateTime + deletedAt: DateTime + taskRuns: TaskRunRelateToManyForUpdateInput +} + +input TaskRunRelateToManyForUpdateInput { + disconnect: [TaskRunWhereUniqueInput!] + set: [TaskRunWhereUniqueInput!] + create: [TaskRunCreateInput!] + connect: [TaskRunWhereUniqueInput!] +} + +input ProjectUpdateArgs { + where: ProjectWhereUniqueInput! + data: ProjectUpdateInput! +} + +input ProjectCreateInput { + slug: String + name: String + createdAt: DateTime + updatedAt: DateTime + deletedAt: DateTime + taskRuns: TaskRunRelateToManyForCreateInput +} + +input TaskRunRelateToManyForCreateInput { + create: [TaskRunCreateInput!] + connect: [TaskRunWhereUniqueInput!] +} + +type TaskRun { + id: ID! + number: Int + friendlyId: String + status: String + taskIdentifier: String + isTest: Boolean + payload: String + payloadType: String + context: JSON + traceId: String + spanId: String + project: Project + createdAt: DateTime + updatedAt: DateTime + attempts(where: TaskRunAttemptWhereInput! = {}, orderBy: [TaskRunAttemptOrderByInput!]! = [], take: Int, skip: Int! = 0, cursor: TaskRunAttemptWhereUniqueInput): [TaskRunAttempt!] + attemptsCount(where: TaskRunAttemptWhereInput! = {}): Int + startedAt: DateTime + completedAt: DateTime +} + +input TaskRunWhereUniqueInput { + id: ID +} + +input TaskRunWhereInput { + AND: [TaskRunWhereInput!] + OR: [TaskRunWhereInput!] + NOT: [TaskRunWhereInput!] + id: IDFilter + number: IntNullableFilter + friendlyId: StringFilter + status: StringNullableFilter + taskIdentifier: StringFilter + isTest: BooleanFilter + payload: StringFilter + payloadType: StringFilter + traceId: StringFilter + spanId: StringFilter + project: ProjectWhereInput + createdAt: DateTimeNullableFilter + updatedAt: DateTimeNullableFilter + attempts: TaskRunAttemptManyRelationFilter + startedAt: DateTimeNullableFilter + completedAt: DateTimeNullableFilter +} + +input IntNullableFilter { + equals: Int + in: [Int!] + notIn: [Int!] + lt: Int + lte: Int + gt: Int + gte: Int + not: IntNullableFilter +} + +input StringNullableFilter { + equals: String + in: [String!] + notIn: [String!] + lt: String + lte: String + gt: String + gte: String + contains: String + startsWith: String + endsWith: String + not: StringNullableFilter +} + +input TaskRunAttemptManyRelationFilter { + every: TaskRunAttemptWhereInput + some: TaskRunAttemptWhereInput + none: TaskRunAttemptWhereInput +} + +input TaskRunOrderByInput { + id: OrderDirection + number: OrderDirection + friendlyId: OrderDirection + status: OrderDirection + taskIdentifier: OrderDirection + isTest: OrderDirection + payload: OrderDirection + payloadType: OrderDirection + traceId: OrderDirection + spanId: OrderDirection + createdAt: OrderDirection + updatedAt: OrderDirection + startedAt: OrderDirection + completedAt: OrderDirection +} + +input TaskRunUpdateInput { + number: Int + friendlyId: String + status: String + taskIdentifier: String + isTest: Boolean + payload: String + payloadType: String + context: JSON + traceId: String + spanId: String + project: ProjectRelateToOneForUpdateInput + createdAt: DateTime + updatedAt: DateTime + attempts: TaskRunAttemptRelateToManyForUpdateInput + startedAt: DateTime + completedAt: DateTime +} + +input ProjectRelateToOneForUpdateInput { + create: ProjectCreateInput + connect: ProjectWhereUniqueInput + disconnect: Boolean +} + +input TaskRunAttemptRelateToManyForUpdateInput { + disconnect: [TaskRunAttemptWhereUniqueInput!] + set: [TaskRunAttemptWhereUniqueInput!] + create: [TaskRunAttemptCreateInput!] + connect: [TaskRunAttemptWhereUniqueInput!] +} + +input TaskRunUpdateArgs { + where: TaskRunWhereUniqueInput! + data: TaskRunUpdateInput! +} + +input TaskRunCreateInput { + number: Int + friendlyId: String + status: String + taskIdentifier: String + isTest: Boolean + payload: String + payloadType: String + context: JSON + traceId: String + spanId: String + project: ProjectRelateToOneForCreateInput + createdAt: DateTime + updatedAt: DateTime + attempts: TaskRunAttemptRelateToManyForCreateInput + startedAt: DateTime + completedAt: DateTime +} + +input ProjectRelateToOneForCreateInput { + create: ProjectCreateInput + connect: ProjectWhereUniqueInput +} + +input TaskRunAttemptRelateToManyForCreateInput { + create: [TaskRunAttemptCreateInput!] + connect: [TaskRunAttemptWhereUniqueInput!] +} + +type TaskRunAttempt { + id: ID! + number: Int + friendlyId: String + taskRun: TaskRun + status: String + createdAt: DateTime + updatedAt: DateTime + startedAt: DateTime + completedAt: DateTime + error: JSON + output: String + outputType: String +} + +input TaskRunAttemptWhereUniqueInput { + id: ID +} + +input TaskRunAttemptWhereInput { + AND: [TaskRunAttemptWhereInput!] + OR: [TaskRunAttemptWhereInput!] + NOT: [TaskRunAttemptWhereInput!] + id: IDFilter + number: IntNullableFilter + friendlyId: StringFilter + taskRun: TaskRunWhereInput + status: StringNullableFilter + createdAt: DateTimeNullableFilter + updatedAt: DateTimeNullableFilter + startedAt: DateTimeNullableFilter + completedAt: DateTimeNullableFilter + output: StringFilter + outputType: StringFilter +} + +input TaskRunAttemptOrderByInput { + id: OrderDirection + number: OrderDirection + friendlyId: OrderDirection + status: OrderDirection + createdAt: OrderDirection + updatedAt: OrderDirection + startedAt: OrderDirection + completedAt: OrderDirection + output: OrderDirection + outputType: OrderDirection +} + +input TaskRunAttemptUpdateInput { + number: Int + friendlyId: String + taskRun: TaskRunRelateToOneForUpdateInput + status: String + createdAt: DateTime + updatedAt: DateTime + startedAt: DateTime + completedAt: DateTime + error: JSON + output: String + outputType: String +} + +input TaskRunRelateToOneForUpdateInput { + create: TaskRunCreateInput + connect: TaskRunWhereUniqueInput + disconnect: Boolean +} + +input TaskRunAttemptUpdateArgs { + where: TaskRunAttemptWhereUniqueInput! + data: TaskRunAttemptUpdateInput! +} + +input TaskRunAttemptCreateInput { + number: Int + friendlyId: String + taskRun: TaskRunRelateToOneForCreateInput + status: String + createdAt: DateTime + updatedAt: DateTime + startedAt: DateTime + completedAt: DateTime + error: JSON + output: String + outputType: String +} + +input TaskRunRelateToOneForCreateInput { + create: TaskRunCreateInput + connect: TaskRunWhereUniqueInput +} + """ The `JSON` scalar type represents JSON values as specified by [ECMA-404](http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf). """ @@ -79,6 +453,24 @@ type Mutation { updateUsers(data: [UserUpdateArgs!]!): [User] deleteUser(where: UserWhereUniqueInput!): User deleteUsers(where: [UserWhereUniqueInput!]!): [User] + createProject(data: ProjectCreateInput!): Project + createProjects(data: [ProjectCreateInput!]!): [Project] + updateProject(where: ProjectWhereUniqueInput!, data: ProjectUpdateInput!): Project + updateProjects(data: [ProjectUpdateArgs!]!): [Project] + deleteProject(where: ProjectWhereUniqueInput!): Project + deleteProjects(where: [ProjectWhereUniqueInput!]!): [Project] + createTaskRun(data: TaskRunCreateInput!): TaskRun + createTaskRuns(data: [TaskRunCreateInput!]!): [TaskRun] + updateTaskRun(where: TaskRunWhereUniqueInput!, data: TaskRunUpdateInput!): TaskRun + updateTaskRuns(data: [TaskRunUpdateArgs!]!): [TaskRun] + deleteTaskRun(where: TaskRunWhereUniqueInput!): TaskRun + deleteTaskRuns(where: [TaskRunWhereUniqueInput!]!): [TaskRun] + createTaskRunAttempt(data: TaskRunAttemptCreateInput!): TaskRunAttempt + createTaskRunAttempts(data: [TaskRunAttemptCreateInput!]!): [TaskRunAttempt] + updateTaskRunAttempt(where: TaskRunAttemptWhereUniqueInput!, data: TaskRunAttemptUpdateInput!): TaskRunAttempt + updateTaskRunAttempts(data: [TaskRunAttemptUpdateArgs!]!): [TaskRunAttempt] + deleteTaskRunAttempt(where: TaskRunAttemptWhereUniqueInput!): TaskRunAttempt + deleteTaskRunAttempts(where: [TaskRunAttemptWhereUniqueInput!]!): [TaskRunAttempt] endSession: Boolean! authenticateUserWithPassword(name: String!, password: String!): UserAuthenticationWithPasswordResult createInitialUser(data: CreateInitialUserInput!): UserAuthenticationWithPasswordSuccess! @@ -104,6 +496,15 @@ type Query { user(where: UserWhereUniqueInput!): User users(where: UserWhereInput! = {}, orderBy: [UserOrderByInput!]! = [], take: Int, skip: Int! = 0, cursor: UserWhereUniqueInput): [User!] usersCount(where: UserWhereInput! = {}): Int + projects(where: ProjectWhereInput! = {}, orderBy: [ProjectOrderByInput!]! = [], take: Int, skip: Int! = 0, cursor: ProjectWhereUniqueInput): [Project!] + project(where: ProjectWhereUniqueInput!): Project + projectsCount(where: ProjectWhereInput! = {}): Int + taskRuns(where: TaskRunWhereInput! = {}, orderBy: [TaskRunOrderByInput!]! = [], take: Int, skip: Int! = 0, cursor: TaskRunWhereUniqueInput): [TaskRun!] + taskRun(where: TaskRunWhereUniqueInput!): TaskRun + taskRunsCount(where: TaskRunWhereInput! = {}): Int + taskRunAttempts(where: TaskRunAttemptWhereInput! = {}, orderBy: [TaskRunAttemptOrderByInput!]! = [], take: Int, skip: Int! = 0, cursor: TaskRunAttemptWhereUniqueInput): [TaskRunAttempt!] + taskRunAttempt(where: TaskRunAttemptWhereUniqueInput!): TaskRunAttempt + taskRunAttemptsCount(where: TaskRunAttemptWhereInput! = {}): Int keystone: KeystoneMeta! authenticatedItem: AuthenticatedItem } diff --git a/examples/auth/schema.prisma b/examples/auth/schema.prisma index 44c1400b78b..a711ebf4eee 100644 --- a/examples/auth/schema.prisma +++ b/examples/auth/schema.prisma @@ -18,3 +18,54 @@ model User { password String isAdmin Boolean @default(false) } + +model Project { + id String @id @default(cuid()) + slug String @unique @default("") + name String @default("") + createdAt DateTime? @default(now()) + updatedAt DateTime? @default(now()) @updatedAt + deletedAt DateTime? + taskRuns TaskRun[] @relation("TaskRun_project") +} + +model TaskRun { + id String @id @default(cuid()) + number Int? @default(0) + friendlyId String @default("") + status String? @default("pending") + taskIdentifier String @default("") + isTest Boolean @default(false) + payload String @default("") + payloadType String @default("application/json") + context String? + traceId String @default("") + spanId String @default("") + project Project? @relation("TaskRun_project", fields: [projectId], references: [id]) + projectId String? @map("project") + createdAt DateTime? @default(now()) + updatedAt DateTime? @default(now()) @updatedAt + attempts TaskRunAttempt[] @relation("TaskRunAttempt_taskRun") + startedAt DateTime? + completedAt DateTime? + + @@index([projectId]) +} + +model TaskRunAttempt { + id String @id @default(cuid()) + number Int? @default(0) + friendlyId String @default("") + taskRun TaskRun? @relation("TaskRunAttempt_taskRun", fields: [taskRunId], references: [id]) + taskRunId String? @map("taskRun") + status String? @default("pending") + createdAt DateTime? @default(now()) + updatedAt DateTime? @default(now()) @updatedAt + startedAt DateTime? + completedAt DateTime? + error String? + output String @default("") + outputType String @default("application/json") + + @@index([taskRunId]) +} diff --git a/examples/auth/schema.ts b/examples/auth/schema.ts index 2879ee9f407..a657f6863c2 100644 --- a/examples/auth/schema.ts +++ b/examples/auth/schema.ts @@ -1,6 +1,6 @@ import { list } from '@keystone-6/core' import { allowAll, denyAll } from '@keystone-6/core/access' -import { text, checkbox, password } from '@keystone-6/core/fields' +import { text, checkbox, password, timestamp, relationship, json, select, integer } from '@keystone-6/core/fields' import type { Lists } from '.keystone/types' // WARNING: this example is for demonstration purposes only @@ -153,4 +153,76 @@ export const lists = { }), }, }), + Project: list({ + access: allowAll, + + fields: { + slug: text({ isIndexed: 'unique' }), + name: text({ validation: { isRequired: true } }), + createdAt: timestamp({ defaultValue: { kind: 'now' } }), + updatedAt: timestamp({ defaultValue: { kind: 'now' }, db: { updatedAt: true } }), + deletedAt: timestamp({}), + taskRuns: relationship({ ref: 'TaskRun.project', many: true }), + } + }), + TaskRun: list({ + access: allowAll, + + fields: { + number: integer({ defaultValue: 0 }), + friendlyId: text({}), + status: select({ + options: [ + { label: 'Pending', value: 'pending' }, + { label: 'Executing', value: 'executing' }, + { label: 'Paused', value: 'paused' }, + { label: 'Canceled', value: 'canceled' }, + { label: 'Interrupted', value: 'interrupted' }, + { label: 'Completed Successfully', value: 'completed_successfully' }, + { label: 'Completed With Errors', value: 'completed_with_errors' }, + { label: 'System Failure', value: 'system_failure' }, + { label: 'Crashed', value: 'crashed' }], + defaultValue: 'pending' + }), + taskIdentifier: text({}), + isTest: checkbox({ defaultValue: false }), + payload: text({}), + payloadType: text({ defaultValue: 'application/json' }), + context: json({}), + traceId: text({}), + spanId: text({}), + project: relationship({ ref: 'Project.taskRuns' }), + createdAt: timestamp({ defaultValue: { kind: 'now' } }), + updatedAt: timestamp({ defaultValue: { kind: 'now' }, db: { updatedAt: true } }), + attempts: relationship({ ref: 'TaskRunAttempt.taskRun', many: true }), + startedAt: timestamp({}), + completedAt: timestamp({}), + } + }), + TaskRunAttempt: list({ + access: allowAll, + + fields: { + number: integer({ defaultValue: 0 }), + friendlyId: text({}), + taskRun: relationship({ ref: 'TaskRun.attempts' }), + status: select({ + options: [ + { label: 'Pending', value: 'pending' }, + { label: 'Executing', value: 'executing' }, + { label: 'Paused', value: 'paused' }, + { label: 'Failed', value: 'failed' }, + { label: 'Canceled', value: 'canceled' }, + { label: 'Completed', value: 'completed' }], + defaultValue: 'pending' + }), + createdAt: timestamp({ defaultValue: { kind: 'now' } }), + updatedAt: timestamp({ defaultValue: { kind: 'now' }, db: { updatedAt: true } }), + startedAt: timestamp({}), + completedAt: timestamp({}), + error: json({}), + output: text({}), + outputType: text({ defaultValue: 'application/json' }), + } + }) } satisfies Lists diff --git a/examples/auth/tsconfig.json b/examples/auth/tsconfig.json new file mode 100644 index 00000000000..ccb2ed95d83 --- /dev/null +++ b/examples/auth/tsconfig.json @@ -0,0 +1,34 @@ +{ + "compilerOptions": { + "lib": [ + "dom", + "dom.iterable", + "esnext" + ], + "allowJs": true, + "skipLibCheck": true, + "strict": false, + "noEmit": true, + "incremental": true, + "module": "esnext", + "esModuleInterop": true, + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "plugins": [ + { + "name": "next" + } + ] + }, + "include": [ + "next-env.d.ts", + ".next/types/**/*.ts", + "**/*.ts", + "**/*.tsx" + ], + "exclude": [ + "node_modules" + ] +} diff --git a/examples/custom-admin-ui-logo/app/(admin)/.admin/index.tsx b/examples/custom-admin-ui-logo/app/(admin)/.admin/index.tsx new file mode 100644 index 00000000000..5dd11446daf --- /dev/null +++ b/examples/custom-admin-ui-logo/app/(admin)/.admin/index.tsx @@ -0,0 +1,18 @@ +/* eslint-disable */ +import * as view0 from '@keystone-6/core/___internal-do-not-use-will-break-in-patch/admin-ui/id-field-view' +import * as view1 from '@keystone-6/core/fields/types/text/views' +import * as view2 from '@keystone-6/core/fields/types/select/views' +import * as view3 from '@keystone-6/core/fields/types/checkbox/views' +import * as view4 from '@keystone-6/core/fields/types/relationship/views' +import * as view5 from '@keystone-6/core/fields/types/timestamp/views' + +import * as adminConfig from '../config' + +export const config = { + lazyMetadataQuery: {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"keystone","loc":{"start":22,"end":30}},"arguments":[],"directives":[],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"adminMeta","loc":{"start":39,"end":48}},"arguments":[],"directives":[],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"lists","loc":{"start":59,"end":64}},"arguments":[],"directives":[],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"key","loc":{"start":77,"end":80}},"arguments":[],"directives":[],"loc":{"start":77,"end":80}},{"kind":"Field","name":{"kind":"Name","value":"isHidden","loc":{"start":91,"end":99}},"arguments":[],"directives":[],"loc":{"start":91,"end":99}},{"kind":"Field","name":{"kind":"Name","value":"fields","loc":{"start":110,"end":116}},"arguments":[],"directives":[],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"path","loc":{"start":131,"end":135}},"arguments":[],"directives":[],"loc":{"start":131,"end":135}},{"kind":"Field","name":{"kind":"Name","value":"createView","loc":{"start":148,"end":158}},"arguments":[],"directives":[],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"fieldMode","loc":{"start":175,"end":184}},"arguments":[],"directives":[],"loc":{"start":175,"end":184}}],"loc":{"start":159,"end":198}},"loc":{"start":148,"end":198}}],"loc":{"start":117,"end":210}},"loc":{"start":110,"end":210}}],"loc":{"start":65,"end":220}},"loc":{"start":59,"end":220}}],"loc":{"start":49,"end":228}},"loc":{"start":39,"end":228}}],"loc":{"start":31,"end":234}},"loc":{"start":22,"end":234}}]}}]}, + fieldViews: [view0,view1,view2,view3,view4,view5], + adminMetaHash: '1mrsjib', + adminConfig, + apiPath: '/api/graphql', + adminPath: '', +}; diff --git a/examples/custom-admin-ui-logo/app/(admin)/[listKey]/[id]/page.tsx b/examples/custom-admin-ui-logo/app/(admin)/[listKey]/[id]/page.tsx new file mode 100644 index 00000000000..c5d7ea2be62 --- /dev/null +++ b/examples/custom-admin-ui-logo/app/(admin)/[listKey]/[id]/page.tsx @@ -0,0 +1,4 @@ +'use client' +import { ItemPage } from '@keystone-6/core/___internal-do-not-use-will-break-in-patch/admin-ui/pages/ItemPage' + +export default ItemPage diff --git a/examples/custom-admin-ui-logo/app/(admin)/[listKey]/create/page.tsx b/examples/custom-admin-ui-logo/app/(admin)/[listKey]/create/page.tsx new file mode 100644 index 00000000000..d6042acaa96 --- /dev/null +++ b/examples/custom-admin-ui-logo/app/(admin)/[listKey]/create/page.tsx @@ -0,0 +1,4 @@ +'use client' +import { CreateItemPage } from '@keystone-6/core/___internal-do-not-use-will-break-in-patch/admin-ui/pages/CreateItemPage' + +export default CreateItemPage diff --git a/examples/custom-admin-ui-logo/app/(admin)/[listKey]/page.tsx b/examples/custom-admin-ui-logo/app/(admin)/[listKey]/page.tsx new file mode 100644 index 00000000000..f6e75f8cfab --- /dev/null +++ b/examples/custom-admin-ui-logo/app/(admin)/[listKey]/page.tsx @@ -0,0 +1,4 @@ +'use client' +import { ListPage } from '@keystone-6/core/___internal-do-not-use-will-break-in-patch/admin-ui/pages/ListPage' + +export default ListPage diff --git a/examples/custom-admin-ui-logo/app/(admin)/config.tsx b/examples/custom-admin-ui-logo/app/(admin)/config.tsx new file mode 100644 index 00000000000..f2b986e29ca --- /dev/null +++ b/examples/custom-admin-ui-logo/app/(admin)/config.tsx @@ -0,0 +1 @@ +export { components } from '../config' \ No newline at end of file diff --git a/examples/custom-admin-ui-logo/app/(admin)/layout.tsx b/examples/custom-admin-ui-logo/app/(admin)/layout.tsx new file mode 100644 index 00000000000..abb5a0f3b2c --- /dev/null +++ b/examples/custom-admin-ui-logo/app/(admin)/layout.tsx @@ -0,0 +1,16 @@ +'use client' +import { Layout } from '@keystone-6/core/___internal-do-not-use-will-break-in-patch/admin-ui/pages/App' +import { config } from './.admin' + + +export default function AdminLayout ({ + children, +}: { + children: React.ReactNode +}) { + return ( + + {children} + + ) +} \ No newline at end of file diff --git a/examples/custom-admin-ui-logo/app/(admin)/no-access/page.tsx b/examples/custom-admin-ui-logo/app/(admin)/no-access/page.tsx new file mode 100644 index 00000000000..70877231fee --- /dev/null +++ b/examples/custom-admin-ui-logo/app/(admin)/no-access/page.tsx @@ -0,0 +1,4 @@ +'use client' +import { getNoAccessPage } from '@keystone-6/core/___internal-do-not-use-will-break-in-patch/admin-ui/pages/NoAccessPage' + +export default getNoAccessPage({ sessionsEnabled: false }) diff --git a/examples/custom-admin-ui-logo/app/(admin)/page.tsx b/examples/custom-admin-ui-logo/app/(admin)/page.tsx new file mode 100644 index 00000000000..5c268390b0f --- /dev/null +++ b/examples/custom-admin-ui-logo/app/(admin)/page.tsx @@ -0,0 +1,2 @@ +'use client' +export { HomePage as default } from '@keystone-6/core/___internal-do-not-use-will-break-in-patch/admin-ui/pages/HomePage' diff --git a/examples/custom-admin-ui-logo/admin/components/CustomLogo.tsx b/examples/custom-admin-ui-logo/app/config/components/CustomLogo.tsx similarity index 100% rename from examples/custom-admin-ui-logo/admin/components/CustomLogo.tsx rename to examples/custom-admin-ui-logo/app/config/components/CustomLogo.tsx diff --git a/examples/custom-admin-ui-logo/admin/config.tsx b/examples/custom-admin-ui-logo/app/config/index.tsx similarity index 100% rename from examples/custom-admin-ui-logo/admin/config.tsx rename to examples/custom-admin-ui-logo/app/config/index.tsx diff --git a/examples/custom-admin-ui-logo/app/layout.tsx b/examples/custom-admin-ui-logo/app/layout.tsx new file mode 100644 index 00000000000..38a4853e3a5 --- /dev/null +++ b/examples/custom-admin-ui-logo/app/layout.tsx @@ -0,0 +1,11 @@ +export default function RootLayout ({ + children, +}: { + children: React.ReactNode +}) { + return ( + + {children} + + ) +} diff --git a/examples/custom-admin-ui-logo/next-env.d.ts b/examples/custom-admin-ui-logo/next-env.d.ts new file mode 100644 index 00000000000..4f11a03dc6c --- /dev/null +++ b/examples/custom-admin-ui-logo/next-env.d.ts @@ -0,0 +1,5 @@ +/// +/// + +// NOTE: This file should not be edited +// see https://nextjs.org/docs/basic-features/typescript for more information. diff --git a/examples/custom-admin-ui-logo/tsconfig.json b/examples/custom-admin-ui-logo/tsconfig.json new file mode 100644 index 00000000000..ccb2ed95d83 --- /dev/null +++ b/examples/custom-admin-ui-logo/tsconfig.json @@ -0,0 +1,34 @@ +{ + "compilerOptions": { + "lib": [ + "dom", + "dom.iterable", + "esnext" + ], + "allowJs": true, + "skipLibCheck": true, + "strict": false, + "noEmit": true, + "incremental": true, + "module": "esnext", + "esModuleInterop": true, + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "plugins": [ + { + "name": "next" + } + ] + }, + "include": [ + "next-env.d.ts", + ".next/types/**/*.ts", + "**/*.ts", + "**/*.tsx" + ], + "exclude": [ + "node_modules" + ] +} diff --git a/examples/custom-admin-ui-navigation/app/(admin)/.admin/index.tsx b/examples/custom-admin-ui-navigation/app/(admin)/.admin/index.tsx new file mode 100644 index 00000000000..283542749b0 --- /dev/null +++ b/examples/custom-admin-ui-navigation/app/(admin)/.admin/index.tsx @@ -0,0 +1,18 @@ +/* eslint-disable */ +import * as view0 from "@keystone-6/core/___internal-do-not-use-will-break-in-patch/admin-ui/id-field-view" +import * as view1 from "@keystone-6/core/fields/types/text/views" +import * as view2 from "@keystone-6/core/fields/types/select/views" +import * as view3 from "@keystone-6/core/fields/types/checkbox/views" +import * as view4 from "@keystone-6/core/fields/types/relationship/views" +import * as view5 from "@keystone-6/core/fields/types/timestamp/views" + +import * as adminConfig from '../config' + +export const config = { + lazyMetadataQuery: {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"keystone","loc":{"start":22,"end":30}},"arguments":[],"directives":[],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"adminMeta","loc":{"start":39,"end":48}},"arguments":[],"directives":[],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"lists","loc":{"start":59,"end":64}},"arguments":[],"directives":[],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"key","loc":{"start":77,"end":80}},"arguments":[],"directives":[],"loc":{"start":77,"end":80}},{"kind":"Field","name":{"kind":"Name","value":"isHidden","loc":{"start":91,"end":99}},"arguments":[],"directives":[],"loc":{"start":91,"end":99}},{"kind":"Field","name":{"kind":"Name","value":"fields","loc":{"start":110,"end":116}},"arguments":[],"directives":[],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"path","loc":{"start":131,"end":135}},"arguments":[],"directives":[],"loc":{"start":131,"end":135}},{"kind":"Field","name":{"kind":"Name","value":"createView","loc":{"start":148,"end":158}},"arguments":[],"directives":[],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"fieldMode","loc":{"start":175,"end":184}},"arguments":[],"directives":[],"loc":{"start":175,"end":184}}],"loc":{"start":159,"end":198}},"loc":{"start":148,"end":198}}],"loc":{"start":117,"end":210}},"loc":{"start":110,"end":210}}],"loc":{"start":65,"end":220}},"loc":{"start":59,"end":220}}],"loc":{"start":49,"end":228}},"loc":{"start":39,"end":228}}],"loc":{"start":31,"end":234}},"loc":{"start":22,"end":234}}]}}]}, + fieldViews: [view0,view1,view2,view3,view4,view5], + adminMetaHash: '1mrsjib', + adminConfig, + apiPath: '/api/graphql', + adminPath: '', +}; diff --git a/examples/custom-admin-ui-navigation/app/(admin)/[listKey]/[id]/page.tsx b/examples/custom-admin-ui-navigation/app/(admin)/[listKey]/[id]/page.tsx new file mode 100644 index 00000000000..c5d7ea2be62 --- /dev/null +++ b/examples/custom-admin-ui-navigation/app/(admin)/[listKey]/[id]/page.tsx @@ -0,0 +1,4 @@ +'use client' +import { ItemPage } from '@keystone-6/core/___internal-do-not-use-will-break-in-patch/admin-ui/pages/ItemPage' + +export default ItemPage diff --git a/examples/custom-admin-ui-navigation/app/(admin)/[listKey]/create/page.tsx b/examples/custom-admin-ui-navigation/app/(admin)/[listKey]/create/page.tsx new file mode 100644 index 00000000000..d6042acaa96 --- /dev/null +++ b/examples/custom-admin-ui-navigation/app/(admin)/[listKey]/create/page.tsx @@ -0,0 +1,4 @@ +'use client' +import { CreateItemPage } from '@keystone-6/core/___internal-do-not-use-will-break-in-patch/admin-ui/pages/CreateItemPage' + +export default CreateItemPage diff --git a/examples/custom-admin-ui-navigation/app/(admin)/[listKey]/page.tsx b/examples/custom-admin-ui-navigation/app/(admin)/[listKey]/page.tsx new file mode 100644 index 00000000000..f6e75f8cfab --- /dev/null +++ b/examples/custom-admin-ui-navigation/app/(admin)/[listKey]/page.tsx @@ -0,0 +1,4 @@ +'use client' +import { ListPage } from '@keystone-6/core/___internal-do-not-use-will-break-in-patch/admin-ui/pages/ListPage' + +export default ListPage diff --git a/examples/custom-admin-ui-navigation/app/(admin)/config.tsx b/examples/custom-admin-ui-navigation/app/(admin)/config.tsx new file mode 100644 index 00000000000..f2b986e29ca --- /dev/null +++ b/examples/custom-admin-ui-navigation/app/(admin)/config.tsx @@ -0,0 +1 @@ +export { components } from '../config' \ No newline at end of file diff --git a/examples/custom-admin-ui-navigation/app/(admin)/layout.tsx b/examples/custom-admin-ui-navigation/app/(admin)/layout.tsx new file mode 100644 index 00000000000..abb5a0f3b2c --- /dev/null +++ b/examples/custom-admin-ui-navigation/app/(admin)/layout.tsx @@ -0,0 +1,16 @@ +'use client' +import { Layout } from '@keystone-6/core/___internal-do-not-use-will-break-in-patch/admin-ui/pages/App' +import { config } from './.admin' + + +export default function AdminLayout ({ + children, +}: { + children: React.ReactNode +}) { + return ( + + {children} + + ) +} \ No newline at end of file diff --git a/examples/custom-admin-ui-navigation/app/(admin)/no-access/page.tsx b/examples/custom-admin-ui-navigation/app/(admin)/no-access/page.tsx new file mode 100644 index 00000000000..70877231fee --- /dev/null +++ b/examples/custom-admin-ui-navigation/app/(admin)/no-access/page.tsx @@ -0,0 +1,4 @@ +'use client' +import { getNoAccessPage } from '@keystone-6/core/___internal-do-not-use-will-break-in-patch/admin-ui/pages/NoAccessPage' + +export default getNoAccessPage({ sessionsEnabled: false }) diff --git a/examples/custom-admin-ui-navigation/app/(admin)/page.tsx b/examples/custom-admin-ui-navigation/app/(admin)/page.tsx new file mode 100644 index 00000000000..5c268390b0f --- /dev/null +++ b/examples/custom-admin-ui-navigation/app/(admin)/page.tsx @@ -0,0 +1,2 @@ +'use client' +export { HomePage as default } from '@keystone-6/core/___internal-do-not-use-will-break-in-patch/admin-ui/pages/HomePage' diff --git a/examples/custom-admin-ui-navigation/admin/components/CustomNavigation.tsx b/examples/custom-admin-ui-navigation/app/config/components/CustomNavigation.tsx similarity index 100% rename from examples/custom-admin-ui-navigation/admin/components/CustomNavigation.tsx rename to examples/custom-admin-ui-navigation/app/config/components/CustomNavigation.tsx diff --git a/examples/custom-admin-ui-navigation/admin/config.ts b/examples/custom-admin-ui-navigation/app/config/index.ts similarity index 100% rename from examples/custom-admin-ui-navigation/admin/config.ts rename to examples/custom-admin-ui-navigation/app/config/index.ts diff --git a/examples/custom-admin-ui-navigation/app/layout.tsx b/examples/custom-admin-ui-navigation/app/layout.tsx new file mode 100644 index 00000000000..38a4853e3a5 --- /dev/null +++ b/examples/custom-admin-ui-navigation/app/layout.tsx @@ -0,0 +1,11 @@ +export default function RootLayout ({ + children, +}: { + children: React.ReactNode +}) { + return ( + + {children} + + ) +} diff --git a/examples/custom-admin-ui-navigation/next-env.d.ts b/examples/custom-admin-ui-navigation/next-env.d.ts new file mode 100644 index 00000000000..4f11a03dc6c --- /dev/null +++ b/examples/custom-admin-ui-navigation/next-env.d.ts @@ -0,0 +1,5 @@ +/// +/// + +// NOTE: This file should not be edited +// see https://nextjs.org/docs/basic-features/typescript for more information. diff --git a/examples/custom-admin-ui-navigation/tsconfig.json b/examples/custom-admin-ui-navigation/tsconfig.json new file mode 100644 index 00000000000..ccb2ed95d83 --- /dev/null +++ b/examples/custom-admin-ui-navigation/tsconfig.json @@ -0,0 +1,34 @@ +{ + "compilerOptions": { + "lib": [ + "dom", + "dom.iterable", + "esnext" + ], + "allowJs": true, + "skipLibCheck": true, + "strict": false, + "noEmit": true, + "incremental": true, + "module": "esnext", + "esModuleInterop": true, + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "plugins": [ + { + "name": "next" + } + ] + }, + "include": [ + "next-env.d.ts", + ".next/types/**/*.ts", + "**/*.ts", + "**/*.tsx" + ], + "exclude": [ + "node_modules" + ] +} diff --git a/examples/custom-admin-ui-pages/app/(admin)/.admin/index.tsx b/examples/custom-admin-ui-pages/app/(admin)/.admin/index.tsx new file mode 100644 index 00000000000..5dd11446daf --- /dev/null +++ b/examples/custom-admin-ui-pages/app/(admin)/.admin/index.tsx @@ -0,0 +1,18 @@ +/* eslint-disable */ +import * as view0 from '@keystone-6/core/___internal-do-not-use-will-break-in-patch/admin-ui/id-field-view' +import * as view1 from '@keystone-6/core/fields/types/text/views' +import * as view2 from '@keystone-6/core/fields/types/select/views' +import * as view3 from '@keystone-6/core/fields/types/checkbox/views' +import * as view4 from '@keystone-6/core/fields/types/relationship/views' +import * as view5 from '@keystone-6/core/fields/types/timestamp/views' + +import * as adminConfig from '../config' + +export const config = { + lazyMetadataQuery: {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"keystone","loc":{"start":22,"end":30}},"arguments":[],"directives":[],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"adminMeta","loc":{"start":39,"end":48}},"arguments":[],"directives":[],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"lists","loc":{"start":59,"end":64}},"arguments":[],"directives":[],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"key","loc":{"start":77,"end":80}},"arguments":[],"directives":[],"loc":{"start":77,"end":80}},{"kind":"Field","name":{"kind":"Name","value":"isHidden","loc":{"start":91,"end":99}},"arguments":[],"directives":[],"loc":{"start":91,"end":99}},{"kind":"Field","name":{"kind":"Name","value":"fields","loc":{"start":110,"end":116}},"arguments":[],"directives":[],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"path","loc":{"start":131,"end":135}},"arguments":[],"directives":[],"loc":{"start":131,"end":135}},{"kind":"Field","name":{"kind":"Name","value":"createView","loc":{"start":148,"end":158}},"arguments":[],"directives":[],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"fieldMode","loc":{"start":175,"end":184}},"arguments":[],"directives":[],"loc":{"start":175,"end":184}}],"loc":{"start":159,"end":198}},"loc":{"start":148,"end":198}}],"loc":{"start":117,"end":210}},"loc":{"start":110,"end":210}}],"loc":{"start":65,"end":220}},"loc":{"start":59,"end":220}}],"loc":{"start":49,"end":228}},"loc":{"start":39,"end":228}}],"loc":{"start":31,"end":234}},"loc":{"start":22,"end":234}}]}}]}, + fieldViews: [view0,view1,view2,view3,view4,view5], + adminMetaHash: '1mrsjib', + adminConfig, + apiPath: '/api/graphql', + adminPath: '', +}; diff --git a/examples/custom-admin-ui-pages/app/(admin)/[listKey]/[id]/page.tsx b/examples/custom-admin-ui-pages/app/(admin)/[listKey]/[id]/page.tsx new file mode 100644 index 00000000000..c5d7ea2be62 --- /dev/null +++ b/examples/custom-admin-ui-pages/app/(admin)/[listKey]/[id]/page.tsx @@ -0,0 +1,4 @@ +'use client' +import { ItemPage } from '@keystone-6/core/___internal-do-not-use-will-break-in-patch/admin-ui/pages/ItemPage' + +export default ItemPage diff --git a/examples/custom-admin-ui-pages/app/(admin)/[listKey]/create/page.tsx b/examples/custom-admin-ui-pages/app/(admin)/[listKey]/create/page.tsx new file mode 100644 index 00000000000..d6042acaa96 --- /dev/null +++ b/examples/custom-admin-ui-pages/app/(admin)/[listKey]/create/page.tsx @@ -0,0 +1,4 @@ +'use client' +import { CreateItemPage } from '@keystone-6/core/___internal-do-not-use-will-break-in-patch/admin-ui/pages/CreateItemPage' + +export default CreateItemPage diff --git a/examples/custom-admin-ui-pages/app/(admin)/[listKey]/page.tsx b/examples/custom-admin-ui-pages/app/(admin)/[listKey]/page.tsx new file mode 100644 index 00000000000..f6e75f8cfab --- /dev/null +++ b/examples/custom-admin-ui-pages/app/(admin)/[listKey]/page.tsx @@ -0,0 +1,4 @@ +'use client' +import { ListPage } from '@keystone-6/core/___internal-do-not-use-will-break-in-patch/admin-ui/pages/ListPage' + +export default ListPage diff --git a/examples/custom-admin-ui-pages/app/(admin)/config.tsx b/examples/custom-admin-ui-pages/app/(admin)/config.tsx new file mode 100644 index 00000000000..f2b986e29ca --- /dev/null +++ b/examples/custom-admin-ui-pages/app/(admin)/config.tsx @@ -0,0 +1 @@ +export { components } from '../config' \ No newline at end of file diff --git a/examples/custom-admin-ui-pages/admin/pages/custom-page.tsx b/examples/custom-admin-ui-pages/app/(admin)/custom-page/page.tsx similarity index 87% rename from examples/custom-admin-ui-pages/admin/pages/custom-page.tsx rename to examples/custom-admin-ui-pages/app/(admin)/custom-page/page.tsx index ccf76ad67aa..346ab365520 100644 --- a/examples/custom-admin-ui-pages/admin/pages/custom-page.tsx +++ b/examples/custom-admin-ui-pages/app/(admin)/custom-page/page.tsx @@ -1,8 +1,7 @@ -/** @jsxRuntime classic */ -/** @jsx jsx */ +'use client' import Link from 'next/link' import { PageContainer } from '@keystone-6/core/admin-ui/components' -import { jsx, Heading } from '@keystone-ui/core' +import { Heading } from '@keystone-ui/core' // Please note that while this capability is driven by Next.js's pages directory // We do not currently support any of the auxillary methods that Next.js provides i.e. `getStaticProps` // Presently the only export from the directory that is supported is the page component itself. @@ -10,7 +9,7 @@ export default function CustomPage () { return ( Custom Page}>

    diff --git a/examples/custom-admin-ui-pages/app/(admin)/layout.tsx b/examples/custom-admin-ui-pages/app/(admin)/layout.tsx new file mode 100644 index 00000000000..abb5a0f3b2c --- /dev/null +++ b/examples/custom-admin-ui-pages/app/(admin)/layout.tsx @@ -0,0 +1,16 @@ +'use client' +import { Layout } from '@keystone-6/core/___internal-do-not-use-will-break-in-patch/admin-ui/pages/App' +import { config } from './.admin' + + +export default function AdminLayout ({ + children, +}: { + children: React.ReactNode +}) { + return ( + + {children} + + ) +} \ No newline at end of file diff --git a/examples/custom-admin-ui-pages/app/(admin)/no-access/page.tsx b/examples/custom-admin-ui-pages/app/(admin)/no-access/page.tsx new file mode 100644 index 00000000000..70877231fee --- /dev/null +++ b/examples/custom-admin-ui-pages/app/(admin)/no-access/page.tsx @@ -0,0 +1,4 @@ +'use client' +import { getNoAccessPage } from '@keystone-6/core/___internal-do-not-use-will-break-in-patch/admin-ui/pages/NoAccessPage' + +export default getNoAccessPage({ sessionsEnabled: false }) diff --git a/examples/custom-admin-ui-pages/app/(admin)/page.tsx b/examples/custom-admin-ui-pages/app/(admin)/page.tsx new file mode 100644 index 00000000000..5c268390b0f --- /dev/null +++ b/examples/custom-admin-ui-pages/app/(admin)/page.tsx @@ -0,0 +1,2 @@ +'use client' +export { HomePage as default } from '@keystone-6/core/___internal-do-not-use-will-break-in-patch/admin-ui/pages/HomePage' diff --git a/examples/custom-admin-ui-pages/admin/components/CustomNavigation.tsx b/examples/custom-admin-ui-pages/app/config/components/CustomNavigation.tsx similarity index 100% rename from examples/custom-admin-ui-pages/admin/components/CustomNavigation.tsx rename to examples/custom-admin-ui-pages/app/config/components/CustomNavigation.tsx diff --git a/examples/custom-admin-ui-pages/admin/config.ts b/examples/custom-admin-ui-pages/app/config/index.ts similarity index 100% rename from examples/custom-admin-ui-pages/admin/config.ts rename to examples/custom-admin-ui-pages/app/config/index.ts diff --git a/examples/custom-admin-ui-pages/app/layout.tsx b/examples/custom-admin-ui-pages/app/layout.tsx new file mode 100644 index 00000000000..38a4853e3a5 --- /dev/null +++ b/examples/custom-admin-ui-pages/app/layout.tsx @@ -0,0 +1,11 @@ +export default function RootLayout ({ + children, +}: { + children: React.ReactNode +}) { + return ( + + {children} + + ) +} diff --git a/examples/custom-admin-ui-pages/next-env.d.ts b/examples/custom-admin-ui-pages/next-env.d.ts new file mode 100644 index 00000000000..4f11a03dc6c --- /dev/null +++ b/examples/custom-admin-ui-pages/next-env.d.ts @@ -0,0 +1,5 @@ +/// +/// + +// NOTE: This file should not be edited +// see https://nextjs.org/docs/basic-features/typescript for more information. diff --git a/examples/custom-admin-ui-pages/tsconfig.json b/examples/custom-admin-ui-pages/tsconfig.json new file mode 100644 index 00000000000..ccb2ed95d83 --- /dev/null +++ b/examples/custom-admin-ui-pages/tsconfig.json @@ -0,0 +1,34 @@ +{ + "compilerOptions": { + "lib": [ + "dom", + "dom.iterable", + "esnext" + ], + "allowJs": true, + "skipLibCheck": true, + "strict": false, + "noEmit": true, + "incremental": true, + "module": "esnext", + "esModuleInterop": true, + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "plugins": [ + { + "name": "next" + } + ] + }, + "include": [ + "next-env.d.ts", + ".next/types/**/*.ts", + "**/*.ts", + "**/*.tsx" + ], + "exclude": [ + "node_modules" + ] +} diff --git a/examples/custom-field-view/app/(admin)/.admin/index.tsx b/examples/custom-field-view/app/(admin)/.admin/index.tsx new file mode 100644 index 00000000000..7c8948ac698 --- /dev/null +++ b/examples/custom-field-view/app/(admin)/.admin/index.tsx @@ -0,0 +1,21 @@ +/* eslint-disable */ +import * as view0 from "@keystone-6/core/___internal-do-not-use-will-break-in-patch/admin-ui/id-field-view" +import * as view1 from "@keystone-6/core/fields/types/text/views" +import * as view2 from "@keystone-6/core/fields/types/select/views" +import * as view3 from "@keystone-6/core/fields/types/checkbox/views" +import * as view4 from "@keystone-6/core/fields/types/relationship/views" +import * as view5 from "@keystone-6/core/fields/types/timestamp/views" +import * as view6 from "@keystone-6/core/fields/types/json/views" +import * as view7 from "../../.././fields/related-links/components" +import * as view8 from "@/fields/related-links/components" + +const adminConfig = {} + +export const config = { + lazyMetadataQuery: {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"keystone","loc":{"start":22,"end":30}},"arguments":[],"directives":[],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"adminMeta","loc":{"start":39,"end":48}},"arguments":[],"directives":[],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"lists","loc":{"start":59,"end":64}},"arguments":[],"directives":[],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"key","loc":{"start":77,"end":80}},"arguments":[],"directives":[],"loc":{"start":77,"end":80}},{"kind":"Field","name":{"kind":"Name","value":"isHidden","loc":{"start":91,"end":99}},"arguments":[],"directives":[],"loc":{"start":91,"end":99}},{"kind":"Field","name":{"kind":"Name","value":"fields","loc":{"start":110,"end":116}},"arguments":[],"directives":[],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"path","loc":{"start":131,"end":135}},"arguments":[],"directives":[],"loc":{"start":131,"end":135}},{"kind":"Field","name":{"kind":"Name","value":"createView","loc":{"start":148,"end":158}},"arguments":[],"directives":[],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"fieldMode","loc":{"start":175,"end":184}},"arguments":[],"directives":[],"loc":{"start":175,"end":184}}],"loc":{"start":159,"end":198}},"loc":{"start":148,"end":198}}],"loc":{"start":117,"end":210}},"loc":{"start":110,"end":210}}],"loc":{"start":65,"end":220}},"loc":{"start":59,"end":220}}],"loc":{"start":49,"end":228}},"loc":{"start":39,"end":228}}],"loc":{"start":31,"end":234}},"loc":{"start":22,"end":234}}]}}]}, + fieldViews: [view0,view1,view2,view3,view4,view5,view6,view7,view8], + adminMetaHash: 'o10uod', + adminConfig, + apiPath: '/api/graphql', + adminPath: '', +}; diff --git a/examples/custom-field-view/app/(admin)/[listKey]/[id]/page.tsx b/examples/custom-field-view/app/(admin)/[listKey]/[id]/page.tsx new file mode 100644 index 00000000000..c5d7ea2be62 --- /dev/null +++ b/examples/custom-field-view/app/(admin)/[listKey]/[id]/page.tsx @@ -0,0 +1,4 @@ +'use client' +import { ItemPage } from '@keystone-6/core/___internal-do-not-use-will-break-in-patch/admin-ui/pages/ItemPage' + +export default ItemPage diff --git a/examples/custom-field-view/app/(admin)/[listKey]/create/page.tsx b/examples/custom-field-view/app/(admin)/[listKey]/create/page.tsx new file mode 100644 index 00000000000..d6042acaa96 --- /dev/null +++ b/examples/custom-field-view/app/(admin)/[listKey]/create/page.tsx @@ -0,0 +1,4 @@ +'use client' +import { CreateItemPage } from '@keystone-6/core/___internal-do-not-use-will-break-in-patch/admin-ui/pages/CreateItemPage' + +export default CreateItemPage diff --git a/examples/custom-field-view/app/(admin)/[listKey]/page.tsx b/examples/custom-field-view/app/(admin)/[listKey]/page.tsx new file mode 100644 index 00000000000..f6e75f8cfab --- /dev/null +++ b/examples/custom-field-view/app/(admin)/[listKey]/page.tsx @@ -0,0 +1,4 @@ +'use client' +import { ListPage } from '@keystone-6/core/___internal-do-not-use-will-break-in-patch/admin-ui/pages/ListPage' + +export default ListPage diff --git a/examples/custom-field-view/app/(admin)/layout.tsx b/examples/custom-field-view/app/(admin)/layout.tsx new file mode 100644 index 00000000000..abb5a0f3b2c --- /dev/null +++ b/examples/custom-field-view/app/(admin)/layout.tsx @@ -0,0 +1,16 @@ +'use client' +import { Layout } from '@keystone-6/core/___internal-do-not-use-will-break-in-patch/admin-ui/pages/App' +import { config } from './.admin' + + +export default function AdminLayout ({ + children, +}: { + children: React.ReactNode +}) { + return ( + + {children} + + ) +} \ No newline at end of file diff --git a/examples/custom-field-view/app/(admin)/no-access/page.tsx b/examples/custom-field-view/app/(admin)/no-access/page.tsx new file mode 100644 index 00000000000..70877231fee --- /dev/null +++ b/examples/custom-field-view/app/(admin)/no-access/page.tsx @@ -0,0 +1,4 @@ +'use client' +import { getNoAccessPage } from '@keystone-6/core/___internal-do-not-use-will-break-in-patch/admin-ui/pages/NoAccessPage' + +export default getNoAccessPage({ sessionsEnabled: false }) diff --git a/examples/custom-field-view/app/(admin)/page.tsx b/examples/custom-field-view/app/(admin)/page.tsx new file mode 100644 index 00000000000..5c268390b0f --- /dev/null +++ b/examples/custom-field-view/app/(admin)/page.tsx @@ -0,0 +1,2 @@ +'use client' +export { HomePage as default } from '@keystone-6/core/___internal-do-not-use-will-break-in-patch/admin-ui/pages/HomePage' diff --git a/examples/custom-field-view/app/layout.tsx b/examples/custom-field-view/app/layout.tsx new file mode 100644 index 00000000000..38a4853e3a5 --- /dev/null +++ b/examples/custom-field-view/app/layout.tsx @@ -0,0 +1,11 @@ +export default function RootLayout ({ + children, +}: { + children: React.ReactNode +}) { + return ( + + {children} + + ) +} diff --git a/examples/custom-field-view/next-env.d.ts b/examples/custom-field-view/next-env.d.ts new file mode 100644 index 00000000000..4f11a03dc6c --- /dev/null +++ b/examples/custom-field-view/next-env.d.ts @@ -0,0 +1,5 @@ +/// +/// + +// NOTE: This file should not be edited +// see https://nextjs.org/docs/basic-features/typescript for more information. diff --git a/examples/custom-field-view/schema.graphql b/examples/custom-field-view/schema.graphql index a7743bbd7f6..20635a2b137 100644 --- a/examples/custom-field-view/schema.graphql +++ b/examples/custom-field-view/schema.graphql @@ -9,6 +9,7 @@ type Task { assignedTo: Person finishBy: DateTime relatedLinks: JSON + moreLinks: JSON } enum TaskPriorityType { @@ -117,6 +118,7 @@ input TaskUpdateInput { assignedTo: PersonRelateToOneForUpdateInput finishBy: DateTime relatedLinks: JSON + moreLinks: JSON } input PersonRelateToOneForUpdateInput { @@ -137,6 +139,7 @@ input TaskCreateInput { assignedTo: PersonRelateToOneForCreateInput finishBy: DateTime relatedLinks: JSON + moreLinks: JSON } input PersonRelateToOneForCreateInput { diff --git a/examples/custom-field-view/schema.prisma b/examples/custom-field-view/schema.prisma index c4b4e68cbe0..dfea6e26130 100644 --- a/examples/custom-field-view/schema.prisma +++ b/examples/custom-field-view/schema.prisma @@ -21,6 +21,7 @@ model Task { assignedToId String? @map("assignedTo") finishBy DateTime? relatedLinks String? + moreLinks String? @@index([assignedToId]) } diff --git a/examples/custom-field-view/schema.ts b/examples/custom-field-view/schema.ts index 8a6516cd78d..ed877eee26f 100644 --- a/examples/custom-field-view/schema.ts +++ b/examples/custom-field-view/schema.ts @@ -30,6 +30,14 @@ export const lists = { itemView: { fieldMode: 'edit' }, }, }), + moreLinks: json({ + ui: { + views: '@/fields/related-links/components', + createView: { fieldMode: 'edit' }, + listView: { fieldMode: 'hidden' }, + itemView: { fieldMode: 'edit' }, + }, + }), }, }), Person: list({ diff --git a/examples/custom-field-view/tsconfig.json b/examples/custom-field-view/tsconfig.json new file mode 100644 index 00000000000..3b0afd8f406 --- /dev/null +++ b/examples/custom-field-view/tsconfig.json @@ -0,0 +1,37 @@ +{ + "compilerOptions": { + "lib": [ + "dom", + "dom.iterable", + "esnext" + ], + "allowJs": true, + "skipLibCheck": true, + "strict": false, + "noEmit": true, + "incremental": true, + "module": "esnext", + "esModuleInterop": true, + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "paths": { + "@/fields/*": ["./fields/*"] + }, + "plugins": [ + { + "name": "next" + } + ] + }, + "include": [ + "next-env.d.ts", + ".next/types/**/*.ts", + "**/*.ts", + "**/*.tsx" + ], + "exclude": [ + "node_modules" + ] +} diff --git a/examples/custom-field/2-stars-field/index.ts b/examples/custom-field/2-stars-field/index.ts index abf8b22df3f..932b6538a76 100644 --- a/examples/custom-field/2-stars-field/index.ts +++ b/examples/custom-field/2-stars-field/index.ts @@ -37,13 +37,22 @@ export const stars = ...config.hooks, // We use the `validateInput` hook to ensure that the user doesn't set an out of range value. // This hook is the key difference on the backend between the stars field type and the integer field type. - async validateInput (args) { - const val = args.resolvedData[meta.fieldKey] - if (!(val == null || (val >= 0 && val <= maxStars))) { - args.addValidationError(`The value must be within the range of 0-${maxStars}`) + validate: { + async create (args) { + const val = args.resolvedData[meta.fieldKey] + if (!(val == null || (val >= 0 && val <= maxStars))) { + args.addValidationError(`The value must be within the range of 0-${maxStars}`) + } + await config.hooks?.validate?.create?.(args) + }, + async update (args) { + const val = args.resolvedData[meta.fieldKey] + if (!(val == null || (val >= 0 && val <= maxStars))) { + args.addValidationError(`The value must be within the range of 0-${maxStars}`) + } + await config.hooks?.validate?.update?.(args) } - await config.hooks?.validateInput?.(args) - }, + } }, // all of these inputs are optional if they don't make sense for a particular field type input: { diff --git a/examples/custom-field/app/(admin)/.admin/index.tsx b/examples/custom-field/app/(admin)/.admin/index.tsx new file mode 100644 index 00000000000..ca1a2d962a6 --- /dev/null +++ b/examples/custom-field/app/(admin)/.admin/index.tsx @@ -0,0 +1,19 @@ +/* eslint-disable */ +import * as view0 from "@keystone-6/core/___internal-do-not-use-will-break-in-patch/admin-ui/id-field-view" +import * as view1 from "../../.././1-text-field/views" +import * as view2 from "../../.././2-stars-field/views" +import * as view3 from "../../.././4-conditional-field/views" +import * as view4 from "../../.././3-pair-field/views" +import * as view5 from "../../.././3-pair-field-nested/views" +import * as view6 from "../../.././3-pair-field-json/views" + +const adminConfig = {} + +export const config = { + lazyMetadataQuery: {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"keystone","loc":{"start":22,"end":30}},"arguments":[],"directives":[],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"adminMeta","loc":{"start":39,"end":48}},"arguments":[],"directives":[],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"lists","loc":{"start":59,"end":64}},"arguments":[],"directives":[],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"key","loc":{"start":77,"end":80}},"arguments":[],"directives":[],"loc":{"start":77,"end":80}},{"kind":"Field","name":{"kind":"Name","value":"isHidden","loc":{"start":91,"end":99}},"arguments":[],"directives":[],"loc":{"start":91,"end":99}},{"kind":"Field","name":{"kind":"Name","value":"fields","loc":{"start":110,"end":116}},"arguments":[],"directives":[],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"path","loc":{"start":131,"end":135}},"arguments":[],"directives":[],"loc":{"start":131,"end":135}},{"kind":"Field","name":{"kind":"Name","value":"createView","loc":{"start":148,"end":158}},"arguments":[],"directives":[],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"fieldMode","loc":{"start":175,"end":184}},"arguments":[],"directives":[],"loc":{"start":175,"end":184}}],"loc":{"start":159,"end":198}},"loc":{"start":148,"end":198}}],"loc":{"start":117,"end":210}},"loc":{"start":110,"end":210}}],"loc":{"start":65,"end":220}},"loc":{"start":59,"end":220}}],"loc":{"start":49,"end":228}},"loc":{"start":39,"end":228}}],"loc":{"start":31,"end":234}},"loc":{"start":22,"end":234}}]}}]}, + fieldViews: [view0,view1,view2,view3,view4,view5,view6], + adminMetaHash: 'fhppxz', + adminConfig, + apiPath: '/api/graphql', + adminPath: '', +}; diff --git a/examples/custom-field/app/(admin)/[listKey]/[id]/page.tsx b/examples/custom-field/app/(admin)/[listKey]/[id]/page.tsx new file mode 100644 index 00000000000..c5d7ea2be62 --- /dev/null +++ b/examples/custom-field/app/(admin)/[listKey]/[id]/page.tsx @@ -0,0 +1,4 @@ +'use client' +import { ItemPage } from '@keystone-6/core/___internal-do-not-use-will-break-in-patch/admin-ui/pages/ItemPage' + +export default ItemPage diff --git a/examples/custom-field/app/(admin)/[listKey]/create/page.tsx b/examples/custom-field/app/(admin)/[listKey]/create/page.tsx new file mode 100644 index 00000000000..d6042acaa96 --- /dev/null +++ b/examples/custom-field/app/(admin)/[listKey]/create/page.tsx @@ -0,0 +1,4 @@ +'use client' +import { CreateItemPage } from '@keystone-6/core/___internal-do-not-use-will-break-in-patch/admin-ui/pages/CreateItemPage' + +export default CreateItemPage diff --git a/examples/custom-field/app/(admin)/[listKey]/page.tsx b/examples/custom-field/app/(admin)/[listKey]/page.tsx new file mode 100644 index 00000000000..f6e75f8cfab --- /dev/null +++ b/examples/custom-field/app/(admin)/[listKey]/page.tsx @@ -0,0 +1,4 @@ +'use client' +import { ListPage } from '@keystone-6/core/___internal-do-not-use-will-break-in-patch/admin-ui/pages/ListPage' + +export default ListPage diff --git a/examples/custom-field/app/(admin)/layout.tsx b/examples/custom-field/app/(admin)/layout.tsx new file mode 100644 index 00000000000..abb5a0f3b2c --- /dev/null +++ b/examples/custom-field/app/(admin)/layout.tsx @@ -0,0 +1,16 @@ +'use client' +import { Layout } from '@keystone-6/core/___internal-do-not-use-will-break-in-patch/admin-ui/pages/App' +import { config } from './.admin' + + +export default function AdminLayout ({ + children, +}: { + children: React.ReactNode +}) { + return ( + + {children} + + ) +} \ No newline at end of file diff --git a/examples/custom-field/app/(admin)/no-access/page.tsx b/examples/custom-field/app/(admin)/no-access/page.tsx new file mode 100644 index 00000000000..70877231fee --- /dev/null +++ b/examples/custom-field/app/(admin)/no-access/page.tsx @@ -0,0 +1,4 @@ +'use client' +import { getNoAccessPage } from '@keystone-6/core/___internal-do-not-use-will-break-in-patch/admin-ui/pages/NoAccessPage' + +export default getNoAccessPage({ sessionsEnabled: false }) diff --git a/examples/custom-field/app/(admin)/page.tsx b/examples/custom-field/app/(admin)/page.tsx new file mode 100644 index 00000000000..5c268390b0f --- /dev/null +++ b/examples/custom-field/app/(admin)/page.tsx @@ -0,0 +1,2 @@ +'use client' +export { HomePage as default } from '@keystone-6/core/___internal-do-not-use-will-break-in-patch/admin-ui/pages/HomePage' diff --git a/examples/custom-field/app/layout.tsx b/examples/custom-field/app/layout.tsx new file mode 100644 index 00000000000..38a4853e3a5 --- /dev/null +++ b/examples/custom-field/app/layout.tsx @@ -0,0 +1,11 @@ +export default function RootLayout ({ + children, +}: { + children: React.ReactNode +}) { + return ( + + {children} + + ) +} diff --git a/examples/custom-field/next-env.d.ts b/examples/custom-field/next-env.d.ts new file mode 100644 index 00000000000..4f11a03dc6c --- /dev/null +++ b/examples/custom-field/next-env.d.ts @@ -0,0 +1,5 @@ +/// +/// + +// NOTE: This file should not be edited +// see https://nextjs.org/docs/basic-features/typescript for more information. diff --git a/examples/custom-field/schema.ts b/examples/custom-field/schema.ts index ac2899f29a1..2bdaf10ce40 100644 --- a/examples/custom-field/schema.ts +++ b/examples/custom-field/schema.ts @@ -20,31 +20,58 @@ export const lists = { }, hooks: { - resolveInput: async ({ resolvedData, operation, inputData, item, fieldKey }) => { - console.log('Post.content.hooks.resolveInput', { + resolveInput: { + create: async ({ resolvedData, operation, inputData, item, fieldKey }) => { + console.log('Post.content.hooks.resolveInput.create', { + resolvedData, + operation, + inputData, + item, + fieldKey, + }) + return resolvedData[fieldKey] + }, + update: async ({ resolvedData, operation, inputData, item, fieldKey }) => { + console.log('Post.content.hooks.resolveInput.update', { + resolvedData, + operation, + inputData, + item, + fieldKey, + }) + return resolvedData[fieldKey] + }, + }, + validate: { + create: async ({ resolvedData, - operation, inputData, item, + addValidationError, fieldKey, - }) - return resolvedData[fieldKey] - }, - - validateInput: async ({ - resolvedData, - inputData, - item, - addValidationError, - fieldKey, - }) => { - console.log('Post.content.hooks.validateInput', { + }) => { + console.log('Post.content.hooks.validateInput.create', { + resolvedData, + inputData, + item, + fieldKey, + }) + }, + update: async ({ resolvedData, inputData, item, + addValidationError, fieldKey, - }) - }, + }) => { + console.log('Post.content.hooks.validateInput.update', { + resolvedData, + inputData, + item, + fieldKey, + }) + }, + } }, }), rating: stars({ @@ -100,13 +127,22 @@ export const lists = { }, }, - validateInput: async ({ resolvedData, operation, inputData, item, addValidationError }) => { - console.log('Post.hooks.validateInput', { resolvedData, operation, inputData, item }) + validate: { + create: async ({ resolvedData, operation, inputData, item, addValidationError }) => { + console.log('Post.hooks.validateInput.create', { resolvedData, operation, inputData, item }) - if (Math.random() > 0.95) { - addValidationError('oh oh, try again, this is part of the example') - } - }, + if (Math.random() > 0.95) { + addValidationError('oh oh, try again, this is part of the example') + } + }, + update: async ({ resolvedData, operation, inputData, item, addValidationError }) => { + console.log('Post.hooks.validateInput.update', { resolvedData, operation, inputData, item }) + + if (Math.random() > 0.95) { + addValidationError('oh oh, try again, this is part of the example') + } + }, + } }, }), } satisfies Lists diff --git a/examples/custom-field/tsconfig.json b/examples/custom-field/tsconfig.json new file mode 100644 index 00000000000..3b0afd8f406 --- /dev/null +++ b/examples/custom-field/tsconfig.json @@ -0,0 +1,37 @@ +{ + "compilerOptions": { + "lib": [ + "dom", + "dom.iterable", + "esnext" + ], + "allowJs": true, + "skipLibCheck": true, + "strict": false, + "noEmit": true, + "incremental": true, + "module": "esnext", + "esModuleInterop": true, + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "paths": { + "@/fields/*": ["./fields/*"] + }, + "plugins": [ + { + "name": "next" + } + ] + }, + "include": [ + "next-env.d.ts", + ".next/types/**/*.ts", + "**/*.ts", + "**/*.tsx" + ], + "exclude": [ + "node_modules" + ] +} diff --git a/examples/custom-output-paths/schema.ts b/examples/custom-output-paths/schema.ts index e81eb845ad0..b3d5b8835a4 100644 --- a/examples/custom-output-paths/schema.ts +++ b/examples/custom-output-paths/schema.ts @@ -13,25 +13,46 @@ export const lists = { }, hooks: { - afterOperation: async ({ context }) => { - const posts = (await context.db.Post.findMany({ - where: { - title: { equals: 'Home' }, - }, - - // we use Typescript's satisfies here as way to ensure that - // this is the contextualised type - you don't need this - // - // it is helpful for us to check that the example is not - // broken by code changes - // - - // TODO: FIXME, babel and pnpm issues - })) as readonly { title: string, content: string }[] - // })) satisfies readonly { title: string; content: string }[]; - - console.log(posts) - }, + afterOperation: { + create: async ({ context }) => { + const posts = (await context.db.Post.findMany({ + where: { + title: { equals: 'Home' }, + }, + + // we use Typescript's satisfies here as way to ensure that + // this is the contextualised type - you don't need this + // + // it is helpful for us to check that the example is not + // broken by code changes + // + + // TODO: FIXME, babel and pnpm issues + })) as readonly { title: string, content: string }[] + // })) satisfies readonly { title: string; content: string }[]; + + console.log(posts) + }, + update: async ({ context }) => { + const posts = (await context.db.Post.findMany({ + where: { + title: { equals: 'Home' }, + }, + + // we use Typescript's satisfies here as way to ensure that + // this is the contextualised type - you don't need this + // + // it is helpful for us to check that the example is not + // broken by code changes + // + + // TODO: FIXME, babel and pnpm issues + })) as readonly { title: string, content: string }[] + // })) satisfies readonly { title: string; content: string }[]; + + console.log(posts) + }, + } }, }), } satisfies Lists diff --git a/examples/custom-session-invalidation/README.md b/examples/custom-session-invalidation/README.md index 3c2a0101f0b..e07d2839d8c 100644 --- a/examples/custom-session-invalidation/README.md +++ b/examples/custom-session-invalidation/README.md @@ -37,11 +37,19 @@ We add one new field, `passwordChangedAt`, to the `Person` list. Setting the `pa passwordChangedAt: timestamp({ access: () => false, hooks: { - resolveInput: ({ resolvedData }) => { - if (resolvedData.password) { - return new Date(); - } - return; + resolveInput: { + create: ({ resolvedData }) => { + if (resolvedData.password) { + return new Date(); + } + return; + }, + update: ({ resolvedData }) => { + if (resolvedData.password) { + return new Date(); + } + return; + }, }, }, ui: { diff --git a/examples/default-values/schema.ts b/examples/default-values/schema.ts index 839dee0e55b..9d6fd54dc14 100644 --- a/examples/default-values/schema.ts +++ b/examples/default-values/schema.ts @@ -17,17 +17,30 @@ export const lists = { { label: 'High', value: 'high' }, ], hooks: { - resolveInput ({ resolvedData, inputData }) { - if (inputData.priority === null) { - // default to high if "urgent" is in the label - if (inputData.label && inputData.label.toLowerCase().includes('urgent')) { - return 'high' - } else { - return 'low' + resolveInput: { + create ({ resolvedData, inputData }) { + if (inputData.priority === null) { + // default to high if "urgent" is in the label + if (inputData.label && inputData.label.toLowerCase().includes('urgent')) { + return 'high' + } else { + return 'low' + } } - } - return resolvedData.priority - }, + return resolvedData.priority + }, + update ({ resolvedData, inputData }) { + if (inputData.priority === null) { + // default to high if "urgent" is in the label + if (inputData.label && inputData.label.toLowerCase().includes('urgent')) { + return 'high' + } else { + return 'low' + } + } + return resolvedData.priority + }, + } }, }), @@ -39,33 +52,58 @@ export const lists = { many: false, hooks: { // dynamic default: if unassigned, find an anonymous user and assign the task to them - async resolveInput ({ context, operation, resolvedData }) { - if (resolvedData.assignedTo === null) { - const [user] = await context.db.Person.findMany({ - where: { name: { equals: 'Anonymous' } }, - }) - - if (user) { - return { connect: { id: user.id } } + resolveInput: { + async create ({ context, operation, resolvedData }) { + if (resolvedData.assignedTo === null) { + const [user] = await context.db.Person.findMany({ + where: { name: { equals: 'Anonymous' } }, + }) + + if (user) { + return { connect: { id: user.id } } + } } - } - - return resolvedData.assignedTo - }, + + return resolvedData.assignedTo + }, + async update ({ context, operation, resolvedData }) { + if (resolvedData.assignedTo === null) { + const [user] = await context.db.Person.findMany({ + where: { name: { equals: 'Anonymous' } }, + }) + + if (user) { + return { connect: { id: user.id } } + } + } + + return resolvedData.assignedTo + }, + } }, }), // dynamic default: we set the due date to be 7 days in the future finishBy: timestamp({ hooks: { - resolveInput ({ resolvedData, inputData, operation }) { - if (inputData.finishBy == null) { - const date = new Date() - date.setUTCDate(new Date().getUTCDate() + 7) - return date - } - return resolvedData.finishBy - }, + resolveInput: { + create ({ resolvedData, inputData, operation }) { + if (inputData.finishBy == null) { + const date = new Date() + date.setUTCDate(new Date().getUTCDate() + 7) + return date + } + return resolvedData.finishBy + }, + update ({ resolvedData, inputData, operation }) { + if (inputData.finishBy == null) { + const date = new Date() + date.setUTCDate(new Date().getUTCDate() + 7) + return date + } + return resolvedData.finishBy + }, + } }, }), diff --git a/examples/document-field/README.md b/examples/document-field/README.md index ffd84eb8744..8712f661bda 100644 --- a/examples/document-field/README.md +++ b/examples/document-field/README.md @@ -1,6 +1,6 @@ ## Feature Example - Document Field -This project demonstrates how to configure [document fields](https://keystonejs.com/docs/guides/document-fields) in your Keystone system and render their data in a frontend application. +This project demonstrates how to configure [document fields](https://keystonejs.com/docs/guides/document-fields) in your Keystone system and render their data in a frontend. It builds on the [Blog](../blog) starter project. ## Instructions @@ -16,13 +16,7 @@ You can use the Admin UI to create items in your database. You can also access a GraphQL Playground at [localhost:3000/api/graphql](http://localhost:3000/api/graphql), which allows you to directly run GraphQL queries and mutations. -In a separate terminal, start the frontend dev server: - -``` -pnpm dev:site -``` - -This will start the frontend at [localhost:3001](http://localhost:3001). +Go to url [localhost:3000/site](http://localhost:3000/site) tp see the page rendering document field content. ## Configuring fields diff --git a/examples/document-field/next-env.d.ts b/examples/document-field/next-env.d.ts index 7b7aa2c7727..4f11a03dc6c 100644 --- a/examples/document-field/next-env.d.ts +++ b/examples/document-field/next-env.d.ts @@ -1,2 +1,5 @@ /// -/// +/// + +// NOTE: This file should not be edited +// see https://nextjs.org/docs/basic-features/typescript for more information. diff --git a/examples/document-field/next.config.js b/examples/document-field/next.config.js deleted file mode 100644 index 00184d61daa..00000000000 --- a/examples/document-field/next.config.js +++ /dev/null @@ -1,5 +0,0 @@ -// you don't need this if you're building something outside of the Keystone repo - -const withPreconstruct = require('@preconstruct/next') - -module.exports = withPreconstruct() diff --git a/examples/document-field/next.config.mjs b/examples/document-field/next.config.mjs new file mode 100644 index 00000000000..da3eee2060e --- /dev/null +++ b/examples/document-field/next.config.mjs @@ -0,0 +1,16 @@ +export default { + experimental: { + // Experimental ESM Externals + // https://nextjs.org/docs/messages/import-esm-externals + // required to fix build admin ui issues related to "react-day-picker" and "date-fn" + esmExternals: 'loose', + // without this, 'Error: Expected Upload to be a GraphQL nullable type.' + serverComponentsExternalPackages: ['graphql'], + }, + typescript: { + ignoreBuildErrors: true, + }, + eslint: { + ignoreDuringBuilds: true, + }, +} diff --git a/examples/document-field/package.json b/examples/document-field/package.json index cde0ce07419..a5094fbfa2a 100644 --- a/examples/document-field/package.json +++ b/examples/document-field/package.json @@ -5,7 +5,6 @@ "license": "MIT", "scripts": { "dev": "keystone dev", - "dev:site": "next dev -p 3001", "start": "keystone start", "build": "keystone build", "postinstall": "keystone postinstall" diff --git a/examples/document-field/src/app/(admin)/.admin/index.tsx b/examples/document-field/src/app/(admin)/.admin/index.tsx new file mode 100644 index 00000000000..e53104746f4 --- /dev/null +++ b/examples/document-field/src/app/(admin)/.admin/index.tsx @@ -0,0 +1,18 @@ +/* eslint-disable */ +import * as view0 from "@keystone-6/core/___internal-do-not-use-will-break-in-patch/admin-ui/id-field-view" +import * as view1 from "@keystone-6/core/fields/types/text/views" +import * as view2 from "@keystone-6/core/fields/types/select/views" +import * as view3 from "@keystone-6/fields-document/views" +import * as view4 from "@keystone-6/core/fields/types/timestamp/views" +import * as view5 from "@keystone-6/core/fields/types/relationship/views" + +const adminConfig = {} + +export const config = { + lazyMetadataQuery: {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"keystone","loc":{"start":22,"end":30}},"arguments":[],"directives":[],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"adminMeta","loc":{"start":39,"end":48}},"arguments":[],"directives":[],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"lists","loc":{"start":59,"end":64}},"arguments":[],"directives":[],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"key","loc":{"start":77,"end":80}},"arguments":[],"directives":[],"loc":{"start":77,"end":80}},{"kind":"Field","name":{"kind":"Name","value":"isHidden","loc":{"start":91,"end":99}},"arguments":[],"directives":[],"loc":{"start":91,"end":99}},{"kind":"Field","name":{"kind":"Name","value":"fields","loc":{"start":110,"end":116}},"arguments":[],"directives":[],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"path","loc":{"start":131,"end":135}},"arguments":[],"directives":[],"loc":{"start":131,"end":135}},{"kind":"Field","name":{"kind":"Name","value":"createView","loc":{"start":148,"end":158}},"arguments":[],"directives":[],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"fieldMode","loc":{"start":175,"end":184}},"arguments":[],"directives":[],"loc":{"start":175,"end":184}}],"loc":{"start":159,"end":198}},"loc":{"start":148,"end":198}}],"loc":{"start":117,"end":210}},"loc":{"start":110,"end":210}}],"loc":{"start":65,"end":220}},"loc":{"start":59,"end":220}}],"loc":{"start":49,"end":228}},"loc":{"start":39,"end":228}}],"loc":{"start":31,"end":234}},"loc":{"start":22,"end":234}}]}}]}, + fieldViews: [view0,view1,view2,view3,view4,view5], + adminMetaHash: '1oos0js', + adminConfig, + apiPath: '/api/graphql', + adminPath: '', +}; diff --git a/examples/document-field/src/app/(admin)/[listKey]/[id]/page.tsx b/examples/document-field/src/app/(admin)/[listKey]/[id]/page.tsx new file mode 100644 index 00000000000..c5d7ea2be62 --- /dev/null +++ b/examples/document-field/src/app/(admin)/[listKey]/[id]/page.tsx @@ -0,0 +1,4 @@ +'use client' +import { ItemPage } from '@keystone-6/core/___internal-do-not-use-will-break-in-patch/admin-ui/pages/ItemPage' + +export default ItemPage diff --git a/examples/document-field/src/app/(admin)/[listKey]/create/page.tsx b/examples/document-field/src/app/(admin)/[listKey]/create/page.tsx new file mode 100644 index 00000000000..d6042acaa96 --- /dev/null +++ b/examples/document-field/src/app/(admin)/[listKey]/create/page.tsx @@ -0,0 +1,4 @@ +'use client' +import { CreateItemPage } from '@keystone-6/core/___internal-do-not-use-will-break-in-patch/admin-ui/pages/CreateItemPage' + +export default CreateItemPage diff --git a/examples/document-field/src/app/(admin)/[listKey]/page.tsx b/examples/document-field/src/app/(admin)/[listKey]/page.tsx new file mode 100644 index 00000000000..f6e75f8cfab --- /dev/null +++ b/examples/document-field/src/app/(admin)/[listKey]/page.tsx @@ -0,0 +1,4 @@ +'use client' +import { ListPage } from '@keystone-6/core/___internal-do-not-use-will-break-in-patch/admin-ui/pages/ListPage' + +export default ListPage diff --git a/examples/document-field/src/app/(admin)/author/[id]/page.tsx b/examples/document-field/src/app/(admin)/author/[id]/page.tsx new file mode 100644 index 00000000000..59d033be780 --- /dev/null +++ b/examples/document-field/src/app/(admin)/author/[id]/page.tsx @@ -0,0 +1,57 @@ +import { type GetStaticPathsResult, type GetStaticPropsContext } from 'next' +import Link from 'next/link' +import React from 'react' +import { DocumentRenderer } from '@keystone-6/document-renderer' +import { fetchGraphQL, gql } from "../../../utils"; + +export default async function Post ({ params }: any) { +const data = await fetchGraphQL( + gql` + query ($id: ID!) { + author(where: { id: $id }) { + name + bio { + document + } + posts(where: { status: { equals: published } }, orderBy: { publishDate: desc }) { + id + title + slug + } + } + } + `, + { id: params!.id } +) +const author = data?.author + return ( +

    +

    {author.name}

    + +

    Bio

    + {author.bio?.document && } + +

    Posts

    + {author.posts.map((post: any) => ( +
  • + {post.title} +
  • + ))} +
    + ) +} + +// TODO - CAN NOT use this in app router properly +// export async function getStaticPaths (): Promise { +// const data = await fetchGraphQL(gql` +// query { +// authors { +// id +// } +// } +// `) +// return { +// paths: data.authors.map((post: any) => ({ params: { id: post.id } })), +// fallback: 'blocking', +// } +// } diff --git a/examples/document-field/src/app/(admin)/layout.tsx b/examples/document-field/src/app/(admin)/layout.tsx new file mode 100644 index 00000000000..abb5a0f3b2c --- /dev/null +++ b/examples/document-field/src/app/(admin)/layout.tsx @@ -0,0 +1,16 @@ +'use client' +import { Layout } from '@keystone-6/core/___internal-do-not-use-will-break-in-patch/admin-ui/pages/App' +import { config } from './.admin' + + +export default function AdminLayout ({ + children, +}: { + children: React.ReactNode +}) { + return ( + + {children} + + ) +} \ No newline at end of file diff --git a/examples/document-field/src/app/(admin)/no-access/page.tsx b/examples/document-field/src/app/(admin)/no-access/page.tsx new file mode 100644 index 00000000000..70877231fee --- /dev/null +++ b/examples/document-field/src/app/(admin)/no-access/page.tsx @@ -0,0 +1,4 @@ +'use client' +import { getNoAccessPage } from '@keystone-6/core/___internal-do-not-use-will-break-in-patch/admin-ui/pages/NoAccessPage' + +export default getNoAccessPage({ sessionsEnabled: false }) diff --git a/examples/document-field/src/app/(admin)/page.tsx b/examples/document-field/src/app/(admin)/page.tsx new file mode 100644 index 00000000000..5c268390b0f --- /dev/null +++ b/examples/document-field/src/app/(admin)/page.tsx @@ -0,0 +1,2 @@ +'use client' +export { HomePage as default } from '@keystone-6/core/___internal-do-not-use-will-break-in-patch/admin-ui/pages/HomePage' diff --git a/examples/document-field/src/pages/post/[slug].tsx b/examples/document-field/src/app/(admin)/post/[slug]/page.tsx similarity index 80% rename from examples/document-field/src/pages/post/[slug].tsx rename to examples/document-field/src/app/(admin)/post/[slug]/page.tsx index eaf941ad802..05edeb0631f 100644 --- a/examples/document-field/src/pages/post/[slug].tsx +++ b/examples/document-field/src/app/(admin)/post/[slug]/page.tsx @@ -2,7 +2,7 @@ import { type GetStaticPathsResult, type GetStaticPropsContext } from 'next' import Link from 'next/link' import React from 'react' import { DocumentRenderer, type DocumentRendererProps } from '@keystone-6/document-renderer' -import { fetchGraphQL, gql } from '../../utils' +import { fetchGraphQL, gql } from "../../../utils"; // By default the DocumentRenderer will render unstyled html elements. // We're customising how headings are rendered here but you can customise @@ -39,7 +39,26 @@ const renderers: DocumentRendererProps['renderers'] = { }, } -export default function Post ({ post }: { post: any }) { +export default async function Post ({ params }: { params: any }) { + const data = await fetchGraphQL( + gql` + query ($slug: String!) { + post(where: { slug: $slug }) { + title + content { + document(hydrateRelationships: true) + } + publishDate + author { + id + name + } + } + } + `, + { slug: params!.slug } + ) + const post = data?.post return (

    {post.title}

    @@ -60,40 +79,17 @@ export default function Post ({ post }: { post: any }) { ) } -export async function getStaticPaths (): Promise { - const data = await fetchGraphQL(gql` - query { - posts { - slug - } - } - `) - return { - paths: data.posts.map((post: any) => ({ params: { slug: post.slug } })), - fallback: 'blocking', - } -} - -export async function getStaticProps ({ params }: GetStaticPropsContext) { - // We use (hydrateRelationships: true) to ensure we have the data we need - // to render the inline relationships. - const data = await fetchGraphQL( - gql` - query ($slug: String!) { - post(where: { slug: $slug }) { - title - content { - document(hydrateRelationships: true) - } - publishDate - author { - id - name - } - } - } - `, - { slug: params!.slug } - ) - return { props: { post: data.post }, revalidate: 60 } -} +// TODO - CAN NOT use this in app router properly +// export async function getStaticPaths (): Promise { +// const data = await fetchGraphQL(gql` +// query { +// posts { +// slug +// } +// } +// `) +// return { +// paths: data.posts.map((post: any) => ({ params: { slug: post.slug } })), +// fallback: 'blocking', +// } +// } diff --git a/examples/document-field/src/app/layout.tsx b/examples/document-field/src/app/layout.tsx new file mode 100644 index 00000000000..38a4853e3a5 --- /dev/null +++ b/examples/document-field/src/app/layout.tsx @@ -0,0 +1,11 @@ +export default function RootLayout ({ + children, +}: { + children: React.ReactNode +}) { + return ( + + {children} + + ) +} diff --git a/examples/document-field/src/pages/index.tsx b/examples/document-field/src/app/site/page.tsx similarity index 58% rename from examples/document-field/src/pages/index.tsx rename to examples/document-field/src/app/site/page.tsx index 737a083edb8..5af491efde4 100644 --- a/examples/document-field/src/pages/index.tsx +++ b/examples/document-field/src/app/site/page.tsx @@ -1,21 +1,44 @@ import Link from 'next/link' import React from 'react' -import { fetchGraphQL, gql } from '../utils' +import { fetchGraphQL, gql } from "../utils"; -type Author = { id: string, name: string, posts: { id: string, slug: string, title: string }[] } +type Author = { + id: string + name: string + posts: { id: string, slug: string, title: string }[] +} + +export default async function Index () { + const data = await fetchGraphQL(gql` + query { + authors { + id + name + posts( + where: { status: { equals: published } } + orderBy: { publishDate: desc } + ) { + id + slug + title + } + } + } + `) + + const authors: Author[] = data?.authors || [] -export default function Index ({ authors }: { authors: Author[] }) { return ( <>

    Keystone Blog Project - Home

      - {authors.map(author => ( + {authors.map((author) => (
    • {author.name}

        - {author.posts.map(post => ( + {author.posts.map((post) => (
      • {post.title}
      • @@ -27,20 +50,3 @@ export default function Index ({ authors }: { authors: Author[] }) { ) } - -export async function getStaticProps () { - const data = await fetchGraphQL(gql` - query { - authors { - id - name - posts(where: { status: { equals: published } }, orderBy: { publishDate: desc }) { - id - slug - title - } - } - } - `) - return { props: { authors: data.authors }, revalidate: 30 } -} diff --git a/examples/document-field/src/utils.tsx b/examples/document-field/src/app/utils.tsx similarity index 100% rename from examples/document-field/src/utils.tsx rename to examples/document-field/src/app/utils.tsx diff --git a/examples/document-field/src/pages/author/[id].tsx b/examples/document-field/src/pages/author/[id].tsx deleted file mode 100644 index f3aa3dbdf80..00000000000 --- a/examples/document-field/src/pages/author/[id].tsx +++ /dev/null @@ -1,59 +0,0 @@ -import { type GetStaticPathsResult, type GetStaticPropsContext } from 'next' -import Link from 'next/link' -import React from 'react' -import { DocumentRenderer } from '@keystone-6/document-renderer' -import { fetchGraphQL, gql } from '../../utils' - -export default function Post ({ author }: { author: any }) { - return ( -
        -

        {author.name}

        - -

        Bio

        - {author.bio?.document && } - -

        Posts

        - {author.posts.map((post: any) => ( -
      • - {post.title} -
      • - ))} -
        - ) -} - -export async function getStaticPaths (): Promise { - const data = await fetchGraphQL(gql` - query { - authors { - id - } - } - `) - return { - paths: data.authors.map((post: any) => ({ params: { id: post.id } })), - fallback: 'blocking', - } -} - -export async function getStaticProps ({ params }: GetStaticPropsContext) { - const data = await fetchGraphQL( - gql` - query ($id: ID!) { - author(where: { id: $id }) { - name - bio { - document - } - posts(where: { status: { equals: published } }, orderBy: { publishDate: desc }) { - id - title - slug - } - } - } - `, - { id: params!.id } - ) - return { props: { author: data.author }, revalidate: 60 } -} diff --git a/examples/document-field/tsconfig.json b/examples/document-field/tsconfig.json index 1ed5fb820cb..64857dc28d3 100644 --- a/examples/document-field/tsconfig.json +++ b/examples/document-field/tsconfig.json @@ -1,7 +1,11 @@ { "compilerOptions": { "target": "esnext", - "lib": ["dom", "dom.iterable", "esnext"], + "lib": [ + "dom", + "dom.iterable", + "esnext" + ], "allowJs": false, "skipLibCheck": false, "strict": true, @@ -12,8 +16,21 @@ "moduleResolution": "node", "resolveJsonModule": true, "isolatedModules": true, - "jsx": "preserve" + "jsx": "preserve", + "incremental": true, + "plugins": [ + { + "name": "next" + } + ] }, - "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], - "exclude": ["node_modules"] + "include": [ + "next-env.d.ts", + "**/*.ts", + "**/*.tsx", + ".next/types/**/*.ts" + ], + "exclude": [ + "node_modules" + ] } diff --git a/examples/extend-graphql-subscriptions/schema.ts b/examples/extend-graphql-subscriptions/schema.ts index ce54e31ff53..db417b85ee2 100644 --- a/examples/extend-graphql-subscriptions/schema.ts +++ b/examples/extend-graphql-subscriptions/schema.ts @@ -12,17 +12,30 @@ export const lists = { access: allowAll, hooks: { // this hook publishes posts to the 'POST_UPDATED' channel when a post mutated - afterOperation: async ({ item }) => { - // WARNING: passing this item directly to pubSub bypasses any contextual access control - // if you want access control, you need to use a different architecture - // - // tl;dr Keystone access filters are not respected in this scenario - console.log('POST_UPDATED', { id: item?.id }) - - pubSub.publish('POST_UPDATED', { - postUpdated: item, - }) - }, + afterOperation: { + create: async ({ item }) => { + // WARNING: passing this item directly to pubSub bypasses any contextual access control + // if you want access control, you need to use a different architecture + // + // tl;dr Keystone access filters are not respected in this scenario + console.log('POST_UPDATED', { id: item?.id }) + + pubSub.publish('POST_UPDATED', { + postUpdated: item, + }) + }, + update: async ({ item }) => { + // WARNING: passing this item directly to pubSub bypasses any contextual access control + // if you want access control, you need to use a different architecture + // + // tl;dr Keystone access filters are not respected in this scenario + console.log('POST_UPDATED', { id: item?.id }) + + pubSub.publish('POST_UPDATED', { + postUpdated: item, + }) + }, + } }, fields: { title: text({ validation: { isRequired: true } }), diff --git a/examples/field-groups/schema.ts b/examples/field-groups/schema.ts index ad73009af9c..0cd551166b7 100644 --- a/examples/field-groups/schema.ts +++ b/examples/field-groups/schema.ts @@ -26,15 +26,12 @@ export const lists = { // for this example, we are going to use a hook for fun // defaultValue: { kind: 'now' } hooks: { - resolveInput: ({ context, operation, resolvedData }) => { - // TODO: text should allow you to prevent a defaultValue, then Prisma create could be non-null - // if (operation === 'create') return resolvedData.title.replace(/ /g, '-').toLowerCase() - if (operation === 'create') { - return resolvedData.title?.replace(/ /g, '-').toLowerCase() - } - - return resolvedData.slug - }, + resolveInput: { + create: ({ context, operation, resolvedData }) => { + // TODO: text should allow you to prevent a defaultValue, then Prisma create could be non-null + return resolvedData.title?.replace(/ /g, '-').toLowerCase() + }, + } }, }), }, diff --git a/examples/hooks/schema.ts b/examples/hooks/schema.ts index 7b5472397ed..a03f5668488 100644 --- a/examples/hooks/schema.ts +++ b/examples/hooks/schema.ts @@ -78,18 +78,10 @@ export const lists = { // defaultValue: { kind: 'now' } hooks: { - resolveInput: ({ context, operation, resolvedData }) => { - if (operation === 'create') return new Date() - return resolvedData.createdAt - }, - }, - - // TODO: this would be nice - // hooks: { - // resolveInput: { - // create: () => new Date() - // } - // } + resolveInput: { + create: () => new Date() + } + } }), updatedBy: text({ ...readOnly }), @@ -102,18 +94,10 @@ export const lists = { // }, hooks: { - resolveInput: ({ context, operation, resolvedData }) => { - if (operation === 'update') return new Date() - return resolvedData.updatedAt - }, - }, - - // TODO: this would be nice - // hooks: { - // resolveInput: { - // update: () => new Date() - // } - // } + resolveInput: { + update: () => new Date() + } + } }), }, }), @@ -131,29 +115,45 @@ export const lists = { return resolvedData }, }, - validateInput: ({ context, operation, inputData, addValidationError }) => { - const { title, content } = inputData + validate: { + create: ({ inputData, addValidationError }) => { + const { title, content } = inputData + + + // an example of a content filter, the prevents the title or content containing the word "Profanity" + if (/profanity/i.test(title)) return addValidationError('Unacceptable title') + if (/profanity/i.test(content)) return addValidationError('Unacceptable content') + }, + update: ({ inputData, addValidationError }) => { + const { title, content } = inputData - if (operation === 'update' && 'feedback' in inputData) { - const { feedback } = inputData - if (/profanity/i.test(feedback ?? '')) return addValidationError('Unacceptable feedback') - } + if ('feedback' in inputData) { + const { feedback } = inputData + if (/profanity/i.test(feedback ?? '')) return addValidationError('Unacceptable feedback') + } - // an example of a content filter, the prevents the title or content containing the word "Profanity" - if (/profanity/i.test(title)) return addValidationError('Unacceptable title') - if (/profanity/i.test(content)) return addValidationError('Unacceptable content') - }, - validateDelete: ({ context, item, addValidationError }) => { - const { preventDelete } = item - - // an example of a content filter, the prevents the title or content containing the word "Profanity" - if (preventDelete) return addValidationError('Cannot delete Post, preventDelete is true') + // an example of a content filter, the prevents the title or content containing the word "Profanity" + if (/profanity/i.test(title)) return addValidationError('Unacceptable title') + if (/profanity/i.test(content)) return addValidationError('Unacceptable content') + }, + delete: ({ context, item, addValidationError }) => { + const { preventDelete } = item + + // an example of a content filter, the prevents the title or content containing the word "Profanity" + if (preventDelete) return addValidationError('Cannot delete Post, preventDelete is true') + }, }, - - beforeOperation: ({ item, resolvedData, operation }) => { - console.log(`Post beforeOperation.${operation}`, resolvedData) + beforeOperation: { + create: ({ item, resolvedData, operation }) => { + console.log(`Post beforeOperation.${operation}`, resolvedData) + }, + update: ({ item, resolvedData, operation }) => { + console.log(`Post beforeOperation.${operation}`, resolvedData) + }, + delete: ({ item, operation }) => { + console.log(`Post beforeOperation.${operation}`, item) + }, }, - afterOperation: { create: ({ inputData, item }) => { console.log(`Post afterOperation.create`, inputData, '->', item) @@ -162,7 +162,6 @@ export const lists = { update: ({ originalItem, item }) => { console.log(`Post afterOperation.update`, originalItem, '->', item) }, - delete: ({ originalItem }) => { console.log(`Post afterOperation.delete`, originalItem, '-> deleted') }, diff --git a/examples/reuse/schema.ts b/examples/reuse/schema.ts index 6cc07c52e6f..ee021c52367 100644 --- a/examples/reuse/schema.ts +++ b/examples/reuse/schema.ts @@ -55,18 +55,23 @@ function trackingByHooks< // FieldKey extends 'createdBy' | 'updatedBy' // TODO: refined types for the return types > (immutable: boolean = false): FieldHooks { return { - async resolveInput ({ context, operation, resolvedData, item, fieldKey }) { - if (operation === 'update') { + resolveInput: { + async create ({ context, operation, resolvedData, item, fieldKey }) { + // TODO: refined types for the return types + // FIXME: CommonFieldConfig need not always be generalised + return `${context.req?.socket.remoteAddress} (${context.req?.headers['user-agent']})` as any + }, + async update ({ context, operation, resolvedData, item, fieldKey }) { if (immutable) return undefined // show we have refined types for compatible item.* fields if (isTrue(item.completed) && resolvedData.completed !== false) return undefined - } - - // TODO: refined types for the return types - // FIXME: CommonFieldConfig need not always be generalised - return `${context.req?.socket.remoteAddress} (${context.req?.headers['user-agent']})` as any - }, + + // TODO: refined types for the return types + // FIXME: CommonFieldConfig need not always be generalised + return `${context.req?.socket.remoteAddress} (${context.req?.headers['user-agent']})` as any + }, + } } } @@ -76,18 +81,23 @@ function trackingAtHooks< > (immutable: boolean = false): FieldHooks { return { // TODO: switch to operation routing when supported for fields - async resolveInput ({ context, operation, resolvedData, item, fieldKey }) { - if (operation === 'update') { + resolveInput: { + async create ({ context, operation, resolvedData, item, fieldKey }) { + // TODO: refined types for the return types + // FIXME: CommonFieldConfig need not always be generalised + return new Date() as any + }, + async update ({ context, operation, resolvedData, item, fieldKey }) { if (immutable) return undefined // show we have refined types for compatible item.* fields if (isTrue(item.completed) && resolvedData.completed !== false) return undefined - } - // TODO: refined types for the return types - // FIXME: CommonFieldConfig need not always be generalised - return new Date() as any - }, + // TODO: refined types for the return types + // FIXME: CommonFieldConfig need not always be generalised + return new Date() as any + }, + } } } diff --git a/examples/usecase-relationship-union/schema.ts b/examples/usecase-relationship-union/schema.ts index efda8ca3385..980b8c8f355 100644 --- a/examples/usecase-relationship-union/schema.ts +++ b/examples/usecase-relationship-union/schema.ts @@ -80,31 +80,34 @@ export const lists = { }, hooks: { - validateInput: async ({ operation, inputData, addValidationError }) => { - if (operation === 'create') { - const { post, link } = inputData - const values = [post, link].filter(x => x?.connect ?? x?.create) - if (values.length === 0) { - return addValidationError('A relationship is required') - } - if (values.length > 1) { - return addValidationError('Only one relationship at a time') - } - } - - if (operation === 'update') { - const { post, link } = inputData - if ([post, link].some(x => x?.disconnect)) { - return addValidationError('Cannot change relationship type') + validate: { + create: async ({ operation, inputData, addValidationError }) => { + if (operation === 'create') { + const { post, link } = inputData + const values = [post, link].filter(x => x?.connect ?? x?.create) + if (values.length === 0) { + return addValidationError('A relationship is required') + } + if (values.length > 1) { + return addValidationError('Only one relationship at a time') + } } - - const values = [post, link].filter(x => x?.connect ?? x?.create) - if (values.length > 1) { - return addValidationError('Only one relationship at a time') + }, + update: async ({ operation, inputData, addValidationError }) => { + if (operation === 'update') { + const { post, link } = inputData + if ([post, link].some(x => x?.disconnect)) { + return addValidationError('Cannot change relationship type') + } + + const values = [post, link].filter(x => x?.connect ?? x?.create) + if (values.length > 1) { + return addValidationError('Only one relationship at a time') + } + + // TODO: prevent item from changing types with implicit disconnect } - - // TODO: prevent item from changing types with implicit disconnect - } + }, }, resolveInput: { update: async ({ context, operation, resolvedData }) => { diff --git a/examples/usecase-roles/schema.ts b/examples/usecase-roles/schema.ts index 246e0786604..3b8873b95c7 100644 --- a/examples/usecase-roles/schema.ts +++ b/examples/usecase-roles/schema.ts @@ -69,15 +69,17 @@ export const lists = { }, }, hooks: { - resolveInput ({ operation, resolvedData, context }) { - if (operation === 'create' && !resolvedData.assignedTo && context.session) { - // Always default new todo items to the current user; this is important because users - // without canManageAllTodos don't see this field when creating new items - return { connect: { id: context.session.itemId } } + resolveInput: { + create ({ operation, resolvedData, context }) { + if (!resolvedData.assignedTo && context.session) { + // Always default new todo items to the current user; this is important because users + // without canManageAllTodos don't see this field when creating new items + return { connect: { id: context.session.itemId } } + } + return resolvedData.assignedTo } - return resolvedData.assignedTo }, - }, + } }), }, }), diff --git a/examples/usecase-versioning/schema.ts b/examples/usecase-versioning/schema.ts index 46ba7cca800..8f3637961e9 100644 --- a/examples/usecase-versioning/schema.ts +++ b/examples/usecase-versioning/schema.ts @@ -27,11 +27,12 @@ export const lists = { }, }, hooks: { - resolveInput: async ({ resolvedData, operation, item }) => { - if (operation === 'create') return resolvedData.version - if (resolvedData.version !== item.version) throw new Error('Out of sync') - - return item.version + 1 + resolveInput: { + update: async ({ resolvedData, operation, item }) => { + if (resolvedData.version !== item.version) throw new Error('Out of sync') + + return item.version + 1 + }, }, }, }), diff --git a/packages/auth/src/index.ts b/packages/auth/src/index.ts index 68448486a4c..31618922940 100644 --- a/packages/auth/src/index.ts +++ b/packages/auth/src/index.ts @@ -1,10 +1,10 @@ import type { AdminFileToWrite, BaseListTypeInfo, - KeystoneConfig, KeystoneContext, SessionStrategy, BaseKeystoneTypeInfo, + ResolvedKeystoneConfig, } from '@keystone-6/core/types' import { password, timestamp } from '@keystone-6/core/fields' @@ -99,19 +99,20 @@ export function createAuth ({ * * The signin page is always included, and the init page is included when initFirstItem is set */ - const authGetAdditionalFiles = () => { + const authGetAdditionalFiles = ({ tsx }: { tsx: boolean }) => { + const ext = tsx ? 'tsx' : 'js' const filesToWrite: AdminFileToWrite[] = [ { mode: 'write', src: signinTemplate({ gqlNames, identityField, secretField }), - outputPath: 'pages/signin.js', + outputPath: `signin/page.${ext}`, }, ] if (initFirstItem) { filesToWrite.push({ mode: 'write', src: initTemplate({ listKey, initFirstItem }), - outputPath: 'pages/init.js', + outputPath: `init/page.${ext}`, }) } return filesToWrite @@ -134,7 +135,7 @@ export function createAuth ({ }) function throwIfInvalidConfig ( - config: KeystoneConfig + config: ResolvedKeystoneConfig ) { if (!(listKey in config.lists)) { throw new Error(`withAuth cannot find the list "${listKey}"`) @@ -215,7 +216,6 @@ export function createAuth ({ }): Promise<{ kind: 'redirect', to: string } | void> { const { req } = context const { pathname } = new URL(req!.url!, 'http://_') - // redirect to init if initFirstItem conditions are met if (pathname !== `${basePath}/init` && (await hasInitFirstItemConditions(context))) { return { kind: 'redirect', to: `${basePath}/init` } @@ -247,8 +247,8 @@ export function createAuth ({ * Automatically extends your configuration with a prescriptive implementation. */ function withAuth ( - config: KeystoneConfig - ): KeystoneConfig { + config: ResolvedKeystoneConfig + ): ResolvedKeystoneConfig { throwIfInvalidConfig(config) let { ui } = config if (!ui?.isDisabled) { @@ -262,7 +262,7 @@ export function createAuth ({ ui = { ...ui, publicPages: [...publicPages, ...authPublicPages], - getAdditionalFiles: [...getAdditionalFiles, authGetAdditionalFiles], + getAdditionalFiles: [...getAdditionalFiles, () => authGetAdditionalFiles({ tsx: ui?.tsx ?? true })], isAccessAllowed: async (context: KeystoneContext) => { if (await hasInitFirstItemConditions(context)) return true diff --git a/packages/auth/src/lib/useFromRedirect.ts b/packages/auth/src/lib/useFromRedirect.ts deleted file mode 100644 index 6b0828e2c4f..00000000000 --- a/packages/auth/src/lib/useFromRedirect.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { useMemo } from 'react' - -// TODO: remove or fix -export function useRedirect () { - return useMemo(() => '/', []) -} diff --git a/packages/auth/src/pages/InitPage.tsx b/packages/auth/src/pages/InitPage.tsx index cc92c2ee987..75d6ef791f2 100644 --- a/packages/auth/src/pages/InitPage.tsx +++ b/packages/auth/src/pages/InitPage.tsx @@ -22,7 +22,6 @@ import { import { guessEmailFromValue, validEmail } from '../lib/emailHeuristics' import { IconTwitter, IconGithub } from '../components/Icons' import { SigninContainer } from '../components/SigninContainer' -import { useRedirect } from '../lib/useFromRedirect' const signupURL = 'https://signup.keystonejs.cloud/api/newsletter-signup' @@ -31,6 +30,7 @@ function Welcome ({ value, onContinue }: { value: any, onContinue: () => void }) const [email, setEmail] = useState(guessEmailFromValue(value)) const [error, setError] = useState(null) const [loading, setLoading] = useState(false) + const { adminPath } = useKeystone() const onSubmit = async (event: React.FormEvent) => { event.preventDefault() @@ -138,7 +138,7 @@ function Welcome ({ value, onContinue }: { value: any, onContinue: () => void }) {error ? 'Try again' : 'Continue'} {error && ( - )} @@ -157,7 +157,7 @@ function InitPage ({ fieldPaths: string[] enableWelcome: boolean }) { - const { adminMeta } = useKeystone() + const { adminMeta, adminPath } = useKeystone() const fields = useMemo(() => { const fields: Record = {} fieldPaths.forEach(fieldPath => { @@ -190,7 +190,6 @@ function InitPage ({ }`) const reinitContext = useReinitContext() const router = useRouter() - const redirect = useRedirect() const onSubmit = async (event: React.FormEvent) => { event.preventDefault() @@ -229,11 +228,11 @@ function InitPage ({ await reinitContext() if (enableWelcome) return setMode('welcome') - router.push(redirect) + router.push(adminPath || '/') } const onComplete = () => { - router.push(redirect) + router.push(adminPath || '/') } return mode === 'init' ? ( diff --git a/packages/auth/src/pages/SigninPage.tsx b/packages/auth/src/pages/SigninPage.tsx index d0b5b7536d7..9772c33971d 100644 --- a/packages/auth/src/pages/SigninPage.tsx +++ b/packages/auth/src/pages/SigninPage.tsx @@ -12,7 +12,6 @@ import { useMutation, gql } from '@keystone-6/core/admin-ui/apollo' import { useRawKeystone, useReinitContext } from '@keystone-6/core/admin-ui/context' import { useRouter } from '@keystone-6/core/admin-ui/router' import { SigninContainer } from '../components/SigninContainer' -import { useRedirect } from '../lib/useFromRedirect' type SigninPageProps = { identityField: string @@ -59,15 +58,14 @@ export function SigninPage ({ const reinitContext = useReinitContext() const router = useRouter() const rawKeystone = useRawKeystone() - const redirect = useRedirect() - + const { adminPath } = useRawKeystone() // if we are signed in, redirect immediately useEffect(() => { if (submitted) return if (rawKeystone.authenticatedItem.state === 'authenticated') { - router.push(redirect) + router.push(adminPath || '/') } - }, [rawKeystone.authenticatedItem, router, redirect, submitted]) + }, [rawKeystone.authenticatedItem, router, adminPath, submitted]) useEffect(() => { if (!submitted) return @@ -79,8 +77,8 @@ export function SigninPage ({ return } - router.push(redirect) - }, [rawKeystone.adminMeta, router, redirect, submitted]) + router.push(adminPath || '/') + }, [rawKeystone.adminMeta, router, adminPath, submitted]) const onSubmit = async (event: FormEvent) => { event.preventDefault() diff --git a/packages/auth/src/templates/init.ts b/packages/auth/src/templates/init.ts index 03e69aa977d..3cbe4f9b624 100644 --- a/packages/auth/src/templates/init.ts +++ b/packages/auth/src/templates/init.ts @@ -8,15 +8,17 @@ type InitTemplateArgs = { export const initTemplate = ({ listKey, initFirstItem }: InitTemplateArgs) => { // -- TEMPLATE START - return `import { getInitPage } from '@keystone-6/auth/pages/InitPage'; + return `'use client' +/* eslint-disable */ +import { getInitPage } from '@keystone-6/auth/pages/InitPage' -const fieldPaths = ${JSON.stringify(initFirstItem.fields)}; +const fieldPaths = ${JSON.stringify(initFirstItem.fields)} export default getInitPage(${JSON.stringify({ listKey, fieldPaths: initFirstItem.fields, enableWelcome: !initFirstItem.skipKeystoneWelcome, - })}); + })}) ` // -- TEMPLATE END } diff --git a/packages/auth/src/templates/signin.ts b/packages/auth/src/templates/signin.ts index 6e333babc59..25542dc9ac3 100644 --- a/packages/auth/src/templates/signin.ts +++ b/packages/auth/src/templates/signin.ts @@ -10,7 +10,9 @@ export const signinTemplate = ({ secretField: string }) => { // -- TEMPLATE START - return `import { getSigninPage } from '@keystone-6/auth/pages/SigninPage' + return `'use client' +/* eslint-disable */ +import { getSigninPage } from '@keystone-6/auth/pages/SigninPage' export default getSigninPage(${JSON.stringify({ identityField: identityField, @@ -18,7 +20,7 @@ export default getSigninPage(${JSON.stringify({ mutationName: gqlNames.authenticateItemWithPassword, successTypename: gqlNames.ItemAuthenticationWithPasswordSuccess, failureTypename: gqlNames.ItemAuthenticationWithPasswordFailure, - })}); + })}) ` // -- TEMPLATE END } diff --git a/packages/core/src/___internal-do-not-use-will-break-in-patch/admin-ui/pages/App/index.tsx b/packages/core/src/___internal-do-not-use-will-break-in-patch/admin-ui/pages/App/index.tsx index 49d0da8982c..a4ee5924f3f 100644 --- a/packages/core/src/___internal-do-not-use-will-break-in-patch/admin-ui/pages/App/index.tsx +++ b/packages/core/src/___internal-do-not-use-will-break-in-patch/admin-ui/pages/App/index.tsx @@ -1,27 +1,30 @@ import React from 'react' import { Core } from '@keystone-ui/core' -import { type AppProps } from 'next/app' import { type DocumentNode } from 'graphql' import { type AdminConfig, type FieldViews } from '../../../../types' import { ErrorBoundary } from '../../../../admin-ui/components' import { KeystoneProvider } from '../../../../admin-ui/context' +type AdminProps = { + children: React.ReactNode + config: AppConfig +} + type AppConfig = { adminConfig: AdminConfig adminMetaHash: string fieldViews: FieldViews lazyMetadataQuery: DocumentNode apiPath: string + adminPath: string } -export const getApp = - (props: AppConfig) => - ({ Component, pageProps }: AppProps) => { - return ( +export function Layout ({ children, config }: AdminProps) { + return ( - + - + {children} diff --git a/packages/core/src/___internal-do-not-use-will-break-in-patch/admin-ui/pages/CreateItemPage/index.tsx b/packages/core/src/___internal-do-not-use-will-break-in-patch/admin-ui/pages/CreateItemPage/index.tsx index e437e65af8f..4721bd9d1d8 100644 --- a/packages/core/src/___internal-do-not-use-will-break-in-patch/admin-ui/pages/CreateItemPage/index.tsx +++ b/packages/core/src/___internal-do-not-use-will-break-in-patch/admin-ui/pages/CreateItemPage/index.tsx @@ -4,7 +4,6 @@ import { Box, jsx } from '@keystone-ui/core' import { LoadingDots } from '@keystone-ui/loading' import { Button } from '@keystone-ui/button' -import { useRouter } from 'next/router' import { Fields } from '../../../../admin-ui/utils' import { PageContainer } from '../../../../admin-ui/components/PageContainer' import { useKeystone, useList } from '../../../../admin-ui' @@ -12,10 +11,12 @@ import { GraphQLErrorNotice } from '../../../../admin-ui/components' import { type ListMeta } from '../../../../types' import { useCreateItem } from '../../../../admin-ui/utils/useCreateItem' import { BaseToolbar, ColumnLayout, ItemPageHeader } from '../ItemPage/common' +import { useRouter } from '../../../../admin-ui/router' function CreatePageForm (props: { list: ListMeta }) { const createItem = useCreateItem(props.list) const router = useRouter() + const { adminPath } = useKeystone() return ( {createItem.error && ( @@ -34,7 +35,7 @@ function CreatePageForm (props: { list: ListMeta }) { onClick={async () => { const item = await createItem.create() if (item) { - router.push(`/${props.list.path}/${item.id}`) + router.push(`${adminPath}/${props.list.path}/${item.id}`) } }} > @@ -45,14 +46,11 @@ function CreatePageForm (props: { list: ListMeta }) { ) } -type CreateItemPageProps = { listKey: string } +type CreateItemPageProps = { params: { listKey: string } } -export const getCreateItemPage = (props: CreateItemPageProps) => () => - - -function CreateItemPage (props: CreateItemPageProps) { - const list = useList(props.listKey) - const { createViewFieldModes } = useKeystone() +export function CreateItemPage ({ params }: CreateItemPageProps) { + const { createViewFieldModes, listsKeyByPath } = useKeystone() + const list = useList(listsKeyByPath[params.listKey]) return ( {hideCreate === false && !list.isSingleton && ( - + Create {list.singular} @@ -158,10 +159,11 @@ export function HomePage () { ) } return Object.keys(lists).map(key => { - if (!visibleLists.lists.has(key)) { + const list = lists[key] + if (!visibleLists.lists.has(list.key)) { return null } - const result = dataGetter.get(key) + const result = dataGetter.get(list.key) return ( - + {props.list.label} diff --git a/packages/core/src/___internal-do-not-use-will-break-in-patch/admin-ui/pages/ItemPage/index.tsx b/packages/core/src/___internal-do-not-use-will-break-in-patch/admin-ui/pages/ItemPage/index.tsx index b5f3d46cd42..cdf4a1206e1 100644 --- a/packages/core/src/___internal-do-not-use-will-break-in-patch/admin-ui/pages/ItemPage/index.tsx +++ b/packages/core/src/___internal-do-not-use-will-break-in-patch/admin-ui/pages/ItemPage/index.tsx @@ -2,7 +2,6 @@ /** @jsx jsx */ import copyToClipboard from 'clipboard-copy' -import { useRouter } from 'next/router' import { Fragment, type HTMLAttributes, @@ -37,15 +36,19 @@ import { } from '../../../../admin-ui/utils' import { gql, useMutation, useQuery } from '../../../../admin-ui/apollo' -import { useList } from '../../../../admin-ui/context' +import { useKeystone, useList } from '../../../../admin-ui/context' import { PageContainer, HEADER_HEIGHT } from '../../../../admin-ui/components/PageContainer' import { GraphQLErrorNotice } from '../../../../admin-ui/components/GraphQLErrorNotice' import { usePreventNavigation } from '../../../../admin-ui/utils/usePreventNavigation' import { CreateButtonLink } from '../../../../admin-ui/components/CreateButtonLink' import { BaseToolbar, ColumnLayout, ItemPageHeader } from './common' +import { useRouter } from '../../../../admin-ui/router' type ItemPageProps = { - listKey: string + params: { + id: string + listKey: string + } } function useEventCallback any>(callback: Func): Func { @@ -267,6 +270,7 @@ function DeleteButton ({ itemId: string list: ListMeta }) { + const { adminPath } = useKeystone() const toasts = useToasts() const [deleteItem, { loading }] = useMutation( gql`mutation ($id: ID!) { @@ -307,7 +311,7 @@ function DeleteButton ({ tone: 'negative', }) } - router.push(list.isSingleton ? '/' : `/${list.path}`) + router.push(`${adminPath}/${list.isSingleton ? '' : `${list.path}`}`) return toasts.addToast({ title: itemLabel, message: `Deleted ${list.singular} item successfully`, @@ -330,11 +334,11 @@ function DeleteButton ({ ) } -export const getItemPage = (props: ItemPageProps) => () => - -function ItemPage ({ listKey }: ItemPageProps) { +export function ItemPage ({ params }: ItemPageProps) { + const { listsKeyByPath } = useKeystone() + const listKey = listsKeyByPath[params.listKey] const list = useList(listKey) - const id = useRouter().query.id as string + const id = params.id as string const { query, selectedFields } = useMemo(() => { const selectedFields = Object.entries(list.fields) diff --git a/packages/core/src/___internal-do-not-use-will-break-in-patch/admin-ui/pages/ListPage/FieldSelection.tsx b/packages/core/src/___internal-do-not-use-will-break-in-patch/admin-ui/pages/ListPage/FieldSelection.tsx index fcfd317dfba..edbd33fb6f0 100644 --- a/packages/core/src/___internal-do-not-use-will-break-in-patch/admin-ui/pages/ListPage/FieldSelection.tsx +++ b/packages/core/src/___internal-do-not-use-will-break-in-patch/admin-ui/pages/ListPage/FieldSelection.tsx @@ -5,9 +5,9 @@ import { Box, jsx } from '@keystone-ui/core' import { ChevronDownIcon } from '@keystone-ui/icons/icons/ChevronDownIcon' import { Options, OptionPrimitive, CheckMark } from '@keystone-ui/options' import { Popover } from '@keystone-ui/popover' -import { useRouter } from 'next/router' import { type ListMeta } from '../../../../types' import { useSelectedFields } from './useSelectedFields' +import { useRouter } from '../../../../admin-ui/router' function isArrayEqual (arrA: string[], arrB: string[]) { if (arrA.length !== arrB.length) return false diff --git a/packages/core/src/___internal-do-not-use-will-break-in-patch/admin-ui/pages/ListPage/index.tsx b/packages/core/src/___internal-do-not-use-will-break-in-patch/admin-ui/pages/ListPage/index.tsx index 78578e317c4..5e1133dab4f 100644 --- a/packages/core/src/___internal-do-not-use-will-break-in-patch/admin-ui/pages/ListPage/index.tsx +++ b/packages/core/src/___internal-do-not-use-will-break-in-patch/admin-ui/pages/ListPage/index.tsx @@ -23,7 +23,7 @@ import { gql, type TypedDocumentNode, useMutation, useQuery } from '../../../../ import { CellLink } from '../../../../admin-ui/components' import { PageContainer, HEADER_HEIGHT } from '../../../../admin-ui/components/PageContainer' import { Pagination, PaginationLabel, usePaginationParams } from '../../../../admin-ui/components/Pagination' -import { useList } from '../../../../admin-ui/context' +import { useKeystone, useList } from '../../../../admin-ui/context' import { GraphQLErrorNotice } from '../../../../admin-ui/components/GraphQLErrorNotice' import { Link, useRouter } from '../../../../admin-ui/router' import { useFilter } from '../../../../fields/types/relationship/views/RelationshipSelect' @@ -36,7 +36,7 @@ import { useFilters } from './useFilters' import { useSelectedFields } from './useSelectedFields' import { useSort } from './useSort' -type ListPageProps = { listKey: string } +type ListPageProps = { params: { listKey: string } } type FetchedFieldMeta = { path: string @@ -98,7 +98,7 @@ function useQueryParamsFromLocalStorage (listKey: string) { return x.startsWith('!') || storeableQueries.includes(x) }) - if (!hasSomeQueryParamsWhichAreAboutListPage && router.isReady) { + if (!hasSomeQueryParamsWhichAreAboutListPage) { const queryParamsFromLocalStorage = localStorage.getItem(localStorageKey) let parsed try { @@ -109,7 +109,7 @@ function useQueryParamsFromLocalStorage (listKey: string) { } } }, - [localStorageKey, router.isReady] + [localStorageKey] ) useEffect(() => { let queryParamsToSerialize: Record = {} @@ -128,9 +128,9 @@ function useQueryParamsFromLocalStorage (listKey: string) { return { resetToDefaults } } -export const getListPage = (props: ListPageProps) => () => - -function ListPage ({ listKey }: ListPageProps) { +export function ListPage ({ params }: ListPageProps) { + const { listsKeyByPath } = useKeystone() + const listKey = listsKeyByPath[params.listKey] const list = useList(listKey) const { query, push } = useRouter() @@ -575,6 +575,7 @@ function ListTable ({ orderableFields: Set }) { const list = useList(listKey) + const { adminPath } = useKeystone() const { query } = useRouter() const shouldShowLinkIcon = !list.fields[selectedFields.keys().next().value].views.Cell.supportsLinkTo @@ -697,8 +698,8 @@ function ListTable ({ alignItems: 'center', justifyContent: 'center', }} - href={`/${list.path}/[id]`} - as={`/${list.path}/${encodeURIComponent(itemId)}`} + href={`${adminPath}/${list.path}/[id]`} + as={`${adminPath}/${list.path}/${encodeURIComponent(itemId)}`} > @@ -718,8 +719,8 @@ function ListTable ({ {i === 0 && Cell.supportsLinkTo ? ( {errorMessage} @@ -740,8 +741,8 @@ function ListTable ({ linkTo={ i === 0 && Cell.supportsLinkTo ? { - href: `/${list.path}/[id]`, - as: `/${list.path}/${encodeURIComponent(itemId)}`, + href: `${adminPath}/${list.path}/[id]`, + as: `${adminPath}/${list.path}/${encodeURIComponent(itemId)}`, } : undefined } diff --git a/packages/core/src/admin-ui/components/CreateButtonLink.tsx b/packages/core/src/admin-ui/components/CreateButtonLink.tsx index f3a7c2e1ef8..10fe9165ef1 100644 --- a/packages/core/src/admin-ui/components/CreateButtonLink.tsx +++ b/packages/core/src/admin-ui/components/CreateButtonLink.tsx @@ -4,8 +4,10 @@ import { Button } from '@keystone-ui/button' import { jsx } from '@keystone-ui/core' import type { ListMeta } from '../../types' import { Link } from '../router' +import { useKeystone } from '../context' export function CreateButtonLink (props: { list: ListMeta }) { + const { adminPath } = useKeystone() return ( diff --git a/packages/core/src/fields/types/relationship/views/index.tsx b/packages/core/src/fields/types/relationship/views/index.tsx index e094d555905..ef1c6bc8dba 100644 --- a/packages/core/src/fields/types/relationship/views/index.tsx +++ b/packages/core/src/fields/types/relationship/views/index.tsx @@ -34,6 +34,7 @@ function LinkToRelatedItems ({ list: ListMeta refFieldKey?: string }) { + const { adminPath } = useKeystone() function constructQuery ({ refFieldKey, itemId, @@ -60,14 +61,14 @@ function LinkToRelatedItems ({ if (value.kind === 'many') { const query = constructQuery({ refFieldKey, value, itemId }) return ( - ) } return ( - ) @@ -249,7 +250,7 @@ export const Field = ({ export const Cell: CellComponent = ({ field, item }) => { const list = useList(field.refListKey) const { colors } = useTheme() - + const { adminPath } = useKeystone() if (field.display === 'count') { const count = item[`${field.path}Count`] ?? 0 return ( @@ -277,7 +278,7 @@ export const Cell: CellComponent = ({ field, item }) => { {displayItems.map((item, index) => ( {!!index ? ', ' : ''} - + {item.label || item.id} @@ -288,6 +289,7 @@ export const Cell: CellComponent = ({ field, item }) => { } export const CardValue: CardValueComponent = ({ field, item }) => { + const { adminPath } = useKeystone() const list = useList(field.refListKey) const data = item[field.path] return ( @@ -298,7 +300,7 @@ export const CardValue: CardValueComponent = ({ field, item } .map((item, index) => ( {!!index ? ', ' : ''} - + {item.label || item.id} diff --git a/packages/core/src/fields/types/select/index.ts b/packages/core/src/fields/types/select/index.ts index ad10c77c12f..c8d5a24d437 100644 --- a/packages/core/src/fields/types/select/index.ts +++ b/packages/core/src/fields/types/select/index.ts @@ -10,7 +10,7 @@ import { import { graphql } from '../../..' import { filters } from '../../filters' import { makeValidateHook } from '../../non-null-graphql' -import { mergeFieldHooks } from '../../resolve-hooks' +import { merge } from '../../resolve-hooks' export type SelectFieldConfig = CommonFieldConfig & @@ -55,7 +55,7 @@ export type SelectFieldConfig = const MAX_INT = 2147483647 const MIN_INT = -2147483648 -export function select (config: SelectFieldConfig): FieldTypeFunc { +export function select (config: SelectFieldConfig): FieldTypeFunc { const { isIndexed, ui: { displayMode = 'select', ...ui } = {}, @@ -95,7 +95,14 @@ export function select (config: SelectFie ...config, mode, ui, - hooks: mergeFieldHooks({ validate }, config.hooks), + hooks: { + ...config.hooks, + validate: { + ...config.hooks?.validate, + create: merge(validate, config.hooks?.validate?.create), + update: merge(validate, config.hooks?.validate?.update), + }, + }, __ksTelemetryFieldTypeName: '@keystone-6/select', views: '@keystone-6/core/fields/types/select/views', getAdminMeta: () => ({ diff --git a/packages/core/src/fields/types/text/index.ts b/packages/core/src/fields/types/text/index.ts index 426908a2763..bf77f75b2ae 100644 --- a/packages/core/src/fields/types/text/index.ts +++ b/packages/core/src/fields/types/text/index.ts @@ -8,7 +8,7 @@ import { import { graphql } from '../../..' import { makeValidateHook } from '../../non-null-graphql' import { filters } from '../../filters' -import { mergeFieldHooks } from '../../resolve-hooks' +import { merge } from '../../resolve-hooks' export type TextFieldConfig = CommonFieldConfig & { @@ -142,7 +142,14 @@ export function text ( extendPrismaSchema: config.db?.extendPrismaSchema, })({ ...config, - hooks: mergeFieldHooks({ validate }, config.hooks), + hooks: { + ...config.hooks, + validate: { + ...config.hooks?.validate, + create: merge(validate, config.hooks?.validate?.create), + update: merge(validate, config.hooks?.validate?.update), + }, + }, input: { uniqueWhere: isIndexed === 'unique' ? { arg: graphql.arg({ type: graphql.String }) } : undefined, where: { diff --git a/packages/core/src/fields/types/timestamp/index.ts b/packages/core/src/fields/types/timestamp/index.ts index d2cdd55d8ac..75cd6c85be5 100644 --- a/packages/core/src/fields/types/timestamp/index.ts +++ b/packages/core/src/fields/types/timestamp/index.ts @@ -8,7 +8,7 @@ import { import { graphql } from '../../..' import { filters } from '../../filters' import { makeValidateHook } from '../../non-null-graphql' -import { mergeFieldHooks } from '../../resolve-hooks' +import { merge } from '../../resolve-hooks' import { type TimestampFieldMeta } from './views' export type TimestampFieldConfig = @@ -73,7 +73,14 @@ export function timestamp ( extendPrismaSchema: config.db?.extendPrismaSchema, })({ ...config, - hooks: mergeFieldHooks({ validate }, config.hooks), + hooks: { + ...config.hooks, + validate: { + ...config.hooks?.validate, + create: merge(validate, config.hooks?.validate?.create), + update: merge(validate, config.hooks?.validate?.update), + }, + }, input: { uniqueWhere: isIndexed === 'unique' ? { arg: graphql.arg({ type: graphql.DateTime }) } : undefined, where: { diff --git a/packages/core/src/lib/assets/createFilesContext.ts b/packages/core/src/lib/assets/createFilesContext.ts index 48e7a3cb36f..9391d9092f7 100644 --- a/packages/core/src/lib/assets/createFilesContext.ts +++ b/packages/core/src/lib/assets/createFilesContext.ts @@ -2,7 +2,7 @@ import { randomBytes } from 'node:crypto' import { type FilesContext, - type __ResolvedKeystoneConfig, + type ResolvedKeystoneConfig, } from '../../types' import { localFileAssetsAPI } from './local' import { s3FileAssetsAPI } from './s3' @@ -21,7 +21,7 @@ function defaultTransformName (path: string) { return `${urlSafeName}-${id}` } -export function createFilesContext (config: __ResolvedKeystoneConfig): FilesContext { +export function createFilesContext (config: ResolvedKeystoneConfig): FilesContext { const adaptersMap = new Map() for (const [storageKey, storageConfig] of Object.entries(config.storage || {})) { diff --git a/packages/core/src/lib/assets/createImagesContext.ts b/packages/core/src/lib/assets/createImagesContext.ts index 8a2d1a6d7e1..46b714dd6c0 100644 --- a/packages/core/src/lib/assets/createImagesContext.ts +++ b/packages/core/src/lib/assets/createImagesContext.ts @@ -3,7 +3,7 @@ import imageSize from 'image-size' import { type ImagesContext, - type __ResolvedKeystoneConfig, + type ResolvedKeystoneConfig, } from '../../types' import type { ImageAdapter } from './types' import { localImageAssetsAPI } from './local' @@ -33,7 +33,7 @@ async function getImageMetadataFromBuffer (buffer: Buffer) { return { width, height, filesize: buffer.length, extension } } -export function createImagesContext (config: __ResolvedKeystoneConfig): ImagesContext { +export function createImagesContext (config: ResolvedKeystoneConfig): ImagesContext { const imageAssetsAPIs = new Map() for (const [storageKey, storageConfig] of Object.entries(config.storage || {})) { if (storageConfig.type === 'image') { diff --git a/packages/core/src/lib/context/createContext.ts b/packages/core/src/lib/context/createContext.ts index 138494c3e2d..687c565b63c 100644 --- a/packages/core/src/lib/context/createContext.ts +++ b/packages/core/src/lib/context/createContext.ts @@ -11,7 +11,7 @@ import { import { type KeystoneContext, type KeystoneGraphQLAPI, - type __ResolvedKeystoneConfig, + type ResolvedKeystoneConfig, } from '../../types' import { type InitialisedList } from '../core/initialise-lists' @@ -27,7 +27,7 @@ export function createContext ({ prismaClient, prismaTypes }: { - config: __ResolvedKeystoneConfig + config: ResolvedKeystoneConfig lists: Record graphQLSchema: GraphQLSchema graphQLSchemaSudo: GraphQLSchema diff --git a/packages/core/src/lib/core/initialise-lists.ts b/packages/core/src/lib/core/initialise-lists.ts index 191e2ff2807..bff8eaa706d 100644 --- a/packages/core/src/lib/core/initialise-lists.ts +++ b/packages/core/src/lib/core/initialise-lists.ts @@ -8,7 +8,7 @@ import { type GraphQLTypesForList, type ListGraphQLTypes, type ListHooks, - type __ResolvedKeystoneConfig, + type ResolvedKeystoneConfig, type MaybePromise, type NextFieldType, type FieldTypeFunc, @@ -153,7 +153,7 @@ function throwIfNotAFilter (x: unknown, listKey: string, fieldKey: string) { throw new Error(`Configuration option '${listKey}.${fieldKey}' must be either a boolean value or a function. Received '${x}'.`) } -type ListConfigType = __ResolvedKeystoneConfig['lists'][string] +type ListConfigType = ResolvedKeystoneConfig['lists'][string] type FieldConfigType = ReturnType> type PartiallyInitialisedList1 = { graphql: { isEnabled: IsListEnabled } } type PartiallyInitialisedList2 = Omit @@ -244,78 +244,27 @@ function defaultListHooksResolveInput ({ resolvedData }: { resolvedData: any }) return resolvedData } -function parseListHooksResolveInput (f: ListHooks['resolveInput']) { - if (typeof f === 'function') { - return { - create: f, - update: f, - } - } - - const { - create = defaultListHooksResolveInput, - update = defaultListHooksResolveInput - } = f ?? {} - return { create, update } -} - -function parseListHooksValidate (f: ListHooks['validate']) { - if (typeof f === 'function') { - return { - create: f, - update: f, - delete: f, - } - } - - const { - create = defaultOperationHook, - update = defaultOperationHook, - delete: delete_ = defaultOperationHook, - } = f ?? {} - return { create, update, delete: delete_ } -} - -function parseListHooksBeforeOperation (f: ListHooks['beforeOperation']) { - if (typeof f === 'function') { - return { - create: f, - update: f, - delete: f, - } - } - - const { - create = defaultOperationHook, - update = defaultOperationHook, - delete: _delete = defaultOperationHook, - } = f ?? {} - return { create, update, delete: _delete } -} - -function parseListHooksAfterOperation (f: ListHooks['afterOperation']) { - if (typeof f === 'function') { - return { - create: f, - update: f, - delete: f, - } - } - - const { - create = defaultOperationHook, - update = defaultOperationHook, - delete: _delete = defaultOperationHook, - } = f ?? {} - return { create, update, delete: _delete } -} - function parseListHooks (hooks: ListHooks): ResolvedListHooks { return { - resolveInput: parseListHooksResolveInput(hooks.resolveInput), - validate: parseListHooksValidate(hooks.validate), - beforeOperation: parseListHooksBeforeOperation(hooks.beforeOperation), - afterOperation: parseListHooksAfterOperation(hooks.afterOperation), + resolveInput: { + create: hooks.resolveInput?.create ?? defaultListHooksResolveInput, + update: hooks.resolveInput?.update ?? defaultListHooksResolveInput, + }, + validate: { + create: hooks.validate?.create ?? defaultOperationHook, + update: hooks.validate?.update ?? defaultOperationHook, + delete: hooks.validate?.delete ?? defaultOperationHook, + }, + beforeOperation: { + create: hooks.beforeOperation?.create ?? defaultOperationHook, + update: hooks.beforeOperation?.update ?? defaultOperationHook, + delete: hooks.beforeOperation?.delete ?? defaultOperationHook, + }, + afterOperation: { + create: hooks.afterOperation?.create ?? defaultOperationHook, + update: hooks.afterOperation?.update ?? defaultOperationHook, + delete: hooks.afterOperation?.delete ?? defaultOperationHook, + }, } } @@ -330,51 +279,33 @@ function defaultFieldHooksResolveInput ({ } function parseFieldHooks ( - fieldKey: string, hooks: FieldHooks, ): ResolvedFieldHooks { - /** @deprecated, TODO: remove in breaking change */ - if (hooks.validate !== undefined) { - if (hooks.validateInput !== undefined) throw new TypeError(`"hooks.validate" conflicts with "hooks.validateInput" for the "${fieldKey}" field`) - if (hooks.validateDelete !== undefined) throw new TypeError(`"hooks.validate" conflicts with "hooks.validateDelete" for the "${fieldKey}" field`) - - if (typeof hooks.validate === 'function') { - return parseFieldHooks(fieldKey, { - ...hooks, - validate: { - create: hooks.validate, - update: hooks.validate, - delete: hooks.validate, - } - }) - } - } - return { resolveInput: { - create: hooks.resolveInput ?? defaultFieldHooksResolveInput, - update: hooks.resolveInput ?? defaultFieldHooksResolveInput, + create: hooks.resolveInput?.create ?? defaultFieldHooksResolveInput, + update: hooks.resolveInput?.update ?? defaultFieldHooksResolveInput, }, validate: { - create: hooks.validate?.create ?? hooks.validateInput ?? defaultOperationHook, - update: hooks.validate?.update ?? hooks.validateInput ?? defaultOperationHook, - delete: hooks.validate?.delete ?? hooks.validateDelete ?? defaultOperationHook, + create: hooks.validate?.create ?? defaultOperationHook, + update: hooks.validate?.update ?? defaultOperationHook, + delete: hooks.validate?.delete ?? defaultOperationHook, }, beforeOperation: { - create: hooks.beforeOperation ?? defaultOperationHook, - update: hooks.beforeOperation ?? defaultOperationHook, - delete: hooks.beforeOperation ?? defaultOperationHook, + create: hooks.beforeOperation?.create ?? defaultOperationHook, + update: hooks.beforeOperation?.update ?? defaultOperationHook, + delete: hooks.beforeOperation?.delete ?? defaultOperationHook, }, afterOperation: { - create: hooks.afterOperation ?? defaultOperationHook, - update: hooks.afterOperation ?? defaultOperationHook, - delete: hooks.afterOperation ?? defaultOperationHook, + create: hooks.afterOperation?.create ?? defaultOperationHook, + update: hooks.afterOperation?.update ?? defaultOperationHook, + delete: hooks.afterOperation?.delete ?? defaultOperationHook, }, } } function getListsWithInitialisedFields ( - config: __ResolvedKeystoneConfig, + config: ResolvedKeystoneConfig, listsRef: Record, ) { const { storage: configStorage, lists: listsConfig, db: { provider } } = config @@ -700,7 +631,7 @@ function getListsWithInitialisedFields ( dbField: f.dbField as ResolvedDBField, access: parseFieldAccessControl(f.access), - hooks: parseFieldHooks(fieldKey, f.hooks ?? {}), + hooks: parseFieldHooks(f.hooks ?? {}), graphql: { cacheHint: f.graphql?.cacheHint, isEnabled: isEnabledField, @@ -872,7 +803,7 @@ function graphqlForOutputField (field: InitialisedField) { }) } -export function initialiseLists (config: __ResolvedKeystoneConfig): Record { +export function initialiseLists (config: ResolvedKeystoneConfig): Record { const listsRef: Record = {} let intermediateLists diff --git a/packages/core/src/lib/core/prisma-schema-printer.ts b/packages/core/src/lib/core/prisma-schema-printer.ts index 3856cdbe6ea..09bfec300a2 100644 --- a/packages/core/src/lib/core/prisma-schema-printer.ts +++ b/packages/core/src/lib/core/prisma-schema-printer.ts @@ -4,9 +4,7 @@ import { } from '../../types' import { type ResolvedDBField } from './resolve-relationships' import { type InitialisedList } from './initialise-lists' -import { - type __ResolvedKeystoneConfig -} from '../../types' +import { type ResolvedKeystoneConfig } from '../../types' import { areArraysEqual, getDBFieldKeyForFieldOnMultiField } from './utils' const modifiers = { @@ -183,7 +181,7 @@ function assertDbFieldIsValidForIdField ( } export function printPrismaSchema ( - config: __ResolvedKeystoneConfig, + config: ResolvedKeystoneConfig, lists: Record, ) { const { diff --git a/packages/core/src/lib/create-admin-meta.ts b/packages/core/src/lib/create-admin-meta.ts index 7785a90c537..979cdab4bb3 100644 --- a/packages/core/src/lib/create-admin-meta.ts +++ b/packages/core/src/lib/create-admin-meta.ts @@ -6,7 +6,7 @@ import { type MaybeItemFunction, type MaybePromise, type MaybeSessionFunction, - type __ResolvedKeystoneConfig, + type ResolvedKeystoneConfig, } from '../types' import { type GraphQLNames @@ -85,7 +85,7 @@ export type AdminMetaRootVal = { } export function createAdminMeta ( - config: __ResolvedKeystoneConfig, + config: ResolvedKeystoneConfig, initialisedLists: Record ) { const { lists } = config diff --git a/packages/core/src/lib/createAdminUIMiddleware.ts b/packages/core/src/lib/createAdminUIMiddleware.ts index 151136791ec..e6ffb993f90 100644 --- a/packages/core/src/lib/createAdminUIMiddleware.ts +++ b/packages/core/src/lib/createAdminUIMiddleware.ts @@ -4,14 +4,14 @@ import type express from 'express' import type next from 'next' import { type KeystoneContext, - type __ResolvedKeystoneConfig, + type ResolvedKeystoneConfig, } from '../types' import { pkgDir } from '../pkg-dir' const adminErrorHTMLFilepath = path.join(pkgDir, 'static', 'admin-error.html') export function createAdminUIMiddlewareWithNextApp ( - config: __ResolvedKeystoneConfig, + config: ResolvedKeystoneConfig, commonContext: KeystoneContext, nextApp: ReturnType ) { @@ -31,7 +31,7 @@ export function createAdminUIMiddlewareWithNextApp ( return async (req: express.Request, res: express.Response) => { const { pathname } = url.parse(req.url) - if (pathname?.startsWith(`${basePath}/_next`) || pathname?.startsWith(`${basePath}/__next`)) { + if (pathname?.startsWith(`/_next`) || pathname?.startsWith(`/__next`)) { return handle(req, res) } diff --git a/packages/core/src/lib/createExpressServer.ts b/packages/core/src/lib/createExpressServer.ts index a63af48780e..23c41b7cb48 100644 --- a/packages/core/src/lib/createExpressServer.ts +++ b/packages/core/src/lib/createExpressServer.ts @@ -13,7 +13,7 @@ import { ApolloServerPluginLandingPageLocalDefault } from '@apollo/server/plugin import graphqlUploadExpress from 'graphql-upload/graphqlUploadExpress.js' import { type KeystoneContext, - type __ResolvedKeystoneConfig, + type ResolvedKeystoneConfig, } from '../types' /* @@ -24,7 +24,7 @@ The Admin UI takes a while to build for dev, and is created separately so the CLI can bring up the dev server early to handle GraphQL requests. */ -function formatError (graphqlConfig: __ResolvedKeystoneConfig['graphql']) { +function formatError (graphqlConfig: ResolvedKeystoneConfig['graphql']) { return (formattedError: GraphQLFormattedError, error: unknown) => { let debug = graphqlConfig.debug if (debug === undefined) { @@ -46,7 +46,7 @@ function formatError (graphqlConfig: __ResolvedKeystoneConfig['graphql']) { } export async function createExpressServer ( - config: Pick<__ResolvedKeystoneConfig, 'graphql' | 'server' | 'storage'>, + config: Pick, context: KeystoneContext ): Promise<{ expressServer: express.Express diff --git a/packages/core/src/lib/createGraphQLSchema.ts b/packages/core/src/lib/createGraphQLSchema.ts index 5e4dff60a6e..71ac300cd08 100644 --- a/packages/core/src/lib/createGraphQLSchema.ts +++ b/packages/core/src/lib/createGraphQLSchema.ts @@ -2,7 +2,7 @@ import { type GraphQLNamedType, GraphQLSchema } from 'graphql' import { graphql } from '../types/schema' import { - type __ResolvedKeystoneConfig + type ResolvedKeystoneConfig } from '../types' import { KeystoneMeta } from './resolve-admin-meta' import type { AdminMetaRootVal } from './create-admin-meta' @@ -98,7 +98,7 @@ function collectTypes ( } export function createGraphQLSchema ( - config: __ResolvedKeystoneConfig, + config: ResolvedKeystoneConfig, lists: Record, adminMeta: AdminMetaRootVal | null, sudo: boolean diff --git a/packages/core/src/lib/createSystem.ts b/packages/core/src/lib/createSystem.ts index 78b039d8627..446b8c831b8 100644 --- a/packages/core/src/lib/createSystem.ts +++ b/packages/core/src/lib/createSystem.ts @@ -1,9 +1,10 @@ import path from 'node:path' +import fs from 'node:fs' import { randomBytes } from 'node:crypto' import { type KeystoneConfig, type FieldData, - type __ResolvedKeystoneConfig + type ResolvedKeystoneConfig } from '../types' import { GraphQLError } from 'graphql' @@ -26,7 +27,7 @@ function posixify (s: string) { return s.split(path.sep).join('/') } -export function getSystemPaths (cwd: string, config: KeystoneConfig | __ResolvedKeystoneConfig) { +export function getSystemPaths (cwd: string, config: KeystoneConfig | ResolvedKeystoneConfig) { const prismaClientPath = config.db.prismaClientPath === '@prisma/client' ? null : config.db.prismaClientPath @@ -49,9 +50,15 @@ export function getSystemPaths (cwd: string, config: KeystoneConfig | __Resolved ? path.join(cwd, config.graphql.schemaPath) // TODO: enforce initConfig before getSystemPaths : path.join(cwd, 'schema.graphql') + const srcPath = path.join(cwd, 'src') + const hasSrc = fs.existsSync(srcPath) + // remove leading `/` if present + const basePath = config.ui?.basePath?.replace(/^\//, '') ?? '' + const adminPath = path.join(cwd, hasSrc ? 'src' : '', `app/${basePath || '(admin)'}`) + return { config: getBuiltKeystoneConfigurationPath(cwd), - admin: path.join(cwd, '.keystone/admin'), + admin: adminPath, prisma: prismaClientPath ?? '@prisma/client', types: { relativePrismaPath, @@ -61,10 +68,11 @@ export function getSystemPaths (cwd: string, config: KeystoneConfig | __Resolved prisma: builtPrismaPath, graphql: builtGraphqlPath, }, + hasSrc, } } -function getSudoGraphQLSchema (config: __ResolvedKeystoneConfig) { +function getSudoGraphQLSchema (config: ResolvedKeystoneConfig) { // This function creates a GraphQLSchema based on a modified version of the provided config. // The modifications are: // * All list level access control is disabled @@ -76,7 +84,7 @@ function getSudoGraphQLSchema (config: __ResolvedKeystoneConfig) { // operations that can be run. // // The resulting schema is used as the GraphQL schema when calling `context.sudo()`. - const transformedConfig: __ResolvedKeystoneConfig = { + const transformedConfig: ResolvedKeystoneConfig = { ...config, ui: { ...config.ui, @@ -183,7 +191,7 @@ function injectNewDefaults (prismaClient: unknown, lists: Record (x: T) { return x } -export function resolveDefaults (config: KeystoneConfig): __ResolvedKeystoneConfig { +export function resolveDefaults (config: KeystoneConfig, injectIdField = false): ResolvedKeystoneConfig { if (!['postgresql', 'sqlite', 'mysql'].includes(config.db.provider)) { throw new TypeError(`"db.provider" only supports "sqlite", "postgresql" or "mysql"`) } @@ -128,7 +109,7 @@ export function resolveDefaults (config: schemaPath: config.graphql?.schemaPath ?? 'schema.graphql', extendGraphqlSchema: config.graphql?.extendGraphqlSchema ?? ((s) => s), }, - lists: injectDefaults(config, defaultIdField), + lists: injectIdField ? injectDefaults(config, defaultIdField) : config.lists as ResolvedKeystoneConfig['lists'], server: { maxFileSize: 200 * 1024 * 1024, // 200 MiB extendExpressApp: config.server?.extendExpressApp ?? noop, @@ -149,6 +130,7 @@ export function resolveDefaults (config: getAdditionalFiles: config.ui?.getAdditionalFiles ?? [], pageMiddleware: config.ui?.pageMiddleware ?? noop, publicPages:config.ui?.publicPages ?? [], + tsx: config.ui?.tsx ?? true, }, } } diff --git a/packages/core/src/lib/id-field.ts b/packages/core/src/lib/id-field.ts index 0ea90ddcaae..5522c0eefc9 100644 --- a/packages/core/src/lib/id-field.ts +++ b/packages/core/src/lib/id-field.ts @@ -6,7 +6,8 @@ import { fieldType, orderDirectionEnum, } from '../types' -import { graphql } from '..' +import { graphql } from '../types/schema' + import { userInputError } from './core/graphql-errors' type IDType = string | number | null diff --git a/packages/core/src/lib/typescript-schema-printer.ts b/packages/core/src/lib/typescript-schema-printer.ts index 2e7fc8d892a..9e6be3d115a 100644 --- a/packages/core/src/lib/typescript-schema-printer.ts +++ b/packages/core/src/lib/typescript-schema-printer.ts @@ -190,7 +190,7 @@ export function printGeneratedTypes ( })(), '}', `export type Context = import('@keystone-6/core/types').KeystoneContext>`, - `export type Config = import('@keystone-6/core/types').KeystoneConfig>`, + `export type Config = import('@keystone-6/core/types').ResolvedKeystoneConfig>`, '', 'export type TypeInfo = {', ` lists: {`, diff --git a/packages/core/src/schema.ts b/packages/core/src/schema.ts index 4d4fba24148..2117692bfbb 100644 --- a/packages/core/src/schema.ts +++ b/packages/core/src/schema.ts @@ -1,3 +1,4 @@ +import { resolveDefaults } from './lib/defaults' import { type BaseFields, type BaseKeystoneTypeInfo, @@ -7,7 +8,7 @@ import { } from './types' export function config (config: KeystoneConfig) { - return config + return resolveDefaults(config) } let i = 0 diff --git a/packages/core/src/scripts/build.ts b/packages/core/src/scripts/build.ts index 14ddbb1f990..46ed9e1df99 100644 --- a/packages/core/src/scripts/build.ts +++ b/packages/core/src/scripts/build.ts @@ -39,11 +39,11 @@ export async function build ( console.log('✨ Generating Admin UI code') const paths = system.getPaths(cwd) - await generateAdminUI(system.config, system.graphQLSchema, system.adminMeta, paths.admin, false) + await generateAdminUI(system.config, system.graphQLSchema, system.adminMeta, paths.admin, paths.hasSrc, false) console.log('✨ Building Admin UI') await nextBuild( - paths.admin, + cwd, undefined, undefined, undefined, diff --git a/packages/core/src/scripts/cli.ts b/packages/core/src/scripts/cli.ts index 5af75a4e39c..3566ccc894a 100644 --- a/packages/core/src/scripts/cli.ts +++ b/packages/core/src/scripts/cli.ts @@ -9,12 +9,12 @@ import { telemetry } from './telemetry' export type Flags = { dbPush: boolean - fix: boolean // TODO: deprecated, remove in breaking change frozen: boolean prisma: boolean server: boolean ui: boolean withMigrations: boolean + resetAdmin: boolean } function defaultFlags (flags: Partial, defaults: Partial) { @@ -58,14 +58,14 @@ export async function cli (cwd: string, argv: string[]) { prisma use prisma commands in a Keystone context Options - --fix (postinstall) @deprecated - do build the graphql or prisma schemas, don't validate them - --frozen (build, migrate) don't build the graphql or prisma schemas, only validate them --no-db-push (dev) don't push any updates of your Prisma schema to your database + + --reset-admin (dev) + reset generated admin files --no-prisma (build, dev) don't build or validate the prisma schema @@ -87,7 +87,7 @@ export async function cli (cwd: string, argv: string[]) { const command = input.join(' ') || 'dev' if (command === 'dev') { - return dev(cwd, defaultFlags(flags, { dbPush: true, prisma: true, server: true, ui: true })) + return dev(cwd, defaultFlags(flags, { dbPush: true, prisma: true, server: true, ui: true, resetAdmin: false })) } if (command === 'migrate create') { @@ -116,11 +116,7 @@ export async function cli (cwd: string, argv: string[]) { // WARNING: postinstall is an alias for `build --frozen --no-ui` if (command === 'postinstall') { - return build(cwd, { - frozen: !defaultFlags(flags, { fix: false }).fix, - prisma: true, - ui: false, - }) + return build(cwd, { frozen: true, prisma: true, ui: false }) } console.log(`${command} is an unknown command`) diff --git a/packages/core/src/scripts/dev.ts b/packages/core/src/scripts/dev.ts index 74c03b3c71f..d8ecf417976 100644 --- a/packages/core/src/scripts/dev.ts +++ b/packages/core/src/scripts/dev.ts @@ -25,7 +25,7 @@ import { generateTypes, getFormattedGraphQLSchema, } from '../artifacts' -import { type KeystoneConfig } from '../types' +import { type ResolvedKeystoneConfig } from '../types' import { printPrismaSchema } from '../lib/core/prisma-schema-printer' import { pkgDir } from '../pkg-dir' import { @@ -33,16 +33,14 @@ import { importBuiltKeystoneConfiguration, } from './utils' import { type Flags } from './cli' +import { noop } from '../lib/defaults' const devLoadingHTMLFilepath = path.join(pkgDir, 'static', 'dev-loading.html') -function stripExtendHttpServer (config: KeystoneConfig): KeystoneConfig { +function stripExtendHttpServer (config: ResolvedKeystoneConfig): ResolvedKeystoneConfig { const { server, ...rest } = config - if (server) { - const { extendHttpServer, ...restServer } = server - return { ...rest, server: restServer } - } - return rest + const { extendHttpServer, ...restServer } = server + return { ...rest, server: { ... restServer, extendHttpServer: noop } } } function resolvablePromise () { @@ -56,7 +54,7 @@ function resolvablePromise () { export async function dev ( cwd: string, - { dbPush, prisma, server, ui }: Pick + { dbPush, prisma, server, ui, resetAdmin }: Pick ) { console.log('✨ Starting Keystone') let lastPromise = resolvablePromise>() @@ -267,11 +265,13 @@ export async function dev ( console.log('✨ Generating Admin UI code') const paths = system.getPaths(cwd) - await fsp.rm(paths.admin, { recursive: true, force: true }) - await generateAdminUI(system.config, system.graphQLSchema, system.adminMeta, paths.admin, false) + if (resetAdmin) { + await fsp.rm(paths.admin, { recursive: true, force: true }) + } + await generateAdminUI(system.config, system.graphQLSchema, system.adminMeta, paths.admin, paths.hasSrc, false) console.log('✨ Preparing Admin UI') - nextApp = next({ dev: true, dir: paths.admin }) + nextApp = next({ dev: true, dir: cwd }) await nextApp.prepare() expressServer.use(createAdminUIMiddlewareWithNextApp(system.config, context, nextApp)) console.log(`✅ Admin UI ready`) @@ -334,7 +334,7 @@ export async function dev ( } await generateTypes(cwd, newSystem) - await generateAdminUI(newSystem.config, newSystem.graphQLSchema, newSystem.adminMeta, paths.admin, true) + await generateAdminUI(newSystem.config, newSystem.graphQLSchema, newSystem.adminMeta, paths.admin, paths.hasSrc, true) if (prismaClientModule) { if (server && lastApolloServer) { const { context: newContext } = newSystem.getKeystone(prismaClientModule) diff --git a/packages/core/src/scripts/start.ts b/packages/core/src/scripts/start.ts index 8e9ccdca72f..dc3a50fb9ed 100644 --- a/packages/core/src/scripts/start.ts +++ b/packages/core/src/scripts/start.ts @@ -35,8 +35,8 @@ export async function start ( console.log(`✅ GraphQL API ready`) if (!system.config.ui?.isDisabled && ui) { - console.log('✨ Preparing Admin UI') - const nextApp = next({ dev: false, dir: paths.admin }) + console.log('✨ Preparing Admin UI Next.js') + const nextApp = next({ dev: false, dir: cwd }) await nextApp.prepare() expressServer.use(await createAdminUIMiddlewareWithNextApp(system.config, keystone.context, nextApp)) console.log(`✅ Admin UI ready`) diff --git a/packages/core/src/types/config/hooks.ts b/packages/core/src/types/config/hooks.ts index 046476ebd3a..dd108fab1eb 100644 --- a/packages/core/src/types/config/hooks.ts +++ b/packages/core/src/types/config/hooks.ts @@ -51,55 +51,37 @@ export type ListHooks = { /** * Used to **modify the input** for create and update operations after default values and access control have been applied */ - resolveInput?: - | ResolveInputListHook - | { - create?: ResolveInputListHook - update?: ResolveInputListHook - } + resolveInput?: { + create?: ResolveInputListHook + update?: ResolveInputListHook + } /** * Used to **validate** if a create, update or delete operation is OK */ - validate?: - | ValidateHook - | { - create?: ValidateHook - update?: ValidateHook - delete?: ValidateHook - } - - /** - * @deprecated, replaced by validate^ - */ - validateInput?: ValidateHook - - /** - * @deprecated, replaced by validate^ - */ - validateDelete?: ValidateHook + validate?: { + create?: ValidateHook + update?: ValidateHook + delete?: ValidateHook + } /** * Used to **cause side effects** before a create, update, or delete operation once all validateInput hooks have resolved */ - beforeOperation?: - | BeforeOperationListHook - | { - create?: BeforeOperationListHook - update?: BeforeOperationListHook - delete?: BeforeOperationListHook - } + beforeOperation?: { + create?: BeforeOperationListHook + update?: BeforeOperationListHook + delete?: BeforeOperationListHook + } /** * Used to **cause side effects** after a create, update, or delete operation operation has occurred */ - afterOperation?: - | AfterOperationListHook - | { - create?: AfterOperationListHook - update?: AfterOperationListHook - delete?: AfterOperationListHook - } + afterOperation?: { + create?: AfterOperationListHook + update?: AfterOperationListHook + delete?: AfterOperationListHook + } } export type ResolvedListHooks = { @@ -131,46 +113,37 @@ export type FieldHooks< /** * Used to **modify the input** for create and update operations after default values and access control have been applied */ - resolveInput?: - | ResolveInputFieldHook -// TODO: add in breaking change -// | { -// create?: ResolveInputFieldHook -// update?: ResolveInputFieldHook -// } + resolveInput?: { + create?: ResolveInputFieldHook + update?: ResolveInputFieldHook + } /** * Used to **validate** if a create, update or delete operation is OK */ - validate?: - | ValidateFieldHook - | { - create?: ValidateFieldHook - update?: ValidateFieldHook - delete?: ValidateFieldHook - } - - /** - * @deprecated, replaced by validate^ - * Used to **validate the input** for create and update operations once all resolveInput hooks resolved - */ - validateInput?: ValidateFieldHook - - /** - * @deprecated, replaced by validate^ - * Used to **validate** that a delete operation can happen after access control has occurred - */ - validateDelete?: ValidateFieldHook + validate?: { + create?: ValidateFieldHook + update?: ValidateFieldHook + delete?: ValidateFieldHook + } /** * Used to **cause side effects** before a create, update, or delete operation once all validateInput hooks have resolved */ - beforeOperation?: BeforeOperationFieldHook + beforeOperation?: { + create?: BeforeOperationFieldHook + update?: BeforeOperationFieldHook + delete?: BeforeOperationFieldHook + } /** * Used to **cause side effects** after a create, update, or delete operation operation has occurred */ - afterOperation?: AfterOperationFieldHook + afterOperation?: { + create?: AfterOperationFieldHook + update?: AfterOperationFieldHook + delete?: AfterOperationFieldHook + } } export type ResolvedFieldHooks< diff --git a/packages/core/src/types/config/index.ts b/packages/core/src/types/config/index.ts index 0e08f70241d..b0bc5e0aae0 100644 --- a/packages/core/src/types/config/index.ts +++ b/packages/core/src/types/config/index.ts @@ -248,10 +248,12 @@ export type KeystoneConfig MaybePromise<{ kind: 'redirect', to: string } | void> + /** Generate .tsx files instead of .js */ + tsx?: boolean } } -export type __ResolvedKeystoneConfig = { +export type ResolvedKeystoneConfig = { types: KeystoneConfig['types'] db: Omit['db']>, 'enableLogging'> & { enableLogging: PrismaLogLevel | Array @@ -277,8 +279,8 @@ export type __ResolvedKeystoneConfig str.split(' ').join('-').toLowerCase() const labelToClass = (str: string) => str.replace(/\s+/g, '') // WARNING: may break in patch -export function __getNames (listKey: string, list: __ResolvedKeystoneConfig['lists'][string]) { +export function __getNames (listKey: string, list: ResolvedKeystoneConfig['lists'][string]) { const { graphql, ui, isSingleton } = list if (ui?.path !== undefined && !/^[a-z-_][a-z0-9-_]*$/.test(ui.path)) { throw new Error(`ui.path for ${listKey} is ${ui.path} but it must only contain lowercase letters, numbers, dashes, and underscores and not start with a number`) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 85abf791d3a..158291a5bc6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -640,6 +640,9 @@ importers: '@prisma/client': specifier: 5.17.0 version: 5.17.0(prisma@5.17.0) + next: + specifier: ^14.2.0 + version: 14.2.5(@babel/core@7.25.2)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) devDependencies: prisma: specifier: 5.17.0 @@ -10572,7 +10575,6 @@ packages: engines: {node: '>=0.6.0', teleport: '>=0.2.0'} deprecated: |- You or someone you depend on is using Q, the JavaScript Promise library that gave JavaScript developers strong feelings about promises. They can almost certainly migrate to the native JavaScript promise now. Thank you literally everyone for joining me in this bet against the odds. Be excellent to each other. - (For a CapTP with native promises, see @endo/eventual-send and @endo/captp) qs@6.11.0: @@ -14626,14 +14628,14 @@ snapshots: '@graphql-tools/optimize@1.4.0(graphql@16.9.0)': dependencies: graphql: 16.9.0 - tslib: 2.4.1 + tslib: 2.6.3 '@graphql-tools/relay-operation-optimizer@6.5.18(graphql@16.9.0)': dependencies: '@ardatan/relay-compiler': 12.0.0(graphql@16.9.0) '@graphql-tools/utils': 9.2.1(graphql@16.9.0) graphql: 16.9.0 - tslib: 2.4.1 + tslib: 2.6.3 transitivePeerDependencies: - encoding - supports-color @@ -14649,7 +14651,7 @@ snapshots: '@graphql-tools/utils@8.13.1(graphql@16.9.0)': dependencies: graphql: 16.9.0 - tslib: 2.4.1 + tslib: 2.6.3 '@graphql-tools/utils@9.2.1(graphql@16.9.0)': dependencies: @@ -18453,7 +18455,7 @@ snapshots: camel-case@4.1.2: dependencies: pascal-case: 3.1.2 - tslib: 2.4.1 + tslib: 2.6.3 camelcase-keys@6.2.2: dependencies: @@ -18474,7 +18476,7 @@ snapshots: capital-case@1.0.4: dependencies: no-case: 3.0.4 - tslib: 2.4.1 + tslib: 2.6.3 upper-case-first: 2.0.2 capture-exit@2.0.0: @@ -18537,7 +18539,7 @@ snapshots: path-case: 3.0.4 sentence-case: 3.0.4 snake-case: 3.0.4 - tslib: 2.4.1 + tslib: 2.6.3 char-regex@1.0.2: {} @@ -18745,7 +18747,7 @@ snapshots: constant-case@3.0.4: dependencies: no-case: 3.0.4 - tslib: 2.4.1 + tslib: 2.6.3 upper-case: 2.0.2 content-disposition@0.5.4: @@ -20226,7 +20228,7 @@ snapshots: header-case@2.0.4: dependencies: capital-case: 1.0.4 - tslib: 2.4.1 + tslib: 2.6.3 hex-rgb@4.3.0: {} @@ -20541,7 +20543,7 @@ snapshots: is-lower-case@2.0.2: dependencies: - tslib: 2.4.1 + tslib: 2.6.3 is-module@1.0.0: {} @@ -20613,7 +20615,7 @@ snapshots: is-upper-case@2.0.2: dependencies: - tslib: 2.4.1 + tslib: 2.6.3 is-windows@1.0.2: {} @@ -21278,11 +21280,11 @@ snapshots: lower-case-first@2.0.2: dependencies: - tslib: 2.4.1 + tslib: 2.6.3 lower-case@2.0.2: dependencies: - tslib: 2.4.1 + tslib: 2.6.3 lowercase-keys@2.0.0: {} @@ -22693,7 +22695,7 @@ snapshots: param-case@3.0.4: dependencies: dot-case: 3.0.4 - tslib: 2.4.1 + tslib: 2.6.3 parent-module@1.0.1: dependencies: @@ -22768,7 +22770,7 @@ snapshots: pascal-case@3.1.2: dependencies: no-case: 3.0.4 - tslib: 2.4.1 + tslib: 2.6.3 pascalcase@0.1.1: {} @@ -22795,7 +22797,7 @@ snapshots: path-case@3.0.4: dependencies: dot-case: 3.0.4 - tslib: 2.4.1 + tslib: 2.6.3 path-exists@3.0.0: {} @@ -23716,7 +23718,7 @@ snapshots: sentence-case@3.0.4: dependencies: no-case: 3.0.4 - tslib: 2.4.1 + tslib: 2.6.3 upper-case-first: 2.0.2 serve-static@1.15.0: @@ -23999,7 +24001,7 @@ snapshots: sponge-case@1.0.1: dependencies: - tslib: 2.4.1 + tslib: 2.6.3 sprintf-js@1.0.3: {} @@ -24178,7 +24180,7 @@ snapshots: swap-case@2.0.2: dependencies: - tslib: 2.4.1 + tslib: 2.6.3 symbol-observable@4.0.0: {} @@ -24253,7 +24255,7 @@ snapshots: title-case@3.0.3: dependencies: - tslib: 2.4.1 + tslib: 2.6.3 tmp@0.0.33: dependencies: @@ -24577,11 +24579,11 @@ snapshots: upper-case-first@2.0.2: dependencies: - tslib: 2.4.1 + tslib: 2.6.3 upper-case@2.0.2: dependencies: - tslib: 2.4.1 + tslib: 2.6.3 uri-js@4.4.1: dependencies: diff --git a/tests/admin-ui-tests/live-reloading.test.ts b/tests/admin-ui-tests/live-reloading.test.ts index 597bb509f47..b1c075cc2a6 100644 --- a/tests/admin-ui-tests/live-reloading.test.ts +++ b/tests/admin-ui-tests/live-reloading.test.ts @@ -29,6 +29,8 @@ let ksProcess = undefined as any let page: Page = undefined as any let browser: Browser = undefined as any +jest.setTimeout(300000) // testing why this one fail + test('start keystone', async () => { // just in case a previous failing test run messed things up, let's reset it await replaceSchema('initial.ts') @@ -60,7 +62,7 @@ test('Creating an item with the GraphQL API and navigating to the item page for expect(value).toBe('blah') }) -test('api routes written with getAdditionalFiles containing [...rest] work', async () => { +test.only('api routes written with getAdditionalFiles containing [...rest] work', async () => { expect( await fetch('http://localhost:3000/api/blah/asdasdas/das/da/sdad').then(x => x.text()) ).toEqual('something') diff --git a/tests/api-tests/auth-header.test.ts b/tests/api-tests/auth-header.test.ts index 9b9cbd43a44..cd010e09e02 100644 --- a/tests/api-tests/auth-header.test.ts +++ b/tests/api-tests/auth-header.test.ts @@ -44,7 +44,7 @@ function setup (options?: any) { }), }, session: statelessSessions(), - }) + } as any) as any }) } @@ -114,7 +114,7 @@ describe('Auth testing', () => { }, session: statelessSessions(), - }), + } as any) as any, }) ).rejects.toMatchInlineSnapshot( `[Error: createAuth was called with an identityField of email on the list User but that field doesn't allow being searched uniquely with a String or ID. You should likely add \`isIndexed: 'unique'\` to the field at User.email]` diff --git a/tests/api-tests/auth.test.ts b/tests/api-tests/auth.test.ts index 4c1ab5f3a40..8ef9af061a2 100644 --- a/tests/api-tests/auth.test.ts +++ b/tests/api-tests/auth.test.ts @@ -57,7 +57,7 @@ const runner = setupTestRunner({ }), }, session: statelessSessions(), - }), + } as any) as any, }) async function authenticateWithPassword ( diff --git a/tests/api-tests/authed-user.test.ts b/tests/api-tests/authed-user.test.ts index d58f0602659..d267c2ac72b 100644 --- a/tests/api-tests/authed-user.test.ts +++ b/tests/api-tests/authed-user.test.ts @@ -29,7 +29,7 @@ const runner = setupTestRunner({ }), }, session: statelessSessions(), - })) + }) as any) as any }) test( diff --git a/tests/api-tests/field-groups.test.ts b/tests/api-tests/field-groups.test.ts index 1ee74812cb7..b0b2aa80c62 100644 --- a/tests/api-tests/field-groups.test.ts +++ b/tests/api-tests/field-groups.test.ts @@ -2,6 +2,7 @@ import { group, list } from '@keystone-6/core' import { allowAll } from '@keystone-6/core/access' import { getContext } from '@keystone-6/core/context' import { integer, text } from '@keystone-6/core/fields' +import { type ResolvedKeystoneConfig } from '@keystone-6/core/types' test('errors with nested field groups', () => { expect(() => @@ -29,7 +30,7 @@ test('errors with nested field groups', () => { }, }), }, - }, {}) + } as unknown as ResolvedKeystoneConfig, {}) ).toThrowErrorMatchingInlineSnapshot(`"groups cannot be nested"`) }) @@ -53,6 +54,6 @@ test('errors if you write a group manually differently to the group function', ( }, }), } - }, {}) + } as unknown as ResolvedKeystoneConfig, {}) ).toThrowErrorMatchingInlineSnapshot(`"unexpected value for a group at User.__group0"`) }) diff --git a/tests/api-tests/fields/types/relationship.test.ts b/tests/api-tests/fields/types/relationship.test.ts index f42a137b089..cad4393ad0c 100644 --- a/tests/api-tests/fields/types/relationship.test.ts +++ b/tests/api-tests/fields/types/relationship.test.ts @@ -24,7 +24,7 @@ function getSchema (field: { }, }), }, - }) + }) as any ).graphQLSchema } @@ -140,7 +140,7 @@ describe('Reference errors', () => { config({ db: { url: 'file:./thing.db', provider: 'sqlite' }, lists, - }) + }) as any ).graphQLSchema } diff --git a/tests/api-tests/hooks.test.ts b/tests/api-tests/hooks.test.ts index 6355758ac2c..f1a067d9158 100644 --- a/tests/api-tests/hooks.test.ts +++ b/tests/api-tests/hooks.test.ts @@ -32,8 +32,8 @@ function makeList ({ : hooks.validate === 'throws' ? makeThrower(`${__name}_${context}`) : ({ operation, resolvedData, addValidationError }: any) => { - addValidationError(`Validate_${__name}_${context}_${operation}`) - // TODO: mixed results + addValidationError(`Validate_${__name}_${context}_${operation}`) + // TODO: mixed results } } @@ -85,14 +85,25 @@ function makeList ({ basis: text(hooks.field ? { db: { isNullable: true }, // drops the implicit validation hook hooks: { - resolveInput: hooks.resolveInput ? replaceF : undefined, + resolveInput: hooks.resolveInput ? { + create: replaceF, + update: replaceF, + } : undefined, validate: { create: makeValidate('FVI'), update: makeValidate('FVI'), delete: makeValidate('FVI'), }, - beforeOperation: hooks.beforeOperation ? makeThrower(`${__name}_FBO`) : undefined, - afterOperation: hooks.afterOperation ? makeThrower(`${__name}_FAO`) : undefined + beforeOperation: hooks.beforeOperation ? { + create: makeThrower(`${__name}_FBO`), + update: makeThrower(`${__name}_FBO`), + delete: makeThrower(`${__name}_FBO`), + } : undefined, + afterOperation: hooks.afterOperation ? { + create: makeThrower(`${__name}_FAO`), + update: makeThrower(`${__name}_FAO`), + delete: makeThrower(`${__name}_FAO`), + } : undefined } } : {}), }, diff --git a/tests/api-tests/relationships/label-search-field-validation.test.ts b/tests/api-tests/relationships/label-search-field-validation.test.ts index 6123fa9aad9..cf2f0b3a1f9 100644 --- a/tests/api-tests/relationships/label-search-field-validation.test.ts +++ b/tests/api-tests/relationships/label-search-field-validation.test.ts @@ -2,6 +2,7 @@ import { list } from '@keystone-6/core' import { allowAll } from '@keystone-6/core/access' import { getContext } from '@keystone-6/core/context' import { integer, relationship, text } from '@keystone-6/core/fields' +import { type ResolvedKeystoneConfig } from '@keystone-6/core/types' const Thing = list({ access: allowAll, @@ -34,7 +35,7 @@ test("labelField that doesn't exist is rejected with displayMode: select", () => }), Thing, }, - }), + }) as unknown as ResolvedKeystoneConfig, {} as any ) ).toThrowErrorMatchingInlineSnapshot( @@ -66,7 +67,7 @@ test("labelField that doesn't exist is rejected with displayMode: cards", () => }), Thing, }, - }), + } as unknown as ResolvedKeystoneConfig), {} as any ) ).toThrowErrorMatchingInlineSnapshot( @@ -96,7 +97,7 @@ test("searchFields that don't exist are rejected with displayMode: select", () = }), Thing, }, - }), + } as unknown as ResolvedKeystoneConfig), {} as any ) ).toThrowErrorMatchingInlineSnapshot( @@ -128,7 +129,7 @@ test("searchFields that don't exist are rejected with displayMode: cards", () => }), Thing, }, - }), + } as unknown as ResolvedKeystoneConfig), {} as any ) ).toThrowErrorMatchingInlineSnapshot( @@ -158,7 +159,7 @@ test("searchFields that aren't searchable are rejected with displayMode: select" }), Thing, }, - }), + } as unknown as ResolvedKeystoneConfig), {} as any ) ).toThrowErrorMatchingInlineSnapshot( @@ -190,7 +191,7 @@ test("searchFields that aren't searchable are rejected with displayMode: cards", }), Thing, }, - }), + } as unknown as ResolvedKeystoneConfig), {} as any ) ).toThrowErrorMatchingInlineSnapshot( diff --git a/tests/api-tests/relationships/nested-mutations/create-many.test.ts b/tests/api-tests/relationships/nested-mutations/create-many.test.ts index 56c8f31a29a..328b55c796b 100644 --- a/tests/api-tests/relationships/nested-mutations/create-many.test.ts +++ b/tests/api-tests/relationships/nested-mutations/create-many.test.ts @@ -69,9 +69,14 @@ const runner2 = setupTestRunner({ content: text(), }, hooks: { - afterOperation () { - afterOperationWasCalled = true - }, + afterOperation: { + create () { + afterOperationWasCalled = true + }, + update () { + afterOperationWasCalled = true + }, + } }, access: allowAll, }), diff --git a/tests/cli-tests/__snapshots__/artifacts.test.ts.snap b/tests/cli-tests/__snapshots__/artifacts.test.ts.snap index eb69067fe4f..1a6e9217fb5 100644 --- a/tests/cli-tests/__snapshots__/artifacts.test.ts.snap +++ b/tests/cli-tests/__snapshots__/artifacts.test.ts.snap @@ -141,7 +141,7 @@ export declare namespace Lists { } } export type Context = import('@keystone-6/core/types').KeystoneContext> -export type Config = import('@keystone-6/core/types').KeystoneConfig> +export type Config = import('@keystone-6/core/types').ResolvedKeystoneConfig> export type TypeInfo = { lists: { diff --git a/tests/cli-tests/artifacts.test.ts b/tests/cli-tests/artifacts.test.ts index 9af15bc64a7..76d45faad66 100644 --- a/tests/cli-tests/artifacts.test.ts +++ b/tests/cli-tests/artifacts.test.ts @@ -45,20 +45,6 @@ const schemasMatch = ['schema.prisma', 'schema.graphql'] // because when they're slow and then run the same code as the postinstall command // (and in the case of the build command we need to spawn a child process which would make each case take a _very_ long time) describe('postinstall', () => { - test('updates the schemas without prompting when --fix is passed', async () => { - const cwd = await testdir({ - ...symlinkKeystoneDeps, - 'keystone.js': basicKeystoneConfig, - }) - - const recording = recordConsole() - await cliMock(cwd, ['postinstall', '--fix']) - const files = await getFiles(cwd, schemasMatch) - - expect(files).toEqual(await getFiles(`${__dirname}/fixtures/basic-project`, schemasMatch)) - expect(recording()).toMatchInlineSnapshot(`"? Generated GraphQL and Prisma schemas"`) - }) - test("does not prompt, error or modify the schemas if they're already up to date", async () => { const cwd = await testdir({ ...symlinkKeystoneDeps, diff --git a/tests/sandbox/configs/7590-add-item-to-relationship-in-hook-cards-ui.ts b/tests/sandbox/configs/7590-add-item-to-relationship-in-hook-cards-ui.ts index 5bd8853e16b..57ff79ec92e 100644 --- a/tests/sandbox/configs/7590-add-item-to-relationship-in-hook-cards-ui.ts +++ b/tests/sandbox/configs/7590-add-item-to-relationship-in-hook-cards-ui.ts @@ -17,12 +17,22 @@ export const lists = { }, hooks: { // every time you save, add a random number - async resolveInput (args) { - return { - ...args.resolvedData[args.fieldKey], - create: { - value: Math.floor(Math.random() * 100000).toString(), - }, + resolveInput: { + create (args) { + return { + ...args.resolvedData[args.fieldKey], + create: { + value: Math.floor(Math.random() * 100000).toString(), + }, + } + }, + update (args) { + return { + ...args.resolvedData[args.fieldKey], + create: { + value: Math.floor(Math.random() * 100000).toString(), + }, + } } }, }, diff --git a/tests/test-projects/live-reloading/keystone.ts b/tests/test-projects/live-reloading/keystone.ts index 62b86624edb..43b9d02523e 100644 --- a/tests/test-projects/live-reloading/keystone.ts +++ b/tests/test-projects/live-reloading/keystone.ts @@ -19,8 +19,9 @@ export default config({ () => [ { mode: 'write', - src: "export default function(req,res) {res.send('something')}", - outputPath: 'pages/api/blah/[...rest].js', + src: "export async function GET() { return new Response('something')}", + overwrite: true, + outputPath: 'api/blah/[...rest]/route.js', }, ], ],