After Width: | Height: | Size: 554 B |
After Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 949 B After Width: | Height: | Size: 928 B |
Before Width: | Height: | Size: 301 B After Width: | Height: | Size: 320 B |
After Width: | Height: | Size: 774 B |
After Width: | Height: | Size: 1.4 KiB |
After Width: | Height: | Size: 1.5 KiB |
After Width: | Height: | Size: 828 B |
After Width: | Height: | Size: 716 B |
After Width: | Height: | Size: 1.2 KiB |
After Width: | Height: | Size: 2.4 KiB |
After Width: | Height: | Size: 1.4 KiB |
After Width: | Height: | Size: 4.1 KiB |
After Width: | Height: | Size: 2.1 KiB |
After Width: | Height: | Size: 9.4 KiB |
After Width: | Height: | Size: 5.1 KiB |
After Width: | Height: | Size: 3.5 KiB |
After Width: | Height: | Size: 4.6 KiB |
After Width: | Height: | Size: 5.3 KiB |
After Width: | Height: | Size: 6.6 KiB |
@ -0,0 +1,129 @@ |
|||||||
|
<script setup lang="ts"> |
||||||
|
const { t } = useI18n() |
||||||
|
|
||||||
|
const { loadSetupApps, emailConfigured, storageConfigured, listModalDlg } = useAccountSetupStoreOrThrow() |
||||||
|
|
||||||
|
// const { appInfo } = useGlobal() |
||||||
|
|
||||||
|
const openedCategory = ref<string | null>(null) |
||||||
|
|
||||||
|
const configs = computed(() => [ |
||||||
|
{ |
||||||
|
title: t('labels.configLabel', { label: t('labels.email') }), |
||||||
|
key: 'email', |
||||||
|
description: |
||||||
|
'Configure your preferred email service to manage how your application sends alerts, notifications and other essential emails.', |
||||||
|
docsLink: 'https://docs.nocodb.com/account-settings/oss-specific-details#configure-email', |
||||||
|
buttonClick: () => { |
||||||
|
navigateTo(`/account/setup/email${emailConfigured.value ? `/${emailConfigured.value.title}` : ''}`) |
||||||
|
}, |
||||||
|
itemClick: () => { |
||||||
|
navigateTo(`/account/setup/email`) |
||||||
|
}, |
||||||
|
configured: emailConfigured.value, |
||||||
|
}, |
||||||
|
{ |
||||||
|
title: t('labels.configLabel', { label: t('labels.storage') }), |
||||||
|
key: 'storage', |
||||||
|
description: 'Set up and manage your preferred storage solution for securely handling and storing your application’s data.', |
||||||
|
docsLink: 'https://docs.nocodb.com/account-settings/oss-specific-details#configure-storage', |
||||||
|
buttonClick: () => { |
||||||
|
navigateTo(`/account/setup/storage${storageConfigured.value ? `/${storageConfigured.value.title}` : ''}`) |
||||||
|
}, |
||||||
|
itemClick: () => { |
||||||
|
navigateTo(`/account/setup/storage`) |
||||||
|
}, |
||||||
|
configured: storageConfigured.value, |
||||||
|
}, |
||||||
|
// { |
||||||
|
// title: t('labels.switchToProd'), |
||||||
|
// key: 'switchToProd', |
||||||
|
// description: 'Switch to production-ready app database from existing application database.', |
||||||
|
// docsLink: 'https://docs.nocodb.com', |
||||||
|
// buttonClick: () => { |
||||||
|
// // TODO: Implement the logic to switch to production |
||||||
|
// }, |
||||||
|
// isPending: !(appInfo.value as any)?.prodReady, |
||||||
|
// }, |
||||||
|
]) |
||||||
|
|
||||||
|
onMounted(async () => { |
||||||
|
await loadSetupApps() |
||||||
|
}) |
||||||
|
</script> |
||||||
|
|
||||||
|
<template> |
||||||
|
<div class="flex flex-col" data-test-id="nc-setup-main"> |
||||||
|
<NcPageHeader> |
||||||
|
<template #icon> |
||||||
|
<div class="flex justify-center items-center h-5 w-5"> |
||||||
|
<GeneralIcon icon="ncSliders" class="flex-none text-[20px]" /> |
||||||
|
</div> |
||||||
|
</template> |
||||||
|
<template #title> |
||||||
|
<span data-rec="true"> |
||||||
|
{{ $t('labels.setup') }} |
||||||
|
</span> |
||||||
|
</template> |
||||||
|
</NcPageHeader> |
||||||
|
|
||||||
|
<div |
||||||
|
class="nc-content-max-w flex-1 max-h-[calc(100vh_-_100px)] overflow-y-auto nc-scrollbar-thin flex flex-col items-center gap-6 p-6" |
||||||
|
> |
||||||
|
<div class="flex flex-col gap-6 w-150"> |
||||||
|
<div |
||||||
|
v-for="config of configs" |
||||||
|
class="flex flex-col border-1 rounded-2xl border-gray-200 p-6 gap-2 hover:(shadow bg-gray-10)" |
||||||
|
:class="{ |
||||||
|
'cursor-pointer': config.itemClick, |
||||||
|
}" |
||||||
|
:data-testid="`nc-setup-${config.key}`" |
||||||
|
@click="config.itemClick" |
||||||
|
> |
||||||
|
<div class="flex gap-3 items-center" data-rec="true"> |
||||||
|
<NcTooltip v-if="!config.configured || config.isPending"> |
||||||
|
<template #title> |
||||||
|
<span> |
||||||
|
{{ $t('activity.pending') }} |
||||||
|
</span> |
||||||
|
</template> |
||||||
|
<GeneralIcon icon="ncAlertCircle" class="text-orange-500 -mt-1 w-6 h-6 nc-pending" /> |
||||||
|
</NcTooltip> |
||||||
|
<GeneralIcon v-else icon="circleCheckSolid" class="text-success w-6 h-6 bg-white-500 nc-configured" /> |
||||||
|
|
||||||
|
<span class="font-bold text-base"> {{ config.title }}</span> |
||||||
|
</div> |
||||||
|
<div class="text-gray-600 text-sm">{{ config.description }}</div> |
||||||
|
|
||||||
|
<div class="flex justify-between mt-4"> |
||||||
|
<NcButton |
||||||
|
size="small" |
||||||
|
type="text" |
||||||
|
:href="config.docsLink" |
||||||
|
target="_blank" |
||||||
|
class="!flex items-center !no-underline" |
||||||
|
rel="noopener noreferer" |
||||||
|
@click.stop |
||||||
|
> |
||||||
|
<div class="flex gap-2 items-center"> |
||||||
|
Go to docs |
||||||
|
<GeneralIcon icon="ncExternalLink" /> |
||||||
|
</div> |
||||||
|
</NcButton> |
||||||
|
<NcButton v-if="config.configured" size="small" type="text" @click.stop="config.buttonClick"> |
||||||
|
<div class="flex gap-2 items-center"> |
||||||
|
<GeneralIcon icon="ncEdit3" /> |
||||||
|
{{ $t('general.edit') }} |
||||||
|
</div> |
||||||
|
</NcButton> |
||||||
|
<NcButton v-else size="small" @click.stop="config.buttonClick">{{ $t('general.configure') }}</NcButton> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
|
||||||
|
<LazyAccountSetupListModal v-if="openedCategory" v-model="listModalDlg" :category="openedCategory" /> |
||||||
|
</div> |
||||||
|
</template> |
||||||
|
|
||||||
|
<style scoped lang="scss"></style> |
@ -0,0 +1,13 @@ |
|||||||
|
<script setup lang="ts"> |
||||||
|
defineProps<{ |
||||||
|
app: { |
||||||
|
title: string |
||||||
|
logo: string |
||||||
|
} |
||||||
|
}>() |
||||||
|
</script> |
||||||
|
|
||||||
|
<template> |
||||||
|
<img v-if="app.title !== 'SMTP'" class="object-contain" :alt="app.title" :src="app.logo" /> |
||||||
|
<GeneralIcon v-else class="text-gray-500" icon="mail" /> |
||||||
|
</template> |
@ -0,0 +1,175 @@ |
|||||||
|
<script setup lang="ts"> |
||||||
|
import dayjs from 'dayjs' |
||||||
|
|
||||||
|
const props = defineProps<{ |
||||||
|
id: string |
||||||
|
modelValue?: boolean |
||||||
|
}>() |
||||||
|
const emit = defineEmits(['saved', 'close', 'update:modelValue']) |
||||||
|
|
||||||
|
const vOpen = useVModel(props, 'modelValue', emit) |
||||||
|
|
||||||
|
const { |
||||||
|
readPluginDetails, |
||||||
|
activePluginFormData: pluginFormData, |
||||||
|
activePlugin: plugin, |
||||||
|
isLoading, |
||||||
|
loadingAction, |
||||||
|
testSettings, |
||||||
|
saveSettings, |
||||||
|
} = useAccountSetupStoreOrThrow() |
||||||
|
|
||||||
|
await readPluginDetails(props.id) |
||||||
|
|
||||||
|
const pluginTypeMap = { |
||||||
|
Input: FormBuilderInputType.Input, |
||||||
|
Select: FormBuilderInputType.Select, |
||||||
|
Checkbox: FormBuilderInputType.Switch, |
||||||
|
LongText: FormBuilderInputType.Input, |
||||||
|
Password: FormBuilderInputType.Password, |
||||||
|
} |
||||||
|
|
||||||
|
const { formState, validate, validateInfos } = useProvideFormBuilderHelper({ |
||||||
|
formSchema: [ |
||||||
|
...plugin.value.formDetails.items.flatMap((item, i) => [ |
||||||
|
{ |
||||||
|
type: pluginTypeMap[item.type] || FormBuilderInputType.Input, |
||||||
|
label: item.label, |
||||||
|
placeholder: item.placeholder, |
||||||
|
model: item.key, |
||||||
|
required: item.required, |
||||||
|
helpText: item.help_text, |
||||||
|
width: '48', |
||||||
|
border: false, |
||||||
|
showHintAsTooltip: true, |
||||||
|
}, |
||||||
|
...(i % 2 |
||||||
|
? [] |
||||||
|
: [ |
||||||
|
{ |
||||||
|
type: FormBuilderInputType.Space, |
||||||
|
width: '4', |
||||||
|
}, |
||||||
|
]), |
||||||
|
]), |
||||||
|
], |
||||||
|
initialState: pluginFormData, |
||||||
|
}) |
||||||
|
|
||||||
|
const doAction = async (action: Action) => { |
||||||
|
try { |
||||||
|
switch (action) { |
||||||
|
case Action.Save: |
||||||
|
await validate() |
||||||
|
pluginFormData.value = formState.value |
||||||
|
await saveSettings() |
||||||
|
vOpen.value = false |
||||||
|
break |
||||||
|
case Action.Test: |
||||||
|
await validate() |
||||||
|
pluginFormData.value = formState.value |
||||||
|
await testSettings() |
||||||
|
break |
||||||
|
} |
||||||
|
} catch (e: any) { |
||||||
|
console.log(e) |
||||||
|
} finally { |
||||||
|
loadingAction.value = null |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
const isValid = computed(() => { |
||||||
|
return Object.values(validateInfos || {}).every((info) => info.validateStatus !== 'error') |
||||||
|
}) |
||||||
|
|
||||||
|
const docLinks = computed(() => { |
||||||
|
return [ |
||||||
|
{ |
||||||
|
title: 'Application Setup', |
||||||
|
url: `https://docs.nocodb.com/account-settings/oss-specific-details#configure-${plugin.value?.category?.toLowerCase()}`, |
||||||
|
}, |
||||||
|
...(plugin.value?.formDetails?.docs || []), |
||||||
|
] |
||||||
|
}) |
||||||
|
</script> |
||||||
|
|
||||||
|
<template> |
||||||
|
<div class="flex flex-col h-full h-[calc(100vh_-_40px)]" data-testid="nc-setup-config"> |
||||||
|
<NcPageHeader> |
||||||
|
<template #title> |
||||||
|
<div class="flex gap-3 items-center"> |
||||||
|
<AccountSetupAppIcon :app="plugin" class="h-8 w-8" /> |
||||||
|
|
||||||
|
<span data-rec="true"> |
||||||
|
{{ plugin.title }} |
||||||
|
</span> |
||||||
|
</div> |
||||||
|
</template> |
||||||
|
</NcPageHeader> |
||||||
|
<div class="h-full flex h-[calc(100%_-_48px)]"> |
||||||
|
<div class="nc-config-left-panel nc-scrollbar-thin relative h-full flex flex-col"> |
||||||
|
<div class="w-full flex items-center gap-3 border-gray-200 py-6 px-6"> |
||||||
|
<span class="font-semibold text-base">{{ $t('labels.configuration') }}</span> |
||||||
|
<div class="flex-grow" /> |
||||||
|
|
||||||
|
<div class="flex gap-2"> |
||||||
|
<NcButton |
||||||
|
v-for="(action, i) in plugin.formDetails.actions" |
||||||
|
:key="i" |
||||||
|
:loading="loadingAction === action.key" |
||||||
|
:type="action.key === Action.Save ? 'primary' : 'default'" |
||||||
|
size="small" |
||||||
|
:disabled="!!loadingAction || !isValid" |
||||||
|
:data-testid="`nc-setup-config-action-${action.key?.toLowerCase()}`" |
||||||
|
@click="doAction(action.key)" |
||||||
|
> |
||||||
|
{{ action.label }} |
||||||
|
</NcButton> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<div class="h-[calc(100%_-_48px)] flex py-4 flex-col p-6 overflow-auto"> |
||||||
|
<div v-if="isLoading || !plugin" class="flex flex-row w-full justify-center items-center h-52"> |
||||||
|
<a-spin size="large" /> |
||||||
|
</div> |
||||||
|
|
||||||
|
<div v-else class="flex"> |
||||||
|
<NcFormBuilder class="w-229 px-2 mx-auto" /> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<div class="nc-config-right-panel"> |
||||||
|
<div class="flex-grow flex flex-col gap-3"> |
||||||
|
<div class="text-gray-500 text-capitalize">{{ $t('labels.documentation') }}</div> |
||||||
|
<a |
||||||
|
v-for="doc of docLinks" |
||||||
|
:key="doc.title" |
||||||
|
:href="doc.url" |
||||||
|
target="_blank" |
||||||
|
rel="noopener noreferrer" |
||||||
|
class="!no-underline !text-current flex gap-2 items-center" |
||||||
|
> |
||||||
|
<GeneralIcon icon="bookOpen" class="text-gray-500" /> |
||||||
|
{{ doc.title }} |
||||||
|
</a> |
||||||
|
|
||||||
|
<NcDivider /> |
||||||
|
|
||||||
|
<div class="text-gray-500 text-capitalize">{{ $t('labels.modifiedOn') }}</div> |
||||||
|
<div class=""> |
||||||
|
{{ dayjs(plugin.created_at).format('DD MMM YYYY HH:mm') }} |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</template> |
||||||
|
|
||||||
|
<style lang="scss" scoped> |
||||||
|
.nc-config-left-panel { |
||||||
|
@apply w-full flex-1 flex justify-stretch; |
||||||
|
} |
||||||
|
|
||||||
|
.nc-config-right-panel { |
||||||
|
@apply p-5 w-[320px] border-l-1 border-gray-200 flex flex-col gap-4 bg-gray-50 rounded-br-2xl; |
||||||
|
} |
||||||
|
</style> |
@ -0,0 +1,149 @@ |
|||||||
|
<script setup lang="ts"> |
||||||
|
const props = defineProps<{ |
||||||
|
category: string |
||||||
|
}>() |
||||||
|
|
||||||
|
const { categorizeApps, resetPlugin: _resetPlugin, showPluginUninstallModal, activePlugin } = useAccountSetupStoreOrThrow() |
||||||
|
|
||||||
|
const apps = computed(() => categorizeApps.value?.[props.category?.toLowerCase()] || []) |
||||||
|
const configuredApp = computed(() => apps.value.find((app: any) => app.active)) |
||||||
|
|
||||||
|
const showResetActiveAppMsg = ref(false) |
||||||
|
const switchingTo = ref(null) |
||||||
|
|
||||||
|
const showResetPluginModal = async (app: any, resetActiveAppMsg = false) => { |
||||||
|
showResetActiveAppMsg.value = resetActiveAppMsg |
||||||
|
showPluginUninstallModal.value = true |
||||||
|
activePlugin.value = app |
||||||
|
} |
||||||
|
|
||||||
|
const selectApp = (app: any) => { |
||||||
|
const activeApp = app !== configuredApp.value && configuredApp.value |
||||||
|
if (activeApp) { |
||||||
|
switchingTo.value = app |
||||||
|
return showResetPluginModal(activeApp, true) |
||||||
|
} |
||||||
|
|
||||||
|
navigateTo(`/account/setup/${props.category}/${app.title}`) |
||||||
|
} |
||||||
|
|
||||||
|
const resetPlugin = async () => { |
||||||
|
await _resetPlugin(activePlugin.value) |
||||||
|
if (showResetActiveAppMsg.value) { |
||||||
|
await selectApp(switchingTo.value) |
||||||
|
switchingTo.value = null |
||||||
|
showResetActiveAppMsg.value = false |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
const closeResetModal = () => { |
||||||
|
activePlugin.value = null |
||||||
|
switchingTo.value = null |
||||||
|
showResetActiveAppMsg.value = false |
||||||
|
showPluginUninstallModal.value = false |
||||||
|
} |
||||||
|
</script> |
||||||
|
|
||||||
|
<template> |
||||||
|
<div class="flex flex-col" data-testid="nc-setup-list"> |
||||||
|
<NcPageHeader> |
||||||
|
<template #title> |
||||||
|
<span data-rec="true"> |
||||||
|
{{ category }} |
||||||
|
</span> |
||||||
|
</template> |
||||||
|
</NcPageHeader> |
||||||
|
<div class="h-[calc(100%_-_58px)] flex"> |
||||||
|
<div class="w-full"> |
||||||
|
<div class="w-950px px-4 mt-3 mx-auto text-lg font-weight-bold">{{ category }} Services</div> |
||||||
|
<div class="container"> |
||||||
|
<div |
||||||
|
v-for="app in apps" |
||||||
|
:key="app.title" |
||||||
|
class="item group" |
||||||
|
:data-testid="`nc-setup-list-item-${app.title}`" |
||||||
|
@click="selectApp(app)" |
||||||
|
> |
||||||
|
<AccountSetupAppIcon :app="app" class="icon" /> |
||||||
|
<span class="title">{{ app.title }}</span> |
||||||
|
<div class="flex-grow" /> |
||||||
|
|
||||||
|
<GeneralIcon |
||||||
|
v-if="app.active" |
||||||
|
icon="delete" |
||||||
|
class="text-error min-w-6 h-6 bg-white-500 !hidden !group-hover:!inline cursor-pointer" |
||||||
|
/> |
||||||
|
<GeneralIcon |
||||||
|
v-if="app === configuredApp" |
||||||
|
icon="circleCheckSolid" |
||||||
|
class="text-success min-w-5 h-5 bg-white-500 nc-configured" |
||||||
|
/> |
||||||
|
|
||||||
|
<NcDropdown :trigger="['click']" overlay-class-name="!rounded-md" @click.stop> |
||||||
|
<GeneralIcon |
||||||
|
v-if="app.active" |
||||||
|
icon="threeDotVertical" |
||||||
|
class="min-w-5 h-5 bg-white-500 text-gray-500 hover:text-current nc-setup-plugin-menu" |
||||||
|
/> |
||||||
|
|
||||||
|
<template #overlay> |
||||||
|
<NcMenu class="min-w-20"> |
||||||
|
<NcMenuItem data-testid="nc-config-reset" @click.stop="showResetPluginModal(app)"> |
||||||
|
<span> {{ $t('general.reset') }} </span> |
||||||
|
</NcMenuItem> |
||||||
|
</NcMenu> |
||||||
|
</template> |
||||||
|
</NcDropdown> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
|
||||||
|
<a-modal |
||||||
|
v-model:visible="showPluginUninstallModal" |
||||||
|
:closable="false" |
||||||
|
width="448px" |
||||||
|
centered |
||||||
|
:footer="null" |
||||||
|
wrap-class-name="nc-modal-plugin-reset-conform" |
||||||
|
> |
||||||
|
<div class="flex flex-col h-full"> |
||||||
|
<div v-if="showResetActiveAppMsg" class="text-base font-weight-bold"> |
||||||
|
Switch to {{ switchingTo && switchingTo.title }} |
||||||
|
</div> |
||||||
|
<div v-else class="text-base font-weight-bold">Reset {{ activePlugin && activePlugin.title }} Configuration</div> |
||||||
|
<div class="flex flex-row mt-2 w-full"> |
||||||
|
<template v-if="showResetActiveAppMsg"> |
||||||
|
Switching to {{ switchingTo && switchingTo.title }} will reset your {{ activePlugin && activePlugin.title }} |
||||||
|
settings. Continue? |
||||||
|
</template> |
||||||
|
<template v-else>Resetting will erase your current configuration.</template> |
||||||
|
</div> |
||||||
|
<div class="flex mt-6 justify-end space-x-2"> |
||||||
|
<NcButton size="small" type="secondary" @click="closeResetModal"> {{ $t('general.cancel') }}</NcButton> |
||||||
|
<NcButton size="small" type="danger" data-testid="nc-reset-confirm-btn" @click="resetPlugin"> |
||||||
|
{{ showResetActiveAppMsg ? `${$t('general.reset')} & ${$t('general.switch')}` : $t('general.reset') }} |
||||||
|
</NcButton> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</a-modal> |
||||||
|
</div> |
||||||
|
</template> |
||||||
|
|
||||||
|
<style scoped lang="scss"> |
||||||
|
.container { |
||||||
|
@apply p-4 w-950px gap-5 mx-auto my-2 grid grid-cols-3; |
||||||
|
|
||||||
|
.item { |
||||||
|
@apply text-base w-296px max-w-296px flex gap-3 border-1 border-gray-200 py-4 px-5 rounded-xl items-center cursor-pointer hover:(shadow bg-gray-50); |
||||||
|
|
||||||
|
.icon { |
||||||
|
@apply !w-8 !h-8 object-contain; |
||||||
|
} |
||||||
|
|
||||||
|
.title { |
||||||
|
@apply font-weight-bold; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
</style> |
@ -0,0 +1,182 @@ |
|||||||
|
<script setup lang="ts"> |
||||||
|
import type { TableType } from 'nocodb-sdk' |
||||||
|
import type { ComponentPublicInstance } from '@vue/runtime-core' |
||||||
|
|
||||||
|
interface Props { |
||||||
|
modelValue?: boolean |
||||||
|
tableMeta: TableType |
||||||
|
sourceId: string |
||||||
|
} |
||||||
|
|
||||||
|
const { tableMeta, ...props } = defineProps<Props>() |
||||||
|
|
||||||
|
const emit = defineEmits(['update:modelValue', 'updated']) |
||||||
|
|
||||||
|
const { $e, $api } = useNuxtApp() |
||||||
|
|
||||||
|
const { setMeta } = useMetas() |
||||||
|
|
||||||
|
const dialogShow = useVModel(props, 'modelValue', emit) |
||||||
|
|
||||||
|
const { loadProjectTables } = useTablesStore() |
||||||
|
|
||||||
|
const baseStore = useBase() |
||||||
|
|
||||||
|
const { loadTables } = baseStore |
||||||
|
|
||||||
|
const { addUndo, defineProjectScope } = useUndoRedo() |
||||||
|
|
||||||
|
const inputEl = ref<HTMLTextAreaElement>() |
||||||
|
|
||||||
|
const loading = ref(false) |
||||||
|
|
||||||
|
const useForm = Form.useForm |
||||||
|
|
||||||
|
const formState = reactive({ |
||||||
|
description: '', |
||||||
|
}) |
||||||
|
|
||||||
|
const validators = computed(() => { |
||||||
|
return { |
||||||
|
description: [ |
||||||
|
{ |
||||||
|
validator: (_: any, _value: any) => { |
||||||
|
return new Promise<void>((resolve, _reject) => { |
||||||
|
resolve() |
||||||
|
}) |
||||||
|
}, |
||||||
|
}, |
||||||
|
], |
||||||
|
} |
||||||
|
}) |
||||||
|
|
||||||
|
const { validateInfos } = useForm(formState, validators) |
||||||
|
|
||||||
|
watchEffect( |
||||||
|
() => { |
||||||
|
if (tableMeta?.description) formState.description = `${tableMeta.description}` |
||||||
|
|
||||||
|
nextTick(() => { |
||||||
|
const input = inputEl.value?.$el as HTMLInputElement |
||||||
|
|
||||||
|
if (input) { |
||||||
|
input.setSelectionRange(0, formState.description.length) |
||||||
|
input.focus() |
||||||
|
} |
||||||
|
}) |
||||||
|
}, |
||||||
|
{ flush: 'post' }, |
||||||
|
) |
||||||
|
|
||||||
|
const updateDescription = async (undo = false) => { |
||||||
|
if (!tableMeta) return |
||||||
|
|
||||||
|
if (formState.description) { |
||||||
|
formState.description = formState.description.trim() |
||||||
|
} |
||||||
|
|
||||||
|
loading.value = true |
||||||
|
try { |
||||||
|
await $api.dbTable.update(tableMeta.id as string, { |
||||||
|
base_id: tableMeta.base_id, |
||||||
|
description: formState.description, |
||||||
|
}) |
||||||
|
|
||||||
|
dialogShow.value = false |
||||||
|
|
||||||
|
await loadProjectTables(tableMeta.base_id!, true) |
||||||
|
|
||||||
|
if (!undo) { |
||||||
|
addUndo({ |
||||||
|
redo: { |
||||||
|
fn: (t: string) => { |
||||||
|
formState.description = t |
||||||
|
updateDescription(true, true) |
||||||
|
}, |
||||||
|
args: [formState.description], |
||||||
|
}, |
||||||
|
undo: { |
||||||
|
fn: (t: string) => { |
||||||
|
formState.description = t |
||||||
|
updateDescription(true, true) |
||||||
|
}, |
||||||
|
args: [tableMeta.description], |
||||||
|
}, |
||||||
|
scope: defineProjectScope({ model: tableMeta }), |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
await loadTables() |
||||||
|
|
||||||
|
// update metas |
||||||
|
const newMeta = await $api.dbTable.read(tableMeta.id as string) |
||||||
|
await setMeta(newMeta) |
||||||
|
|
||||||
|
$e('a:table:description:update') |
||||||
|
|
||||||
|
dialogShow.value = false |
||||||
|
} catch (e: any) { |
||||||
|
message.error(await extractSdkResponseErrorMsg(e)) |
||||||
|
} |
||||||
|
|
||||||
|
loading.value = false |
||||||
|
} |
||||||
|
</script> |
||||||
|
|
||||||
|
<template> |
||||||
|
<NcModal v-model:visible="dialogShow" size="small" :show-separator="false"> |
||||||
|
<template #header> |
||||||
|
<div class="flex flex-row items-center gap-x-2"> |
||||||
|
<GeneralIcon icon="table" class="w-6 h-6 text-gray-700" /> |
||||||
|
<span class="text-gray-900 font-bold"> |
||||||
|
{{ tableMeta?.title ?? tableMeta?.table_name }} |
||||||
|
</span> |
||||||
|
</div> |
||||||
|
</template> |
||||||
|
<div class="mt-1"> |
||||||
|
<a-form layout="vertical" :model="formState" name="create-new-table-form"> |
||||||
|
<a-form-item :label="$t('labels.description')" v-bind="validateInfos.description"> |
||||||
|
<a-textarea |
||||||
|
ref="inputEl" |
||||||
|
v-model:value="formState.description" |
||||||
|
class="nc-input-sm !py-2 nc-text-area nc-input-shadow" |
||||||
|
hide-details |
||||||
|
size="small" |
||||||
|
:placeholder="$t('msg.info.enterTableDescription')" |
||||||
|
@keydown.enter.exact="() => updateDescription()" |
||||||
|
/> |
||||||
|
</a-form-item> |
||||||
|
</a-form> |
||||||
|
<div class="flex flex-row justify-end gap-x-2 mt-5"> |
||||||
|
<NcButton type="secondary" size="small" @click="dialogShow = false">{{ $t('general.cancel') }}</NcButton> |
||||||
|
|
||||||
|
<NcButton |
||||||
|
key="submit" |
||||||
|
type="primary" |
||||||
|
size="small" |
||||||
|
:disabled=" |
||||||
|
validateInfos?.description?.validateStatus === 'error' || formState.description?.trim() === tableMeta?.description |
||||||
|
" |
||||||
|
:loading="loading" |
||||||
|
@click="() => updateDescription()" |
||||||
|
> |
||||||
|
{{ $t('general.save') }} |
||||||
|
</NcButton> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</NcModal> |
||||||
|
</template> |
||||||
|
|
||||||
|
<style scoped lang="scss"> |
||||||
|
.nc-text-area { |
||||||
|
@apply !py-2 min-h-[120px] max-h-[200px]; |
||||||
|
} |
||||||
|
|
||||||
|
:deep(.ant-form-item-label > label) { |
||||||
|
@apply !text-md font-base !leading-[20px] text-gray-800 flex; |
||||||
|
|
||||||
|
&.ant-form-item-required:not(.ant-form-item-required-mark-optional)::before { |
||||||
|
@apply content-[''] m-0; |
||||||
|
} |
||||||
|
} |
||||||
|
</style> |
@ -0,0 +1,170 @@ |
|||||||
|
<script setup lang="ts"> |
||||||
|
import type { ViewType } from 'nocodb-sdk' |
||||||
|
import type { ComponentPublicInstance } from '@vue/runtime-core' |
||||||
|
|
||||||
|
interface Props { |
||||||
|
modelValue?: boolean |
||||||
|
view: ViewType |
||||||
|
sourceId?: string |
||||||
|
} |
||||||
|
|
||||||
|
const { view, ...props } = defineProps<Props>() |
||||||
|
|
||||||
|
const emit = defineEmits(['update:modelValue', 'updated']) |
||||||
|
|
||||||
|
const { $e, $api } = useNuxtApp() |
||||||
|
|
||||||
|
const dialogShow = useVModel(props, 'modelValue', emit) |
||||||
|
|
||||||
|
const { loadViews } = useViewsStore() |
||||||
|
|
||||||
|
const { addUndo, defineProjectScope } = useUndoRedo() |
||||||
|
|
||||||
|
const inputEl = ref<ComponentPublicInstance>() |
||||||
|
|
||||||
|
const loading = ref(false) |
||||||
|
|
||||||
|
const useForm = Form.useForm |
||||||
|
|
||||||
|
const formState = reactive({ |
||||||
|
description: '', |
||||||
|
}) |
||||||
|
|
||||||
|
const validators = computed(() => { |
||||||
|
return { |
||||||
|
description: [ |
||||||
|
{ |
||||||
|
validator: (_: any, _value: any) => { |
||||||
|
return new Promise<void>((resolve, _reject) => { |
||||||
|
resolve() |
||||||
|
}) |
||||||
|
}, |
||||||
|
}, |
||||||
|
], |
||||||
|
} |
||||||
|
}) |
||||||
|
|
||||||
|
const { validateInfos } = useForm(formState, validators) |
||||||
|
|
||||||
|
watchEffect( |
||||||
|
() => { |
||||||
|
if (view?.description) formState.description = `${view.description}` |
||||||
|
|
||||||
|
nextTick(() => { |
||||||
|
const input = inputEl.value?.$el as HTMLInputElement |
||||||
|
|
||||||
|
if (input) { |
||||||
|
input.setSelectionRange(0, formState.description.length) |
||||||
|
input.focus() |
||||||
|
} |
||||||
|
}) |
||||||
|
}, |
||||||
|
{ flush: 'post' }, |
||||||
|
) |
||||||
|
|
||||||
|
const updateDescription = async (undo = false) => { |
||||||
|
if (!view) return |
||||||
|
|
||||||
|
if (formState.description) { |
||||||
|
formState.description = formState.description.trim() |
||||||
|
} |
||||||
|
|
||||||
|
loading.value = true |
||||||
|
try { |
||||||
|
await $api.dbView.update(view.id as string, { |
||||||
|
description: formState.description, |
||||||
|
}) |
||||||
|
|
||||||
|
dialogShow.value = false |
||||||
|
|
||||||
|
if (!undo) { |
||||||
|
addUndo({ |
||||||
|
redo: { |
||||||
|
fn: (t: string) => { |
||||||
|
formState.description = t |
||||||
|
updateDescription(true, true) |
||||||
|
}, |
||||||
|
args: [formState.description], |
||||||
|
}, |
||||||
|
undo: { |
||||||
|
fn: (t: string) => { |
||||||
|
formState.description = t |
||||||
|
updateDescription(true, true) |
||||||
|
}, |
||||||
|
args: [view.description], |
||||||
|
}, |
||||||
|
scope: defineProjectScope({ view }), |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
await loadViews({ tableId: view.fk_model_id, ignoreLoading: true, force: true }) |
||||||
|
|
||||||
|
$e('a:view:description:update') |
||||||
|
|
||||||
|
dialogShow.value = false |
||||||
|
} catch (e: any) { |
||||||
|
message.error(await extractSdkResponseErrorMsg(e)) |
||||||
|
} |
||||||
|
|
||||||
|
loading.value = false |
||||||
|
} |
||||||
|
</script> |
||||||
|
|
||||||
|
<template> |
||||||
|
<NcModal v-model:visible="dialogShow" size="small" :show-separator="false"> |
||||||
|
<template #header> |
||||||
|
<div class="flex flex-row items-center gap-x-2"> |
||||||
|
<GeneralViewIcon :meta="view" class="mt-0.5 !text-2xl" /> |
||||||
|
|
||||||
|
<span class="text-gray-900 font-semibold"> |
||||||
|
{{ view?.title }} |
||||||
|
</span> |
||||||
|
</div> |
||||||
|
</template> |
||||||
|
<div class="mt-1"> |
||||||
|
<a-form layout="vertical" :model="formState" name="create-new-table-form"> |
||||||
|
<a-form-item :label="$t('labels.description')" v-bind="validateInfos.description"> |
||||||
|
<a-textarea |
||||||
|
ref="inputEl" |
||||||
|
v-model:value="formState.description" |
||||||
|
class="nc-input-sm !py-2 nc-text-area !text-gray-800 nc-input-shadow" |
||||||
|
hide-details |
||||||
|
size="small" |
||||||
|
:placeholder="$t('msg.info.enterTableDescription')" |
||||||
|
@keydown.enter.exact="() => updateDescription()" |
||||||
|
/> |
||||||
|
</a-form-item> |
||||||
|
</a-form> |
||||||
|
<div class="flex flex-row justify-end gap-x-2 mt-5"> |
||||||
|
<NcButton type="secondary" size="small" @click="dialogShow = false">{{ $t('general.cancel') }}</NcButton> |
||||||
|
|
||||||
|
<NcButton |
||||||
|
key="submit" |
||||||
|
type="primary" |
||||||
|
size="small" |
||||||
|
:disabled=" |
||||||
|
validateInfos?.description?.validateStatus === 'error' || formState.description?.trim() === view?.description |
||||||
|
" |
||||||
|
:loading="loading" |
||||||
|
@click="() => updateDescription()" |
||||||
|
> |
||||||
|
{{ $t('general.save') }} |
||||||
|
</NcButton> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</NcModal> |
||||||
|
</template> |
||||||
|
|
||||||
|
<style scoped lang="scss"> |
||||||
|
.nc-text-area { |
||||||
|
@apply !py-2 min-h-[120px] max-h-[200px]; |
||||||
|
} |
||||||
|
|
||||||
|
:deep(.ant-form-item-label > label) { |
||||||
|
@apply !leading-[20px] font-base !text-md text-gray-800 flex; |
||||||
|
|
||||||
|
&.ant-form-item-required:not(.ant-form-item-required-mark-optional)::before { |
||||||
|
@apply content-[''] m-0; |
||||||
|
} |
||||||
|
} |
||||||
|
</style> |
@ -0,0 +1,27 @@ |
|||||||
|
<script lang="ts" setup> |
||||||
|
const props = withDefaults( |
||||||
|
defineProps<{ |
||||||
|
type: keyof typeof allIntegrationsMapByValue |
||||||
|
size: 'sx' | 'sm' | 'md' | 'lg' | 'xl' | 'xxl' |
||||||
|
}>(), |
||||||
|
{ |
||||||
|
size: 'sm', |
||||||
|
}, |
||||||
|
) |
||||||
|
</script> |
||||||
|
|
||||||
|
<template> |
||||||
|
<component |
||||||
|
:is="allIntegrationsMapByValue[props.type]?.icon" |
||||||
|
v-if="allIntegrationsMapByValue[props.type]?.icon" |
||||||
|
class="stroke-transparent flex-none" |
||||||
|
:class="{ |
||||||
|
'w-3.5 h-3.5': size === 'sx', |
||||||
|
'w-4 h-4': size === 'sm', |
||||||
|
'w-5 h-5': size === 'md', |
||||||
|
'w-6 h-6': size === 'lg', |
||||||
|
'w-7 h-7': size === 'xl', |
||||||
|
'w-8 h-8': size === 'xxl', |
||||||
|
}" |
||||||
|
/> |
||||||
|
</template> |
@ -0,0 +1,81 @@ |
|||||||
|
<script lang="ts" setup> |
||||||
|
const initState = ref({ |
||||||
|
someDefaultProp: 'value', |
||||||
|
}) |
||||||
|
|
||||||
|
const { formState, isLoading, submit } = useProvideFormBuilderHelper({ |
||||||
|
formSchema: [ |
||||||
|
{ |
||||||
|
type: FormBuilderInputType.Input, |
||||||
|
label: 'Sample Input', |
||||||
|
width: 100, |
||||||
|
model: 'title', |
||||||
|
placeholder: 'Some placeholder', |
||||||
|
category: 'General', |
||||||
|
required: true, |
||||||
|
}, |
||||||
|
{ |
||||||
|
type: FormBuilderInputType.Input, |
||||||
|
label: 'Input To Nested Path', |
||||||
|
width: 50, |
||||||
|
model: 'config.sample', |
||||||
|
placeholder: 'This is added to config.sample', |
||||||
|
category: 'Sample Category', |
||||||
|
helpText: 'This is a sample help text', |
||||||
|
required: true, |
||||||
|
}, |
||||||
|
{ |
||||||
|
type: FormBuilderInputType.Space, |
||||||
|
width: 50, |
||||||
|
category: 'Sample Category', |
||||||
|
}, |
||||||
|
{ |
||||||
|
type: FormBuilderInputType.Input, |
||||||
|
label: 'Multiple Elements in Category', |
||||||
|
width: 50, |
||||||
|
model: 'config.sample2', |
||||||
|
placeholder: 'This is added to config.sample2', |
||||||
|
category: 'Sample Category', |
||||||
|
required: false, |
||||||
|
}, |
||||||
|
{ |
||||||
|
type: FormBuilderInputType.Select, |
||||||
|
label: 'Sample Select', |
||||||
|
width: 100, |
||||||
|
model: 'config.select', |
||||||
|
category: 'Settings', |
||||||
|
options: [ |
||||||
|
{ label: 'Option 1', value: 'option1' }, |
||||||
|
{ label: 'Option 2', value: 'option2' }, |
||||||
|
{ label: 'Option 3', value: 'option3' }, |
||||||
|
], |
||||||
|
defaultValue: 'option2', |
||||||
|
required: true, |
||||||
|
}, |
||||||
|
{ |
||||||
|
type: FormBuilderInputType.Switch, |
||||||
|
label: 'Sample Switch', |
||||||
|
width: 100, |
||||||
|
model: 'config.switch', |
||||||
|
category: 'Misc', |
||||||
|
helpText: 'This is a sample switch', |
||||||
|
required: false, |
||||||
|
border: true, |
||||||
|
}, |
||||||
|
], |
||||||
|
onSubmit: async () => { |
||||||
|
console.log('submit', formState) |
||||||
|
}, |
||||||
|
initialState: initState, |
||||||
|
}) |
||||||
|
</script> |
||||||
|
|
||||||
|
<template> |
||||||
|
<div class="h-full"> |
||||||
|
<NcFormBuilder /> |
||||||
|
<div class="mt-10"></div> |
||||||
|
<NcButton :loading="isLoading" type="primary" @click="submit">Submit</NcButton> |
||||||
|
</div> |
||||||
|
</template> |
||||||
|
|
||||||
|
<style lang="scss" scoped></style> |
@ -0,0 +1,265 @@ |
|||||||
|
<script lang="ts" setup> |
||||||
|
const { form, formState, formElementsCategorized, isLoading, validateInfos } = useFormBuilderHelperOrThrow() |
||||||
|
|
||||||
|
const deepReference = (path: string): any => { |
||||||
|
return path.split('.').reduce((acc, key) => acc[key], formState.value) |
||||||
|
} |
||||||
|
|
||||||
|
const setFormState = (path: string, value: any) => { |
||||||
|
// update nested prop in formState |
||||||
|
const keys = path.split('.') |
||||||
|
const lastKey = keys.pop() |
||||||
|
|
||||||
|
if (!lastKey) return |
||||||
|
|
||||||
|
const target = keys.reduce((acc, key) => { |
||||||
|
if (!acc[key]) { |
||||||
|
acc[key] = {} |
||||||
|
} |
||||||
|
return acc[key] |
||||||
|
}, formState.value) |
||||||
|
target[lastKey] = value |
||||||
|
} |
||||||
|
</script> |
||||||
|
|
||||||
|
<template> |
||||||
|
<div class="nc-form-builder nc-scrollbar-thin relative"> |
||||||
|
<a-form ref="form" :model="formState" hide-required-mark layout="vertical" class="flex flex-col gap-4"> |
||||||
|
<template v-for="category in Object.keys(formElementsCategorized)" :key="category"> |
||||||
|
<div class="nc-form-section"> |
||||||
|
<div v-if="category !== FORM_BUILDER_NON_CATEGORIZED" class="nc-form-section-title">{{ category }}</div> |
||||||
|
<div class="nc-form-section-body"> |
||||||
|
<div class="flex flex-wrap"> |
||||||
|
<template v-for="field in formElementsCategorized[category]" :key="field.model"> |
||||||
|
<div |
||||||
|
v-if="field.type === FormBuilderInputType.Space" |
||||||
|
:style="`width:${+field.width || 100}%`" |
||||||
|
class="w-full" |
||||||
|
></div> |
||||||
|
<a-form-item |
||||||
|
v-else |
||||||
|
v-bind="validateInfos[field.model]" |
||||||
|
class="nc-form-item" |
||||||
|
:style="`width:${+field.width || 100}%`" |
||||||
|
:required="false" |
||||||
|
:data-testid="`nc-form-input-${field.model}`" |
||||||
|
> |
||||||
|
<template v-if="![FormBuilderInputType.Switch].includes(field.type)" #label> |
||||||
|
<div class="flex items-center gap-1"> |
||||||
|
<span>{{ field.label }}</span> |
||||||
|
<span v-if="field.required" class="text-red-500">*</span> |
||||||
|
<NcTooltip v-if="field.helpText && field.showHintAsTooltip"> |
||||||
|
<template #title> |
||||||
|
<div class="text-xs"> |
||||||
|
{{ field.helpText }} |
||||||
|
</div> |
||||||
|
</template> |
||||||
|
<GeneralIcon icon="info" class="text-gray-500 h-4" /> |
||||||
|
</NcTooltip> |
||||||
|
</div> |
||||||
|
</template> |
||||||
|
<template v-if="field.type === FormBuilderInputType.Input"> |
||||||
|
<a-input |
||||||
|
autocomplete="off" |
||||||
|
class="!w-full" |
||||||
|
:value="deepReference(field.model)" |
||||||
|
@update:value="setFormState(field.model, $event)" |
||||||
|
/> |
||||||
|
</template> |
||||||
|
<template v-else-if="field.type === FormBuilderInputType.Password"> |
||||||
|
<a-input-password |
||||||
|
readonly |
||||||
|
onfocus="this.removeAttribute('readonly');" |
||||||
|
onblur="this.setAttribute('readonly', true);" |
||||||
|
autocomplete="off" |
||||||
|
:value="deepReference(field.model)" |
||||||
|
@update:value="setFormState(field.model, $event)" |
||||||
|
/> |
||||||
|
</template> |
||||||
|
<template v-else-if="field.type === FormBuilderInputType.Select"> |
||||||
|
<NcSelect |
||||||
|
:value="deepReference(field.model)" |
||||||
|
:options="field.options" |
||||||
|
@update:value="setFormState(field.model, $event)" |
||||||
|
/> |
||||||
|
</template> |
||||||
|
<template v-else-if="field.type === FormBuilderInputType.Switch"> |
||||||
|
<div class="flex flex-col p-2" :class="field.border ? 'border-1 rounded-lg shadow' : ''"> |
||||||
|
<div class="flex items-center"> |
||||||
|
<NcSwitch :checked="!!deepReference(field.model)" @update:checked="setFormState(field.model, $event)" /> |
||||||
|
<span class="ml-[6px] font-bold">{{ field.label }}</span> |
||||||
|
<NcTooltip v-if="field.helpText"> |
||||||
|
<template #title> |
||||||
|
<div class="text-xs"> |
||||||
|
{{ field.helpText }} |
||||||
|
</div> |
||||||
|
</template> |
||||||
|
<GeneralIcon icon="info" class="text-gray-500 h-4 ml-1" /> |
||||||
|
</NcTooltip> |
||||||
|
</div> |
||||||
|
<div v-if="field.helpText && !field.showHintAsTooltip" class="w-full mt-1 pl-[35px]"> |
||||||
|
<div class="text-xs text-gray-500">{{ field.helpText }}</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</template> |
||||||
|
<div |
||||||
|
v-if="field.helpText && field.type !== FormBuilderInputType.Switch && !field.showHintAsTooltip" |
||||||
|
class="w-full mt-1" |
||||||
|
> |
||||||
|
<div class="text-xs text-gray-500">{{ field.helpText }}</div> |
||||||
|
</div> |
||||||
|
</a-form-item> |
||||||
|
</template> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</template> |
||||||
|
</a-form> |
||||||
|
<general-overlay :model-value="isLoading" inline transition class="!bg-opacity-15"> |
||||||
|
<div class="flex items-center justify-center h-full w-full !bg-white !bg-opacity-85 z-1000"> |
||||||
|
<a-spin size="large" /> |
||||||
|
</div> |
||||||
|
</general-overlay> |
||||||
|
</div> |
||||||
|
</template> |
||||||
|
|
||||||
|
<style lang="scss" scoped> |
||||||
|
.nc-form-item { |
||||||
|
margin-bottom: 12px; |
||||||
|
} |
||||||
|
|
||||||
|
:deep(.ant-collapse-header) { |
||||||
|
@apply !-mt-4 !p-0 flex items-center !cursor-default children:first:flex; |
||||||
|
} |
||||||
|
|
||||||
|
:deep(.ant-collapse-icon-position-right > .ant-collapse-item > .ant-collapse-header .ant-collapse-arrow) { |
||||||
|
@apply !right-0; |
||||||
|
} |
||||||
|
|
||||||
|
:deep(.ant-collapse-content-box) { |
||||||
|
@apply !px-0 !pb-0 !pt-3; |
||||||
|
} |
||||||
|
|
||||||
|
:deep(.ant-form-item-explain-error) { |
||||||
|
@apply !text-xs; |
||||||
|
} |
||||||
|
|
||||||
|
:deep(.ant-divider) { |
||||||
|
@apply m-0; |
||||||
|
} |
||||||
|
|
||||||
|
:deep(.ant-form-item-with-help .ant-form-item-explain) { |
||||||
|
@apply !min-h-0; |
||||||
|
} |
||||||
|
|
||||||
|
:deep(.ant-select .ant-select-selector .ant-select-selection-item) { |
||||||
|
@apply font-weight-400; |
||||||
|
} |
||||||
|
|
||||||
|
.nc-form-builder { |
||||||
|
:deep(.ant-input-affix-wrapper), |
||||||
|
:deep(.ant-input), |
||||||
|
:deep(.ant-select) { |
||||||
|
@apply !appearance-none border-solid rounded-md; |
||||||
|
} |
||||||
|
|
||||||
|
:deep(.ant-input-password) { |
||||||
|
input { |
||||||
|
@apply !border-none my-0; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
.nc-form-section { |
||||||
|
@apply flex flex-col gap-3; |
||||||
|
} |
||||||
|
|
||||||
|
.nc-form-section-title { |
||||||
|
@apply text-sm font-bold text-gray-800; |
||||||
|
} |
||||||
|
|
||||||
|
.nc-form-section-body { |
||||||
|
@apply flex flex-col gap-3; |
||||||
|
} |
||||||
|
|
||||||
|
:deep(.ant-form-item-label > label.ant-form-item-required:after) { |
||||||
|
@apply content-['*'] inline-block text-inherit text-red-500 ml-1; |
||||||
|
} |
||||||
|
|
||||||
|
:deep(.ant-form-item) { |
||||||
|
&.ant-form-item-has-error { |
||||||
|
&:not(:has(.ant-input-password)) .ant-input { |
||||||
|
&:not(:hover):not(:focus):not(:disabled) { |
||||||
|
@apply shadow-default; |
||||||
|
} |
||||||
|
|
||||||
|
&:hover:not(:focus):not(:disabled) { |
||||||
|
@apply shadow-hover; |
||||||
|
} |
||||||
|
|
||||||
|
&:focus { |
||||||
|
@apply shadow-error ring-0; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
.ant-input-number, |
||||||
|
.ant-input-affix-wrapper.ant-input-password { |
||||||
|
&:not(:hover):not(:focus-within):not(:disabled) { |
||||||
|
@apply shadow-default; |
||||||
|
} |
||||||
|
|
||||||
|
&:hover:not(:focus-within):not(:disabled) { |
||||||
|
@apply shadow-hover; |
||||||
|
} |
||||||
|
|
||||||
|
&:focus-within { |
||||||
|
@apply shadow-error ring-0; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
&:not(.ant-form-item-has-error) { |
||||||
|
&:not(:has(.ant-input-password)) .ant-input { |
||||||
|
&:not(:hover):not(:focus):not(:disabled) { |
||||||
|
@apply shadow-default border-gray-200; |
||||||
|
} |
||||||
|
|
||||||
|
&:hover:not(:focus):not(:disabled) { |
||||||
|
@apply border-gray-200 shadow-hover; |
||||||
|
} |
||||||
|
|
||||||
|
&:focus { |
||||||
|
@apply shadow-selected ring-0; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
.ant-input-number, |
||||||
|
.ant-input-affix-wrapper.ant-input-password { |
||||||
|
&:not(:hover):not(:focus-within):not(:disabled) { |
||||||
|
@apply shadow-default border-gray-200; |
||||||
|
} |
||||||
|
|
||||||
|
&:hover:not(:focus-within):not(:disabled) { |
||||||
|
@apply border-gray-200 shadow-hover; |
||||||
|
} |
||||||
|
|
||||||
|
&:focus-within { |
||||||
|
@apply shadow-selected ring-0; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
:deep(.ant-row:not(.ant-form-item)) { |
||||||
|
@apply !-mx-1.5; |
||||||
|
& > .ant-col { |
||||||
|
@apply !px-1.5; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
:deep(.ant-form-item) { |
||||||
|
@apply !mb-6; |
||||||
|
} |
||||||
|
} |
||||||
|
</style> |
||||||
|
|
||||||
|
<style lang="scss"></style> |