mirror of https://github.com/nocodb/nocodb
Pranav C
2 years ago
6 changed files with 393 additions and 2 deletions
@ -0,0 +1,273 @@ |
|||||||
|
<script setup lang="ts"> |
||||||
|
import useTableCreate from '../../composables/useTableCreate' |
||||||
|
import { validateTableName } from '~/utils/validation' |
||||||
|
import useProject from '~/composables/useProject' |
||||||
|
|
||||||
|
const { modelValue } = defineProps<{ modelValue?: boolean }>() |
||||||
|
|
||||||
|
const emit = defineEmits(['update:modelValue']) |
||||||
|
|
||||||
|
const dialogShow = computed({ |
||||||
|
get() { |
||||||
|
return modelValue |
||||||
|
}, |
||||||
|
set(v) { |
||||||
|
emit('update:modelValue', v) |
||||||
|
}, |
||||||
|
}) |
||||||
|
|
||||||
|
const valid = ref(false) |
||||||
|
const isAdvanceOptVisible = ref(false) |
||||||
|
const { table, createTable } = useTableCreate() |
||||||
|
const { tables, project } = useProject() |
||||||
|
|
||||||
|
const prefix = computed(() => project?.value?.prefix || '') |
||||||
|
|
||||||
|
const validateDuplicateAlias = (v: string) => { |
||||||
|
return (tables?.value || []).every((t) => t.title !== (v || '')) || 'Duplicate table alias' |
||||||
|
} |
||||||
|
const validateLeadingOrTrailingWhiteSpace = (v: string) => { |
||||||
|
return !/^\s+|\s+$/.test(v) || 'Leading or trailing whitespace not allowed in table name' |
||||||
|
} |
||||||
|
const validateDuplicate = (v: string) => { |
||||||
|
return (tables?.value || []).every((t) => t.table_name.toLowerCase() !== (v || '').toLowerCase()) || 'Duplicate table name' |
||||||
|
} |
||||||
|
|
||||||
|
/* import { validateTableName } from '~/helpers' |
||||||
|
|
||||||
|
export default { |
||||||
|
name: 'DlgTableCreate', |
||||||
|
props: ['value'], |
||||||
|
data() { |
||||||
|
return { |
||||||
|
isAdvanceOptVisible: false, |
||||||
|
table: { |
||||||
|
name: '', |
||||||
|
columns: ['id', 'title', 'created_at', 'updated_at'], |
||||||
|
}, |
||||||
|
isIdToggleAllowed: false, |
||||||
|
valid: false, |
||||||
|
idType: 'AI', |
||||||
|
idTypes: [ |
||||||
|
{ value: 'AI', text: 'Auto increment number' }, |
||||||
|
{ value: 'AG', text: 'Auto generated string' }, |
||||||
|
], |
||||||
|
} |
||||||
|
}, |
||||||
|
computed: { |
||||||
|
dialogShow: { |
||||||
|
get() { |
||||||
|
return this.value |
||||||
|
}, |
||||||
|
set(v) { |
||||||
|
this.$emit('input', v) |
||||||
|
}, |
||||||
|
}, |
||||||
|
projectPrefix() { |
||||||
|
return this.$store.getters['project/GtrProjectPrefix'] |
||||||
|
}, |
||||||
|
tables() { |
||||||
|
return this.$store.state.project.tables || [] |
||||||
|
}, |
||||||
|
}, |
||||||
|
watch: { |
||||||
|
'table.alias': function (alias) { |
||||||
|
this.$set(this.table, 'name', `${this.projectPrefix || ''}${alias}`) |
||||||
|
}, |
||||||
|
}, |
||||||
|
created() { |
||||||
|
this.populateDefaultTitle() |
||||||
|
}, |
||||||
|
mounted() { |
||||||
|
setTimeout(() => { |
||||||
|
const el = this.$refs.input.$el |
||||||
|
el.querySelector('input').focus() |
||||||
|
el.querySelector('input').select() |
||||||
|
}, 100) |
||||||
|
}, |
||||||
|
|
||||||
|
methods: { |
||||||
|
populateDefaultTitle() { |
||||||
|
let c = 1 |
||||||
|
while (this.tables.some((t) => t.title === `Sheet${c}`)) { |
||||||
|
c++ |
||||||
|
} |
||||||
|
this.$set(this.table, 'alias', `Sheet${c}`) |
||||||
|
}, |
||||||
|
validateTableName(v) { |
||||||
|
return validateTableName(v, this.$store.getters['project/GtrProjectIsGraphql']) |
||||||
|
}, |
||||||
|
validateDuplicateAlias(v) { |
||||||
|
return (this.tables || []).every((t) => t.title !== (v || '')) || 'Duplicate table alias' |
||||||
|
}, |
||||||
|
validateLedingOrTrailingWhiteSpace(v) { |
||||||
|
return !/^\s+|\s+$/.test(v) || 'Leading or trailing whitespace not allowed in table name' |
||||||
|
}, |
||||||
|
validateDuplicate(v) { |
||||||
|
return (this.tables || []).every((t) => t.table_name.toLowerCase() !== (v || '').toLowerCase()) || 'Duplicate table name' |
||||||
|
}, |
||||||
|
onCreateBtnClick() { |
||||||
|
this.$emit('create', { |
||||||
|
...this.table, |
||||||
|
columns: this.table.columns.map((c) => (c === 'id' && this.idType === 'AG' ? 'id_ag' : c)), |
||||||
|
}) |
||||||
|
}, |
||||||
|
}, |
||||||
|
} */ |
||||||
|
</script> |
||||||
|
|
||||||
|
<template> |
||||||
|
<v-dialog |
||||||
|
v-model="dialogShow" |
||||||
|
persistent |
||||||
|
max-width="550" |
||||||
|
@keydown.esc="dialogShow = false" |
||||||
|
@keydown.enter="$emit('create', table)" |
||||||
|
> |
||||||
|
<!-- Create A New Table --> |
||||||
|
<v-card class="elevation-1 backgroundColor nc-create-table-card"> |
||||||
|
<v-form ref="form" v-model="valid"> |
||||||
|
<v-card-title class="primary subheading white--text py-2"> |
||||||
|
{{ $t('activity.createTable') }} |
||||||
|
</v-card-title> |
||||||
|
|
||||||
|
<v-card-text class="py-6 px-10"> |
||||||
|
<!-- hint="Enter table name" --> |
||||||
|
<v-text-field |
||||||
|
ref="input" |
||||||
|
v-model="table.title" |
||||||
|
solo |
||||||
|
flat |
||||||
|
persistent-hint |
||||||
|
dense |
||||||
|
hide-details1 |
||||||
|
:rules="[validateTableName, validateDuplicateAlias]" |
||||||
|
:hint="$t('msg.info.enterTableName')" |
||||||
|
class="mt-4 caption nc-table-name" |
||||||
|
/> |
||||||
|
|
||||||
|
<div class="d-flex justify-end"> |
||||||
|
<div class="grey--text caption pointer" @click="isAdvanceOptVisible = !isAdvanceOptVisible"> |
||||||
|
{{ isAdvanceOptVisible ? 'Hide' : 'Show' }} more |
||||||
|
<v-icon x-small color="grey"> |
||||||
|
{{ isAdvanceOptVisible ? 'mdi-minus-circle-outline' : 'mdi-plus-circle-outline' }} |
||||||
|
</v-icon> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
|
||||||
|
<div class="nc-table-advanced-options" :class="{ active: isAdvanceOptVisible }"> |
||||||
|
<!-- hint="Table name as saved in database" --> |
||||||
|
<v-text-field |
||||||
|
v-if="!projectPrefix" |
||||||
|
v-model="table.table_name" |
||||||
|
solo |
||||||
|
flat |
||||||
|
dense |
||||||
|
persistent-hint |
||||||
|
:rules="[validateDuplicate]" |
||||||
|
:hint="$t('msg.info.tableNameInDb')" |
||||||
|
class="mt-4 caption nc-table-name-alias" |
||||||
|
/> |
||||||
|
|
||||||
|
<div class="mt-5"> |
||||||
|
<label class="add-default-title grey--text"> |
||||||
|
<!-- Add Default Columns --> |
||||||
|
{{ $t('msg.info.addDefaultColumns') }} |
||||||
|
</label> |
||||||
|
|
||||||
|
<div class="d-flex caption justify-space-between align-center"> |
||||||
|
<v-checkbox |
||||||
|
key="chk1" |
||||||
|
v-model="table.columns" |
||||||
|
dense |
||||||
|
class="mt-0" |
||||||
|
color="info" |
||||||
|
hide-details |
||||||
|
value="id" |
||||||
|
@click.capture.prevent.stop=" |
||||||
|
() => { |
||||||
|
$toast.info('ID column is required, you can rename this later if required.').goAway(3000) |
||||||
|
if (!table.columns.includes('id')) { |
||||||
|
table.columns.push('id') |
||||||
|
} |
||||||
|
} |
||||||
|
" |
||||||
|
> |
||||||
|
<template #label> |
||||||
|
<div> |
||||||
|
<span v-if="!isIdToggleAllowed" class="caption" @dblclick="isIdToggleAllowed = true">id</span> |
||||||
|
<v-select |
||||||
|
v-else |
||||||
|
v-model="idType" |
||||||
|
style="max-width: 100px" |
||||||
|
class="caption" |
||||||
|
outlined |
||||||
|
dense |
||||||
|
hide-details |
||||||
|
:items="idTypes" |
||||||
|
/> |
||||||
|
</div> |
||||||
|
</template> |
||||||
|
</v-checkbox> |
||||||
|
<v-checkbox key="chk2" v-model="table.columns" dense class="mt-0" color="info" hide-details value="title"> |
||||||
|
<template #label> |
||||||
|
<span class="caption">title</span> |
||||||
|
</template> |
||||||
|
</v-checkbox> |
||||||
|
<v-checkbox key="chk3" v-model="table.columns" dense class="mt-0" color="info" hide-details value="created_at"> |
||||||
|
<template #label> |
||||||
|
<span class="caption">created_at</span> |
||||||
|
</template> |
||||||
|
</v-checkbox> |
||||||
|
<v-checkbox key="chk4" v-model="table.columns" dense class="mt-0" color="info" hide-details value="updated_at"> |
||||||
|
<template #label> |
||||||
|
<span class="caption">updated_at</span> |
||||||
|
</template> |
||||||
|
</v-checkbox> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</v-card-text> |
||||||
|
<v-divider /> |
||||||
|
<v-card-actions class="py-4 px-10"> |
||||||
|
<v-spacer /> |
||||||
|
<v-btn class="" @click="dialogShow = false"> |
||||||
|
{{ $t('general.cancel') }} |
||||||
|
</v-btn> |
||||||
|
<v-btn :disabled="!valid" color="primary" class="nc-create-table-submit" @click="createTable"> |
||||||
|
{{ $t('general.submit') }} |
||||||
|
</v-btn> |
||||||
|
</v-card-actions> |
||||||
|
</v-form> |
||||||
|
</v-card> |
||||||
|
</v-dialog> |
||||||
|
</template> |
||||||
|
|
||||||
|
<style scoped lang="scss"> |
||||||
|
::v-deep { |
||||||
|
.v-text-field__details { |
||||||
|
padding: 0 2px !important; |
||||||
|
|
||||||
|
.v-messages:not(.error--text) { |
||||||
|
.v-messages__message { |
||||||
|
color: grey; |
||||||
|
font-size: 0.65rem; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
.add-default-title { |
||||||
|
font-size: 0.65rem; |
||||||
|
} |
||||||
|
|
||||||
|
.nc-table-advanced-options { |
||||||
|
max-height: 0; |
||||||
|
transition: 0.3s max-height; |
||||||
|
overflow: hidden; |
||||||
|
|
||||||
|
&.active { |
||||||
|
max-height: 200px; |
||||||
|
} |
||||||
|
} |
||||||
|
</style> |
@ -0,0 +1,46 @@ |
|||||||
|
import { useNuxtApp } from '#app' |
||||||
|
|
||||||
|
export default (onTableCreate?: (tableMeta: any) => void) => { |
||||||
|
const table = reactive<{ title: string; table_name: string }>({ |
||||||
|
title: '', |
||||||
|
table_name: '', |
||||||
|
}) |
||||||
|
|
||||||
|
const { sqlUi, project } = useProject() |
||||||
|
const { $api } = useNuxtApp() |
||||||
|
|
||||||
|
const createTable = async () => { |
||||||
|
if (!sqlUi?.value) return |
||||||
|
const columns = sqlUi?.value?.getNewTableColumns().filter((col) => { |
||||||
|
// if (col.column_name === "id" && newTable.columns.includes("id_ag")) {
|
||||||
|
// Object.assign(col, sqlUi?.value?.getDataTypeForUiType({ uidt: UITypes.ID }, "AG"));
|
||||||
|
//
|
||||||
|
// col.dtxp = sqlUi?.value?.getDefaultLengthForDatatype(col.dt);
|
||||||
|
// col.dtxs = sqlUi?.value?.getDefaultScaleForDatatype(col.dt);
|
||||||
|
//
|
||||||
|
// return true;
|
||||||
|
// }
|
||||||
|
// return this.nodes.newTable.columns.includes(col.column_name);
|
||||||
|
return true |
||||||
|
}) |
||||||
|
await $api.dbTable.create(project?.value?.id as string, { |
||||||
|
...table, |
||||||
|
columns, |
||||||
|
}) |
||||||
|
|
||||||
|
onTableCreate?.({}) |
||||||
|
} |
||||||
|
|
||||||
|
watch( |
||||||
|
() => table.title, |
||||||
|
(title) => { |
||||||
|
table.table_name = `${project?.value?.prefix || ''}${title}` |
||||||
|
}, |
||||||
|
) |
||||||
|
|
||||||
|
const generateUniqueTitle = () => { |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
return { table, createTable } |
||||||
|
} |
@ -1,2 +1,67 @@ |
|||||||
export const isEmail = (v: string) => |
export const isEmail = (v: string) => |
||||||
/^(([^<>()[\].,;:\s@"]+(\.[^<>()[\].,;:\s@"]+)*)|(".+"))@(([^<>()[\].,;:\s@"]+\.)+[^<>()[\].,;:\s@"]{2,})$/i.test(v) |
/^(([^<>()[\].,;:\s@"]+(\.[^<>()[\].,;:\s@"]+)*)|(".+"))@(([^<>()[\].,;:\s@"]+\.)+[^<>()[\].,;:\s@"]{2,})$/i.test(v) |
||||||
|
|
||||||
|
export function validateTableName(v: string, isGQL = false) { |
||||||
|
if (!v) { |
||||||
|
return 'Table name required' |
||||||
|
} |
||||||
|
|
||||||
|
// GraphQL naming convention
|
||||||
|
// http://spec.graphql.org/June2018/#Name
|
||||||
|
|
||||||
|
if (isGQL) { |
||||||
|
if (/^[_A-Za-z][_0-9A-Za-z]*$/.test(v)) { |
||||||
|
return true |
||||||
|
} |
||||||
|
|
||||||
|
if (/^[^_A-Za-z]/.test(v)) { |
||||||
|
return 'Name should start with an alphabet or _' |
||||||
|
} |
||||||
|
const m = v.match(/[^_A-Za-z\d]/g) |
||||||
|
if (m) { |
||||||
|
return `Following characters are not allowed ${m.map((c) => JSON.stringify(c)).join(', ')}` |
||||||
|
} |
||||||
|
} else { |
||||||
|
// exclude . / \
|
||||||
|
// rest all characters allowed
|
||||||
|
// https://documentation.sas.com/doc/en/pgmsascdc/9.4_3.5/acreldb/n0rfg6x1shw0ppn1cwhco6yn09f7.htm#:~:text=By%20default%2C%20MySQL%20encloses%20column,not%20truncate%20a%20longer%20name.
|
||||||
|
const m = v.match(/[./\\]/g) |
||||||
|
if (m) { |
||||||
|
return `Following characters are not allowed ${m.map((c) => JSON.stringify(c)).join(', ')}` |
||||||
|
} |
||||||
|
|
||||||
|
return true |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export function validateColumnName(v: string, isGQL = false) { |
||||||
|
if (!v) { |
||||||
|
return 'Column name required' |
||||||
|
} |
||||||
|
|
||||||
|
// GraphQL naming convention
|
||||||
|
// http://spec.graphql.org/June2018/#Name
|
||||||
|
if (isGQL) { |
||||||
|
if (/^[_A-Za-z][_0-9A-Za-z]*$/.test(v)) { |
||||||
|
return true |
||||||
|
} |
||||||
|
|
||||||
|
if (/^[^_A-Za-z]/.test(v)) { |
||||||
|
return 'Name should start with an alphabet or _' |
||||||
|
} |
||||||
|
const m = v.match(/[^_A-Za-z\d]/g) |
||||||
|
if (m) { |
||||||
|
return `Following characters are not allowed ${m.map((c) => JSON.stringify(c)).join(', ')}` |
||||||
|
} |
||||||
|
} else { |
||||||
|
// exclude . / \
|
||||||
|
// rest all characters allowed
|
||||||
|
// https://documentation.sas.com/doc/en/pgmsascdc/9.4_3.5/acreldb/n0rfg6x1shw0ppn1cwhco6yn09f7.htm#:~:text=By%20default%2C%20MySQL%20encloses%20column,not%20truncate%20a%20longer%20name.
|
||||||
|
const m = v.match(/[./\\]/g) |
||||||
|
if (m) { |
||||||
|
return `Following characters are not allowed ${m.map((c) => JSON.stringify(c)).join(', ')}` |
||||||
|
} |
||||||
|
|
||||||
|
return true |
||||||
|
} |
||||||
|
} |
||||||
|
Loading…
Reference in new issue