mirror of
https://github.com/nocodb/nocodb.git
synced 2026-02-02 02:57:23 +00:00
Merge pull request #12951 from nocodb/nc-feat/managed-app-ui-changes
Nc feat/managed app UI changes
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
|
||||
@@ -1,7 +1,7 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_715_5581)">
|
||||
<path d="M8.00065 14.6663C11.6825 14.6663 14.6673 11.6816 14.6673 7.99967C14.6673 4.31778 11.6825 1.33301 8.00065 1.33301C4.31875 1.33301 1.33398 4.31778 1.33398 7.99967C1.33398 11.6816 4.31875 14.6663 8.00065 14.6663Z" stroke="#17803D" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M11.5 5.5L6.6875 10.5L4.5 8.22727" stroke="#17803D" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M8.00065 14.6663C11.6825 14.6663 14.6673 11.6816 14.6673 7.99967C14.6673 4.31778 11.6825 1.33301 8.00065 1.33301C4.31875 1.33301 1.33398 4.31778 1.33398 7.99967C1.33398 11.6816 4.31875 14.6663 8.00065 14.6663Z" stroke="currentColor" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M11.5 5.5L6.6875 10.5L4.5 8.22727" stroke="currentColor" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_715_5581">
|
||||
|
||||
|
Before Width: | Height: | Size: 689 B After Width: | Height: | Size: 699 B |
@@ -369,12 +369,6 @@ const duplicateProject = (base: BaseType) => {
|
||||
isDuplicateDlgOpen.value = true
|
||||
}
|
||||
|
||||
const isConvertToManagedAppDlgOpen = ref(false)
|
||||
|
||||
const convertToManagedApp = () => {
|
||||
isConvertToManagedAppDlgOpen.value = true
|
||||
}
|
||||
|
||||
const tableDelete = () => {
|
||||
isTableDeleteDialogVisible.value = true
|
||||
$e('c:table:delete')
|
||||
@@ -619,7 +613,6 @@ defineExpose({
|
||||
@click-menu="onClickMenu"
|
||||
@rename="enableEditMode()"
|
||||
@duplicate-project="duplicateProject($event)"
|
||||
@convert-to-managed-app="convertToManagedApp"
|
||||
@copy-project-info="copyProjectInfo()"
|
||||
@open-erd-view="openErdView($event)"
|
||||
@open-base-settings="openBaseSettings($event)"
|
||||
@@ -680,7 +673,6 @@ defineExpose({
|
||||
@click-menu="onClickMenu"
|
||||
@rename="enableEditMode(true)"
|
||||
@duplicate-project="duplicateProject($event)"
|
||||
@convert-to-managed-app="convertToManagedApp"
|
||||
@copy-project-info="copyProjectInfo()"
|
||||
@open-erd-view="openErdView($event)"
|
||||
@open-base-settings="openBaseSettings($event)"
|
||||
@@ -765,7 +757,6 @@ defineExpose({
|
||||
/>
|
||||
<DlgBaseDelete v-model:visible="isBaseDeleteDialogVisible" :base-id="base?.id" />
|
||||
<DlgBaseDuplicate v-if="selectedProjectToDuplicate" v-model="isDuplicateDlgOpen" :base="selectedProjectToDuplicate" />
|
||||
<DlgConvertToManagedApp v-if="base?.id" v-model:visible="isConvertToManagedAppDlgOpen" :base-id="base.id" />
|
||||
<GeneralModal v-model:visible="isErdModalOpen" size="large">
|
||||
<div class="h-[80vh]">
|
||||
<LazyDashboardSettingsErd :base-id="base?.id" :source-id="activeBaseId" />
|
||||
|
||||
@@ -407,8 +407,8 @@ const collapseKey = ref('')
|
||||
<div class="my-5">
|
||||
<NcListSourceSelector
|
||||
ref="sourceSelectorRef"
|
||||
:base-id="baseId"
|
||||
v-model:source-id="sourceIdRef"
|
||||
:base-id="baseId"
|
||||
:show-source-selector="showSourceSelector"
|
||||
force-layout="vertical"
|
||||
/>
|
||||
|
||||
@@ -1,185 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { FormBuilderValidatorType } from 'nocodb-sdk'
|
||||
import { FORM_BUILDER_NON_CATEGORIZED, FormBuilderInputType } from '#imports'
|
||||
|
||||
const props = defineProps<{
|
||||
baseId: string
|
||||
}>()
|
||||
|
||||
const visible = defineModel<boolean>('visible', { required: true })
|
||||
|
||||
const { $api } = useNuxtApp()
|
||||
const { t } = useI18n()
|
||||
|
||||
const initialManagedAppFormState = ref<Record<string, any>>({
|
||||
title: '',
|
||||
description: '',
|
||||
category: '',
|
||||
visibility: 'private',
|
||||
})
|
||||
|
||||
const { base } = storeToRefs(useBase())
|
||||
|
||||
const basesStore = useBases()
|
||||
|
||||
const convertToManagedApp = async (formState: Record<string, any>) => {
|
||||
try {
|
||||
const response = await $api.internal.postOperation(
|
||||
base.value!.fk_workspace_id as string,
|
||||
props.baseId,
|
||||
{
|
||||
operation: 'managedAppCreate',
|
||||
} as any,
|
||||
{
|
||||
title: formState.title,
|
||||
description: formState.description,
|
||||
category: formState.category,
|
||||
visibility: formState.visibility,
|
||||
},
|
||||
)
|
||||
|
||||
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) {
|
||||
const currentBase = basesStore.bases.get(props.baseId)
|
||||
if (currentBase) {
|
||||
;(currentBase as any).managed_app_id = response.managed_app_id
|
||||
}
|
||||
}
|
||||
|
||||
// Reload base to ensure all managed app data is loaded
|
||||
await basesStore.loadProject(props.baseId, true)
|
||||
} 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'),
|
||||
},
|
||||
],
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
type: FormBuilderInputType.Textarea,
|
||||
label: t('labels.managedAppDescription'),
|
||||
span: 24,
|
||||
model: 'description',
|
||||
placeholder: "Describe your application's capabilities",
|
||||
category: FORM_BUILDER_NON_CATEGORIZED,
|
||||
},
|
||||
{
|
||||
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 () => {
|
||||
return await convertToManagedApp(formState.value)
|
||||
},
|
||||
initialState: initialManagedAppFormState,
|
||||
})
|
||||
|
||||
watch(visible, (isVisible) => {
|
||||
if (isVisible && base.value) {
|
||||
formState.value = {
|
||||
title: base.value.title || '',
|
||||
description: '',
|
||||
category: '',
|
||||
visibility: 'private',
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NcModal
|
||||
v-model:visible="visible"
|
||||
size="sm"
|
||||
height="auto"
|
||||
centered
|
||||
wrap-class-name="nc-modal-convert-to-managed-app "
|
||||
nc-modal-class-name="!p-0"
|
||||
>
|
||||
<div class="p-4 w-full flex items-center gap-3 border-b border-nc-border-gray-medium">
|
||||
<div class="w-10 h-10 rounded-lg bg-gradient-to-br from-brand-500 to-brand-600 flex items-center justify-center">
|
||||
<GeneralIcon icon="ncBox" class="w-5 h-5 text-white" />
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<div class="font-semibold text-lg text-nc-content-gray-emphasis">Convert to Managed App</div>
|
||||
<div class="text-xs text-nc-content-gray-subtle2">{{ $t('labels.publishToAppStore') }}</div>
|
||||
</div>
|
||||
|
||||
<NcButton size="small" type="text" class="self-start" @click="visible = false">
|
||||
<GeneralIcon icon="close" class="text-nc-content-gray-subtle2" />
|
||||
</NcButton>
|
||||
</div>
|
||||
|
||||
<div class="flex-1 p-6 nc-scrollbar-thin">
|
||||
<NcFormBuilder>
|
||||
<template #header>
|
||||
<NcAlert
|
||||
type="info"
|
||||
align="top"
|
||||
description="Convert this base into a living 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-4 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>
|
||||
Convert to Managed App
|
||||
</NcButton>
|
||||
</div>
|
||||
</NcModal>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.nc-modal-convert-to-managed-app {
|
||||
.nc-modal {
|
||||
max-height: min(90vh, 540px) !important;
|
||||
height: min(90vh, 540px) !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1036,8 +1036,8 @@ watch(
|
||||
<div class="mb-4">
|
||||
<NcListSourceSelector
|
||||
ref="sourceSelectorRef"
|
||||
:base-id="baseId"
|
||||
v-model:source-id="sourceIdRef"
|
||||
:base-id="baseId"
|
||||
:show-source-selector="showSourceSelector"
|
||||
force-layout="vertical"
|
||||
/>
|
||||
|
||||
@@ -488,8 +488,8 @@ watch(_baseId, () => {
|
||||
|
||||
<NcListSourceSelector
|
||||
ref="sourceSelectorRef"
|
||||
:base-id="baseId"
|
||||
v-model:source-id="sourceIdRef"
|
||||
:base-id="baseId"
|
||||
:show-source-selector="showSourceSelector"
|
||||
/>
|
||||
</template>
|
||||
|
||||
@@ -149,7 +149,7 @@ if (stopEventPropogation.value) {
|
||||
<div
|
||||
ref="ncModalRef"
|
||||
class="flex flex-col nc-modal p-6 h-full"
|
||||
:class="[`${ncModalClassName}`]"
|
||||
:class="[`nc-modal-size-${size} ${ncModalClassName}`]"
|
||||
:style="{
|
||||
maxHeight: height,
|
||||
...(modalSizes[size] ? { height } : {}),
|
||||
|
||||
@@ -1,717 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
const props = defineProps<{
|
||||
visible: boolean
|
||||
managedApp: any
|
||||
currentVersion: any
|
||||
initialTab?: 'publish' | 'fork' | 'deployments'
|
||||
}>()
|
||||
|
||||
const emit = defineEmits(['update:visible', 'published', 'forked'])
|
||||
|
||||
const { $api } = useNuxtApp()
|
||||
const baseStore = useBase()
|
||||
const { base } = storeToRefs(baseStore)
|
||||
|
||||
const activeTab = ref('publish')
|
||||
const isLoading = ref(false)
|
||||
const isLoadingDeployments = ref(false)
|
||||
const versions = ref<any[]>([])
|
||||
const deploymentStats = ref<any>(null)
|
||||
|
||||
// Version deployments modal
|
||||
const showVersionDeploymentsModal = ref(false)
|
||||
const selectedVersion = ref<any>(null)
|
||||
|
||||
// Publish form (for draft versions)
|
||||
const publishForm = reactive({
|
||||
releaseNotes: '',
|
||||
})
|
||||
|
||||
// Fork form (for creating new draft from published)
|
||||
const forkForm = reactive({
|
||||
version: '',
|
||||
releaseNotes: '',
|
||||
})
|
||||
|
||||
const isPublished = computed(() => props.currentVersion?.status === 'published')
|
||||
const isDraft = computed(() => props.currentVersion?.status === 'draft')
|
||||
|
||||
// Load versions
|
||||
const loadVersions = async () => {
|
||||
if (!props.managedApp?.id || !base.value?.fk_workspace_id) return
|
||||
|
||||
try {
|
||||
const response = await $api.internal.getOperation(base.value.fk_workspace_id, base.value.id!, {
|
||||
operation: 'managedAppVersionsList',
|
||||
managedAppId: props.managedApp.id,
|
||||
} as any)
|
||||
if (response?.list) {
|
||||
versions.value = response.list
|
||||
}
|
||||
} catch (e: any) {
|
||||
console.error('Failed to load versions:', e)
|
||||
}
|
||||
}
|
||||
|
||||
// Load real deployment statistics
|
||||
const loadDeployments = async () => {
|
||||
if (!props.managedApp?.id || !base.value?.fk_workspace_id) return
|
||||
|
||||
isLoadingDeployments.value = true
|
||||
try {
|
||||
const response = await $api.internal.getOperation(base.value.fk_workspace_id, base.value.id!, {
|
||||
operation: 'managedAppDeployments',
|
||||
managedAppId: props.managedApp.id,
|
||||
} as any)
|
||||
if (response) {
|
||||
deploymentStats.value = response
|
||||
}
|
||||
} catch (e: any) {
|
||||
console.error('Failed to load deployments:', e)
|
||||
} finally {
|
||||
isLoadingDeployments.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const publishCurrentDraft = async () => {
|
||||
if (!base.value?.fk_workspace_id || !base.value?.id || !props.currentVersion?.id) return
|
||||
|
||||
isLoading.value = true
|
||||
try {
|
||||
await $api.internal.postOperation(
|
||||
base.value.fk_workspace_id,
|
||||
base.value.id,
|
||||
{
|
||||
operation: 'managedAppPublish',
|
||||
},
|
||||
{
|
||||
managedAppVersionId: props.currentVersion.id,
|
||||
},
|
||||
)
|
||||
|
||||
// Reload base to get updated managed app version info
|
||||
if (base.value?.id) {
|
||||
await baseStore.loadProject()
|
||||
}
|
||||
|
||||
message.success(`Version ${props.currentVersion.version} published successfully!`)
|
||||
emit('published')
|
||||
emit('update:visible', 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 || !props.managedApp?.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: props.managedApp.id,
|
||||
version: forkForm.version,
|
||||
},
|
||||
)
|
||||
|
||||
// Reload base to get updated managed app version info
|
||||
if (base.value?.id) {
|
||||
await baseStore.loadProject()
|
||||
}
|
||||
|
||||
message.success(`New draft version ${forkForm.version} created successfully!`)
|
||||
emit('forked')
|
||||
emit('update:visible', false)
|
||||
} catch (e: any) {
|
||||
message.error(await extractSdkResponseErrorMsg(e))
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const formatDate = (dateString: string) => {
|
||||
if (!dateString) return 'N/A'
|
||||
const date = new Date(dateString)
|
||||
return new Intl.DateTimeFormat('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
}).format(date)
|
||||
}
|
||||
|
||||
const suggestNextVersion = () => {
|
||||
if (!props.currentVersion?.version) {
|
||||
forkForm.version = '1.0.0'
|
||||
return
|
||||
}
|
||||
|
||||
const currentVersion = props.currentVersion.version
|
||||
const versionParts = currentVersion.split('.')
|
||||
if (versionParts.length === 3) {
|
||||
// Increment minor version for new draft
|
||||
versionParts[1] = String(Number(versionParts[1]) + 1)
|
||||
versionParts[2] = '0'
|
||||
forkForm.version = versionParts.join('.')
|
||||
} else {
|
||||
forkForm.version = ''
|
||||
}
|
||||
}
|
||||
|
||||
const openVersionDeploymentsModal = (version: any) => {
|
||||
selectedVersion.value = version
|
||||
showVersionDeploymentsModal.value = true
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.visible,
|
||||
async (val) => {
|
||||
if (val) {
|
||||
// Use initialTab if provided, otherwise default based on version status
|
||||
if (props.initialTab) {
|
||||
activeTab.value = props.initialTab
|
||||
} else if (isDraft.value) {
|
||||
activeTab.value = 'publish'
|
||||
} else if (isPublished.value) {
|
||||
activeTab.value = 'fork'
|
||||
} else {
|
||||
// Fallback to deployments if version status is unclear
|
||||
activeTab.value = 'deployments'
|
||||
}
|
||||
|
||||
await loadVersions()
|
||||
await loadDeployments()
|
||||
if (!isDraft.value && !props.initialTab) {
|
||||
suggestNextVersion()
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NcModal
|
||||
:visible="visible"
|
||||
size="lg"
|
||||
nc-modal-class-name="nc-modal-managed-app-management"
|
||||
centered
|
||||
@update:visible="emit('update:visible', $event)"
|
||||
>
|
||||
<div class="flex flex-col h-full">
|
||||
<!-- Header with Tabs -->
|
||||
<div class="nc-managed-app-header">
|
||||
<div class="flex items-center gap-3 flex-1">
|
||||
<div class="nc-managed-app-icon">
|
||||
<GeneralIcon icon="ncBox" class="h-5 w-5 text-white" />
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<div class="text-lg font-semibold text-nc-content-gray-emphasis">Managed App Management</div>
|
||||
<div class="text-xs text-nc-content-gray-subtle2">Manage versions and track deployments</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tabs (Segmented Control) -->
|
||||
<div class="nc-managed-app-tabs">
|
||||
<div class="flex items-center">
|
||||
<div
|
||||
v-if="isDraft"
|
||||
class="nc-managed-app-tab"
|
||||
:class="{ selected: activeTab === 'publish' }"
|
||||
@click="activeTab = 'publish'"
|
||||
>
|
||||
<GeneralIcon icon="upload" class="h-4 w-4 flex-none opacity-75" />
|
||||
<span>Publish</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="isPublished"
|
||||
class="nc-managed-app-tab"
|
||||
:class="{ selected: activeTab === 'fork' }"
|
||||
@click="activeTab = 'fork'"
|
||||
>
|
||||
<GeneralIcon icon="ncGitBranch" class="h-4 w-4 flex-none opacity-75" />
|
||||
<span>Fork</span>
|
||||
</div>
|
||||
<div class="nc-managed-app-tab" :class="{ selected: activeTab === 'deployments' }" @click="activeTab = 'deployments'">
|
||||
<GeneralIcon icon="ncServer" class="h-4 w-4 flex-none opacity-75" />
|
||||
<span>Deployments</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<NcButton size="small" type="text" @click="emit('update:visible', false)">
|
||||
<GeneralIcon icon="close" class="text-nc-content-gray-muted h-4 w-4" />
|
||||
</NcButton>
|
||||
</div>
|
||||
|
||||
<!-- Content -->
|
||||
<div class="flex-1 overflow-y-auto">
|
||||
<!-- Publish Tab -->
|
||||
<div v-if="activeTab === 'publish'" class="p-6">
|
||||
<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 #description>
|
||||
Publishing version <strong>{{ currentVersion?.version }}</strong> will make it available in the App Store and
|
||||
automatically update all installations.
|
||||
</template>
|
||||
</NcAlert>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label class="text-nc-content-gray text-sm font-medium mb-2 block">Version</label>
|
||||
<a-input :value="currentVersion?.version" disabled size="large" class="rounded-lg nc-input-sm">
|
||||
<template #prefix>
|
||||
<span class="text-nc-content-gray-subtle2">v</span>
|
||||
</template>
|
||||
</a-input>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="text-nc-content-gray text-sm font-medium mb-2 block">Release Notes (Optional)</label>
|
||||
<a-textarea
|
||||
v-model:value="publishForm.releaseNotes"
|
||||
placeholder="Describe what's new in this version"
|
||||
:rows="6"
|
||||
size="large"
|
||||
class="rounded-lg nc-input-sm"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Fork Tab -->
|
||||
<div v-if="activeTab === 'fork'" class="p-6">
|
||||
<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 #description>
|
||||
Create a new draft version to work on updates. Current published version
|
||||
<strong>{{ currentVersion?.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">
|
||||
New Version <span class="text-nc-content-red-dark">*</span>
|
||||
</label>
|
||||
<a-input
|
||||
v-model:value="forkForm.version"
|
||||
placeholder="e.g., 2.0.0"
|
||||
size="large"
|
||||
class="rounded-lg nc-input-sm nc-input-shadow"
|
||||
>
|
||||
<template #prefix>
|
||||
<span class="text-nc-content-gray-subtle2">v</span>
|
||||
</template>
|
||||
</a-input>
|
||||
<div 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>
|
||||
</div>
|
||||
|
||||
<!-- Deployments Tab -->
|
||||
<div v-if="activeTab === 'deployments'" class="nc-deployments-content">
|
||||
<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-icon-wrapper bg-nc-bg-blue-light">
|
||||
<GeneralIcon icon="ncServer" class="w-5 h-5 text-nc-content-blue-dark" />
|
||||
</div>
|
||||
<div class="nc-stat-content">
|
||||
<div class="nc-stat-value">{{ deploymentStats.statistics?.totalDeployments || 0 }}</div>
|
||||
<div class="nc-stat-label">Total Installs</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Active -->
|
||||
<div class="nc-stat-card">
|
||||
<div class="nc-stat-icon-wrapper bg-nc-bg-green-light">
|
||||
<GeneralIcon icon="check" class="w-5 h-5 text-nc-content-green-dark" />
|
||||
</div>
|
||||
<div class="nc-stat-content">
|
||||
<div class="nc-stat-value">{{ deploymentStats.statistics?.activeDeployments || 0 }}</div>
|
||||
<div class="nc-stat-label">Active</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Failed -->
|
||||
<div class="nc-stat-card">
|
||||
<div class="nc-stat-icon-wrapper bg-nc-bg-red-light">
|
||||
<GeneralIcon icon="alertTriangle" class="w-5 h-5 text-nc-content-red-dark" />
|
||||
</div>
|
||||
<div class="nc-stat-content">
|
||||
<div class="nc-stat-value">{{ deploymentStats.statistics?.failedDeployments || 0 }}</div>
|
||||
<div class="nc-stat-label">Failed</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Versions -->
|
||||
<div class="nc-stat-card">
|
||||
<div class="nc-stat-icon-wrapper bg-nc-bg-purple-light">
|
||||
<GeneralIcon icon="ncGitBranch" class="w-5 h-5 text-nc-content-purple-dark" />
|
||||
</div>
|
||||
<div class="nc-stat-content">
|
||||
<div class="nc-stat-value">{{ deploymentStats.statistics?.totalVersions || 0 }}</div>
|
||||
<div class="nc-stat-label">Versions</div>
|
||||
</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-emphasis">Version History</h3>
|
||||
<p class="text-xs text-nc-content-gray-subtle2 mt-0.5">
|
||||
{{ deploymentStats.versionStats.length }}
|
||||
{{ deploymentStats.versionStats.length === 1 ? 'version' : 'versions' }}
|
||||
published
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="nc-version-list">
|
||||
<div
|
||||
v-for="(versionStat, index) 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="index === 0 && versionStat.status === 'published'" class="nc-version-badge">
|
||||
<GeneralIcon icon="check" class="w-3 h-3" />
|
||||
<span>Current</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="nc-version-date">Published {{ formatDate(versionStat.publishedAt) }}</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>
|
||||
<span class="text-nc-content-gray-muted">{{
|
||||
versionStat.deploymentCount === 1 ? 'install' : 'installs'
|
||||
}}</span>
|
||||
</div>
|
||||
<GeneralIcon
|
||||
v-if="versionStat.deploymentCount > 0"
|
||||
icon="chevronRight"
|
||||
class="w-4 h-4 text-nc-content-gray-subtle2 nc-chevron-icon"
|
||||
/>
|
||||
</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>
|
||||
|
||||
<!-- Footer -->
|
||||
<div v-if="activeTab === 'publish' || activeTab === 'fork'" class="nc-managed-app-footer">
|
||||
<div class="flex justify-end gap-2">
|
||||
<NcButton type="secondary" size="small" @click="emit('update:visible', false)"> Cancel </NcButton>
|
||||
|
||||
<NcButton v-if="activeTab === 'publish'" type="primary" size="small" :loading="isLoading" @click="publishCurrentDraft">
|
||||
<template #icon>
|
||||
<GeneralIcon icon="upload" />
|
||||
</template>
|
||||
Publish
|
||||
</NcButton>
|
||||
|
||||
<NcButton
|
||||
v-if="activeTab === 'fork'"
|
||||
type="primary"
|
||||
size="small"
|
||||
:loading="isLoading"
|
||||
:disabled="!forkForm.version"
|
||||
@click="createNewDraft"
|
||||
>
|
||||
<template #icon>
|
||||
<GeneralIcon icon="plus" />
|
||||
</template>
|
||||
Create Draft
|
||||
</NcButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Version Deployments Modal -->
|
||||
<SmartsheetTopbarManagedAppVersionDeploymentsModal
|
||||
v-model:visible="showVersionDeploymentsModal"
|
||||
:managed-app="managedApp"
|
||||
:version="selectedVersion"
|
||||
/>
|
||||
</NcModal>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.nc-managed-app-header {
|
||||
@apply flex items-center gap-4 px-4 py-3 border-b-1 border-nc-border-gray-medium;
|
||||
}
|
||||
|
||||
.nc-managed-app-icon {
|
||||
@apply w-10 h-10 rounded-xl flex items-center justify-center;
|
||||
background: linear-gradient(135deg, var(--nc-content-brand) 0%, var(--nc-content-blue-medium) 100%);
|
||||
box-shadow: 0 2px 4px rgba(51, 102, 255, 0.15);
|
||||
}
|
||||
|
||||
.nc-managed-app-tabs {
|
||||
@apply flex bg-nc-bg-gray-medium rounded-lg p-1;
|
||||
}
|
||||
|
||||
.nc-managed-app-tab {
|
||||
@apply px-3 py-1.5 flex items-center gap-2 text-xs rounded-md select-none cursor-pointer;
|
||||
@apply text-nc-content-gray-subtle2 transition-all duration-200;
|
||||
|
||||
&.selected {
|
||||
@apply bg-nc-bg-default text-nc-content-gray-emphasis;
|
||||
box-shadow: 0px 3px 1px -2px rgba(0, 0, 0, 0.06), 0px 5px 3px -2px rgba(0, 0, 0, 0.02);
|
||||
}
|
||||
|
||||
&:hover:not(.selected) {
|
||||
@apply text-nc-content-gray;
|
||||
}
|
||||
}
|
||||
|
||||
.nc-managed-app-footer {
|
||||
@apply px-6 py-3 border-t-1 border-nc-border-gray-medium;
|
||||
}
|
||||
|
||||
// Deployments Tab Styles
|
||||
.nc-deployments-content {
|
||||
@apply p-6;
|
||||
}
|
||||
|
||||
.nc-deployments-loading {
|
||||
@apply flex flex-col items-center justify-center py-16;
|
||||
}
|
||||
|
||||
.nc-deployment-stats {
|
||||
@apply grid grid-cols-4 gap-4 mb-6;
|
||||
}
|
||||
|
||||
.nc-stat-card {
|
||||
@apply bg-nc-bg-gray-extralight border-1 border-nc-border-gray-light rounded-xl p-4 transition-all duration-200 hover:(border-nc-border-gray-medium shadow-hover);
|
||||
}
|
||||
|
||||
.nc-stat-icon-wrapper {
|
||||
@apply w-10 h-10 rounded-lg flex items-center justify-center mb-3;
|
||||
}
|
||||
|
||||
.nc-stat-content {
|
||||
@apply space-y-1;
|
||||
}
|
||||
|
||||
.nc-stat-value {
|
||||
@apply text-3xl font-bold text-nc-content-gray-emphasis leading-none;
|
||||
}
|
||||
|
||||
.nc-stat-label {
|
||||
@apply text-xs font-medium text-nc-content-gray-subtle2 uppercase tracking-wide;
|
||||
}
|
||||
|
||||
.nc-version-list-wrapper {
|
||||
@apply mt-6;
|
||||
}
|
||||
|
||||
.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-emphasis;
|
||||
}
|
||||
|
||||
.nc-version-badge {
|
||||
@apply inline-flex items-center gap-1 px-2 py-0.5 rounded-full;
|
||||
@apply bg-nc-bg-green-light text-nc-content-green-dark;
|
||||
@apply text-xs font-semibold;
|
||||
}
|
||||
|
||||
.nc-version-date {
|
||||
@apply text-xs text-nc-content-gray-subtle2;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
// Responsive
|
||||
@media (max-width: 1024px) {
|
||||
.nc-deployment-stats {
|
||||
@apply grid-cols-2 gap-3;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.nc-deployment-stats {
|
||||
@apply grid-cols-1;
|
||||
}
|
||||
|
||||
.nc-version-item {
|
||||
@apply flex-col items-start;
|
||||
}
|
||||
|
||||
.nc-version-installs {
|
||||
@apply w-full justify-between;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss">
|
||||
.nc-modal-managed-app-management {
|
||||
@apply !p-0;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,3 @@
|
||||
<template>
|
||||
<NcSpanHidden />
|
||||
</template>
|
||||
@@ -1,513 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { DeploymentStatus } from 'nocodb-sdk'
|
||||
|
||||
const props = defineProps<{
|
||||
visible: boolean
|
||||
managedApp: any
|
||||
version: any
|
||||
}>()
|
||||
|
||||
const emit = defineEmits(['update:visible'])
|
||||
|
||||
const { $api } = useNuxtApp()
|
||||
const { base } = storeToRefs(useBase())
|
||||
|
||||
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 (!props.managedApp?.id || !props.version?.versionId || !base.value?.fk_workspace_id) return
|
||||
|
||||
isLoading.value = true
|
||||
try {
|
||||
const offset = (page - 1) * pageSize
|
||||
const response = await $api.internal.getOperation(base.value.fk_workspace_id, base.value.id!, {
|
||||
operation: 'managedAppVersionDeployments',
|
||||
managedAppId: props.managedApp.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'
|
||||
const date = new Date(dateString)
|
||||
return new Intl.DateTimeFormat('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
}).format(date)
|
||||
}
|
||||
|
||||
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-nc-content-green-dark bg-nc-bg-green-light',
|
||||
[DeploymentStatus.FAILED]: 'text-nc-content-red-dark bg-nc-bg-red-light',
|
||||
[DeploymentStatus.PENDING]: 'text-nc-content-orange-dark bg-nc-bg-orange-light',
|
||||
[DeploymentStatus.IN_PROGRESS]: 'text-nc-content-blue-dark bg-nc-bg-blue-light',
|
||||
}
|
||||
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(
|
||||
() => props.visible,
|
||||
(val) => {
|
||||
if (val) {
|
||||
loadDeployments(1)
|
||||
expandedBaseId.value = null
|
||||
deploymentLogs.value = []
|
||||
}
|
||||
},
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<GeneralModal :visible="visible" size="large" centered @update:visible="emit('update:visible', $event)">
|
||||
<div class="nc-deployments-modal">
|
||||
<!-- Header -->
|
||||
<div class="nc-deployments-header">
|
||||
<div class="nc-deployments-header-icon">
|
||||
<GeneralIcon icon="ncServer" class="w-5 h-5 text-white" />
|
||||
</div>
|
||||
<div class="flex-1 min-w-0">
|
||||
<h2 class="text-base font-semibold text-nc-content-gray-emphasis m-0">Version Deployments</h2>
|
||||
<p class="text-xs text-nc-content-gray-subtle2 m-0 mt-0.5">
|
||||
Tracking installations for
|
||||
<span class="font-mono font-semibold text-nc-content-brand">v{{ version?.version }}</span>
|
||||
</p>
|
||||
</div>
|
||||
<NcButton type="text" size="small" class="!px-1" @click="emit('update:visible', false)">
|
||||
<GeneralIcon icon="close" class="w-4 h-4" />
|
||||
</NcButton>
|
||||
</div>
|
||||
|
||||
<!-- 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">•</span>
|
||||
<div class="nc-log-time">
|
||||
<GeneralIcon icon="clock" class="w-3.5 h-3.5 opacity-60" />
|
||||
<span>{{ formatDate(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>
|
||||
|
||||
<!-- Deployments Pagination -->
|
||||
<div v-if="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>
|
||||
</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>
|
||||
</div>
|
||||
</GeneralModal>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.nc-deployments-modal {
|
||||
@apply flex flex-col h-full max-h-[80vh];
|
||||
}
|
||||
|
||||
.nc-deployments-header {
|
||||
@apply flex items-center gap-3 px-4 py-3 border-b-1 border-nc-border-gray-medium;
|
||||
}
|
||||
|
||||
.nc-deployments-header-icon {
|
||||
@apply w-9 h-9 rounded-lg flex items-center justify-center flex-shrink-0;
|
||||
background: linear-gradient(135deg, var(--nc-content-brand) 0%, var(--nc-content-blue-medium) 100%);
|
||||
box-shadow: 0 2px 4px rgba(51, 102, 255, 0.15);
|
||||
}
|
||||
|
||||
.nc-deployments-content {
|
||||
@apply flex-1 overflow-y-auto p-6;
|
||||
}
|
||||
|
||||
.nc-deployments-loading {
|
||||
@apply 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-emphasis 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-emphasis 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 text-nc-content-gray 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,251 +1,16 @@
|
||||
<script setup lang="ts">
|
||||
import { FormBuilderValidatorType } from 'nocodb-sdk'
|
||||
import { FORM_BUILDER_NON_CATEGORIZED, FormBuilderInputType } from '#imports'
|
||||
|
||||
const props = defineProps<{
|
||||
defineProps<{
|
||||
visible: boolean
|
||||
baseId?: string
|
||||
title?: string
|
||||
subTitle?: string
|
||||
alertDescription?: string
|
||||
submitButtonText?: string
|
||||
}>()
|
||||
|
||||
const emit = defineEmits(['update:visible'])
|
||||
|
||||
const visible = useVModel(props, 'visible', emit)
|
||||
|
||||
const { $api } = useNuxtApp()
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const { navigateToProject } = useGlobal()
|
||||
|
||||
const initialSanboxFormState = ref<Record<string, any>>({
|
||||
title: '',
|
||||
description: '',
|
||||
category: '',
|
||||
visibility: 'private',
|
||||
startFrom: 'new',
|
||||
baseId: undefined,
|
||||
})
|
||||
|
||||
const workspaceStore = useWorkspace()
|
||||
|
||||
const { activeWorkspaceId } = storeToRefs(workspaceStore)
|
||||
|
||||
const basesStore = useBases()
|
||||
|
||||
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 (response?.base_id || formState.baseId) {
|
||||
navigateToProject({
|
||||
baseId: response?.base_id || formState.baseId,
|
||||
workspaceId: activeWorkspaceId.value as string,
|
||||
})
|
||||
}
|
||||
} 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,
|
||||
},
|
||||
{
|
||||
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?.managed_app_id,
|
||||
},
|
||||
{
|
||||
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 (formState.value.startFrom === 'new' && formState.value.baseId) {
|
||||
formState.value.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">
|
||||
<div class="p-4 w-full flex items-center gap-3 border-b border-nc-border-gray-medium">
|
||||
<div class="nc-managed-app-icon">
|
||||
<GeneralIcon icon="ncBox" class="h-5 w-5" />
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<div class="font-semibold text-lg text-nc-content-gray-emphasis">Create Managed App</div>
|
||||
<div class="text-xs text-nc-content-gray-subtle2">{{ $t('labels.publishToAppStore') }}</div>
|
||||
</div>
|
||||
|
||||
<NcButton size="small" type="text" class="self-start" @click="visible = false">
|
||||
<GeneralIcon icon="close" class="text-nc-content-gray-subtle2" />
|
||||
</NcButton>
|
||||
</div>
|
||||
|
||||
<div class="flex-1 p-6 nc-scrollbar-thin">
|
||||
<NcFormBuilder>
|
||||
<template #header>
|
||||
<NcAlert
|
||||
type="info"
|
||||
align="top"
|
||||
description="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>
|
||||
Create managed app
|
||||
</NcButton>
|
||||
</div>
|
||||
</div>
|
||||
<NcSpanHidden />
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.nc-modal-convert-to-managed-app {
|
||||
.nc-modal {
|
||||
max-height: min(90vh, 540px) !important;
|
||||
height: min(90vh, 540px) !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.nc-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>
|
||||
|
||||
@@ -302,6 +302,7 @@
|
||||
"refresh": "Refresh",
|
||||
"reset": "Reset",
|
||||
"install": "Install",
|
||||
"installed": "Installed",
|
||||
"show": "Show",
|
||||
"access": "Access",
|
||||
"visibility": "Visibility",
|
||||
@@ -1185,6 +1186,9 @@
|
||||
"updateDetails": "Update Details",
|
||||
"published": "Published",
|
||||
"draft": "Draft",
|
||||
"live": "Live",
|
||||
"upToDate": "Up to date",
|
||||
"updateAvailable": "Update available",
|
||||
"managedAppTitle": "Title",
|
||||
"managedAppDescription": "Description",
|
||||
"managedAppCategory": "Category",
|
||||
|
||||
@@ -229,6 +229,8 @@ type NcProject = BaseType & {
|
||||
managed_app_master?: boolean
|
||||
managed_app_id?: string
|
||||
managed_app_version_id?: string
|
||||
managed_app_version?: string
|
||||
managed_app_published_at?: string
|
||||
auto_update?: boolean
|
||||
managed_app_schema_locked?: boolean
|
||||
}
|
||||
|
||||
@@ -18,8 +18,16 @@ export const useBase = defineStore('baseStore', () => {
|
||||
|
||||
const basesStore = useBases()
|
||||
|
||||
const managedApp = ref<any>(null)
|
||||
|
||||
const managedAppVersions = ref<any[]>([])
|
||||
|
||||
const managedAppVersionsInfo = computed(() => {})
|
||||
|
||||
const isManagedAppMaster = ref(false)
|
||||
|
||||
const isManagedAppInstaller = ref(false)
|
||||
|
||||
const baseId = computed(() => {
|
||||
// In shared base mode, use activeProjectId from basesStore which has the correct base ID
|
||||
if (route.value.params.typeOrId === 'base') {
|
||||
@@ -268,6 +276,10 @@ export const useBase = defineStore('baseStore', () => {
|
||||
return `${basUrl}${projectPage ? `?page=${projectPage}` : ''}`
|
||||
}
|
||||
|
||||
const loadManagedApp = async () => {}
|
||||
|
||||
const loadCurrentVersion = async () => {}
|
||||
|
||||
watch(
|
||||
() => route.value.params.baseType,
|
||||
(n) => {
|
||||
@@ -343,6 +355,12 @@ export const useBase = defineStore('baseStore', () => {
|
||||
isPrivateBase,
|
||||
showBaseAccessRequestOverlay,
|
||||
isManagedAppMaster,
|
||||
isManagedAppInstaller,
|
||||
managedApp,
|
||||
loadManagedApp,
|
||||
loadCurrentVersion,
|
||||
managedAppVersions,
|
||||
managedAppVersionsInfo,
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -104,3 +104,20 @@ export const extractAiBaseCreateQueryParams = (query: any) => {
|
||||
|
||||
return searchQuery
|
||||
}
|
||||
|
||||
export const suggestManagedAppNextVersion = (currentVersion?: string) => {
|
||||
if (!currentVersion) {
|
||||
return '1.0.0'
|
||||
}
|
||||
|
||||
const versionParts = currentVersion.split('.')
|
||||
|
||||
if (versionParts.length === 3) {
|
||||
// Increment minor version for new draft
|
||||
versionParts[1] = String(Number(versionParts[1]) + 1)
|
||||
versionParts[2] = '0'
|
||||
return versionParts.join('.')
|
||||
} else {
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
@@ -127,8 +127,9 @@ export const OPERATION_SCOPES = {
|
||||
|
||||
// Managed App Operations
|
||||
managedAppStoreList: 'org',
|
||||
managedAppList: 'workspace',
|
||||
managedAppGet: 'org',
|
||||
managedAppVersionsList: 'org',
|
||||
managedAppList: 'workspace',
|
||||
managedAppCreate: 'workspace',
|
||||
managedAppUpdate: 'base',
|
||||
managedAppDelete: 'base',
|
||||
@@ -137,7 +138,6 @@ export const OPERATION_SCOPES = {
|
||||
managedAppUnpublish: 'base',
|
||||
managedAppInstall: 'workspace',
|
||||
managedAppGetUpdates: 'base',
|
||||
managedAppVersionsList: 'base',
|
||||
managedAppInstallationsList: 'base',
|
||||
managedAppDeployments: 'base',
|
||||
managedAppVersionDeployments: 'base',
|
||||
|
||||
@@ -59,6 +59,9 @@ export default class Base implements BaseType {
|
||||
managed_app_id?: string; // Points to MANAGED_APPS (for both master and installed instances)
|
||||
managed_app_version_id?: string; // Current version ID from MANAGED_APP_VERSIONS
|
||||
auto_update?: boolean; // For installed instances: auto-update to new published versions
|
||||
// managed app info (populated fields)
|
||||
managed_app_version?: string; // Current version string
|
||||
managed_app_published_at?: string; // When this version was published
|
||||
managed_app_schema_locked?: boolean; // Computed: whether schema modifications are allowed
|
||||
|
||||
constructor(base: Partial<Base>) {
|
||||
@@ -69,8 +72,8 @@ export default class Base implements BaseType {
|
||||
return base && new Base(base);
|
||||
}
|
||||
|
||||
public static async computeSchemaLocked(_base: Base): Promise<boolean> {
|
||||
return false;
|
||||
public static async populateManagedAppInfo(_base: Base): Promise<void> {
|
||||
return;
|
||||
}
|
||||
|
||||
public static async createProject(
|
||||
@@ -289,9 +292,8 @@ export default class Base implements BaseType {
|
||||
}
|
||||
const base = this.castType(baseData);
|
||||
|
||||
// Compute managed_app_schema_locked
|
||||
if (base && base.managed_app_id) {
|
||||
base.managed_app_schema_locked = await this.computeSchemaLocked(base);
|
||||
await this.populateManagedAppInfo(base);
|
||||
}
|
||||
|
||||
return base;
|
||||
@@ -370,9 +372,8 @@ export default class Base implements BaseType {
|
||||
if (baseData) {
|
||||
const base = this.castType(baseData);
|
||||
|
||||
// Compute managed_app_schema_locked
|
||||
if (base.managed_app_id) {
|
||||
base.managed_app_schema_locked = await this.computeSchemaLocked(base);
|
||||
await this.populateManagedAppInfo(base);
|
||||
}
|
||||
|
||||
await base.getSources(includeConfig, ncMeta);
|
||||
|
||||
Reference in New Issue
Block a user