mirror of
https://github.com/nocodb/nocodb.git
synced 2026-02-01 20:37:46 +00:00
chore: move files in ee
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -104,4 +104,5 @@ result
|
||||
|
||||
# Temp
|
||||
migrate-colors.js
|
||||
antd.variable.css
|
||||
antd.variable.css
|
||||
CLAUDE.md
|
||||
132
CLAUDE.md
132
CLAUDE.md
@@ -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)
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
<NcDropdown v-if="isManagedAppMaster || isManagedAppInstaller" v-model:visible="isOpenDropdown" placement="bottomRight">
|
||||
<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>
|
||||
<NcSpanHidden />
|
||||
</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>
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
<script setup lang="ts">
|
||||
import { BaseVersion, FormBuilderValidatorType, type FormDefinition } from 'nocodb-sdk'
|
||||
import { FORM_BUILDER_NON_CATEGORIZED, FormBuilderInputType } from '#imports'
|
||||
|
||||
const props = defineProps<{
|
||||
visible: boolean
|
||||
baseId?: string
|
||||
@@ -11,243 +8,9 @@ const props = defineProps<{
|
||||
submitButtonText?: string
|
||||
}>()
|
||||
|
||||
const emit = 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,
|
||||
})
|
||||
defineEmits(['update:visible'])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col h-full">
|
||||
<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>
|
||||
<NcSpanHidden />
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user