mirror of
https://github.com/nocodb/nocodb.git
synced 2026-02-01 23:48:33 +00:00
fix: clean up oss and convert to managed app code
This commit is contained in:
@@ -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" />
|
||||
|
||||
@@ -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>
|
||||
60
packages/nc-gui/components/dlg/ManagedApp/Header.vue
Normal file
60
packages/nc-gui/components/dlg/ManagedApp/Header.vue
Normal file
@@ -0,0 +1,60 @@
|
||||
<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-emphasis" :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>
|
||||
59
packages/nc-gui/components/dlg/ManagedApp/Index.vue
Normal file
59
packages/nc-gui/components/dlg/ManagedApp/Index.vue
Normal file
@@ -0,0 +1,59 @@
|
||||
<script lang="ts" setup>
|
||||
interface Props {
|
||||
visible: boolean
|
||||
modalSize: 'small' | 'medium' | 'large' | keyof typeof modalSizes
|
||||
title?: string
|
||||
subTitle?: string
|
||||
variant?: 'convertToManagedApp'
|
||||
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 } = 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>
|
||||
<slot name="header">
|
||||
<NcDlgManagedAppHeader :visible="vVisible" :modalSize="modalSize" :title="title" :subTitle="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" scoped></style>
|
||||
|
||||
<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,16 +1,22 @@
|
||||
<script setup lang="ts">
|
||||
import { BaseVersion, FormBuilderValidatorType } from 'nocodb-sdk'
|
||||
import { type FormDefinition, BaseVersion, FormBuilderValidatorType } from 'nocodb-sdk'
|
||||
import { FORM_BUILDER_NON_CATEGORIZED, FormBuilderInputType } from '#imports'
|
||||
|
||||
const props = 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 { title, subTitle, alertDescription, submitButtonText } = toRefs(props)
|
||||
|
||||
const { $api } = useNuxtApp()
|
||||
|
||||
const { t } = useI18n()
|
||||
@@ -22,8 +28,8 @@ const initialSanboxFormState = ref<Record<string, any>>({
|
||||
description: '',
|
||||
category: '',
|
||||
visibility: 'private',
|
||||
startFrom: 'new',
|
||||
baseId: undefined,
|
||||
startFrom: props.baseId ? 'existing' : 'new',
|
||||
baseId: props.baseId,
|
||||
})
|
||||
|
||||
const workspaceStore = useWorkspace()
|
||||
@@ -32,6 +38,10 @@ 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(
|
||||
@@ -77,11 +87,14 @@ const createManagedApp = async (formState: Record<string, any>) => {
|
||||
await basesStore.loadProjects()
|
||||
}
|
||||
|
||||
if (response?.base_id || formState.baseId) {
|
||||
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))
|
||||
@@ -117,42 +130,47 @@ const { formState, isLoading, submit } = useProvideFormBuilderHelper({
|
||||
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.version === BaseVersion.V3 && !base.managed_app_id,
|
||||
helpText: 'Only V3 bases can be published as managed apps',
|
||||
showHintAsTooltip: true,
|
||||
},
|
||||
...(!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'),
|
||||
@@ -176,10 +194,14 @@ const { formState, isLoading, submit } = useProvideFormBuilderHelper({
|
||||
},
|
||||
],
|
||||
onSubmit: async () => {
|
||||
if (formState.value.startFrom === 'new' && formState.value.baseId) {
|
||||
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)
|
||||
@@ -190,19 +212,11 @@ const { formState, isLoading, submit } = useProvideFormBuilderHelper({
|
||||
|
||||
<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>
|
||||
<DlgManagedAppHeader
|
||||
v-model:visible="visible"
|
||||
:title="title || 'Create Managed App'"
|
||||
:subTitle="subTitle || $t('labels.publishToAppStore')"
|
||||
/>
|
||||
|
||||
<div class="flex-1 p-6 nc-scrollbar-thin">
|
||||
<NcFormBuilder>
|
||||
@@ -210,7 +224,10 @@ const { formState, isLoading, submit } = useProvideFormBuilderHelper({
|
||||
<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."
|
||||
: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>
|
||||
@@ -229,25 +246,8 @@ const { formState, isLoading, submit } = useProvideFormBuilderHelper({
|
||||
<template #icon>
|
||||
<GeneralIcon icon="ncBox" />
|
||||
</template>
|
||||
Create managed app
|
||||
{{ submitButtonText || 'Create managed app' }}
|
||||
</NcButton>
|
||||
</div>
|
||||
</div>
|
||||
</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>
|
||||
|
||||
Reference in New Issue
Block a user