feat: experimental generate resource

This commit is contained in:
arashsheyda
2023-04-23 00:25:11 +03:00
parent a95cb78c6a
commit 5de7715356
17 changed files with 617 additions and 57 deletions

View File

@ -0,0 +1,261 @@
<script lang="ts" setup>
interface ColumnInterface {
name: string
type: string
required: boolean
unique: boolean
default: any
}
const emit = defineEmits(['refresh'])
const router = useRouter()
const schema = ref(true)
const bread = reactive({
browse: {
active: true,
type: 'index',
},
read: {
active: true,
type: 'show',
by: '_id',
},
edit: {
active: true,
type: 'put',
by: '_id',
},
add: {
active: true,
type: 'create',
},
delete: {
active: true,
type: 'delete',
by: '_id',
},
})
const hasBread = computed({
get() {
return bread.browse.active || bread.read.active || bread.edit.active || bread.add.active || bread.delete.active
},
set(value: boolean) {
bread.browse.active = value
bread.read.active = value
bread.edit.active = value
bread.add.active = value
bread.delete.active = value
},
})
const collection = ref('')
const fields = ref<ColumnInterface[]>([
{
name: 'name',
type: 'string',
required: true,
unique: false,
default: '',
},
{
name: 'slug',
type: 'string',
required: true,
unique: true,
default: '',
},
])
const allFields = computed(() => {
return [{ name: '_id', type: 'ObjectId', required: true, unique: true, default: '' }, ...fields.value]
})
const fieldsLabels = ['name', 'type', 'required', 'unique', 'default']
const mongoTypes = [
'string',
'number',
'boolean',
'date',
'object',
'array',
'ObjectId',
]
function addField(index: number) {
fields.value.splice(index + 1, 0, {
name: '',
type: 'string',
required: false,
unique: false,
default: '',
})
}
function removeField(index: number) {
fields.value.splice(index, 1)
}
const convertedBread = computed(() => {
const breads: any = []
// add active breads
// eslint-disable-next-line unused-imports/no-unused-vars
for (const [key, value] of Object.entries(bread) as any) {
if (value.active) {
breads.push({
type: value.type,
by: value?.by,
})
}
}
return breads
})
const formattedFields = computed(() => {
return fields.value.map((field) => {
for (const [key, value] of Object.entries(field)) {
if (!value) {
// @ts-expect-error - no need for type checking
delete field[key]
}
}
return field
})
})
async function generate() {
await rpc.generateResource(
{
name: collection.value,
fields: schema.value ? formattedFields.value : undefined,
},
convertedBread.value,
).then(() => {
emit('refresh')
router.push(`/?table=${collection.value}`)
})
}
// TODO: remove this
const toggleSchema = computed({
get() {
if (hasBread.value)
return schema.value = true
return schema.value
},
set(value: boolean) {
schema.value = value
if (!schema.value)
return hasBread.value = false
hasBread.value = true
},
})
</script>
<template>
<div relative h-full of-hidden>
<div sticky top-0 px8 py4 n-bg-base>
<div mb2 flex items-center>
<span text-2xl font-bold mt1>
<NCheckbox v-model="hasBread" n="green">
BREAD
|
</NCheckbox>
</span>
<span flex gap4 mt2 ml2>
<NCheckbox v-model="bread.browse.active" n="blue">
Browse
</NCheckbox>
<div flex gap2>
<NCheckbox v-model="bread.read.active" n="cyan">
Read
</NCheckbox>
<NSelect v-if="bread.read.active" v-model="bread.read.by">
<option v-for="field in allFields" :key="field.name" :value="field.name">
{{ field.name }}
</option>
</NSelect>
</div>
<div flex gap2>
<NCheckbox v-model="bread.edit.active" n="purple">
Edit
</NCheckbox>
<NSelect v-if="bread.edit.active" v-model="bread.edit.by">
<option v-for="field in allFields" :key="field.name" :value="field.name">
{{ field.name }}
</option>
</NSelect>
</div>
<NCheckbox v-model="bread.add.active" n="green">
Add
</NCheckbox>
<div flex gap2>
<NCheckbox v-model="bread.delete.active" n="red">
Delete
</NCheckbox>
<NSelect v-if="bread.delete.active" v-model="bread.delete.by">
<option v-for="field in allFields" :key="field.name" :value="field.name">
{{ field.name }}
</option>
</NSelect>
</div>
</span>
</div>
<div flex gap4>
<NTextInput v-model="collection" flex-auto placeholder="Collection name" />
<NCheckbox v-model="toggleSchema" n="green">
Generate Schema
</NCheckbox>
</div>
</div>
<div v-if="schema" px8 h-full of-auto>
<div grid="~ cols-1" gap-2 mt4 mb4>
<div grid="~ cols-6" text-center>
<div v-for="label in fieldsLabels" :key="label">
{{ label }}
</div>
<div>
Actions
</div>
</div>
<div v-for="(column, index) in fields" :key="index" grid="~ cols-6" items-center text-center gap4>
<div>
<NTextInput v-model="column.name" />
</div>
<div>
<NSelect v-model="column.type">
<option v-for="mongoType of mongoTypes" :key="mongoType" :value="mongoType">
{{ mongoType }}
</option>
</NSelect>
</div>
<div>
<NCheckbox v-model="column.required" n="green" />
</div>
<div>
<NCheckbox v-model="column.unique" n="cyan" />
</div>
<div>
<NTextInput v-if="column.type === 'string'" v-model="column.default" type="string" n="orange" />
<NTextInput v-else-if="column.type === 'number'" v-model="column.default" type="number" n="orange" />
<NCheckbox v-else-if="column.type === 'boolean'" v-model="column.default" n="orange" />
<NTextInput v-else-if="column.type === 'date'" v-model="column.default" type="date" n="orange" />
<NTextInput v-else-if="column.type === 'ObjectId'" placeholder="no-default" disabled n="orange" />
<NTextInput v-else v-model="column.default" n="orange" />
</div>
<div flex justify-center gap2>
<NIconButton icon="carbon-add" n="cyan" @click="addField(index)" />
<NIconButton icon="carbon-delete" n="red" @click="removeField(index)" />
</div>
</div>
</div>
</div>
<NButton absolute right-8 bottom-8 px8 py2 icon="carbon-magic-wand-filled" n="green" @click="generate">
Create
</NButton>
</div>
</template>
<style></style>

View File

@ -1,3 +1,4 @@
<!-- eslint-disable no-console -->
<script lang="ts" setup>
const props = defineProps({
collection: {
@ -10,9 +11,15 @@ const documents = computedAsync(async () => {
return await rpc.listDocuments(props.collection)
})
const schema = computedAsync<any>(async () => {
return await rpc.resourceSchema(props.collection)
})
const fields = computed(() => {
if (documents.value && documents.value.length > 0)
return Object.keys(documents.value[0])
if (schema.value)
return Object.keys(schema.value)
return []
})
@ -34,14 +41,21 @@ const filtered = computed(() => {
})
function addDocument() {
// TODO: validate & show errors
if (editing.value)
return
editing.value = true
selectedDocument.value = {}
for (const field of fields.value) {
if (field !== '_id')
if (schema.value) {
for (const field of Object.keys(schema.value))
selectedDocument.value[field] = ''
}
else {
for (const field of fields.value) {
if (field !== '_id')
selectedDocument.value[field] = ''
}
}
const parent = dbContainer.value?.parentElement
parent?.scrollTo(0, parent.scrollHeight)
@ -78,10 +92,22 @@ async function deleteDocument(document: any) {
rpc.deleteDocument(props.collection, document._id)
documents.value = await rpc.listDocuments(props.collection)
}
const fieldRefs = ref<any>([])
function handleClickOutside(event: any) {
if (editing.value && selectedDocument.value) {
const isClickOutside = fieldRefs.value.every((ref: any) => !ref.contains(event.target))
if (isClickOutside) {
editing.value = false
selectedDocument.value = null
}
}
}
// useEventListener('click', handleClickOutside)
</script>
<template>
<div ref="dbContainer">
<div ref="dbContainer" :class="{ 'h-full': !documents?.length }">
<Navbar v-model:search="search" sticky top-0 px4 py2 backdrop-blur z-10>
<template #actions>
<NButton icon="carbon:add" n="green" @click="addDocument">
@ -93,7 +119,7 @@ async function deleteDocument(document: any) {
<span>{{ documents?.length }} documents in total</span>
</div>
</Navbar>
<table v-if="documents?.length" w-full mb10 :class="{ 'editing-mode': editing }">
<table v-if="documents?.length || selectedDocument" w-full mb10 :class="{ 'editing-mode': editing }">
<thead>
<tr>
<th v-for="field of fields" :key="field" text-start>
@ -105,8 +131,9 @@ async function deleteDocument(document: any) {
</tr>
</thead>
<tbody>
<!-- hover-bg-green hover-bg-opacity-5 hover-text-green cursor-pointer -->
<tr v-for="document in filtered" :key="document._id" :class="{ isEditing: editing && selectedDocument._id === document._id }">
<td v-for="field of fields" :key="field" hover-bg-green hover-bg-opacity-5 hover-text-green cursor-pointer @dblclick="editDocument(document)">
<td v-for="field of fields" :key="field" ref="fieldRefs" @dblclick="editDocument(document)">
<template v-if="editing && selectedDocument._id === document._id">
<input v-model="selectedDocument[field]" :disabled="field === '_id'">
</template>

View File

@ -0,0 +1,52 @@
<script setup lang="ts">
const props = defineProps<{
modelValue?: boolean
navbar?: HTMLElement
autoClose?: boolean
}>()
const emit = defineEmits<{
(e: 'close'): void
}>()
const el = ref<HTMLElement>()
const { height: top } = useElementSize(() => props.navbar, undefined, { box: 'border-box' })
onClickOutside(el, () => {
if (props.modelValue && props.autoClose)
emit('close')
}, {})
</script>
<script lang="ts">
export default {
inheritAttrs: false,
}
</script>
<template>
<Transition
enter-active-class="duration-200 ease-in"
enter-from-class="transform translate-x-1/1"
enter-to-class="opacity-100"
leave-active-class="duration-200 ease-out"
leave-from-class="opacity-100"
leave-to-class="transform translate-x-1/1"
>
<div
v-if="modelValue"
ref="el"
border="l base"
flex="~ col gap-1"
fixed bottom-0 right-0 z-10 z-20 of-auto text-sm backdrop-blur-lg
:style="{ top: `${top}px` }"
v-bind="$attrs"
>
<NIconButton absolute right-2 top-2 z-20 text-xl icon="carbon-close" @click="$emit('close')" />
<div relative h-full w-full of-auto>
<slot />
</div>
</div>
</Transition>
</template>