chore: move files in ee

This commit is contained in:
Ramesh Mane
2026-01-27 11:47:16 +00:00
parent 783c25d093
commit c7dbfc9dbb
10 changed files with 5 additions and 1928 deletions

3
.gitignore vendored
View File

@@ -104,4 +104,5 @@ result
# Temp # Temp
migrate-colors.js migrate-colors.js
antd.variable.css antd.variable.css
CLAUDE.md

132
CLAUDE.md
View File

@@ -1,132 +0,0 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Documentation
For comprehensive product documentation, features, and API references: https://nocodb.com/llms.txt
## Project Overview
NocoDB is an open-source Airtable alternative that turns any database into a smart spreadsheet. This is the enterprise edition (EE) monorepo.
Key capabilities:
- Transforms MySQL, PostgreSQL, SQL Server, SQLite & MariaDB into collaborative spreadsheets
- Rich field types: text, numerical, date-time, relational (Links), formula-based
- Multiple views: Grid, Form, Calendar, Kanban, Gallery, Map
- Auto-generated REST APIs
- Automation: workflows, webhooks, scripts
- NocoAI for AI capabilities, NocoSync for external data integration
- MCP (Model Context Protocol) server support
## Monorepo Structure
- **packages/nocodb** - Backend (NestJS + TypeScript)
- **packages/nc-gui** - Frontend (Nuxt 3 + Vue 3)
- **packages/nocodb-sdk** - JavaScript/TypeScript SDK
- **packages/nocodb-sdk-v2** - V2 SDK
- **packages/nc-sql-executor** - SQL execution service
- **packages/noco-integrations** - Integration plugins (Slack, Discord, AWS, etc.)
- **packages/nc-knex-dialects** - Custom Knex dialects (Snowflake, Databricks)
- **tests/playwright** - E2E testing suite
## Essential Commands
```bash
# Installation (pnpm is enforced - npm/yarn will fail)
pnpm bootstrap # Full EE bootstrap
pnpm bootstrap:ce # Community Edition bootstrap
# Development
pnpm start:backend # Backend at http://localhost:8080
pnpm start:frontend # Frontend at http://localhost:3000
pnpm start:sql-executor # SQL executor service
# Database containers for testing
pnpm start:pg # Start PostgreSQL
pnpm stop:pg
pnpm start:mysql # Start MySQL
pnpm stop:mysql
# E2E Testing
pnpm start:playwright:pg # Backend + Frontend for E2E (PostgreSQL)
pnpm start:playwright:pg:ee # Enterprise Edition E2E
```
### Backend (packages/nocodb)
```bash
pnpm start # Dev mode with watch
pnpm watch:run:ee # EE dev build
pnpm build:ee # Production EE build
pnpm lint # ESLint
pnpm test:unit # Mocha unit tests
pnpm test:unit:pg:ee # Unit tests with PostgreSQL + EE
```
### Frontend (packages/nc-gui)
```bash
pnpm dev # Nuxt dev server
pnpm dev:ee # EE frontend dev
pnpm build:ee # Production EE build
pnpm lint # ESLint
pnpm test # Vitest unit tests
```
## Architecture
### Backend (NestJS)
- **src/controllers/** - REST API controllers (~98 controllers)
- **src/models/** - Data models (Base, Model, View, Column, User, Integration, etc.)
- **src/services/** - Business logic
- **src/db/** - Database layer with BaseModelSqlv2, CustomKnex, formula evaluation
- **src/ee/, src/ee-on-prem/, src/ee-cloud/** - Enterprise Edition code
- **src/run/** - Entry points
### Frontend (Nuxt + Vue)
- **pages/** - Route-based pages
- **components/** - Vue components (~34 directories)
- **composables/** - Vue composables (~95 files)
- **store/** - Pinia stores
- **lang/** - i18n (42+ languages)
- **ee/** - Enterprise Edition features
- Uses hash-based routing (SPA mode)
### Database Model Hierarchy
```
Workspace → Base → Model (Table) → Column
→ View (Grid, Gallery, Form, Kanban, Calendar, Map)
```
## Key Environment Variables
- `NC_DB` - Database connection string
- `EE=true` - Enable Enterprise Edition
- `NC_AUTH_JWT_SECRET` - JWT secret
- `NC_DISABLE_TELE` - Disable telemetry
## Build Memory Issues
For large builds that run out of memory:
```bash
NODE_OPTIONS="--max-old-space-size=8192" pnpm build:ee
```
## Git Workflow
- **develop** - Main development branch (target for PRs)
- **master** - Stable release snapshots only
- Branch naming: `feat/`, `fix/`, `enhancement/`
- Commits follow [Conventional Commits](https://www.conventionalcommits.org/): `feat:`, `fix:`, `docs:`, `chore:`, etc.
## Tech Stack
- **Backend**: NestJS, Knex, Socket.io, Bull (job queue), Redis
- **Frontend**: Nuxt 3, Vue 3, Pinia, WindiCSS, Ant Design Vue
- **Databases**: SQLite (default), PostgreSQL, MySQL, Snowflake, Databricks
- **Build**: Rspack (backend), Vite (frontend)
- **Testing**: Mocha (backend unit), Vitest (frontend unit), Playwright (E2E)

View File

@@ -1,249 +0,0 @@
<script lang="ts" setup>
interface Props {
visible: boolean
}
const props = withDefaults(defineProps<Props>(), {})
const emits = defineEmits(['update:visible'])
const vVisible = useVModel(props, 'visible', emits)
const { $api } = useNuxtApp()
const baseStore = useBase()
const { loadManagedApp, loadCurrentVersion } = baseStore
const { base, managedApp, managedAppVersionsInfo } = storeToRefs(baseStore)
const isDraft = computed(() => managedAppVersionsInfo.value.current?.status === 'draft')
const isLoading = ref(false)
const title = computed(() => {
if (isDraft.value) {
return `Publish v${managedAppVersionsInfo.value.current?.version || '1.0.0'}?`
} else {
return `Fork to Draft`
}
})
const subTitle = computed(() => {
if (isDraft.value) {
return managedAppVersionsInfo.value.published
? `Replace v${managedAppVersionsInfo.value.published.version || '1.0.0'} and go live`
: `Go live`
} else {
return `Create v${suggestManagedAppNextVersion(managedAppVersionsInfo.value.published?.version || '1.0.0')} to make changes`
}
})
// Publish form (for draft versions)
const publishForm = reactive({
releaseNotes: '',
})
// Fork form (for creating new draft from published)
const forkForm = reactive({
version: '',
})
const loadManagedAppAndCurrentVersion = async () => {
await loadManagedApp()
await loadCurrentVersion()
}
const publishCurrentDraft = async () => {
if (!base.value?.fk_workspace_id || !base.value?.id || !managedAppVersionsInfo.value.current?.id) return
isLoading.value = true
try {
await $api.internal.postOperation(
base.value.fk_workspace_id,
base.value.id,
{
operation: 'managedAppPublish',
},
{
managedAppVersionId: managedAppVersionsInfo.value.current.id,
releaseNotes: publishForm.releaseNotes,
},
)
// Reload base to get updated managed app version info
if (base.value?.id) {
await baseStore.loadProject()
}
await loadManagedAppAndCurrentVersion()
message.success(`Version ${managedAppVersionsInfo.value.current?.version || '1.0.0'} published successfully!`)
vVisible.value = false
} catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e))
} finally {
isLoading.value = false
}
}
const createNewDraft = async () => {
if (!base.value?.fk_workspace_id || !base.value?.id || !managedApp.value?.id) return
if (!forkForm.version) {
message.error('Please provide a version')
return
}
isLoading.value = true
try {
await $api.internal.postOperation(
base.value.fk_workspace_id,
base.value.id,
{
operation: 'managedAppCreateDraft',
},
{
managedAppId: managedApp.value.id,
version: forkForm.version,
},
)
// Reload base to get updated managed app version info
if (base.value?.id) {
await baseStore.loadProject()
}
await loadManagedAppAndCurrentVersion()
message.success(`New draft version ${forkForm.version} created successfully!`)
vVisible.value = false
} catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e))
} finally {
isLoading.value = false
}
}
watch(
isDraft,
(newValue) => {
if (newValue) {
forkForm.version = managedAppVersionsInfo.value.current?.version || '1.0.0'
} else {
forkForm.version = suggestManagedAppNextVersion(managedAppVersionsInfo.value.current?.version)
}
},
{
immediate: true,
},
)
</script>
<template>
<div class="flex flex-col h-full">
<DlgManagedAppHeader v-model:visible="vVisible" :title="title" :sub-title="subTitle" />
<div class="flex-1 p-6 nc-scrollbar-thin">
<NcAlert
type="info"
align="top"
class="!p-3 !items-start bg-nc-bg-blue-light border-1 !border-nc-blue-200 rounded-lg p-3 mb-4"
>
<template #icon>
<GeneralIcon icon="info" class="w-4 h-4 mt-0.5 text-nc-content-blue-dark flex-none" />
</template>
<template v-if="isDraft" #description>
Publishing version <strong>{{ managedAppVersionsInfo.current?.version }}</strong> will make it available in the App
Store and automatically update all installations.
</template>
<template v-else #description>
Create a new draft version to work on updates. Current published version
<strong>{{ managedAppVersionsInfo.current?.version }}</strong> will remain unchanged.
</template>
</NcAlert>
<div class="space-y-4">
<div>
<label class="text-nc-content-gray text-sm font-medium mb-2 block">
<template v-if="isDraft"> Version </template>
<template v-else> New Version <span class="text-nc-content-red-dark">*</span> </template>
</label>
<a-input
v-model:value="forkForm.version"
placeholder="e.g., 2.0.0"
size="large"
:disabled="isDraft"
class="rounded-lg nc-input-sm nc-input-shadow"
>
<template #prefix>
<span class="text-nc-content-gray-subtle2">v</span>
</template>
</a-input>
<div v-if="!isDraft" class="text-xs text-nc-content-gray-subtle2 mt-1.5">
Use semantic versioning (e.g., 2.0.0, 2.1.0)
</div>
</div>
<div v-if="isDraft">
<label class="text-nc-content-gray text-sm font-medium mb-2 block">Changelog</label>
<div class="nc-changelog-editor-wrapper">
<LazyCellRichText
v-model:value="publishForm.releaseNotes"
class="nc-changelog-editor allow-vertical-resize"
placeholder="Describe what's new in this version"
show-menu
hide-mention
/>
</div>
</div>
</div>
</div>
<div class="nc-dlg-managed-app-footer">
<div class="flex justify-end gap-2">
<NcButton type="secondary" size="small" @click="vVisible = false">{{ $t('general.cancel') }} </NcButton>
<NcButton v-if="isDraft" type="primary" size="small" :loading="isLoading" @click="publishCurrentDraft">
<template #icon>
<GeneralIcon icon="upload" />
</template>
Publish
</NcButton>
<NcButton v-else type="primary" size="small" :loading="isLoading" :disabled="!forkForm.version" @click="createNewDraft">
<template #icon>
<GeneralIcon icon="plus" />
</template>
Create Draft
</NcButton>
</div>
</div>
</div>
</template>
<style lang="scss" scoped>
.nc-dlg-managed-app-footer {
@apply px-6 py-3 border-t-1 border-nc-border-gray-medium;
}
.nc-changelog-editor-wrapper {
@apply relative pt-11 border-1 border-nc-border-gray-medium rounded-lg focus-within:border-nc-border-brand focus-within:shadow-selected transition-all duration-200;
}
</style>
<style lang="scss">
.nc-changelog-editor-wrapper {
.nc-changelog-editor {
@apply border-t-1 border-nc-border-gray-medium;
.nc-textarea-rich-editor {
.ProseMirror {
@apply border-0 rounded-none min-h-42 max-h-150 p-3;
}
.ProseMirror-focused {
@apply border-0;
}
}
}
}
</style>

View File

@@ -1,60 +0,0 @@
<script lang="ts" setup>
interface Props {
visible: boolean
iconClass?: string
title?: string
subTitle?: string
titleClass?: string
subTitleClass?: string
}
const props = withDefaults(defineProps<Props>(), {
iconClass: '',
titleClass: '',
subTitleClass: '',
})
const emits = defineEmits(['update:visible'])
const { title, subTitle } = toRefs(props)
const vVisible = useVModel(props, 'visible', emits)
</script>
<template>
<div class="p-4 w-full flex items-center gap-3 border-b border-nc-border-gray-medium">
<div class="nc-dlg-managed-app-icon" :class="iconClass">
<slot name="icon">
<GeneralIcon icon="ncBox" class="h-5 w-5" />
</slot>
</div>
<div class="flex-1">
<div class="font-semibold text-lg text-nc-content-gray" :class="titleClass">
<slot name="title">
{{ title }}
</slot>
</div>
<div v-if="$slots.subTitle || subTitle" class="text-xs text-nc-content-gray-subtle2" :class="subTitleClass">
<slot name="subTitle">
{{ subTitle }}
</slot>
</div>
</div>
<slot name="rightExtra"> </slot>
<slot name="closeButton">
<NcButton size="small" type="text" class="self-start" @click="vVisible = false">
<GeneralIcon icon="close" class="text-nc-content-gray-subtle2" />
</NcButton>
</slot>
</div>
</template>
<style lang="scss" scoped>
.nc-dlg-managed-app-icon {
@apply w-10 h-10 rounded-xl flex items-center justify-center text-white shadow-sm;
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);
}
</style>

View File

@@ -1,63 +0,0 @@
<script lang="ts" setup>
interface Props {
visible: boolean
modalSize: 'small' | 'medium' | 'large' | keyof typeof modalSizes
title?: string
subTitle?: string
variant?: 'draftOrPublish' | 'versionHistory'
contentClass?: string
maskClosable?: boolean
}
const props = withDefaults(defineProps<Props>(), {
modalSize: 'sm',
contentClass: '',
maskClosable: true,
})
const emits = defineEmits(['update:visible'])
const vVisible = useVModel(props, 'visible', emits)
const { modalSize, variant } = toRefs(props)
</script>
<template>
<NcModal
v-model:visible="vVisible"
:size="modalSize"
:height="modalSize === 'sm' ? 'auto' : undefined"
:mask-closable="maskClosable"
nc-modal-class-name="nc-modal-dlg-managed-app"
>
<slot v-if="$slots.default"> </slot>
<template v-else-if="variant === 'draftOrPublish'">
<DlgManagedAppDraftOrPublish v-model:visible="vVisible" />
</template>
<template v-else-if="variant === 'versionHistory'">
<DlgManagedAppVersionHistory v-model:visible="vVisible" />
</template>
<template v-else>
<slot name="header">
<DlgManagedAppHeader v-model:visible="vVisible" :modal-size="modalSize" :title="title" :sub-title="subTitle" />
</slot>
<div class="flex-1 nc-scrollbar-thin" :class="contentClass">
<slot name="content"> </slot>
</div>
<slot v-if="$slots.footer" name="footer"> </slot>
</template>
</NcModal>
</template>
<style lang="scss">
.nc-modal-dlg-managed-app {
@apply !p-0;
&.nc-modal-size-sm {
max-height: min(90vh, 560px) !important;
height: min(90vh, 560px) !important;
}
}
</style>

View File

@@ -1,493 +0,0 @@
<script lang="ts" setup>
import { DeploymentStatus } from 'nocodb-sdk'
interface Props {
visible: boolean
version: any
}
const props = withDefaults(defineProps<Props>(), {})
const emits = defineEmits(['update:visible'])
const vVisible = useVModel(props, 'visible', emits)
const { $api } = useNuxtApp()
const baseStore = useBase()
const { base, managedApp } = storeToRefs(baseStore)
const isLoading = ref(false)
const deployments = ref<any[]>([])
const pageInfo = ref<any>({})
const currentPage = ref(1)
const pageSize = 10
// Expanded deployment logs state
const expandedBaseId = ref<string | null>(null)
const deploymentLogs = ref<any[]>([])
const logsPageInfo = ref<any>({})
const logsCurrentPage = ref(1)
const logsPageSize = 10
const isLoadingLogs = ref(false)
const loadDeployments = async (page = 1) => {
if (!managedApp.value?.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: 'managedAppVersionDeployments',
managedAppId: managedApp.value.id,
versionId: props.version.versionId,
limit: pageSize,
offset,
} as any)
if (response) {
deployments.value = response.list || []
pageInfo.value = response.pageInfo || {}
currentPage.value = page
}
} catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e))
} finally {
isLoading.value = false
}
}
const loadDeploymentLogs = async (baseId: string, page = 1) => {
if (!base.value?.fk_workspace_id) return
isLoadingLogs.value = true
try {
const offset = (page - 1) * logsPageSize
const response = await $api.internal.getOperation(base.value.fk_workspace_id, base.value.id!, {
operation: 'managedAppDeploymentLogs',
baseId,
limit: logsPageSize,
offset,
} as any)
if (response) {
deploymentLogs.value = response.logs || []
logsPageInfo.value = response.pageInfo || {}
logsCurrentPage.value = page
}
} catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e))
} finally {
isLoadingLogs.value = false
}
}
const toggleExpand = async (deployment: any) => {
if (expandedBaseId.value === deployment.baseId) {
// Collapse
expandedBaseId.value = null
deploymentLogs.value = []
logsCurrentPage.value = 1
} else {
// Expand and load logs
expandedBaseId.value = deployment.baseId
await loadDeploymentLogs(deployment.baseId, 1)
}
}
const formatDate = (dateString: string) => {
if (!dateString) return 'N/A'
return parseStringDateTime(dateString, 'MMM DD, YYYY, hh:mm A')
}
const getDeploymentTypeLabel = (type: string) => {
const labels = {
install: 'Initial Install',
update: 'Update',
}
return labels[type as keyof typeof labels] || type
}
const getStatusColor = (status: string) => {
const colors = {
[DeploymentStatus.SUCCESS]: 'text-green-600 bg-nc-green-50 dark:bg-nc-green-20',
[DeploymentStatus.FAILED]: 'text-red-600 bg-nc-red-50 dark:bg-nc-red-20',
[DeploymentStatus.PENDING]: 'text-orange-600 bg-nc-orange-20 dark:bg-nc-orange-20',
[DeploymentStatus.IN_PROGRESS]: 'text-blue-600 bg-nc-blue-50 dark:bg-nc-blue-20',
}
return colors[status as keyof typeof colors] || 'text-nc-content-gray bg-nc-bg-gray-light'
}
const handlePageChange = (page: number) => {
loadDeployments(page)
}
const handleLogsPageChange = (page: number) => {
if (expandedBaseId.value) {
loadDeploymentLogs(expandedBaseId.value, page)
}
}
watch(
vVisible,
(val) => {
if (val) {
loadDeployments(1)
expandedBaseId.value = null
deploymentLogs.value = []
}
},
{
immediate: true,
},
)
</script>
<template>
<div class="flex flex-col h-full">
<DlgManagedAppHeader v-model:visible="vVisible" title="Version Deployments">
<template #icon>
<GeneralIcon icon="ncServer" class="w-5 h-5 text-white" />
</template>
<template #subTitle>
Tracking installations for
<span class="font-mono font-semibold text-nc-content-brand">v{{ version?.version }}</span>
</template>
</DlgManagedAppHeader>
<!-- Content -->
<div class="nc-deployments-content">
<div v-if="isLoading" class="nc-deployments-loading">
<a-spin size="large" />
<div class="text-sm text-nc-content-gray-muted mt-3">Loading deployments...</div>
</div>
<template v-else-if="deployments.length > 0">
<div class="nc-deployments-list">
<div v-for="deployment in deployments" :key="deployment.baseId" class="nc-deployment-item">
<!-- Deployment Row -->
<div class="nc-deployment-row" @click="toggleExpand(deployment)">
<div class="nc-deployment-info">
<!-- Expand Icon -->
<div class="nc-expand-icon">
<GeneralIcon
:icon="expandedBaseId === deployment.baseId ? 'ncChevronDown' : 'ncChevronRight'"
class="w-4 h-4"
/>
</div>
<!-- Deployment Details -->
<div class="nc-deployment-icon">
<GeneralIcon icon="ncServer" class="w-4 h-4" />
</div>
<div class="nc-deployment-details">
<div class="nc-deployment-title">{{ deployment.baseTitle }}</div>
<div class="nc-deployment-date">
<GeneralIcon icon="calendar" class="w-3.5 h-3.5 opacity-60" />
<span>Installed {{ formatDate(deployment.installedAt) }}</span>
</div>
</div>
</div>
<!-- Status Badge -->
<div class="nc-deployment-status">
<div class="nc-status-badge" :class="getStatusColor(deployment.status)">
<span>{{ deployment.status }}</span>
</div>
</div>
</div>
<!-- Expanded Logs Section -->
<div v-if="expandedBaseId === deployment.baseId" class="nc-deployment-logs-wrapper">
<div class="nc-deployment-logs">
<div class="nc-logs-header">
<GeneralIcon icon="ncFileText" class="w-4 h-4 text-nc-content-gray-subtle2" />
<span>Deployment History</span>
</div>
<div v-if="isLoadingLogs" class="nc-logs-loading">
<a-spin size="small" />
<span class="text-xs text-nc-content-gray-muted ml-2">Loading history...</span>
</div>
<template v-else-if="deploymentLogs.length > 0">
<div class="nc-logs-list">
<div v-for="log in deploymentLogs" :key="log.id" class="nc-log-item">
<div class="nc-log-content">
<div class="nc-log-header">
<div class="nc-log-badges">
<div class="nc-log-status" :class="getStatusColor(log.status)">
{{ log.status }}
</div>
<div class="nc-log-type">
<GeneralIcon
:icon="log.deploymentType === 'install' ? 'download' : 'reload'"
class="w-3 h-3 opacity-60"
/>
<span>{{ getDeploymentTypeLabel(log.deploymentType) }}</span>
</div>
</div>
</div>
<div class="nc-log-meta">
<div class="nc-log-version">
<div v-if="log.fromVersion" class="flex items-center gap-1.5">
<span class="font-mono text-nc-content-gray-subtle2">v{{ log.fromVersion.version }}</span>
<GeneralIcon icon="arrowRight" class="w-3 h-3 text-nc-content-gray-subtle2" />
</div>
<span class="font-mono font-semibold text-nc-content-brand">v{{ log.toVersion?.version }}</span>
</div>
<span class="nc-log-divider flex-1"></span>
<div class="nc-log-time">
<GeneralIcon icon="ncClock" class="w-3.5 h-3.5 opacity-60" />
<span>{{ timeAgo(log.createdAt) }}</span>
</div>
</div>
<div v-if="log.errorMessage" class="nc-log-error">
<GeneralIcon icon="alertTriangle" class="w-3.5 h-3.5 flex-shrink-0" />
<span>{{ log.errorMessage }}</span>
</div>
</div>
</div>
</div>
<!-- Logs Pagination -->
<div v-if="logsPageInfo.totalRows > logsPageSize" class="nc-logs-pagination">
<a-pagination
v-model:current="logsCurrentPage"
:total="logsPageInfo.totalRows"
:page-size="logsPageSize"
:show-size-changer="false"
size="small"
@change="handleLogsPageChange"
/>
</div>
</template>
<div v-else class="nc-logs-empty">
<GeneralIcon icon="inbox" class="w-8 h-8 text-nc-content-gray-subtle2 mb-2" />
<div class="text-sm text-nc-content-gray-subtle2">No deployment history available</div>
</div>
</div>
</div>
</div>
</div>
</template>
<!-- Empty State -->
<div v-else class="nc-deployments-empty">
<div class="nc-empty-icon">
<GeneralIcon icon="ncServer" class="w-10 h-10 text-nc-content-gray-muted" />
</div>
<div class="text-base font-semibold text-nc-content-gray mb-1">No installations found</div>
<div class="text-sm text-nc-content-gray-subtle max-w-md text-center">
Version <span class="font-mono font-semibold">v{{ version?.version }}</span> hasn't been installed by any users yet
</div>
</div>
</div>
<!-- Deployments Pagination -->
<div v-if="deployments.length > 0 && pageInfo.totalRows > pageSize" class="nc-deployments-pagination">
<a-pagination
v-model:current="currentPage"
:total="pageInfo.totalRows"
:page-size="pageSize"
:show-size-changer="false"
@change="handlePageChange"
/>
</div>
</div>
</template>
<style lang="scss" scoped>
.nc-deployments-content {
@apply flex-1 nc-scrollbar-thin p-6;
}
.nc-deployments-loading {
@apply h-full flex flex-col items-center justify-center py-16;
}
.nc-deployments-list {
@apply space-y-3;
}
.nc-deployment-item {
@apply bg-nc-bg-default border-1 border-nc-border-gray-medium rounded-xl overflow-hidden relative;
@apply transition-all duration-200 ease-in-out;
&::before {
@apply absolute left-0 top-0 bottom-0 w-1 bg-nc-content-brand opacity-0;
@apply transition-opacity duration-200 ease-in-out;
content: '';
}
&:hover {
@apply border-nc-border-brand transform translate-x-0.5;
box-shadow: 0 4px 12px rgba(51, 102, 255, 0.08);
&::before {
@apply opacity-100;
}
.nc-deployment-icon {
@apply transform scale-105;
box-shadow: 0 4px 8px rgba(51, 102, 255, 0.15);
}
}
}
.nc-deployment-row {
@apply flex items-center justify-between gap-4 p-4 cursor-pointer;
}
.nc-deployment-info {
@apply flex items-center gap-3 flex-1 min-w-0;
}
.nc-expand-icon {
@apply w-6 h-6 flex items-center justify-center text-nc-content-gray-subtle2 flex-shrink-0;
@apply transition-transform duration-200;
}
.nc-deployment-icon {
@apply w-9 h-9 rounded-lg bg-nc-bg-gray-light border-1 border-nc-border-gray-light;
@apply flex items-center justify-center text-nc-content-gray flex-shrink-0;
@apply transition-all duration-200 ease-in-out;
}
.nc-deployment-details {
@apply flex-1 min-w-0;
}
.nc-deployment-title {
@apply font-semibold text-sm text-nc-content-gray truncate mb-1;
}
.nc-deployment-date {
@apply flex items-center gap-1.5 text-xs text-nc-content-gray-subtle2;
}
.nc-deployment-status {
@apply flex-shrink-0;
}
.nc-status-badge {
@apply inline-flex items-center px-2.5 py-1 rounded-full text-xs font-semibold;
}
.nc-deployment-logs-wrapper {
@apply border-t-1 border-nc-border-gray-medium bg-nc-bg-gray-extralight;
}
.nc-deployment-logs {
@apply p-4;
}
.nc-logs-header {
@apply flex items-center gap-2 text-xs font-semibold text-nc-content-gray mb-3;
@apply uppercase tracking-wide;
}
.nc-logs-loading {
@apply flex items-center justify-center py-8;
}
.nc-logs-list {
@apply space-y-2;
}
.nc-log-item {
@apply bg-nc-bg-default border-1 border-nc-border-gray-light rounded-lg p-3;
@apply transition-all duration-150;
&:hover {
@apply border-nc-border-gray-medium shadow-sm;
}
}
.nc-log-content {
@apply space-y-2;
}
.nc-log-header {
@apply flex items-center justify-between gap-2;
}
.nc-log-badges {
@apply flex items-center gap-2 flex-wrap;
}
.nc-log-status {
@apply inline-flex px-2 py-0.5 rounded text-xs font-semibold;
}
.nc-log-type {
@apply inline-flex items-center gap-1 px-2 py-0.5 rounded;
@apply bg-nc-bg-gray-light dark:bg-nc-bg-gray-extralight text-nc-content-gray-subtle2 text-xs font-medium;
}
.nc-log-meta {
@apply flex items-center gap-2 text-xs text-nc-content-gray-subtle2 flex-wrap;
}
.nc-log-version {
@apply flex items-center gap-1.5;
}
.nc-log-divider {
@apply text-nc-content-gray-subtle2;
}
.nc-log-time {
@apply flex items-center gap-1;
}
.nc-log-error {
@apply flex items-start gap-2 p-2 rounded-lg;
@apply bg-nc-bg-red-light text-nc-content-red-dark text-xs;
}
.nc-logs-pagination {
@apply flex justify-center mt-4 pt-4 border-t-1 border-nc-border-gray-light;
}
.nc-logs-empty {
@apply flex flex-col items-center justify-center py-12 text-center;
}
.nc-deployments-pagination {
@apply flex justify-center mt-6;
}
.nc-deployments-empty {
@apply flex flex-col items-center justify-center py-16;
}
.nc-empty-icon {
@apply w-16 h-16 rounded-full bg-nc-bg-gray-light;
@apply flex items-center justify-center mb-4;
}
// Responsive
@media (max-width: 640px) {
.nc-deployment-row {
@apply flex-col items-start;
}
.nc-deployment-info {
@apply w-full;
}
.nc-deployment-status {
@apply w-full;
}
.nc-log-meta {
@apply flex-col items-start gap-1;
}
}
</style>

View File

@@ -1,357 +0,0 @@
<script lang="ts" setup>
interface Props {
visible: boolean
}
const props = withDefaults(defineProps<Props>(), {})
const emits = defineEmits(['update:visible'])
const vVisible = useVModel(props, 'visible', emits)
const { $api } = useNuxtApp()
const baseStore = useBase()
const { base, managedApp, managedAppVersionsInfo } = storeToRefs(baseStore)
const isLoadingDeployments = ref(true)
const deploymentStats = ref<any>(null)
// Version deployments modal
const showVersionDeploymentsModal = ref(false)
const selectedVersion = ref<any>(null)
// Load real deployment statistics
const loadDeployments = async () => {
if (!managedApp.value?.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: 'managedAppDeployments',
managedAppId: managedApp.value.id,
} as any)
if (response) {
deploymentStats.value = response
}
} catch (e: any) {
console.error('Failed to load deployments:', e)
} finally {
isLoadingDeployments.value = false
}
}
const formatDate = (dateString: string) => {
if (!dateString) return 'N/A'
return parseStringDateTime(dateString, 'MMM DD, YYYY, hh:mm A')
}
const openVersionDeploymentsModal = (version: any) => {
selectedVersion.value = version
showVersionDeploymentsModal.value = true
}
watch(
vVisible,
(val) => {
if (val) {
loadDeployments()
}
},
{
immediate: true,
},
)
</script>
<template>
<div class="flex flex-col h-full">
<DlgManagedAppHeader v-model:visible="vVisible" title="Version History" sub-title="Manage versions and track deployments" />
<div class="flex-1 nc-scrollbar-thin">
<div
class="nc-deployments-content"
:class="{
'h-full': isLoadingDeployments,
}"
>
<div v-if="isLoadingDeployments" class="nc-deployments-loading">
<a-spin size="large" />
<div class="text-sm text-nc-content-gray-muted mt-3">Loading deployment statistics...</div>
</div>
<template v-else-if="deploymentStats">
<!-- Stats Cards -->
<div class="nc-deployment-stats">
<!-- Total Deployments -->
<div class="nc-stat-card">
<div class="nc-stat-value">{{ deploymentStats.statistics?.totalDeployments || 0 }}</div>
<div class="nc-stat-label">Total Installs</div>
</div>
<!-- Active -->
<div class="nc-stat-card">
<div class="nc-stat-value text-green-600">{{ deploymentStats.statistics?.activeDeployments || 0 }}</div>
<div class="nc-stat-label">Active</div>
</div>
<!-- Failed -->
<div class="nc-stat-card">
<div class="nc-stat-value text-red-600">{{ deploymentStats.statistics?.failedDeployments || 0 }}</div>
<div class="nc-stat-label">Failed</div>
</div>
<!-- Versions -->
<div class="nc-stat-card">
<div class="nc-stat-value">{{ deploymentStats.statistics?.totalVersions || 0 }}</div>
<div class="nc-stat-label">Versions</div>
</div>
</div>
<!-- Version List -->
<div v-if="deploymentStats.versionStats && deploymentStats.versionStats.length > 0" class="nc-version-list-wrapper">
<div class="nc-version-list-header">
<div>
<h3 class="text-sm font-semibold text-nc-content-gray">Version History</h3>
<p class="text-xs text-nc-content-gray-subtle2 mt-0.5 mb-0">
{{ deploymentStats.versionStats.length }}
{{ deploymentStats.versionStats.length === 1 ? 'version' : 'versions' }}
published
</p>
</div>
</div>
<div class="nc-version-list">
<div
v-for="versionStat in deploymentStats.versionStats"
:key="versionStat.versionId"
class="nc-version-item"
:class="{ 'nc-version-item-clickable': versionStat.deploymentCount > 0 }"
@click="versionStat.deploymentCount > 0 && openVersionDeploymentsModal(versionStat)"
>
<div class="nc-version-info">
<div class="nc-version-icon">
<GeneralIcon icon="ncGitBranch" class="w-4 h-4" />
</div>
<div class="nc-version-details">
<div class="nc-version-title">
<span class="nc-version-number">v{{ versionStat.version }}</span>
<div v-if="managedAppVersionsInfo.published?.id === versionStat.versionId" class="nc-version-badge">
{{ $t('labels.live') }}
</div>
<div
v-else-if="managedAppVersionsInfo.current?.id === versionStat.versionId && versionStat.status === 'draft'"
class="nc-version-badge nc-version-badge-draft"
>
{{ $t('labels.draft') }}
</div>
</div>
<div class="nc-version-date">
<GeneralIcon icon="calendar" class="w-3.5 h-3.5 opacity-60" />
<span>Published {{ formatDate(versionStat.publishedAt) }}</span>
</div>
</div>
</div>
<div class="nc-version-installs">
<div class="nc-installs-count">
<GeneralIcon icon="download" class="w-4 h-4 text-nc-content-gray-subtle2" />
<span class="font-bold">{{ versionStat.deploymentCount }}</span>
</div>
<GeneralIcon
v-if="versionStat.deploymentCount > 0"
icon="chevronRight"
class="w-4 h-4 text-nc-content-gray-subtle2"
/>
</div>
</div>
</div>
</div>
<!-- Empty State -->
<div v-else class="nc-deployments-empty">
<div class="nc-empty-icon">
<GeneralIcon icon="ncServer" class="w-10 h-10 text-nc-content-gray-muted" />
</div>
<div class="text-base font-semibold text-nc-content-gray mb-1">No installations yet</div>
<div class="text-sm text-nc-content-gray-subtle max-w-md text-center">
Once users install your application from the App Store, their deployments will appear here.
</div>
</div>
</template>
<div v-else class="nc-deployments-error">
<div class="nc-error-icon">
<GeneralIcon icon="alertTriangle" class="w-10 h-10 text-nc-content-red-dark" />
</div>
<div class="text-base font-semibold text-nc-content-gray mb-1">Failed to load statistics</div>
<div class="text-sm text-nc-content-gray-subtle mb-4">There was an error loading deployment data</div>
<NcButton size="small" type="secondary" @click="loadDeployments">
<template #icon>
<GeneralIcon icon="reload" />
</template>
Retry
</NcButton>
</div>
</div>
</div>
<DlgManagedApp v-model:visible="showVersionDeploymentsModal" modal-size="sm">
<DlgManagedAppVersionDeployments v-model:visible="showVersionDeploymentsModal" :version="selectedVersion" />
</DlgManagedApp>
</div>
</template>
<style lang="scss" scoped>
// Deployments Tab Styles
.nc-deployments-content {
@apply px-6 pb-6;
}
.nc-deployments-loading {
@apply h-full flex flex-col items-center justify-center py-16;
}
.nc-deployment-stats {
@apply grid grid-cols-4 mb-4;
}
.nc-stat-card {
@apply flex flex-col gap-1 border-r-1 border-nc-border-gray-medium last:border-r-0 p-4 text-nc-content-gray-subtle text-center;
}
.nc-stat-icon-wrapper {
@apply w-10 h-10 rounded-lg flex items-center justify-center mb-3;
}
.nc-stat-value {
@apply text-subHeading1 font-normal;
}
.nc-stat-label {
@apply text-tiny text-nc-content-gray-muted uppercase;
}
.nc-version-list-wrapper {
@apply mt-4;
}
.nc-version-list-header {
@apply flex items-center justify-between mb-4;
}
.nc-version-list {
@apply space-y-2;
}
.nc-version-item {
@apply bg-nc-bg-default border-1 border-nc-border-gray-medium rounded-xl p-4 flex items-center justify-between gap-4 relative overflow-hidden;
@apply transition-all duration-200 ease-in-out;
&::before {
@apply absolute left-0 top-0 bottom-0 w-1 bg-nc-content-brand opacity-0;
@apply transition-opacity duration-200 ease-in-out;
content: '';
}
&.nc-version-item-clickable {
@apply cursor-pointer;
&:hover {
@apply border-nc-border-brand transform translate-x-0.5;
box-shadow: 0 4px 12px rgba(51, 102, 255, 0.08);
&::before {
@apply opacity-100;
}
.nc-version-icon {
@apply transform scale-105;
box-shadow: 0 4px 8px rgba(51, 102, 255, 0.15);
}
.nc-chevron-icon {
@apply opacity-100;
}
}
}
&:last-child {
@apply mb-0;
}
}
.nc-chevron-icon {
@apply opacity-0 transition-opacity duration-200 ease-in-out;
}
.nc-version-info {
@apply flex items-center gap-3 flex-1 min-w-0;
}
.nc-version-icon {
@apply w-9 h-9 rounded-lg bg-nc-bg-gray-light border-1 border-nc-border-gray-light;
@apply flex items-center justify-center text-nc-content-gray flex-shrink-0;
@apply transition-all duration-200 ease-in-out;
}
.nc-version-details {
@apply flex-1 min-w-0;
}
.nc-version-title {
@apply flex items-center gap-2 mb-1;
}
.nc-version-number {
@apply font-mono font-bold text-base text-nc-content-gray;
}
.nc-version-badge {
@apply inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-semibold;
&.nc-version-badge-draft {
@apply bg-nc-orange-20 dark:bg-nc-orange-20 text-orange-600;
}
&:not(.nc-version-badge-draft) {
@apply bg-nc-green-50 dark:bg-nc-green-20 text-green-600;
}
}
.nc-version-date {
@apply text-xs text-nc-content-gray-subtle2 flex items-center gap-1.5;
}
.nc-version-installs {
@apply flex items-center gap-3;
}
.nc-installs-count {
@apply flex items-center gap-2 text-sm;
@apply px-3 py-1.5 rounded-lg bg-nc-bg-gray-light;
}
.nc-deployments-empty {
@apply flex flex-col items-center justify-center py-16;
}
.nc-empty-icon {
@apply w-16 h-16 rounded-full bg-nc-bg-gray-light;
@apply flex items-center justify-center mb-4;
}
.nc-deployments-error {
@apply flex flex-col items-center justify-center py-16;
}
.nc-error-icon {
@apply w-16 h-16 rounded-full bg-nc-bg-red-light;
@apply flex items-center justify-center mb-4;
}
</style>

View File

@@ -1,52 +0,0 @@
<script lang="ts" setup>
interface Props {
label?: string
subtext?: string
icon?: IconMapKey
iconWrapperClass?: string
clickable?: boolean
}
withDefaults(defineProps<Props>(), {})
</script>
<template>
<div :tabindex="clickable ? 0 : undefined" class="nc-managed-app-status-menu-item" :class="{ 'nc-clickable': clickable }">
<div class="nc-icon-wrapper" :class="iconWrapperClass">
<slot name="icon">
<GeneralIcon v-if="icon" :icon="icon" class="h-4 w-4 flex-none" />
</slot>
</div>
<div class="nc-content-wrapper flex-1">
<div v-if="$slots.label || label" 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>
<slot name="extraRight"> </slot>
</div>
</template>
<style lang="scss" scoped>
.nc-managed-app-status-menu-item {
@apply flex items-center gap-3 p-2 my-0.5 mx-1 rounded-lg transition-colors;
&.nc-clickable {
@apply cursor-pointer select-none hover:bg-nc-bg-gray-extralight;
}
}
.nc-icon-wrapper {
@apply w-8 h-8 rounded-lg flex items-center justify-center children:flex-none;
}
.nc-content-label {
@apply text-caption font-600 text-nc-content-gray-subtle2;
}
.nc-content-subtext {
@apply text-captionSm text-nc-content-gray-muted mt-0.25;
}
</style>

View File

@@ -1,284 +1,3 @@
<script setup lang="ts">
const { t } = useI18n()
const baseStore = useBase()
const { loadManagedApp, loadCurrentVersion } = baseStore
const { base, isManagedAppMaster, isManagedAppInstaller, managedAppVersionsInfo } = storeToRefs(baseStore)
const isModalVisible = ref(false)
const modalVariant = ref<'draftOrPublish' | 'versionHistory' | undefined>(undefined)
const isOpenDropdown = ref<boolean>(false)
const isDraft = computed(() => managedAppVersionsInfo.value.current?.status === 'draft')
const openModal = (variant?: 'draftOrPublish' | 'versionHistory') => {
isOpenDropdown.value = false
modalVariant.value = variant
nextTick(() => {
isModalVisible.value = true
})
}
const loadManagedAppAndCurrentVersion = async () => {
await loadManagedApp()
await loadCurrentVersion()
}
watch(
() => (base.value as any)?.managed_app_id,
async (managedAppId) => {
if (!managedAppId) return
await loadManagedAppAndCurrentVersion()
},
{ immediate: true },
)
const colors = {
green: {
bg: 'bg-nc-green-50 dark:bg-nc-green-20',
border: 'border-green-200 dark:border-nc-green-100',
text: 'text-green-600 dark:text-nc-green-300',
},
orange: {
bg: 'bg-nc-orange-20 dark:bg-nc-orange-20',
border: 'border-nc-orange-200 dark:border-orange-600/40',
text: 'text-orange-600',
},
brand: {
bg: 'bg-nc-brand-50 dark:bg-nc-brand-20/40',
border: 'border-nc-brand-200 dark:border-nc-content-brand/50',
text: 'text-nc-content-brand',
},
}
const badgeConfig = computed(() => {
const result = {
colors: colors.green,
icon: 'circleCheck3',
subText: '',
}
if (isManagedAppInstaller.value) {
if (managedAppVersionsInfo.value.updateAvailable) {
result.colors = colors.brand
result.icon = 'ncDownload'
result.subText = t('labels.updateAvailable')
} else {
result.colors = colors.green
result.icon = 'circleCheck3'
result.subText = t('labels.upToDate')
}
} else if (isDraft.value) {
result.colors = colors.orange
result.icon = 'pencil'
result.subText = t('labels.draft')
} else {
result.colors = colors.green
result.icon = 'circleCheck3'
result.subText = t('labels.published')
}
return result
})
</script>
<template> <template>
<NcDropdown v-if="isManagedAppMaster || isManagedAppInstaller" v-model:visible="isOpenDropdown" placement="bottomRight"> <NcSpanHidden />
<div class="flex items-center gap-2">
<!-- Version Badge (clickable to open modal) -->
<div
class="flex items-center gap-2 px-2.5 py-1 h-8 rounded-lg border-1 cursor-pointer transition-colors select-none"
:class="[badgeConfig.colors.bg, badgeConfig.colors.border, badgeConfig.colors.text]"
>
<GeneralIcon :icon="badgeConfig.icon as IconMapKey" class="w-3.5 h-3.5 text-current nc-managed-app-status-info-icon" />
<span class="text-xs font-mono font-medium"> v{{ managedAppVersionsInfo.current?.version || '1.0.0' }}</span>
<span class="py-0.5 text-xs font-medium whitespace-nowrap">
{{ badgeConfig.subText }}
</span>
<GeneralIcon
icon="chevronDown"
class="w-3.5 h-3.5 text-nc-content-gray-muted opacity-80 transform transition-all duration-200"
:class="{
'rotate-180': isOpenDropdown,
}"
/>
</div>
</div>
<template #overlay>
<div class="nc-managed-app-status-menu flex flex-col">
<div class="nc-managed-app-status-menu-header">
<span class="uppercase">{{ isManagedAppInstaller ? 'Installed Version' : 'Current State' }}</span>
<span v-if="managedAppVersionsInfo.current?.status === 'draft' && managedAppVersionsInfo.published"
>Live: v{{ managedAppVersionsInfo.published.version }}
</span>
</div>
<!-- Publisher application -->
<template v-if="isManagedAppMaster">
<!-- Live state -->
<template v-if="managedAppVersionsInfo.published && !isDraft">
<SmartsheetTopbarManagedAppStatusMenuItem
:label="`v${managedAppVersionsInfo.published.version || '1.0.0'}`"
icon-wrapper-class="bg-green-50 dark:bg-nc-green-20"
>
<template #icon>
<GeneralIcon icon="circleCheck3" class="text-green-600" />
</template>
<template #subtext>
<span class="text-green-600"> Live & Serving Users </span>
</template>
</SmartsheetTopbarManagedAppStatusMenuItem>
<NcDivider class="!my-1" />
<SmartsheetTopbarManagedAppStatusMenuItem
clickable
label="Fork to Draft"
:subtext="`Create v${suggestManagedAppNextVersion(
managedAppVersionsInfo.published.version || '1.0.0',
)} to make changes`"
icon-wrapper-class="bg-nc-bg-gray-light dakr:bg-nc-bg-gray-light/75"
@click="openModal('draftOrPublish')"
>
<template #icon>
<GeneralIcon icon="ncCopy" class="text-nc-content-gray-muted" />
</template>
</SmartsheetTopbarManagedAppStatusMenuItem>
</template>
<!-- Draft state -->
<template v-if="isDraft">
<SmartsheetTopbarManagedAppStatusMenuItem
:label="`v${managedAppVersionsInfo.current.version || '1.0.0'}`"
icon-wrapper-class="bg-orange-50 dark:bg-nc-orange-20"
>
<template #icon>
<GeneralIcon icon="edit" class="text-orange-600" />
</template>
<template #subtext>
<span class="text-orange-600"> Editing Draft </span>
</template>
</SmartsheetTopbarManagedAppStatusMenuItem>
<NcDivider class="!my-1" />
<SmartsheetTopbarManagedAppStatusMenuItem
clickable
icon-wrapper-class="bg-green-50 dark:bg-nc-green-20"
@click="openModal('draftOrPublish')"
>
<template #icon>
<GeneralIcon icon="ncArrowUp" class="text-green-600" />
</template>
<template #label>
<span class="text-green-600"> Publish v{{ managedAppVersionsInfo.current.version || '1.0.0' }} </span>
</template>
<template #subtext>
{{
managedAppVersionsInfo.published
? `Replace v${managedAppVersionsInfo.published.version || '1.0.0'} and go live`
: `Go live`
}}
</template>
</SmartsheetTopbarManagedAppStatusMenuItem>
</template>
<!-- Initial draft state -->
<SmartsheetTopbarManagedAppStatusMenuItem
v-if="managedAppVersionsInfo.published && isDraft"
clickable
label="Discard Draft"
:subtext="`Return to v${managedAppVersionsInfo.published.version || '1.0.0'}`"
icon-wrapper-class="bg-nc-bg-gray-light"
>
<template #icon>
<GeneralIcon icon="delete" class="text-nc-content-gray-muted" />
</template>
</SmartsheetTopbarManagedAppStatusMenuItem>
<NcDivider class="!my-1" />
<!-- Version history -->
<SmartsheetTopbarManagedAppStatusMenuItem
clickable
label="View version history"
subtext="Manage versions & track deployments"
icon-wrapper-class="bg-nc-bg-gray-light"
@click="openModal('versionHistory')"
>
<template #icon>
<GeneralIcon icon="ncClock" class="text-nc-content-gray-muted" />
</template>
</SmartsheetTopbarManagedAppStatusMenuItem>
</template>
<!-- Installer application -->
<template v-if="isManagedAppInstaller">
<SmartsheetTopbarManagedAppStatusMenuItem
:label="`v${managedAppVersionsInfo.current?.version || '1.0.0'}`"
icon-wrapper-class="bg-green-50 dark:bg-nc-green-20"
>
<template #icon>
<GeneralIcon icon="circleCheck3" class="text-green-600" />
</template>
<template v-if="managedAppVersionsInfo.current?.published_at" #subtext>
<span class="text-green-600">
Published {{ parseStringDateTime(managedAppVersionsInfo.current?.published_at, 'MMM DD, YYYY, hh:mm A') }}
</span>
</template>
</SmartsheetTopbarManagedAppStatusMenuItem>
<div
v-if="!base.auto_update && managedAppVersionsInfo.updateAvailable"
class="bg-nc-brand-20 dark:bg-nc-brand-20/40 -mb-1.5"
>
<SmartsheetTopbarManagedAppStatusMenuItem
:label="`v${managedAppVersionsInfo.published?.version || '1.0.0'}`"
icon-wrapper-class="bg-brand-50 dark:bg-nc-brand-50"
>
<template #icon>
<GeneralIcon icon="ncDownload" class="text-brand-600" />
</template>
<template #subtext>
<span class="text-brand-600"> New version available </span>
</template>
<template #extraRight>
<NcButton size="small"> Update </NcButton>
</template>
</SmartsheetTopbarManagedAppStatusMenuItem>
</div>
<NcDivider class="!my-1" />
<SmartsheetTopbarManagedAppStatusMenuItem
clickable
label="View Changelog"
subtext="See what's new in each version"
icon-wrapper-class="bg-nc-bg-gray-light"
>
<template #icon>
<GeneralIcon icon="file" class="text-nc-content-gray-muted" />
</template>
</SmartsheetTopbarManagedAppStatusMenuItem>
</template>
</div>
</template>
</NcDropdown>
<DlgManagedApp v-model:visible="isModalVisible" modal-size="sm" :variant="modalVariant"> </DlgManagedApp>
</template> </template>
<style lang="scss" scoped>
.nc-managed-app-status-menu {
@apply w-[318px] pb-2;
}
.nc-managed-app-status-menu-header {
@apply flex items-center justify-between gap-2 pt-3 px-3 mb-1 text-nc-content-gray-muted text-captionSm;
}
</style>

View File

@@ -1,7 +1,4 @@
<script setup lang="ts"> <script setup lang="ts">
import { BaseVersion, FormBuilderValidatorType, type FormDefinition } from 'nocodb-sdk'
import { FORM_BUILDER_NON_CATEGORIZED, FormBuilderInputType } from '#imports'
const props = defineProps<{ const props = defineProps<{
visible: boolean visible: boolean
baseId?: string baseId?: string
@@ -11,243 +8,9 @@ const props = defineProps<{
submitButtonText?: string submitButtonText?: string
}>() }>()
const emit = defineEmits(['update:visible']) defineEmits(['update:visible'])
const visible = useVModel(props, 'visible', emit)
const { title, subTitle, alertDescription, submitButtonText } = toRefs(props)
const { $api } = useNuxtApp()
const { t } = useI18n()
const { navigateToProject } = useGlobal()
const initialSanboxFormState = ref<Record<string, any>>({
title: '',
description: '',
category: '',
visibility: 'private',
startFrom: props.baseId ? 'existing' : 'new',
baseId: props.baseId,
})
const workspaceStore = useWorkspace()
const { activeWorkspaceId } = storeToRefs(workspaceStore)
const basesStore = useBases()
const baseStore = useBase()
const { base } = storeToRefs(baseStore)
const createManagedApp = async (formState: Record<string, any>) => {
try {
const response = await $api.internal.postOperation(
activeWorkspaceId.value as string,
formState.baseId || NO_SCOPE,
{
operation: 'managedAppCreate',
} as any,
{
title: formState.title,
description: formState.description,
category: formState.category,
visibility: formState.visibility,
...(!formState.baseId
? {
basePayload: {
title: formState.title,
default_role: '' as NcProject['default_role'],
meta: JSON.stringify({
iconColor: baseIconColors[Math.floor(Math.random() * 1000) % baseIconColors.length],
}),
},
}
: {}),
},
)
message.success(t('msg.success.managedAppCreated'))
visible.value = false
// 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).managed_app_id = response.managed_app_id
}
}
// Reload base to ensure all managed app data is loaded
if (formState.baseId) {
await basesStore.loadProject(formState.baseId, true)
} else {
await basesStore.loadProjects()
}
if (!props.baseId && (response?.base_id || formState.baseId) && base.value?.id !== (response?.base_id || formState.baseId)) {
navigateToProject({
baseId: response?.base_id || formState.baseId,
workspaceId: activeWorkspaceId.value as string,
})
} else if (base.value?.id && base.value.id === formState.baseId) {
baseStore.loadManagedApp()
baseStore.loadCurrentVersion()
}
} catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e))
}
}
const { formState, isLoading, submit } = useProvideFormBuilderHelper({
formSchema: [
{
type: FormBuilderInputType.Input,
label: t('labels.managedAppTitle'),
span: 24,
model: 'title',
placeholder: 'Enter a descriptive title',
category: FORM_BUILDER_NON_CATEGORIZED,
validators: [
{
type: FormBuilderValidatorType.Required,
message: t('labels.titleRequired'),
},
{
type: FormBuilderValidatorType.Custom,
validator: baseTitleValidator('App').validator,
},
],
required: true,
},
{
type: FormBuilderInputType.Textarea,
label: t('labels.managedAppDescription'),
span: 24,
model: 'description',
placeholder: "Describe your application's capabilities",
category: FORM_BUILDER_NON_CATEGORIZED,
},
...(!props.baseId
? ([
{
type: FormBuilderInputType.Select,
label: 'Start from',
span: 12,
model: 'startFrom',
category: FORM_BUILDER_NON_CATEGORIZED,
options: [
{ label: 'New', value: 'new', icon: 'plus' },
{ label: 'Existing Base', value: 'existing', icon: 'copy' },
],
defaultValue: 'new',
},
{
type: FormBuilderInputType.Space,
span: 12,
category: FORM_BUILDER_NON_CATEGORIZED,
condition: {
model: 'startFrom',
equal: 'new',
},
},
{
type: FormBuilderInputType.SelectBase,
label: 'Select base',
span: 12,
model: 'baseId',
category: FORM_BUILDER_NON_CATEGORIZED,
condition: {
model: 'startFrom',
equal: 'existing',
},
defaultValue: undefined,
filterOption: (base) => base && base.version === BaseVersion.V3 && !base.managed_app_id,
helpText: 'Only V3 bases can be published as managed apps',
showHintAsTooltip: true,
},
] as FormDefinition)
: []),
{
type: FormBuilderInputType.Input,
label: t('labels.managedAppCategory'),
span: 12,
model: 'category',
placeholder: 'e.g., CRM, HR',
category: FORM_BUILDER_NON_CATEGORIZED,
},
{
type: FormBuilderInputType.Select,
label: t('labels.managedAppVisibility'),
span: 12,
model: 'visibility',
category: FORM_BUILDER_NON_CATEGORIZED,
options: [
// { label: 'Public', value: 'public', icon: 'eye' },
{ label: 'Private', value: 'private', icon: 'lock' },
{ label: 'Unlisted', value: 'unlisted', icon: 'ncEyeOff' },
],
defaultValue: 'private',
},
],
onSubmit: async () => {
if (!props.baseId && formState.value.startFrom === 'new' && formState.value.baseId) {
formState.value.baseId = ''
}
if (props.baseId) {
formState.value.baseId = props.baseId
}
formState.value.title = formState.value.title.trim()
return await createManagedApp(formState.value)
},
initialState: initialSanboxFormState,
})
</script> </script>
<template> <template>
<div class="flex flex-col h-full"> <NcSpanHidden />
<DlgManagedAppHeader
v-model:visible="visible"
:title="title || 'Create Managed App'"
:sub-title="subTitle || $t('labels.publishToAppStore')"
/>
<div class="flex-1 p-6 nc-scrollbar-thin">
<NcFormBuilder>
<template #header>
<NcAlert
type="info"
align="top"
:description="
alertDescription ||
'Create managed application that can be published to the App Store. You\'ll be able to manage versions and push updates to all installations.'
"
class="!p-3 !items-start bg-nc-bg-blue-light border-1 !border-nc-blue-200 rounded-lg p-3 mb-4"
>
<template #icon>
<GeneralIcon icon="info" class="w-4 h-4 mt-0.5 text-nc-content-blue-dark flex-none" />
</template>
</NcAlert>
</template>
</NcFormBuilder>
</div>
<div class="flex justify-end gap-2 px-6 py-3 border-t border-nc-border-gray-medium">
<NcButton size="small" type="secondary" :disabled="isLoading" @click="visible = false">
{{ $t('general.cancel') }}
</NcButton>
<NcButton size="small" type="primary" :loading="isLoading" @click="submit">
<template #icon>
<GeneralIcon icon="ncBox" />
</template>
{{ submitButtonText || 'Create managed app' }}
</NcButton>
</div>
</div>
</template> </template>