feat: use dropdown menu for base create options instead of modal

This commit is contained in:
Ramesh Mane
2026-01-22 10:31:54 +00:00
parent 74dc117bbc
commit 845d0e11b6
10 changed files with 254 additions and 238 deletions

View File

@@ -0,0 +1,70 @@
<script lang="ts" setup>
interface Props {
visible: boolean
variant: 'modal' | 'dropdown'
baseCreateMode: NcBaseCreateMode | null
}
const props = withDefaults(defineProps<Props>(), {
variant: 'dropdown',
})
const emits = defineEmits<{
(e: 'update:visible', value: boolean): void
(e: 'update:baseCreateMode', value: NcBaseCreateMode | null): void
(e: 'onSelect', mode: NcBaseCreateMode): void
}>()
const vVisible = useVModel(props, 'visible', emits)
const baseCreateMode = useVModel(props, 'baseCreateMode', emits)
const workspaceStore = useWorkspace()
const { navigateToTemplates } = workspaceStore
const { isTemplatesFeatureEnabled } = storeToRefs(workspaceStore)
const { isAiFeaturesEnabled } = useNocoAi()
const onClickOption = (mode: NcBaseCreateMode) => {
if (isTemplatesFeatureEnabled.value && mode === NcBaseCreateMode.FROM_TEMPLATE) {
vVisible.value = false
navigateToTemplates()
return
}
baseCreateMode.value = mode
}
onMounted(() => {
if (!isAiFeaturesEnabled.value && props.variant === 'modal') {
baseCreateMode.value = NcBaseCreateMode.FROM_SCRATCH
}
})
</script>
<template>
<NcMenu v-if="variant === 'dropdown'" variant="large" data-testid="nc-home-create-new-menu"
@click="vVisible = false">
<DashboardTreeViewCreateProjectMenuItem v-e="['c:base:create:scratch']" icon="plus" label="From Scratch"
subtext="Start with an empty base" @click="onClickOption(NcBaseCreateMode.FROM_SCRATCH)" />
<DashboardTreeViewCreateProjectMenuItem v-if="isAiFeaturesEnabled" v-e="['c:base:ai:create']"
icon="ncAutoAwesome" label="Build with AI" subtext="Pre-built structures for common use cases"
@click="onClickOption(NcBaseCreateMode.BUILD_WITH_AI)" />
</NcMenu>
<div v-else class="flex flex-row gap-6 flex-wrap max-w-[min(80vw,738px)] children:(!w-[230px] !max-w-[230px])">
<ProjectActionItem v-e="['c:base:create:scratch']" icon="plus" label="From Scratch"
subtext="Start with an empty base" @click="onClickOption(NcBaseCreateMode.FROM_SCRATCH)" />
<ProjectActionItem v-if="isAiFeaturesEnabled" v-e="['c:base:ai:create']" icon="ncAutoAwesome"
label="Build with AI" subtext="Pre-built structures for common use cases"
@click="onClickOption(NcBaseCreateMode.BUILD_WITH_AI)" />
</div>
</template>

View File

@@ -0,0 +1,39 @@
<script lang="ts" setup>
interface Props {
label: string
subtext?: string
icon?: IconMapKey
}
withDefaults(defineProps<Props>(), {
})
</script>
<template>
<NcMenuItem :inner-class="`w-full ${$slots.subtext || subtext ? '!items-start' : ''}`">
<slot name="icon">
<GeneralIcon v-if="icon" :icon="icon" class="h-4 w-4 flex-none" :class="{
'mt-0.5': $slots.subtext || subtext
}" />
</slot>
<div class="nc-content-wrapper">
<div class="nc-content-label">
<slot name="label">{{ label }}</slot>
</div>
<div v-if="$slots.subtext || subtext" class="nc-content-subtext">
<slot name="subtext">{{ subtext }}</slot>
</div>
</div>
</NcMenuItem>
</template>
<style lang="scss" scoped>
.nc-content-wrapper {
.nc-content-subtext {
@apply text-tiny !leading-4 text-nc-content-gray-muted;
}
}
</style>

View File

@@ -4,22 +4,22 @@ defineProps<{
label: string
subtext?: string
isLoading?: boolean
icon?: IconMapKey
}>()
</script>
<template>
<div
role="button"
class="nc-base-view-all-table-btn"
:class="{
disabled,
'loading cursor-wait': isLoading,
'cursor-pointer': !isLoading,
}"
>
<div role="button" class="nc-base-view-all-table-btn" :class="{
disabled,
'loading cursor-wait': isLoading,
'cursor-pointer': !isLoading,
}">
<div class="icon-wrapper">
<a-skeleton-avatar v-if="isLoading" active shape="square" class="!h-full !w-full !children:(rounded-md w-8 h-8)" />
<slot v-else name="icon" />
<a-skeleton-avatar v-if="isLoading" active shape="square"
class="!h-full !w-full !children:(rounded-md w-8 h-8)" />
<slot v-else name="icon">
<GeneralIcon v-if="icon" :icon="icon" />
</slot>
</div>
<div class="flex flex-col gap-1">
<div class="label">
@@ -70,6 +70,7 @@ defineProps<{
:deep(.ant-skeleton-title) {
@apply !my-0;
}
:deep(.ant-skeleton-paragraph) {
@apply !mb-1;
}

View File

@@ -64,67 +64,43 @@ const onCreateBaseClick = () => {
</script>
<template>
<div
class="nc-all-tables-view p-6 nc-scrollbar-thin"
:style="{
height: 'calc(100vh - var(--topbar-height) - 44px)',
}"
>
<div class="nc-all-tables-view p-6 nc-scrollbar-thin" :style="{
height: 'calc(100vh - var(--topbar-height) - 44px)',
}">
<div class="text-subHeading2 text-nc-content-gray mb-5">{{ $t('labels.actions') }}</div>
<div
class="flex flex-row gap-6 flex-wrap max-w-[1000px]"
:class="{
'pointer-events-none': base?.isLoading,
}"
>
<div class="flex flex-row gap-6 flex-wrap max-w-[1000px]" :class="{
'pointer-events-none': base?.isLoading,
}">
<template v-if="base?.isLoading">
<ProjectActionItem v-for="item in 7" :key="item" is-loading label="loading" />
</template>
<template v-else>
<ProjectActionItem
v-if="isUIAllowed('tableCreate', { source: base?.sources?.[0] })"
:label="$t('dashboards.create_new_table')"
:subtext="$t('msg.subText.startFromScratch')"
data-testid="proj-view-btn__add-new-table"
@click="openTableCreateDialog()"
>
<ProjectActionItem v-if="isUIAllowed('tableCreate', { source: base?.sources?.[0] })"
:label="$t('dashboards.create_new_table')" :subtext="$t('msg.subText.startFromScratch')"
data-testid="proj-view-btn__add-new-table" @click="openTableCreateDialog()">
<template #icon>
<GeneralIcon icon="addOutlineBox" class="!h-8 !w-8 !text-nc-content-brand" />
</template>
</ProjectActionItem>
<ProjectActionItem
v-if="isUIAllowed('tableCreate', { source: base?.sources?.[0] })"
v-e="['c:table:import']"
data-testid="proj-view-btn__import-data"
:label="`${$t('activity.import')} ${$t('general.data')}`"
:subtext="$t('msg.subText.importData')"
@click="isImportModalOpen = true"
>
<ProjectActionItem v-if="isUIAllowed('tableCreate', { source: base?.sources?.[0] })" v-e="['c:table:import']"
data-testid="proj-view-btn__import-data" :label="`${$t('activity.import')} ${$t('general.data')}`"
:subtext="$t('msg.subText.importData')" @click="isImportModalOpen = true">
<template #icon>
<GeneralIcon icon="download" class="!h-7.5 !w-7.5 !text-nc-content-orange-dark" />
</template>
</ProjectActionItem>
<NcTooltip
v-if="isUIAllowed('sourceCreate')"
placement="bottom"
:disabled="!isDataSourceLimitReached"
class="flex-none flex"
>
<NcTooltip v-if="isUIAllowed('sourceCreate')" placement="bottom" :disabled="!isDataSourceLimitReached"
class="flex-none flex">
<template #title>
{{ $t('tooltip.reachedSourceLimit') }}
</template>
<ProjectActionItem
v-e="['c:table:create-source']"
data-testid="proj-view-btn__create-source"
:disabled="isDataSourceLimitReached"
:label="$t('labels.connectDataSource')"
:subtext="$t('msg.subText.connectExternalData')"
@click="onCreateBaseClick"
>
<ProjectActionItem v-e="['c:table:create-source']" data-testid="proj-view-btn__create-source"
:disabled="isDataSourceLimitReached" :label="$t('labels.connectDataSource')"
:subtext="$t('msg.subText.connectExternalData')" @click="onCreateBaseClick">
<template #icon>
<GeneralIcon icon="server1" class="!h-7 !w-7 !text-nc-content-green-dark" />
</template>

View File

@@ -457,12 +457,11 @@ watch(
</div>
<!-- Footer -->
<div v-if="activeTab === 'publish' || activeTab === 'fork'"
class="px-6 py-4 border-t border-nc-border-gray-medium">
<div v-if="activeTab === 'publish' || activeTab === 'fork'" class="px- py-3 border-t border-nc-border-gray-medium">
<div class="flex justify-end gap-2">
<NcButton type="secondary" size="medium" @click="emit('update:visible', false)"> Cancel </NcButton>
<NcButton type="secondary" size="small" @click="emit('update:visible', false)"> Cancel </NcButton>
<NcButton v-if="activeTab === 'publish'" type="primary" size="medium" :loading="isLoading"
<NcButton v-if="activeTab === 'publish'" type="primary" size="small" :loading="isLoading"
@click="publishCurrentDraft">
<template #icon>
<GeneralIcon icon="upload" />
@@ -470,7 +469,7 @@ watch(
Publish Version
</NcButton>
<NcButton v-if="activeTab === 'fork'" type="primary" size="medium" :loading="isLoading"
<NcButton v-if="activeTab === 'fork'" type="primary" size="small" :loading="isLoading"
:disabled="!forkForm.version" @click="createNewDraft">
<template #icon>
<GeneralIcon icon="plus" />

View File

@@ -12,6 +12,8 @@ const { isUIAllowed } = useRoles()
const { orgRoles, workspaceRoles } = useRoles()
const { baseCreateMode } = storeToRefs(useBases())
const baseStore = useBase()
const { isSharedBase } = storeToRefs(baseStore)
@@ -22,30 +24,28 @@ const baseCreateDlg = ref(false)
const size = computed(() => props.size || 'small')
const centered = computed(() => props.centered ?? true)
onMounted(() => {
baseCreateMode.value = NcBaseCreateMode.FROM_SCRATCH
})
</script>
<template>
<NcButton
v-if="isUIAllowed('baseCreate', { roles: workspaceRoles ?? orgRoles }) && !isSharedBase"
v-e="['c:base:create']"
type="text"
:size="size"
:centered="centered"
full-width
@click="baseCreateDlg = true"
>
<NcButton v-if="isUIAllowed('baseCreate', { roles: workspaceRoles ?? orgRoles }) && !isSharedBase"
v-e="['c:base:create']" type="text" :size="size" :centered="centered" full-width @click="baseCreateDlg = true">
<slot>
<div class="flex items-center gap-2 w-full">
<GeneralIcon icon="ncPlusCircleSolid" />
<div class="flex flex-1">{{ $t('title.createBase') }}</div>
<div class="px-1 flex-none text-bodySmBold !leading-[18px] text-nc-content-gray-subtle bg-nc-bg-gray-medium rounded">
<div
class="px-1 flex-none text-bodySmBold !leading-[18px] text-nc-content-gray-subtle bg-nc-bg-gray-medium rounded">
{{ renderAltOrOptlKey(true) }} D
</div>
</div>
</slot>
<WorkspaceCreateProjectDlg v-model="baseCreateDlg" />
<WorkspaceCreateProjectDlg v-model="baseCreateDlg" :default-base-create-mode="baseCreateMode" />
</NcButton>
</template>

View File

@@ -4,6 +4,7 @@ import { stringToViewTypeMap } from 'nocodb-sdk'
interface Props {
dialogShow: boolean
aiMode: boolean | null
baseCreateMode: NcBaseCreateMode | null
workspaceId?: string
isCreateNewActionMenu?: boolean
initialValue?: {
@@ -14,7 +15,7 @@ interface Props {
const props = withDefaults(defineProps<Props>(), {})
const emit = defineEmits(['update:dialogShow', 'update:aiMode', 'navigateToProject'])
const emit = defineEmits(['update:dialogShow', 'update:aiMode', 'update:baseCreateMode', 'navigateToProject'])
enum SchemaPreviewTabs {
TABLES_AND_VIEWS = 'TABLES_AND_VIEWS',
@@ -27,6 +28,8 @@ const dialogShow = useVModel(props, 'dialogShow', emit)
const aiMode = useVModel(props, 'aiMode', emit)
const baseCreateMode = useVModel(props, 'baseCreateMode', emit)
const { workspaceId } = toRefs(props)
const { navigateToProject } = useGlobal()
@@ -311,6 +314,7 @@ const handleMouseLeaveTag = () => {
const resetToDefault = () => {
aiMode.value = null
baseCreateMode.value = null
aiStep.value = AI_STEP.PROMPT
oldAiFormState.value = null
aiFormState.value = defaultAiFormState
@@ -370,10 +374,8 @@ onMounted(() => {
</div>
<div class="h-[calc(100%_-_49px)] flex">
<div
ref="leftPaneContentRef"
class="w-[480px] h-full relative flex flex-col nc-scrollbar-thin border-r-1 border-nc-border-purple-light"
>
<div ref="leftPaneContentRef"
class="w-[480px] h-full relative flex flex-col nc-scrollbar-thin border-r-1 border-nc-border-purple-light">
<!-- create base config panel -->
<div class="flex-1 p-6 flex flex-col gap-6">
<div class="text-sm font-bold text-nc-content-purple-dark dark:text-nc-content-purple-medium">
@@ -383,38 +385,27 @@ onMounted(() => {
<!-- Predefined tags -->
<template v-for="prompt of predefinedBasePrompts" :key="prompt.tag">
<a-tag
class="nc-ai-base-schema-tag nc-ai-suggested-tag relative"
:class="{
'nc-selected': prompt.description === aiFormState.prompt.trim(),
'nc-disabled': !aiIntegrationAvailable || (aiLoading && callFunction === 'onPredictSchema'),
}"
:disabled="!aiIntegrationAvailable || (aiLoading && callFunction === 'onPredictSchema')"
@mouseover="handleMouseOverTag(prompt.description)"
@mouseleave="handleMouseLeaveTag"
@click="handleUpdatePrompt(prompt.description)"
>
<a-tag class="nc-ai-base-schema-tag nc-ai-suggested-tag relative" :class="{
'nc-selected': prompt.description === aiFormState.prompt.trim(),
'nc-disabled': !aiIntegrationAvailable || (aiLoading && callFunction === 'onPredictSchema'),
}" :disabled="!aiIntegrationAvailable || (aiLoading && callFunction === 'onPredictSchema')"
@mouseover="handleMouseOverTag(prompt.description)" @mouseleave="handleMouseLeaveTag"
@click="handleUpdatePrompt(prompt.description)">
<div class="flex flex-row items-center gap-1 py-1 text-sm">
<div>{{ prompt.tag }}</div>
</div>
<div
v-if="prompt.description === aiFormState.prompt.trim()"
class="bg-nc-fill-purple-dark text-nc-content-inverted-primary rounded-full absolute -right-[3px] -top-[4px] h-3 w-3 grid place-items-center"
>
<div v-if="prompt.description === aiFormState.prompt.trim()"
class="bg-nc-fill-purple-dark text-nc-content-inverted-primary rounded-full absolute -right-[3px] -top-[4px] h-3 w-3 grid place-items-center">
<svg xmlns="http://www.w3.org/2000/svg" width="8" height="8" viewBox="0 0 8 8" fill="none">
<path
d="M6.66659 2L2.99992 5.66667L1.33325 4"
stroke="currentColor"
stroke-width="1.33333"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path d="M6.66659 2L2.99992 5.66667L1.33325 4" stroke="currentColor" stroke-width="1.33333"
stroke-linecap="round" stroke-linejoin="round" />
</svg>
</div>
</a-tag>
</template>
<NcButton size="xs" type="text" icon-position="right" class="nc-show-more-tags-btn" @click="onToggleShowMore">
<NcButton size="xs" type="text" icon-position="right" class="nc-show-more-tags-btn"
@click="onToggleShowMore">
{{ isExpandedPredefiendBasePromts ? $t('general.showLess') : $t('general.showMore') }}
<template #icon>
@@ -423,16 +414,11 @@ onMounted(() => {
</NcButton>
</div>
<div>
<a-textarea
ref="aiPromptInputRef"
:value="aiFormState.onHoverTagPrompt || aiFormState.prompt"
<a-textarea ref="aiPromptInputRef" :value="aiFormState.onHoverTagPrompt || aiFormState.prompt"
placeholder="Type something..."
class="!w-full !min-h-[120px] !rounded-lg mt-2 overflow-y-auto nc-scrollbar-thin nc-input-shadow nc-ai-input"
size="middle"
:disabled="!aiIntegrationAvailable || (aiLoading && callFunction === 'onPredictSchema')"
:maxlength="8192"
@update:value="aiFormState.prompt = $event"
/>
size="middle" :disabled="!aiIntegrationAvailable || (aiLoading && callFunction === 'onPredictSchema')"
:maxlength="8192" @update:value="aiFormState.prompt = $event" />
</div>
<a-collapse v-model:active-key="expansionPanel" ghost class="flex-1 flex flex-col">
@@ -440,22 +426,13 @@ onMounted(() => {
<a-collapse-panel :key="ExpansionPanelKeys.additionalDetails" collapsible="disabled">
<template #header>
<div class="flex">
<NcButton
size="small"
type="text"
icon-position="right"
class="-ml-[7px]"
@click="handleUpdateExpansionPanel(ExpansionPanelKeys.additionalDetails)"
>
<NcButton size="small" type="text" icon-position="right" class="-ml-[7px]"
@click="handleUpdateExpansionPanel(ExpansionPanelKeys.additionalDetails)">
{{ $t('title.additionalDetails') }}
<template #icon>
<GeneralIcon
icon="arrowDown"
class="transform transition-all opacity-80"
:class="{
'rotate-180': expansionPanel.includes(ExpansionPanelKeys.additionalDetails),
}"
/>
<GeneralIcon icon="arrowDown" class="transform transition-all opacity-80" :class="{
'rotate-180': expansionPanel.includes(ExpansionPanelKeys.additionalDetails),
}" />
</template>
</NcButton>
</div>
@@ -464,25 +441,18 @@ onMounted(() => {
<div class="flex flex-col gap-6 pt-6">
<div v-for="field of additionalDetails" :key="field.title" class="flex items-center gap-2">
<div class="min-w-[120px] text-nc-content-gray">{{ field.title }}</div>
<a-input
v-model:value="aiFormState[field.key]"
class="nc-input-sm nc-input-shadow nc-ai-input"
hide-details
:placeholder="field.placeholder"
:disabled="!aiIntegrationAvailable || (aiLoading && callFunction === 'onPredictSchema')"
/>
<a-input v-model:value="aiFormState[field.key]" class="nc-input-sm nc-input-shadow nc-ai-input"
hide-details :placeholder="field.placeholder"
:disabled="!aiIntegrationAvailable || (aiLoading && callFunction === 'onPredictSchema')" />
</div>
</div>
</a-collapse-panel>
</a-collapse>
</div>
<div
class="sticky bottom-0 w-full bg-nc-bg-default px-6 pt-3 pb-6 border-t-1 flex flex-col gap-3"
:class="{
'border-nc-border-gray-medium': showBtnTopBorder,
'border-transparent': !showBtnTopBorder,
}"
>
<div class="sticky bottom-0 w-full bg-nc-bg-default px-6 pt-3 pb-6 border-t-1 flex flex-col gap-3" :class="{
'border-nc-border-gray-medium': showBtnTopBorder,
'border-transparent': !showBtnTopBorder,
}">
<div v-if="aiError" class="w-full flex items-start gap-3 bg-nc-bg-red-light rounded-lg p-4">
<GeneralIcon icon="ncInfoSolid" class="flex-none !text-nc-content-red-dark w-6 h-6" />
@@ -497,35 +467,20 @@ onMounted(() => {
</div>
</div>
<div v-if="aiIntegrationAvailable" class="flex items-center gap-3">
<NcButton
size="small"
:type="aiStep !== AI_STEP.MODIFY || isOldPromptChanged ? 'primary' : 'secondary'"
theme="ai"
class="w-1/2"
<NcButton size="small" :type="aiStep !== AI_STEP.MODIFY || isOldPromptChanged ? 'primary' : 'secondary'"
theme="ai" class="w-1/2"
:disabled="!aiFormState.prompt?.trim() || (aiLoading && callFunction === 'onPredictSchema')"
:loading="aiLoading && callFunction === 'onPredictSchema'"
@click="onPredictSchema"
>
:loading="aiLoading && callFunction === 'onPredictSchema'" @click="onPredictSchema">
<template #icon>
<GeneralIcon icon="ncAutoAwesome" class="h-4 w-4" />
</template>
{{ $t('labels.suggestTablesViews') }}
</NcButton>
<NcButton
v-e="['a:base:ai:create']"
type="primary"
size="small"
theme="ai"
class="w-1/2"
:disabled="
aiStep !== AI_STEP.MODIFY ||
finalSchema?.tables?.length === 0 ||
(aiLoading && callFunction === 'onCreateSchema') ||
isOldPromptChanged
"
:loading="aiLoading && callFunction === 'onCreateSchema'"
@click="onCreateSchema"
>
<NcButton v-e="['a:base:ai:create']" type="primary" size="small" theme="ai" class="w-1/2" :disabled="aiStep !== AI_STEP.MODIFY ||
finalSchema?.tables?.length === 0 ||
(aiLoading && callFunction === 'onCreateSchema') ||
isOldPromptChanged
" :loading="aiLoading && callFunction === 'onCreateSchema'" @click="onCreateSchema">
{{ $t('activity.createProject') }}
</NcButton>
</div>
@@ -546,10 +501,8 @@ onMounted(() => {
<!-- create base preview panel -->
<template v-if="aiStep === AI_STEP.LOADING || aiStep === AI_STEP.PROMPT">
<div
v-if="aiStep === AI_STEP.LOADING"
class="text-sm font-bold text-nc-content-purple-dark dark:text-nc-content-purple-medium"
>
<div v-if="aiStep === AI_STEP.LOADING"
class="text-sm font-bold text-nc-content-purple-dark dark:text-nc-content-purple-medium">
{{ $t('title.generatingBaseTailoredToYourRequirement') }}
</div>
<div v-else class="text-sm font-bold text-nc-content-purple-dark dark:text-nc-content-purple-medium">
@@ -557,34 +510,23 @@ onMounted(() => {
</div>
<template v-if="aiStep === AI_STEP.LOADING">
<div
v-for="(loadingText, idx) of activeLoadingText"
:key="idx"
class="text-sm text-nc-content-purple-light flex items-center"
>
<div v-for="(loadingText, idx) of activeLoadingText" :key="idx"
class="text-sm text-nc-content-purple-light flex items-center">
{{ loadingText }}
<div v-if="loadingText.length === loadingMessages[idx]?.length" class="nc-animate-dots"></div>
</div>
<div class="rounded-xl border-1 border-nc-border-purple-light">
<div
v-for="idx in 7"
:key="idx"
class="px-3 py-2 flex items-center gap-2 border-b-1 border-nc-border-purple-light !last-of-type:border-b-0"
>
<div v-for="idx in 7" :key="idx"
class="px-3 py-2 flex items-center gap-2 border-b-1 border-nc-border-purple-light !last-of-type:border-b-0">
<div class="flex-1 flex items-center gap-2">
<a-skeleton-input
:active="aiStep === AI_STEP.LOADING"
class="!w-4 !h-4 !rounded overflow-hidden !bg-nc-bg-purple-light"
/>
<a-skeleton-input
:active="aiStep === AI_STEP.LOADING"
class="!h-4 !rounded overflow-hidden !bg-nc-bg-purple-light"
:class="{
<a-skeleton-input :active="aiStep === AI_STEP.LOADING"
class="!w-4 !h-4 !rounded overflow-hidden !bg-nc-bg-purple-light" />
<a-skeleton-input :active="aiStep === AI_STEP.LOADING"
class="!h-4 !rounded overflow-hidden !bg-nc-bg-purple-light" :class="{
'!w-[133px]': idx % 2 === 0,
'!w-[90px]': idx % 2 !== 0,
}"
/>
}" />
</div>
<div class="grid place-items-center h-6 w-6">
<GeneralIcon icon="arrowDown" class="text-nc-content-purple-light" />
@@ -602,30 +544,19 @@ onMounted(() => {
</div>
<template v-if="predictedSchema?.tables">
<AiWizardCard
v-if="aiMode"
v-model:active-tab="activePreviewTab"
:tabs="previewTabs"
class="!rounded-xl flex-1 flex flex-col min-w-[320px]"
content-class-name="flex-1 flex flex-col"
>
<AiWizardCard v-model:active-tab="activePreviewTab" :tabs="previewTabs"
class="!rounded-xl flex-1 flex flex-col min-w-[320px]" content-class-name="flex-1 flex flex-col">
<template #tabContent>
<a-collapse
v-if="activePreviewTab === SchemaPreviewTabs.TABLES_AND_VIEWS"
v-model:active-key="previewExpansionPanel"
class="nc-schema-preview-table flex flex-col"
>
<a-collapse v-if="activePreviewTab === SchemaPreviewTabs.TABLES_AND_VIEWS"
v-model:active-key="previewExpansionPanel" class="nc-schema-preview-table flex flex-col">
<template #expandIcon> </template>
<a-collapse-panel v-for="table in predictedSchema.tables" :key="table.title" collapsible="disabled">
<template #header>
<div
class="w-full flex items-center px-4 py-2"
@click="handleUpdatePreviewExpansionPanel(table.title, !viewsGrouped[table.title]?.length)"
>
<div class="w-full flex items-center px-4 py-2"
@click="handleUpdatePreviewExpansionPanel(table.title, !viewsGrouped[table.title]?.length)">
<div
class="flex-1 flex items-center gap-3 text-nc-content-purple-dark dark:text-nc-content-purple-medium"
>
class="flex-1 flex items-center gap-3 text-nc-content-purple-dark dark:text-nc-content-purple-medium">
<NcCheckbox :checked="!table.excluded" theme="ai" @click.stop="onExcludeTable(table)" />
<GeneralIcon icon="table" class="flex-none !h-4 opacity-85" />
@@ -637,24 +568,13 @@ onMounted(() => {
{{ table.title }}
</NcTooltip>
</div>
<NcButton
size="xs"
type="text"
theme="ai"
icon-only
class="!px-0 !h-6 !w-6 !min-w-6"
:class="{
hidden: !viewsGrouped[table.title]?.length,
}"
>
<NcButton size="xs" type="text" theme="ai" icon-only class="!px-0 !h-6 !w-6 !min-w-6" :class="{
hidden: !viewsGrouped[table.title]?.length,
}">
<template #icon>
<GeneralIcon
icon="arrowDown"
class="transform transition-all opacity-80"
:class="{
'rotate-180': previewExpansionPanel.includes(table.title),
}"
/>
<GeneralIcon icon="arrowDown" class="transform transition-all opacity-80" :class="{
'rotate-180': previewExpansionPanel.includes(table.title),
}" />
</template>
</NcButton>
</div>

View File

@@ -10,29 +10,25 @@ interface SandboxType {
interface Props {
workspaceId: string
visible: boolean
}
const props = defineProps<Props>()
const emit = defineEmits(['close', 'installed'])
const emit = defineEmits(['update:visible', 'installed'])
const visible = useVModel(props, 'visible', emit)
const { $api } = useNuxtApp()
const { t } = useI18n()
const visible = ref(true)
const sandboxes = ref<SandboxType[]>([])
const loading = ref(false)
const installing = ref<string | null>(null)
const searchQuery = ref('')
const selectedCategory = ref<string | null>(null)
// Watch visible to emit close when modal is closed by clicking outside
watch(visible, (newVal) => {
if (!newVal) {
emit('close')
}
})
const categories = computed(() => {
const cats = new Set<string>()
sandboxes.value.forEach((sb) => {
@@ -104,7 +100,7 @@ const installSandbox = async (sandbox: SandboxType) => {
message.success(t('msg.success.baseInstalled'))
emit('installed', sandbox)
emit('close')
visible.value = false
} catch (e) {
message.error(await extractSdkResponseErrorMsg(e))
} finally {
@@ -124,12 +120,12 @@ watch(
</script>
<template>
<NcModal v-model:visible="visible" :footer="null" nc-modal-class-name="!p-0" size="large" @close="emit('close')">
<div class="flex flex-col">
<div class="flex items-center gap-3 px-4 py-3 border-b-1 border-b-nc-border-gray-medium">
<GeneralIcon icon="ncBox" class="h-5 w-5" />
<div class="flex-1 text-bodyLgBold">{{ t('labels.appMarket') }}</div>
<NcButton size="small" type="text" @click="emit('close')" class="self-start">
<NcButton size="small" type="text" @click="visible = false" class="self-start">
<GeneralIcon icon="close" class="text-nc-content-gray-subtle2" />
</NcButton>
</div>
@@ -137,13 +133,15 @@ watch(
<div class="flex flex-col gap-4 h-[600px] p-6">
<!-- Search and Filter Bar -->
<div class="flex gap-3">
<a-input v-model:value="searchQuery" class="flex-1 nc-input-sm nc-input-shadow !rounded-md" :placeholder="t('placeholder.searchByTitle')" allow-clear>
<a-input v-model:value="searchQuery" class="flex-1 nc-input-sm nc-input-shadow !rounded-md"
:placeholder="t('placeholder.searchByTitle')" allow-clear>
<template #prefix>
<GeneralIcon icon="search" class="h-4 w-4 text-nc-content-gray-muted" />
</template>
</a-input>
<NcSelect v-model:value="selectedCategory" class="w-48 nc-select-sm" :placeholder="t('labels.category')" allow-clear>
<NcSelect v-model:value="selectedCategory" class="w-48 nc-select-sm" :placeholder="t('labels.category')"
allow-clear>
<a-select-option v-for="cat in categories" :key="cat" :value="cat">
{{ cat }}
</a-select-option>
@@ -156,7 +154,7 @@ watch(
</div>
<div v-else-if="filteredSandboxes.length === 0" class="flex flex-col items-center justify-center h-full gap-3">
<a-empty :image="Empty.PRESENTED_IMAGE_SIMPLE" class="!my-0" >
<a-empty :image="Empty.PRESENTED_IMAGE_SIMPLE" class="!my-0">
<template #description>
<div class="text-nc-content-gray-muted mt-1">{{ t('msg.info.noSandboxesFound') }}</div>
</template>
@@ -198,7 +196,7 @@ watch(
</div>
</div>
</div>
</NcModal>
</div>
</template>
<style lang="scss" scoped>

View File

@@ -220,3 +220,13 @@ export const EventBusEnum = {
RealtimeViewMeta: Symbol('RealtimeViewMeta'),
SmartsheetActions: Symbol('SmartSheetActions'),
}
export enum NcBaseCreateMode {
FROM_SCRATCH = 'fromScratch',
FROM_TEMPLATE = 'fromTemplate',
BUILD_WITH_AI = 'buildWithAi',
FROM_APP_STORE = 'fromAppStore',
MANAGED_APP = 'managedApp',
SANDBOX_APP = 'sandboxApp',
}

View File

@@ -15,6 +15,8 @@ export const useBases = defineStore('basesStore', () => {
const { isUIAllowed } = useRoles()
const baseCreateMode = ref<NcBaseCreateMode | null>(null)
const baseRoles = ref<Record<string, any>>({})
const bases = ref<Map<string, NcProject>>(new Map())
@@ -309,7 +311,7 @@ export const useBases = defineStore('basesStore', () => {
}
try {
meta = (isString(base.meta) ? JSON.parse(base.meta) : base.meta) ?? meta
} catch {}
} catch { }
return meta
}
@@ -393,7 +395,7 @@ export const useBases = defineStore('basesStore', () => {
else navigateTo('/')
}
const toggleStarred = async (..._args: any) => {}
const toggleStarred = async (..._args: any) => { }
watch(
() => route.value.params.baseId,
@@ -446,19 +448,20 @@ export const useBases = defineStore('basesStore', () => {
const basesTeams = ref<Map<string, Record<string, any>[]>>(new Map())
const getBaseTeams = async (..._args: any[]) => {}
const getBaseTeams = async (..._args: any[]) => { }
const baseTeamList = async (..._args: any[]) => {}
const baseTeamGet = async (..._args: any[]) => {}
const baseTeamAdd = async (..._args: any[]) => {}
const baseTeamUpdate = async (..._args: any[]) => {}
const baseTeamRemove = async (..._args: any[]) => {}
const baseTeamList = async (..._args: any[]) => { }
const baseTeamGet = async (..._args: any[]) => { }
const baseTeamAdd = async (..._args: any[]) => { }
const baseTeamUpdate = async (..._args: any[]) => { }
const baseTeamRemove = async (..._args: any[]) => { }
/**
* Teams section end here
*/
return {
baseCreateMode,
bases,
basesList,
loadProjects,