Skip to content

Commit

Permalink
feat: add generator to create web admin ui
Browse files Browse the repository at this point in the history
  • Loading branch information
beeman committed Aug 9, 2023
1 parent f54ba87 commit df64805
Show file tree
Hide file tree
Showing 31 changed files with 732 additions and 7 deletions.
5 changes: 5 additions & 0 deletions libs/tools/generators.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@
"factory": "./src/generators/prisma-model/prisma-model-generator",
"schema": "./src/generators/prisma-model/prisma-model-schema.json",
"description": "prisma-model generator"
},
"web-feature": {
"factory": "./src/generators/web-feature/web-feature-generator",
"schema": "./src/generators/web-feature/web-feature-schema.json",
"description": "web-feature generator"
}
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { readProjectConfiguration, Tree } from '@nx/devkit'
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing'
import { createMockApi } from '../../lib/api/create-mock-api'
import { createMockApiApp } from '../../lib/api/create-mock-api-app'

import { apiFeatureGenerator } from './api-feature-generator'
import { ApiFeatureGeneratorSchema } from './api-feature-schema'
Expand All @@ -14,7 +14,7 @@ describe('api-feature generator', () => {
})

it('should generate the feature libraries', async () => {
await createMockApi(tree, options.app)
await createMockApiApp(tree, options.app)

// By default, we generate two libraries: data-access and feature
const libs = ['data-access', 'feature']
Expand Down Expand Up @@ -51,7 +51,7 @@ describe('api-feature generator', () => {
})

it('should generate the feature libraries with util lib', async () => {
await createMockApi(tree, options.app)
await createMockApiApp(tree, options.app)

// By default, we generate two libraries: data-access and feature
const libs = ['data-access', 'feature', 'util']
Expand All @@ -65,7 +65,7 @@ describe('api-feature generator', () => {

it('should generate the feature with different name', async () => {
const testOptions = { ...options, name: 'company' }
await createMockApi(tree, testOptions.app)
await createMockApiApp(tree, testOptions.app)

// By default, we generate two libraries: data-access and feature
const libs = ['data-access', 'feature', 'util']
Expand All @@ -78,7 +78,7 @@ describe('api-feature generator', () => {
})

it('should generate the feature libraries with custom names', async () => {
await createMockApi(tree, options.app)
await createMockApiApp(tree, options.app)

// By default, we generate two libraries: data-access and feature
const libs = ['data-access', 'feature']
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { Tree } from '@nx/devkit'
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing'
import { createMockWebApp } from '../../lib/web/create-mock-web-app'

import { webFeatureGenerator } from './web-feature-generator'
import { WebFeatureGeneratorSchema } from './web-feature-schema'

describe('web-feature generator', () => {
let tree: Tree
const rawOptions: WebFeatureGeneratorSchema = { app: 'web', name: 'test' }

beforeEach(() => {
tree = createTreeWithEmptyWorkspace()
})

it('should run successfully', async () => {
await createMockWebApp(tree, rawOptions.app)
await webFeatureGenerator(tree, rawOptions)

expect(true).toBeTruthy()
})
})
13 changes: 13 additions & 0 deletions libs/tools/src/generators/web-feature/web-feature-generator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { formatFiles, Tree } from '@nx/devkit'
import { ensureNxProjectExists } from '../../lib/utils/ensure-nx-project-exists'
import { generateWebFeature, normalizeWebFeatureSchema } from '../../lib/web'
import { WebFeatureGeneratorSchema } from './web-feature-schema'

export async function webFeatureGenerator(tree: Tree, rawOptions: WebFeatureGeneratorSchema) {
const options = normalizeWebFeatureSchema(rawOptions)
ensureNxProjectExists(tree, options.app)
await generateWebFeature(tree, options)
await formatFiles(tree)
}

export default webFeatureGenerator
13 changes: 13 additions & 0 deletions libs/tools/src/generators/web-feature/web-feature-schema.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
export type WebFeatureGeneratorSchema = Partial<NormalizedWebFeatureSchema>

export interface NormalizedWebFeatureSchema {
app: string
name: string
label: string
modelName: string
pluralModelName: string
skipAdminCrud: boolean
skipDataAccess: boolean
skipFeature: boolean
skipUi: boolean
}
43 changes: 43 additions & 0 deletions libs/tools/src/generators/web-feature/web-feature-schema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
{
"$schema": "http://json-schema.org/schema",
"$id": "WebFeature",
"title": "",
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "",
"$default": {
"$source": "argv",
"index": 0
},
"x-prompt": "What name would you like to use?"
},
"app": {
"type": "string",
"description": "The name of the application you are adding the feature to.",
"default": "web"
},
"skipAdminCrud": {
"type": "boolean",
"description": "Do not create an admin crud library for this feature.",
"default": false
},
"skipDataAccess": {
"type": "boolean",
"description": "Do not create a data access library for this feature.",
"default": false
},
"skipFeature": {
"type": "boolean",
"description": "Do not create a feature library for this feature.",
"default": false
},
"skipUi": {
"type": "boolean",
"description": "Do not create a ui library for this feature.",
"default": false
}
},
"required": ["name"]
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Tree } from '@nx/devkit'
import { applicationGenerator, libraryGenerator, serviceGenerator } from '@nx/nest'

export async function createMockApi(tree: Tree, app: string) {
export async function createMockApiApp(tree: Tree, app: string) {
// Build the mock app and core libs
await applicationGenerator(tree, { name: app, directory: 'apps', skipFormat: true })
// Create the core data access lib
Expand Down
2 changes: 1 addition & 1 deletion libs/tools/src/lib/api/get-api-core-feature-info.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ export function getApiCoreFeatureInfo(tree: Tree, app: string) {
const modulePath = `${lib.project.sourceRoot}/lib/${lib.project.name}.module.ts`

if (!tree.exists(modulePath)) {
throw new Error(`getApiFeatureModuleInfo: ${modulePath} does not exist in ${lib.project.sourceRoot}`)
throw new Error(`getApiCoreFeatureInfo: ${modulePath} does not exist in ${lib.project.sourceRoot}`)
}

const { className: moduleClassName } = names(`${lib.project.name}-module`)
Expand Down
1 change: 1 addition & 0 deletions libs/tools/src/lib/types/web-feature.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export type WebLibType = 'data-access' | 'feature' | 'ui'
10 changes: 10 additions & 0 deletions libs/tools/src/lib/utils/add-export.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,13 @@ export function addExport(tree: Tree, path: string, exportPath: string) {
return source
})
}

export function addExports(tree: Tree, path: string, exportPaths: string | string[]) {
const paths = Array.isArray(exportPaths) ? exportPaths : [exportPaths]
updateSourceFile(tree, path, (source) => {
for (const exportPath of paths) {
source.addExportDeclaration({ moduleSpecifier: exportPath.replace('.ts', '') })
}
return source
})
}
50 changes: 50 additions & 0 deletions libs/tools/src/lib/web/create-mock-web-app.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { Tree } from '@nx/devkit'
import { Linter } from '@nx/linter'
import { applicationGenerator, componentGenerator, libraryGenerator } from '@nx/react'

export async function createMockWebApp(tree: Tree, app: string) {
// Build the mock app and shell libs
await applicationGenerator(tree, {
directory: 'apps',
e2eTestRunner: 'none',
linter: Linter.EsLint,
name: app,
routing: true,
skipFormat: true,
style: 'css',
})
// Create the shell data access lib
await libraryGenerator(tree, {
linter: Linter.EsLint,
style: 'css',
name: `data-access`,
directory: `libs/${app}/shell`,
skipFormat: true,
})

// Create the shell feature lib
await libraryGenerator(tree, {
directory: `libs/${app}/shell`,
linter: Linter.EsLint,
name: `feature`,
skipFormat: true,
style: 'css',
})

// Create the shell feature lib
await createMockComponent(tree, `${app}-shell-feature`, `${app}-shell-feature`)

// Create the shell routes libs
await createMockComponent(tree, `${app}-shell-feature`, `${app}-shell.routes`)
await createMockComponent(tree, `${app}-shell-feature`, `${app}-admin.routes`)
}

function createMockComponent(tree: Tree, project: string, name: string) {
return componentGenerator(tree, {
name,
project,
style: 'none',
flat: true,
skipTests: true,
})
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { AdminCreate<%= modelClassName %>Input, AdminFindMany<%= modelClassName %>Input } from '@<%= npmScope %>/sdk'
import { useWebSdk } from '@<%= npmScope %>/<%= app %>/shell/data-access'

import { useUiPagination } from '@<%= npmScope %>/<%= app %>/ui/core'
import { showNotificationError, showNotificationSuccess } from '@<%= npmScope %>/<%= app %>/ui/notifications'
import { useQuery } from '@tanstack/react-query'
import { useState } from 'react'

export function useAdminFindMany<%= modelClassName %>(props?: Partial<AdminFindMany<%= modelClassName %>Input>) {
const sdk = useWebSdk()
const [take, setTake] = useState(props?.take ?? 10)
const [skip, setSkip] = useState(props?.skip ?? 0)
const [search, setSearch] = useState<string>(props?.search ?? '')

const input: AdminFindMany<%= modelClassName %>Input = { skip, take, search }
const query = useQuery(['admin', '<%= modelPropertyName %>s', 'find', input], () => sdk.adminFindMany<%= modelClassName %>s({ input }).then((res) => res.data))
const total = query.data?.count?.total ?? 0

return {
create<%= modelClassName %>: (input: AdminCreate<%= modelClassName %>Input) =>
sdk
.adminCreate<%= modelClassName %>({ input })
.then((res) => res.data)
.then((res) => {
if (res.created) {
showNotificationSuccess(`<%= modelClassName %> created`)
} else {
showNotificationError(`<%= modelClassName %> not created`)
}
return res.created
})
.catch((err) => {
showNotificationError(err.message)
return undefined
}),
delete<%= modelClassName %>: (<%= modelPropertyName %>Id: string) =>
sdk.adminDelete<%= modelClassName %>({ <%= modelPropertyName %>Id }).then(() => {
showNotificationSuccess('<%= modelClassName %> deleted')
return query.refetch()
}),
query,
setSearch,
pagination: useUiPagination({
skip,
setSkip,
take,
setTake,
total,
}),
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { AdminUpdate<%= modelClassName %>Input } from '@<%= npmScope %>/sdk'
import { useWebSdk } from '@<%= npmScope %>/<%= app %>/shell/data-access'
import { showNotificationError, showNotificationSuccess } from '@<%= npmScope %>/<%= app %>/ui/notifications'
import { useQuery } from '@tanstack/react-query'

export function useAdminFindOne<%= modelClassName %>(<%= modelPropertyName %>Id: string) {
const sdk = useWebSdk()
const query = useQuery(
['admin', '<%= modelPropertyName %>s', 'get', <%= modelPropertyName %>Id],
() => sdk.adminFindOne<%= modelClassName %>({ <%= modelPropertyName %>Id }).then((res) => res.data),
{ retry: 0 },
)
const <%= modelPropertyName %> = query.data?.item ?? undefined

return {
<%= modelPropertyName %>,
query,
update<%= modelClassName %>: async (input: AdminUpdate<%= modelClassName %>Input) =>
sdk
.adminUpdate<%= modelClassName %>({ <%= modelPropertyName %>Id, input })
.then((res) => res.data)
.then(async (res) => {
if (res) {
showNotificationSuccess('<%= modelClassName %> updated')
await query.refetch()
return true
}
showNotificationError('<%= modelClassName %> not updated')
return false
})
.catch((err) => {
showNotificationError(err.message)
return false
}),
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { Button, Group } from '@mantine/core'
import { AdminCreate<%= modelClassName %>Input } from '@<%= npmScope %>/sdk'
import { UiBack, UiAdminPage, UiCard } from '@<%= npmScope %>/<%= app %>/ui/core'
import { showNotificationError } from '@<%= npmScope %>/<%= app %>/ui/notifications'
import { useAdminFindMany<%= modelClassName %> } from '@<%= npmScope %>/<%= app %>/<%= modelFileName %>/data-access'
import { AdminUiCreate<%= modelClassName %>Form } from '@<%= npmScope %>/<%= app %>/<%= modelFileName %>/ui'
import { useNavigate } from 'react-router-dom'

export function WebAdmin<%= modelClassName %>CreateFeature() {
const navigate = useNavigate()
const { create<%= modelClassName %> } = useAdminFindMany<%= modelClassName %>()

const submit = async (input: AdminCreate<%= modelClassName %>Input) =>
create<%= modelClassName %>(input)
.then((res) => navigate(`/admin/<%= modelFileName %>s/${res?.id}`))
.then(() => true)
.catch((err) => {
showNotificationError(err.message)
return false
})

return (
<UiAdminPage leftAction={<UiBack />} title="Create <%= modelClassName %>">
<UiCard>
<AdminUiCreate<%= modelClassName %>Form submit={submit}>
<Group position="right">
<Button type="submit">Create</Button>
</Group>
</AdminUiCreate<%= modelClassName %>Form>
</UiCard>
</UiAdminPage>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { UiAlert, UiCard } from '@<%= npmScope %>/<%= app %>/ui/core'
import { useAdminFindOne<%= modelClassName %> } from '@<%= npmScope %>/<%= app %>/<%= modelFileName %>/data-access'
import { AdminUiUpdate<%= modelClassName %>Form } from '@<%= npmScope %>/<%= app %>/<%= modelFileName %>/ui'

export function WebAdmin<%= modelClassName %>DetailSettingsTab({ <%= modelPropertyName %>Id }: { <%= modelPropertyName %>Id: string }) {
const { <%= modelPropertyName %>, update<%= modelClassName %> } = useAdminFindOne<%= modelClassName %>(<%= modelPropertyName %>Id)

return (
<UiCard>
{<%= modelPropertyName %> ? <AdminUiUpdate<%= modelClassName %>Form <%= modelPropertyName %>={<%= modelPropertyName %>} submit={update<%= modelClassName %>} /> : <UiAlert message="<%= modelClassName %> not found." />}
</UiCard>
)
}
Loading

0 comments on commit df64805

Please sign in to comment.