refactor: sandbox to managed app remaining

This commit is contained in:
mertmit
2026-01-23 08:34:16 +00:00
parent 4256e89a99
commit c541ab3a3e
17 changed files with 224 additions and 212 deletions

View File

@@ -369,10 +369,10 @@ const duplicateProject = (base: BaseType) => {
isDuplicateDlgOpen.value = true
}
const isConvertToSandboxDlgOpen = ref(false)
const isConvertToManagedAppDlgOpen = ref(false)
const convertToSandbox = () => {
isConvertToSandboxDlgOpen.value = true
const convertToManagedApp = () => {
isConvertToManagedAppDlgOpen.value = true
}
const tableDelete = () => {
@@ -619,7 +619,7 @@ defineExpose({
@click-menu="onClickMenu"
@rename="enableEditMode()"
@duplicate-project="duplicateProject($event)"
@convert-to-sandbox="convertToSandbox"
@convert-to-managed-app="convertToManagedApp"
@copy-project-info="copyProjectInfo()"
@open-erd-view="openErdView($event)"
@open-base-settings="openBaseSettings($event)"
@@ -680,7 +680,7 @@ defineExpose({
@click-menu="onClickMenu"
@rename="enableEditMode(true)"
@duplicate-project="duplicateProject($event)"
@convert-to-sandbox="convertToSandbox"
@convert-to-managed-app="convertToManagedApp"
@copy-project-info="copyProjectInfo()"
@open-erd-view="openErdView($event)"
@open-base-settings="openBaseSettings($event)"
@@ -765,7 +765,7 @@ defineExpose({
/>
<DlgBaseDelete v-model:visible="isBaseDeleteDialogVisible" :base-id="base?.id" />
<DlgBaseDuplicate v-if="selectedProjectToDuplicate" v-model="isDuplicateDlgOpen" :base="selectedProjectToDuplicate" />
<DlgConvertToSandbox v-if="base?.id" v-model:visible="isConvertToSandboxDlgOpen" :base-id="base.id" />
<DlgConvertToManagedApp v-if="base?.id" v-model:visible="isConvertToManagedAppDlgOpen" :base-id="base.id" />
<GeneralModal v-model:visible="isErdModalOpen" size="large">
<div class="h-[80vh]">
<LazyDashboardSettingsErd :base-id="base?.id" :source-id="activeBaseId" />

View File

@@ -11,7 +11,7 @@ const visible = defineModel<boolean>('visible', { required: true })
const { $api } = useNuxtApp()
const { t } = useI18n()
const initialSanboxFormState = ref<Record<string, any>>({
const initialManagedAppFormState = ref<Record<string, any>>({
title: '',
description: '',
category: '',
@@ -22,13 +22,13 @@ const { base } = storeToRefs(useBase())
const basesStore = useBases()
const convertToSandbox = async (formState: Record<string, any>) => {
const convertToManagedApp = async (formState: Record<string, any>) => {
try {
const response = await $api.internal.postOperation(
base.value!.fk_workspace_id as string,
props.baseId,
{
operation: 'sandboxCreate',
operation: 'managedAppCreate',
} as any,
{
title: formState.title,
@@ -38,18 +38,18 @@ const convertToSandbox = async (formState: Record<string, any>) => {
},
)
message.success(t('msg.success.sandboxCreated'))
message.success(t('msg.success.managedAppCreated'))
visible.value = false
// Update the base with the sandbox_id from response
if (response && response.sandbox_id) {
// Update the base with the managed_app_id from response
if (response && response.managed_app_id) {
const currentBase = basesStore.bases.get(props.baseId)
if (currentBase) {
;(currentBase as any).sandbox_id = response.sandbox_id
;(currentBase as any).managed_app_id = response.managed_app_id
}
}
// Reload base to ensure all sandbox data is loaded
// Reload base to ensure all managed app data is loaded
await basesStore.loadProject(props.baseId, true)
} catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e))
@@ -60,7 +60,7 @@ const { formState, isLoading, submit } = useProvideFormBuilderHelper({
formSchema: [
{
type: FormBuilderInputType.Input,
label: t('labels.sandboxTitle'),
label: t('labels.managedAppTitle'),
span: 24,
model: 'title',
placeholder: 'Enter a descriptive title',
@@ -75,7 +75,7 @@ const { formState, isLoading, submit } = useProvideFormBuilderHelper({
},
{
type: FormBuilderInputType.Textarea,
label: t('labels.sandboxDescription'),
label: t('labels.managedAppDescription'),
span: 24,
model: 'description',
placeholder: "Describe your application's capabilities",
@@ -83,7 +83,7 @@ const { formState, isLoading, submit } = useProvideFormBuilderHelper({
},
{
type: FormBuilderInputType.Input,
label: t('labels.sandboxCategory'),
label: t('labels.managedAppCategory'),
span: 12,
model: 'category',
placeholder: 'e.g., CRM, HR',
@@ -91,7 +91,7 @@ const { formState, isLoading, submit } = useProvideFormBuilderHelper({
},
{
type: FormBuilderInputType.Select,
label: t('labels.sandboxVisibility'),
label: t('labels.managedAppVisibility'),
span: 12,
model: 'visibility',
category: FORM_BUILDER_NON_CATEGORIZED,
@@ -104,9 +104,9 @@ const { formState, isLoading, submit } = useProvideFormBuilderHelper({
},
],
onSubmit: async () => {
return await convertToSandbox(formState.value)
return await convertToManagedApp(formState.value)
},
initialState: initialSanboxFormState,
initialState: initialManagedAppFormState,
})
watch(visible, (isVisible) => {
@@ -127,7 +127,7 @@ watch(visible, (isVisible) => {
size="sm"
height="auto"
centered
wrap-class-name="nc-modal-convert-to-sandbox "
wrap-class-name="nc-modal-convert-to-managed-app "
nc-modal-class-name="!p-0"
>
<div class="p-4 w-full flex items-center gap-3 border-b border-nc-border-gray-medium">
@@ -135,7 +135,7 @@ watch(visible, (isVisible) => {
<GeneralIcon icon="ncBox" class="w-5 h-5 text-white" />
</div>
<div class="flex-1">
<div class="font-semibold text-lg text-nc-content-gray-emphasis">Convert to Sandbox</div>
<div class="font-semibold text-lg text-nc-content-gray-emphasis">Convert to Managed App</div>
<div class="text-xs text-nc-content-gray-subtle2">{{ $t('labels.publishToAppStore') }}</div>
</div>
@@ -169,14 +169,14 @@ watch(visible, (isVisible) => {
<template #icon>
<GeneralIcon icon="ncBox" />
</template>
Convert to sandbox
Convert to Managed App
</NcButton>
</div>
</NcModal>
</template>
<style lang="scss">
.nc-modal-convert-to-sandbox {
.nc-modal-convert-to-managed-app {
.nc-modal {
max-height: min(90vh, 540px) !important;
height: min(90vh, 540px) !important;

View File

@@ -12,7 +12,7 @@ const { visibility, showShareModal } = storeToRefs(useShare())
const { activeTable } = storeToRefs(useTablesStore())
const { base, isSharedBase, isSandboxMaster } = storeToRefs(useBase())
const { base, isSharedBase, isManagedAppMaster } = storeToRefs(useBase())
const { hideSharedBaseBtn } = storeToRefs(useConfigStore())
@@ -46,7 +46,7 @@ const copySharedBase = async () => {
<template>
<div
v-if="!isSharedBase && !isSandboxMaster && isUIAllowed('baseShare') && visibility !== 'hidden' && (activeTable || base)"
v-if="!isSharedBase && !isManagedAppMaster && isUIAllowed('baseShare') && visibility !== 'hidden' && (activeTable || base)"
class="nc-share-base-button flex flex-col justify-center"
data-testid="share-base-button"
:data-sharetype="visibility"

View File

@@ -232,7 +232,7 @@ onMounted(() => {
</div>
</div>
<div v-if="!showEmptySkeleton && !isMobileMode" class="flex items-center gap-2">
<SmartsheetTopbarSandboxStatus />
<SmartsheetTopbarManagedAppStatus />
<LazyGeneralShareProject />
</div>
</div>

View File

@@ -72,8 +72,8 @@ const topbarBreadcrumbItemWidth = computed(() => {
<div class="flex items-center justify-end gap-2 flex-1">
<GeneralApiLoader v-if="!isMobileMode && !activeScriptId && !activeDashboardId" />
<!-- Sandbox Status -->
<LazySmartsheetTopbarSandboxStatus v-if="!isSharedBase && !isMobileMode" />
<!-- Managed App Status -->
<LazySmartsheetTopbarManagedAppStatus v-if="!isSharedBase && !isMobileMode" />
<NcButton
v-if="

View File

@@ -1,7 +1,7 @@
<script setup lang="ts">
const props = defineProps<{
visible: boolean
sandbox: any
managedApp: any
currentVersion: any
initialTab?: 'publish' | 'fork' | 'deployments'
}>()
@@ -38,12 +38,12 @@ const isDraft = computed(() => props.currentVersion?.status === 'draft')
// Load versions
const loadVersions = async () => {
if (!props.sandbox?.id || !base.value?.fk_workspace_id) return
if (!props.managedApp?.id || !base.value?.fk_workspace_id) return
try {
const response = await $api.internal.getOperation(base.value.fk_workspace_id, base.value.id!, {
operation: 'sandboxVersionsList',
sandboxId: props.sandbox.id,
operation: 'managedAppVersionsList',
managedAppId: props.managedApp.id,
} as any)
if (response?.list) {
versions.value = response.list
@@ -55,13 +55,13 @@ const loadVersions = async () => {
// Load real deployment statistics
const loadDeployments = async () => {
if (!props.sandbox?.id || !base.value?.fk_workspace_id) return
if (!props.managedApp?.id || !base.value?.fk_workspace_id) return
isLoadingDeployments.value = true
try {
const response = await $api.internal.getOperation(base.value.fk_workspace_id, base.value.id!, {
operation: 'sandboxDeployments',
sandboxId: props.sandbox.id,
operation: 'managedAppDeployments',
managedAppId: props.managedApp.id,
} as any)
if (response) {
deploymentStats.value = response
@@ -82,14 +82,14 @@ const publishCurrentDraft = async () => {
base.value.fk_workspace_id,
base.value.id,
{
operation: 'sandboxPublish',
operation: 'managedAppPublish',
},
{
sandboxVersionId: props.currentVersion.id,
managedAppVersionId: props.currentVersion.id,
},
)
// Reload base to get updated sandbox version info
// Reload base to get updated managed app version info
if (base.value?.id) {
await baseStore.loadProject()
}
@@ -105,7 +105,7 @@ const publishCurrentDraft = async () => {
}
const createNewDraft = async () => {
if (!base.value?.fk_workspace_id || !base.value?.id || !props.sandbox?.id) return
if (!base.value?.fk_workspace_id || !base.value?.id || !props.managedApp?.id) return
if (!forkForm.version) {
message.error('Please provide a version')
return
@@ -117,15 +117,15 @@ const createNewDraft = async () => {
base.value.fk_workspace_id,
base.value.id,
{
operation: 'sandboxCreateDraft',
operation: 'managedAppCreateDraft',
},
{
sandboxId: props.sandbox.id,
managedAppId: props.managedApp.id,
version: forkForm.version,
},
)
// Reload base to get updated sandbox version info
// Reload base to get updated managed app version info
if (base.value?.id) {
await baseStore.loadProject()
}
@@ -205,29 +205,29 @@ watch(
<NcModal
:visible="visible"
size="lg"
nc-modal-class-name="nc-modal-sandbox-management"
nc-modal-class-name="nc-modal-managed-app-management"
centered
@update:visible="emit('update:visible', $event)"
>
<div class="flex flex-col h-full">
<!-- Header with Tabs -->
<div class="nc-sandbox-header">
<div class="nc-managed-app-header">
<div class="flex items-center gap-3 flex-1">
<div class="nc-sandbox-icon">
<div class="nc-managed-app-icon">
<GeneralIcon icon="ncBox" class="h-5 w-5 text-white" />
</div>
<div class="flex-1">
<div class="text-lg font-semibold text-nc-content-gray-emphasis">Sandbox Management</div>
<div class="text-lg font-semibold text-nc-content-gray-emphasis">Managed App Management</div>
<div class="text-xs text-nc-content-gray-subtle2">Manage versions and track deployments</div>
</div>
</div>
<!-- Tabs (Segmented Control) -->
<div class="nc-sandbox-tabs">
<div class="nc-managed-app-tabs">
<div class="flex items-center">
<div
v-if="isDraft"
class="nc-sandbox-tab"
class="nc-managed-app-tab"
:class="{ selected: activeTab === 'publish' }"
@click="activeTab = 'publish'"
>
@@ -236,14 +236,14 @@ watch(
</div>
<div
v-if="isPublished"
class="nc-sandbox-tab"
class="nc-managed-app-tab"
:class="{ selected: activeTab === 'fork' }"
@click="activeTab = 'fork'"
>
<GeneralIcon icon="ncGitBranch" class="h-4 w-4 flex-none opacity-75" />
<span>Fork</span>
</div>
<div class="nc-sandbox-tab" :class="{ selected: activeTab === 'deployments' }" @click="activeTab = 'deployments'">
<div class="nc-managed-app-tab" :class="{ selected: activeTab === 'deployments' }" @click="activeTab = 'deployments'">
<GeneralIcon icon="ncServer" class="h-4 w-4 flex-none opacity-75" />
<span>Deployments</span>
</div>
@@ -473,7 +473,7 @@ watch(
</div>
<!-- Footer -->
<div v-if="activeTab === 'publish' || activeTab === 'fork'" class="nc-sandbox-footer">
<div v-if="activeTab === 'publish' || activeTab === 'fork'" class="nc-managed-app-footer">
<div class="flex justify-end gap-2">
<NcButton type="secondary" size="small" @click="emit('update:visible', false)"> Cancel </NcButton>
@@ -502,30 +502,30 @@ watch(
</div>
<!-- Version Deployments Modal -->
<SmartsheetTopbarSandboxVersionDeploymentsModal
<SmartsheetTopbarManagedAppVersionDeploymentsModal
v-model:visible="showVersionDeploymentsModal"
:sandbox="sandbox"
:managed-app="managedApp"
:version="selectedVersion"
/>
</NcModal>
</template>
<style lang="scss" scoped>
.nc-sandbox-header {
.nc-managed-app-header {
@apply flex items-center gap-4 px-4 py-3 border-b-1 border-nc-border-gray-medium;
}
.nc-sandbox-icon {
.nc-managed-app-icon {
@apply w-10 h-10 rounded-xl flex items-center justify-center;
background: linear-gradient(135deg, var(--nc-content-brand) 0%, var(--nc-content-blue-medium) 100%);
box-shadow: 0 2px 4px rgba(51, 102, 255, 0.15);
}
.nc-sandbox-tabs {
.nc-managed-app-tabs {
@apply flex bg-nc-bg-gray-medium rounded-lg p-1;
}
.nc-sandbox-tab {
.nc-managed-app-tab {
@apply px-3 py-1.5 flex items-center gap-2 text-xs rounded-md select-none cursor-pointer;
@apply text-nc-content-gray-subtle2 transition-all duration-200;
@@ -539,7 +539,7 @@ watch(
}
}
.nc-sandbox-footer {
.nc-managed-app-footer {
@apply px-6 py-3 border-t-1 border-nc-border-gray-medium;
}
@@ -711,7 +711,7 @@ watch(
</style>
<style lang="scss">
.nc-modal-sandbox-management {
.nc-modal-managed-app-management {
@apply !p-0;
}
</style>

View File

@@ -5,40 +5,40 @@ const { $api } = useNuxtApp()
const isModalVisible = ref(false)
const initialTab = ref<'publish' | 'fork' | 'deployments' | undefined>(undefined)
const sandbox = ref<any>(null)
const managedApp = ref<any>(null)
const currentVersion = ref<any>(null)
const isSandboxMaster = computed(() => !!(base.value as any)?.sandbox_master && !!(base.value as any)?.sandbox_id)
const isManagedAppMaster = computed(() => !!(base.value as any)?.managed_app_master && !!(base.value as any)?.managed_app_id)
// Load sandbox info and current version
const loadSandbox = async () => {
if (!(base.value as any)?.sandbox_id || !base.value?.fk_workspace_id) return
// Load managed app info and current version
const loadManagedApp = async () => {
if (!(base.value as any)?.managed_app_id || !base.value?.fk_workspace_id) return
try {
const response = await $api.internal.getOperation(base.value.fk_workspace_id, base.value.id!, {
operation: 'sandboxGet',
operation: 'managedAppGet',
baseId: base.value.id,
} as any)
if (response) {
sandbox.value = response
managedApp.value = response
}
} catch (e) {
console.error('Failed to load sandbox:', e)
console.error('Failed to load managed app:', e)
}
}
// Load current version info
const loadCurrentVersion = async () => {
if (!base.value?.sandbox_version_id || !base.value?.fk_workspace_id) return
if (!base.value?.managed_app_version_id || !base.value?.fk_workspace_id) return
try {
// Get version details from versions list
const response = await $api.internal.getOperation(base.value.fk_workspace_id, base.value.id!, {
operation: 'sandboxVersionsList',
sandboxId: (base.value as any).sandbox_id,
operation: 'managedAppVersionsList',
managedAppId: (base.value as any).managed_app_id,
} as any)
if (response?.list) {
currentVersion.value = response.list.find((v: any) => v.id === base.value.sandbox_version_id)
currentVersion.value = response.list.find((v: any) => v.id === base.value.managed_app_version_id)
}
} catch (e) {
console.error('Failed to load current version:', e)
@@ -51,20 +51,20 @@ const openModal = (tab?: 'publish' | 'fork' | 'deployments') => {
}
const handlePublished = async () => {
await loadSandbox()
await loadManagedApp()
await loadCurrentVersion()
}
const handleForked = async () => {
await loadSandbox()
await loadManagedApp()
await loadCurrentVersion()
}
watch(
() => (base.value as any)?.sandbox_id,
async (sandboxId) => {
if (sandboxId) {
await loadSandbox()
() => (base.value as any)?.managed_app_id,
async (managedAppId) => {
if (managedAppId) {
await loadManagedApp()
await loadCurrentVersion()
}
},
@@ -73,13 +73,13 @@ watch(
</script>
<template>
<div v-if="isSandboxMaster" class="flex items-center gap-2">
<div v-if="isManagedAppMaster" class="flex items-center gap-2">
<!-- Version Badge (clickable to open modal) -->
<div
class="flex items-center gap-1.5 px-2.5 py-1 bg-nc-bg-gray-light rounded-md border-1 border-nc-border-gray-medium cursor-pointer hover:(bg-nc-bg-gray-medium border-nc-border-gray-dark) transition-colors"
@click="openModal()"
>
<GeneralIcon icon="ncInfoSolid" class="w-3.5 h-3.5 text-nc-content-gray nc-sanbox-status-info-icon" />
<GeneralIcon icon="ncInfoSolid" class="w-3.5 h-3.5 text-nc-content-gray nc-managed-app-status-info-icon" />
<span class="text-xs font-mono font-semibold text-nc-content-gray-emphasis">v{{ currentVersion?.version || '1.0.0' }}</span>
<div
v-if="currentVersion?.status === 'draft'"
@@ -96,10 +96,10 @@ watch(
</div>
</div>
<!-- Sandbox Modal -->
<SmartsheetTopbarSandboxModal
<!-- Managed App Modal -->
<SmartsheetTopbarManagedAppModal
v-model:visible="isModalVisible"
:sandbox="sandbox"
:managed-app="managedApp"
:current-version="currentVersion"
:initial-tab="initialTab"
@published="handlePublished"
@@ -108,7 +108,7 @@ watch(
</template>
<style lang="scss" scoped>
:deep(.nc-sanbox-status-info-icon path.nc-icon-inner) {
:deep(.nc-managed-app-status-info-icon path.nc-icon-inner) {
stroke: var(--nc-bg-gray-light) !important;
}
</style>

View File

@@ -3,7 +3,7 @@ import { DeploymentStatus } from 'nocodb-sdk'
const props = defineProps<{
visible: boolean
sandbox: any
managedApp: any
version: any
}>()
@@ -27,14 +27,14 @@ const logsPageSize = 10
const isLoadingLogs = ref(false)
const loadDeployments = async (page = 1) => {
if (!props.sandbox?.id || !props.version?.versionId || !base.value?.fk_workspace_id) return
if (!props.managedApp?.id || !props.version?.versionId || !base.value?.fk_workspace_id) return
isLoading.value = true
try {
const offset = (page - 1) * pageSize
const response = await $api.internal.getOperation(base.value.fk_workspace_id, base.value.id!, {
operation: 'sandboxVersionDeployments',
sandboxId: props.sandbox.id,
operation: 'managedAppVersionDeployments',
managedAppId: props.managedApp.id,
versionId: props.version.versionId,
limit: pageSize,
offset,
@@ -59,7 +59,7 @@ const loadDeploymentLogs = async (baseId: string, page = 1) => {
try {
const offset = (page - 1) * logsPageSize
const response = await $api.internal.getOperation(base.value.fk_workspace_id, base.value.id!, {
operation: 'sandboxDeploymentLogs',
operation: 'managedAppDeploymentLogs',
baseId,
limit: logsPageSize,
offset,

View File

@@ -1,5 +1,5 @@
<script lang="ts" setup>
interface SandboxType {
interface ManagedAppType {
id: string
title: string
description?: string
@@ -22,7 +22,7 @@ const visible = useVModel(props, 'visible', emit)
const { $api } = useNuxtApp()
const { t } = useI18n()
const sandboxes = ref<SandboxType[]>([])
const managedApps = ref<ManagedAppType[]>([])
const loading = ref(false)
const installing = ref<string | null>(null)
const searchQuery = ref('')
@@ -30,10 +30,10 @@ const selectedCategory = ref<string | undefined>(undefined)
const categories = computed(() => {
const cats = new Set<string>()
sandboxes.value.forEach((sb) => {
if (sb.category) {
managedApps.value.forEach((ma) => {
if (ma.category) {
// Split comma-separated categories
sb.category.split(',').forEach((cat) => {
ma.category.split(',').forEach((cat) => {
const trimmed = cat.trim()
if (trimmed) cats.add(trimmed)
})
@@ -42,28 +42,28 @@ const categories = computed(() => {
return Array.from(cats).sort()
})
const filteredSandboxes = computed(() => {
let filtered = sandboxes.value
const filteredManagedApps = computed(() => {
let filtered = managedApps.value
if (selectedCategory.value) {
const selected = selectedCategory.value
filtered = filtered.filter((sb) => {
if (!sb.category) return false
filtered = filtered.filter((ma) => {
if (!ma.category) return false
// Check if selected category exists in comma-separated list
const categories = sb.category.split(',').map((c) => c.trim())
const categories = ma.category.split(',').map((c) => c.trim())
return categories.includes(selected)
})
}
if (searchQuery.value) {
const query = searchQuery.value.toLowerCase()
filtered = filtered.filter((sb) => searchCompare([sb.title, sb.description, sb.category], query))
filtered = filtered.filter((ma) => searchCompare([ma.title, ma.description, ma.category], query))
}
return filtered
})
const loadSandboxes = async () => {
const loadManagedApps = async () => {
if (!props.workspaceId) {
console.error('WorkspaceId is required')
return
@@ -77,10 +77,10 @@ const loadSandboxes = async () => {
loading.value = true
try {
const response = await $api.internal.getOperation(props.workspaceId, NO_SCOPE, {
operation: 'sandboxStoreList',
operation: 'managedAppStoreList',
})
sandboxes.value = response?.list || []
managedApps.value = response?.list || []
} catch (e: any) {
console.error('API error:', e)
message.error(await extractSdkResponseErrorMsg(e))
@@ -89,23 +89,23 @@ const loadSandboxes = async () => {
}
}
const installSandbox = async (sandbox: SandboxType) => {
installing.value = sandbox.id
const installManagedApp = async (managedApp: ManagedAppType) => {
installing.value = managedApp.id
try {
await $api.internal.postOperation(
props.workspaceId,
NO_SCOPE,
{
operation: 'sandboxInstall',
operation: 'managedAppInstall',
},
{
sandboxId: sandbox.id,
managedAppId: managedApp.id,
target_workspace_id: props.workspaceId,
},
)
message.success(t('msg.success.baseInstalled'))
emit('installed', sandbox)
emit('installed', managedApp)
visible.value = false
} catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e))
@@ -129,7 +129,7 @@ watch(
() => props.workspaceId,
(newVal) => {
if (newVal) {
loadSandboxes()
loadManagedApps()
}
},
{ immediate: true },
@@ -177,8 +177,8 @@ watch(
</div>
<!-- Results count -->
<div v-if="!loading && filteredSandboxes.length > 0" class="mt-3 text-xs text-nc-content-gray-muted">
{{ filteredSandboxes.length }} {{ filteredSandboxes.length === 1 ? 'app' : 'apps' }} available
<div v-if="!loading && filteredManagedApps.length > 0" class="mt-3 text-xs text-nc-content-gray-muted">
{{ filteredManagedApps.length }} {{ filteredManagedApps.length === 1 ? 'app' : 'apps' }} available
</div>
</div>
@@ -193,7 +193,7 @@ watch(
</div>
<!-- Empty State -->
<div v-else-if="filteredSandboxes.length === 0" class="nc-app-market-empty">
<div v-else-if="filteredManagedApps.length === 0" class="nc-app-market-empty">
<div class="nc-empty-icon">
<GeneralIcon icon="ncBox" class="h-10 w-10 text-nc-content-gray-muted" />
</div>
@@ -209,7 +209,7 @@ watch(
<!-- App List -->
<div v-else class="nc-app-market-list">
<div v-for="sandbox in filteredSandboxes" :key="sandbox.id" class="nc-app-item">
<div v-for="managedApp in filteredManagedApps" :key="managedApp.id" class="nc-app-item">
<div class="nc-app-item-content">
<!-- App Icon & Info -->
<div class="nc-app-info">
@@ -218,10 +218,10 @@ watch(
</div>
<div class="nc-app-details">
<div class="nc-app-title-row">
<h3 class="nc-app-title">{{ sandbox.title }}</h3>
<div v-if="sandbox.category" class="nc-app-categories">
<h3 class="nc-app-title">{{ managedApp.title }}</h3>
<div v-if="managedApp.category" class="nc-app-categories">
<div
v-for="cat in sandbox.category
v-for="cat in managedApp.category
.split(',')
.map((c) => c.trim())
.filter(Boolean)"
@@ -236,20 +236,20 @@ watch(
<p
class="nc-app-description"
:class="{
'!text-nc-content-gray-muted': !sandbox.description,
'!text-nc-content-gray-muted': !managedApp.description,
}"
>
{{ sandbox.description || 'No description available' }}
{{ managedApp.description || 'No description available' }}
</p>
<div class="nc-app-meta">
<span class="nc-app-meta-item">
<GeneralIcon icon="download" class="h-3.5 w-3.5" />
<span class="font-medium">{{ formatInstallCount(sandbox.install_count || 0) }}</span>
<span class="font-medium">{{ formatInstallCount(managedApp.install_count || 0) }}</span>
<span class="text-nc-content-gray-muted">installs</span>
</span>
<span v-if="sandbox.version" class="nc-app-meta-item">
<span v-if="managedApp.version" class="nc-app-meta-item">
<GeneralIcon icon="gitCommit" class="h-3.5 w-3.5" />
<span>v{{ sandbox.version }}</span>
<span>v{{ managedApp.version }}</span>
</span>
</div>
</div>
@@ -258,16 +258,16 @@ watch(
<!-- Install Button -->
<div class="nc-app-action">
<NcButton
:loading="installing === sandbox.id"
:loading="installing === managedApp.id"
:disabled="!!installing"
size="small"
type="primary"
@click="installSandbox(sandbox)"
@click="installManagedApp(managedApp)"
>
<template #icon>
<GeneralIcon icon="download" class="h-4 w-4" />
</template>
{{ installing === sandbox.id ? 'Installing...' : t('general.install') }}
{{ installing === managedApp.id ? 'Installing...' : t('general.install') }}
</NcButton>
</div>
</div>

View File

@@ -11,7 +11,7 @@ interface Props {
const props = withDefaults(defineProps<Props>(), {})
const emit = defineEmits(['update:aiMode', 'update:mode', 'sandboxInstalled', 'close'])
const emit = defineEmits(['update:aiMode', 'update:mode', 'managedAppInstalled', 'close'])
const aiMode = useVModel(props, 'aiMode', emit)
@@ -48,9 +48,9 @@ const selectMode = (mode: CreateMode) => {
emit('update:mode', mode)
}
const onSandboxInstalled = (sandbox: any) => {
const onManagedAppInstalled = (managedApp: any) => {
showAppMarket.value = false
emit('sandboxInstalled', sandbox)
emit('managedAppInstalled', managedApp)
}
const onAppMarketClose = () => {
@@ -93,7 +93,7 @@ onMounted(() => {
</div>
</div>
<div
v-if="isFeatureEnabled(FEATURE_FLAG.SANDBOXES)"
v-if="isFeatureEnabled(FEATURE_FLAG.MANAGED_APPS)"
v-e="['c:base:market:create']"
class="nc-create-base-market"
@click="selectMode('market')"
@@ -115,7 +115,7 @@ onMounted(() => {
v-if="showAppMarket && workspaceId"
:workspace-id="workspaceId"
@close="onAppMarketClose"
@installed="onSandboxInstalled"
@installed="onManagedAppInstalled"
/>
</div>
</template>

View File

@@ -38,7 +38,7 @@ const createManagedApp = async (formState: Record<string, any>) => {
activeWorkspaceId.value as string,
formState.baseId || NO_SCOPE,
{
operation: 'sandboxCreate',
operation: 'managedAppCreate',
} as any,
{
title: formState.title,
@@ -59,18 +59,18 @@ const createManagedApp = async (formState: Record<string, any>) => {
},
)
message.success(t('msg.success.sandboxCreated'))
message.success(t('msg.success.managedAppCreated'))
visible.value = false
// Update the base with the sandbox_id from response
if (response && response.sandbox_id && formState.baseId) {
// Update the base with the managed_app_id from response
if (response && response.managed_app_id && formState.baseId) {
const currentBase = basesStore.bases.get(formState.baseId as string)
if (currentBase) {
;(currentBase as any).sandbox_id = response.sandbox_id
;(currentBase as any).managed_app_id = response.managed_app_id
}
}
// Reload base to ensure all sandbox data is loaded
// Reload base to ensure all managed app data is loaded
if (formState.baseId) {
await basesStore.loadProject(formState.baseId, true)
} else {
@@ -92,7 +92,7 @@ const { formState, isLoading, submit } = useProvideFormBuilderHelper({
formSchema: [
{
type: FormBuilderInputType.Input,
label: t('labels.sandboxTitle'),
label: t('labels.managedAppTitle'),
span: 24,
model: 'title',
placeholder: 'Enter a descriptive title',
@@ -111,7 +111,7 @@ const { formState, isLoading, submit } = useProvideFormBuilderHelper({
},
{
type: FormBuilderInputType.Textarea,
label: t('labels.sandboxDescription'),
label: t('labels.managedAppDescription'),
span: 24,
model: 'description',
placeholder: "Describe your application's capabilities",
@@ -149,11 +149,11 @@ const { formState, isLoading, submit } = useProvideFormBuilderHelper({
equal: 'existing',
},
defaultValue: undefined,
filterOption: (base) => base && !base?.sandbox_id,
filterOption: (base) => base && !base?.managed_app_id,
},
{
type: FormBuilderInputType.Input,
label: t('labels.sandboxCategory'),
label: t('labels.managedAppCategory'),
span: 12,
model: 'category',
placeholder: 'e.g., CRM, HR',
@@ -161,7 +161,7 @@ const { formState, isLoading, submit } = useProvideFormBuilderHelper({
},
{
type: FormBuilderInputType.Select,
label: t('labels.sandboxVisibility'),
label: t('labels.managedAppVisibility'),
span: 12,
model: 'visibility',
category: FORM_BUILDER_NON_CATEGORIZED,
@@ -234,7 +234,7 @@ const { formState, isLoading, submit } = useProvideFormBuilderHelper({
</template>
<style lang="scss">
.nc-modal-convert-to-sandbox {
.nc-modal-convert-to-managed-app {
.nc-modal {
max-height: min(90vh, 540px) !important;
height: min(90vh, 540px) !important;

View File

@@ -3,9 +3,9 @@ import rfdc from 'rfdc'
const deepClone = rfdc()
const FEATURES = [
{
id: 'sandboxes',
title: 'Sandboxes',
description: 'Allow users to create replicable sandbox environments',
id: 'managed_apps',
title: 'Managed Apps',
description: 'Allow users to create replicable managed app environments',
enabled: false,
isEngineering: true,
isAdvanced: true,

View File

@@ -1173,11 +1173,11 @@
"visibilityAndDataHandling": "Visibility & Data Handling",
"visibilityConfigLabel": "Base specific additional configurations to customise data display & default behaviours.",
"migrateToV3": "Migrate to V3",
"sandbox": "Sandbox",
"sandboxAppStore": "Sandbox App Store",
"managedApp": "Managed App",
"managedAppStore": "Managed App Store",
"baseMustBeV3": "Base must be V3",
"createSandbox": "Create Sandbox",
"sandboxDetails": "Sandbox Details",
"createManagedApp": "Create Managed App",
"managedAppDetails": "Managed App Details",
"publishToAppStore": "Publish to App Store",
"publishChanges": "Publish Changes",
"publishingChanges": "Publishing changes",
@@ -1185,11 +1185,11 @@
"updateDetails": "Update Details",
"published": "Published",
"draft": "Draft",
"sandboxTitle": "Title",
"sandboxDescription": "Description",
"sandboxCategory": "Category",
"sandboxTags": "Tags",
"sandboxVisibility": "Visibility",
"managedAppTitle": "Title",
"managedAppDescription": "Description",
"managedAppCategory": "Category",
"managedAppTags": "Tags",
"managedAppVisibility": "Visibility",
"snapShotSubText": "Snapshots serve as comprehensive backups of your base, capturing its state at the time of creation. Restoring a snapshot creates a new instance of the base in the designated workspace.",
"newSnapshot": "New Snapshot",
"searchASnapshot": "Search a snapshot",
@@ -2480,7 +2480,7 @@
"deleteProject": "Do you want to delete the base?",
"shareBasePrivate": "Generate publicly shareable readonly base",
"shareBasePublic": "Anyone on the internet with this link can view",
"noSandboxesFound": "No apps found in the marketplace",
"noManagedAppsFound": "No apps found in the marketplace",
"userInviteNoSMTP": "Looks like you have not configured mailer yet! Please copy above invite link and send it to",
"dragDropHide": "Drag and drop fields here to hide",
"formInput": "Enter form input label",
@@ -2807,10 +2807,10 @@
},
"success": {
"passwordSet": "Password set successfully",
"sandboxCreated": "Sandbox created successfully",
"sandboxUpdated": "Sandbox updated successfully",
"sandboxPublished": "Sandbox published successfully",
"sandboxUnpublished": "Sandbox unpublished successfully",
"managedAppCreated": "Managed App created successfully",
"managedAppUpdated": "Managed App updated successfully",
"managedAppPublished": "Managed App published successfully",
"managedAppUnpublished": "Managed App unpublished successfully",
"changesPublished": "Changes published to all installations successfully",
"mcpTokenDeleted": "MCP Token Deleted",
"mcpTokenUpdated": "MCP Token Updated",

View File

@@ -149,6 +149,10 @@ export enum ImportWorkerResponse {
ERROR = 'error',
}
export enum FeatureFlag {
MANAGED_APP = 'MANAGED_APP',
}
export enum ImportType {
EXCEL = 'excel',
CSV = 'csv',
@@ -227,5 +231,4 @@ export enum NcBaseCreateMode {
BUILD_WITH_AI = 'buildWithAi',
FROM_APP_STORE = 'fromAppStore',
MANAGED_APP = 'managedApp',
SANDBOX_APP = 'sandboxApp',
}

View File

@@ -225,12 +225,12 @@ type NcProject = BaseType & {
users?: User[]
default_role?: ProjectRoles | string
version?: BaseVersion
// Sandbox fields
sandbox_master?: boolean
sandbox_id?: string
sandbox_version_id?: string
// Managed App fields
managed_app_master?: boolean
managed_app_id?: string
managed_app_version_id?: string
auto_update?: boolean
sandbox_schema_locked?: boolean
managed_app_schema_locked?: boolean
}
interface UndoRedoAction {

View File

@@ -18,7 +18,7 @@ export const useBase = defineStore('baseStore', () => {
const basesStore = useBases()
const isSandboxMaster = ref(false)
const isManagedAppMaster = ref(false)
const baseId = computed(() => {
// In shared base mode, use activeProjectId from basesStore which has the correct base ID
@@ -342,7 +342,7 @@ export const useBase = defineStore('baseStore', () => {
idUserMap,
isPrivateBase,
showBaseAccessRequestOverlay,
isSandboxMaster,
isManagedAppMaster,
}
})

View File

@@ -3,7 +3,7 @@ import { MetaTable } from '~/utils/globals';
const up = async (knex: Knex) => {
// Create sandboxes table
await knex.schema.createTable(MetaTable.SANDBOXES, (table) => {
await knex.schema.createTable(MetaTable.SANDBOXES_OLD, (table) => {
table.string('id', 20).primary();
table.string('fk_workspace_id', 20).notNullable();
table.string('base_id', 20).notNullable().unique();
@@ -44,7 +44,7 @@ const up = async (knex: Knex) => {
});
// Create sandbox_versions table to store serialized schemas for each published version
await knex.schema.createTable(MetaTable.SANDBOX_VERSIONS, (table) => {
await knex.schema.createTable(MetaTable.SANDBOX_VERSIONS_OLD, (table) => {
table.string('id', 20).primary();
table.string('fk_workspace_id', 20).notNullable();
table.string('fk_sandbox_id', 20).notNullable();
@@ -92,11 +92,11 @@ const up = async (knex: Knex) => {
// Is this base a sandbox master?
table.boolean('sandbox_master').defaultTo(false);
// Points to SANDBOXES.id (for both master and installed instances)
// Points to SANDBOXES_OLD.id (for both master and installed instances)
table.string('sandbox_id', 20);
// Current version: for master=draft/published being worked on, for installed=installed version
// Points to SANDBOX_VERSIONS.id
// Points to SANDBOX_VERSIONS_OLD.id
table.string('sandbox_version_id', 20);
// For installed instances: auto-update to new published versions
@@ -115,60 +115,69 @@ const up = async (knex: Knex) => {
});
// Create sandbox_deployment_logs table
await knex.schema.createTable(MetaTable.SANDBOX_DEPLOYMENT_LOGS, (table) => {
table.string('id', 20).primary();
table.string('fk_workspace_id', 20).notNullable();
await knex.schema.createTable(
MetaTable.SANDBOX_DEPLOYMENT_LOGS_OLD,
(table) => {
table.string('id', 20).primary();
table.string('fk_workspace_id', 20).notNullable();
// Installation reference
table.string('base_id', 20).notNullable(); // The installed base
table.string('fk_sandbox_id', 20).notNullable(); // The sandbox being deployed
// Installation reference
table.string('base_id', 20).notNullable(); // The installed base
table.string('fk_sandbox_id', 20).notNullable(); // The sandbox being deployed
// Version tracking
table.string('from_version_id', 20); // NULL for initial install
table.string('to_version_id', 20).notNullable(); // Target version
// Version tracking
table.string('from_version_id', 20); // NULL for initial install
table.string('to_version_id', 20).notNullable(); // Target version
// Deployment status: 'pending', 'in_progress', 'success', 'failed'
table.string('status', 20).notNullable().defaultTo('pending');
// Deployment status: 'pending', 'in_progress', 'success', 'failed'
table.string('status', 20).notNullable().defaultTo('pending');
// Deployment type: 'install', 'update'
table.string('deployment_type', 20).notNullable();
// Deployment type: 'install', 'update'
table.string('deployment_type', 20).notNullable();
// Deployment details
table.text('error_message'); // Error message if failed
table.text('deployment_log'); // Detailed logs (JSON or text)
table.text('meta'); // Additional metadata (JSON)
// Deployment details
table.text('error_message'); // Error message if failed
table.text('deployment_log'); // Detailed logs (JSON or text)
table.text('meta'); // Additional metadata (JSON)
// Timestamps
table.timestamps(true, true);
table.timestamp('started_at');
table.timestamp('completed_at');
// Timestamps
table.timestamps(true, true);
table.timestamp('started_at');
table.timestamp('completed_at');
// Indexes for performance
table.index(
['fk_workspace_id'],
'nc_sandbox_deployment_logs_workspace_id_idx',
);
table.index(['base_id'], 'nc_sandbox_deployment_logs_base_id_idx');
table.index(['fk_sandbox_id'], 'nc_sandbox_deployment_logs_sandbox_id_idx');
table.index(
['base_id', 'created_at'],
'nc_sandbox_deployment_logs_base_created_idx',
);
table.index(['status'], 'nc_sandbox_deployment_logs_status_idx');
table.index(
['from_version_id'],
'nc_sandbox_deployment_logs_from_version_idx',
);
table.index(['to_version_id'], 'nc_sandbox_deployment_logs_to_version_idx');
});
// Indexes for performance
table.index(
['fk_workspace_id'],
'nc_sandbox_deployment_logs_workspace_id_idx',
);
table.index(['base_id'], 'nc_sandbox_deployment_logs_base_id_idx');
table.index(
['fk_sandbox_id'],
'nc_sandbox_deployment_logs_sandbox_id_idx',
);
table.index(
['base_id', 'created_at'],
'nc_sandbox_deployment_logs_base_created_idx',
);
table.index(['status'], 'nc_sandbox_deployment_logs_status_idx');
table.index(
['from_version_id'],
'nc_sandbox_deployment_logs_from_version_idx',
);
table.index(
['to_version_id'],
'nc_sandbox_deployment_logs_to_version_idx',
);
},
);
};
const down = async (knex: Knex) => {
// Drop sandbox_deployment_logs table
await knex.schema.dropTable(MetaTable.SANDBOX_DEPLOYMENT_LOGS);
await knex.schema.dropTable(MetaTable.SANDBOX_DEPLOYMENT_LOGS_OLD);
// Drop sandbox_versions table
await knex.schema.dropTable(MetaTable.SANDBOX_VERSIONS);
await knex.schema.dropTable(MetaTable.SANDBOX_VERSIONS_OLD);
// Drop indexes from bases table
await knex.schema.alterTable(MetaTable.PROJECT, (table) => {
@@ -190,7 +199,7 @@ const down = async (knex: Knex) => {
});
// Drop sandboxes table
await knex.schema.dropTable(MetaTable.SANDBOXES);
await knex.schema.dropTable(MetaTable.SANDBOXES_OLD);
};
export { up, down };