fix: update app market modal

This commit is contained in:
Ramesh Mane
2026-01-22 10:31:55 +00:00
parent 1562b862e7
commit d9701acc4a

View File

@@ -26,7 +26,7 @@ const sandboxes = ref<SandboxType[]>([])
const loading = ref(false)
const installing = ref<string | null>(null)
const searchQuery = ref('')
const selectedCategory = ref<string | null>(null)
const selectedCategory = ref<string | undefined>(undefined)
const categories = computed(() => {
const cats = new Set<string>()
@@ -74,7 +74,7 @@ const loadSandboxes = async () => {
})
sandboxes.value = response?.list || []
} catch (e) {
} catch (e: any) {
console.error('API error:', e)
message.error(await extractSdkResponseErrorMsg(e))
} finally {
@@ -100,13 +100,24 @@ const installSandbox = async (sandbox: SandboxType) => {
message.success(t('msg.success.baseInstalled'))
emit('installed', sandbox)
visible.value = false
} catch (e) {
} catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e))
} finally {
installing.value = null
}
}
const formatInstallCount = (count: number | null | undefined): string => {
const num = count || 0
if (num >= 1000000) {
return `${(num / 1000000).toFixed(1)}M`
}
if (num >= 1000) {
return `${(num / 1000).toFixed(1)}k`
}
return num.toString()
}
watch(
() => props.workspaceId,
(newVal) => {
@@ -119,22 +130,30 @@ watch(
</script>
<template>
<div class="flex flex-col">
<div class="flex items-center gap-3 px-4 py-3 border-b-1 border-b-nc-border-gray-medium">
<GeneralIcon icon="ncBox" class="h-5 w-5" />
<div class="flex-1 text-bodyLgBold">{{ t('labels.appMarket') }}</div>
<div class="nc-app-market flex flex-col h-full">
<!-- Header -->
<div class="nc-app-market-header">
<div class="flex items-center gap-3">
<div class="nc-app-market-icon">
<GeneralIcon icon="ncBox" class="h-5 w-5" />
</div>
<div class="flex-1">
<div class="text-lg font-semibold text-nc-content-gray-emphasis">{{ t('labels.appMarket') }}</div>
<div class="text-xs text-nc-content-gray-subtle2">Discover and install managed applications</div>
</div>
<NcButton size="small" type="text" class="self-start" @click="visible = false">
<GeneralIcon icon="close" class="text-nc-content-gray-subtle2" />
</NcButton>
<NcButton size="small" type="text" @click="visible = false">
<GeneralIcon icon="close" class="text-nc-content-gray-muted h-4 w-4" />
</NcButton>
</div>
</div>
<div class="flex flex-col gap-4 h-[600px] p-6">
<!-- Search and Filter Bar -->
<!-- Search and Filter Bar -->
<div class="nc-app-market-filters">
<div class="flex gap-3">
<a-input
v-model:value="searchQuery"
class="flex-1 nc-input-sm nc-input-shadow !rounded-md"
class="flex-1 nc-input-sm nc-input-shadow !rounded-lg"
:placeholder="t('placeholder.searchByTitle')"
allow-clear
>
@@ -150,57 +169,92 @@ watch(
</NcSelect>
</div>
<!-- Sandbox List -->
<!-- Results count -->
<div v-if="!loading && filteredSandboxes.length > 0" class="mt-3 text-xs text-nc-content-gray-muted">
{{ filteredSandboxes.length }} {{ filteredSandboxes.length === 1 ? 'app' : 'apps' }} available
</div>
</div>
<!-- Content Area -->
<div class="flex-1 overflow-y-auto nc-scrollbar-thin">
<!-- Loading State -->
<div v-if="loading" class="flex items-center justify-center h-full">
<a-spin size="large" />
<div class="flex flex-col items-center gap-3">
<a-spin size="large" />
<div class="text-sm text-nc-content-gray-muted">Loading applications...</div>
</div>
</div>
<div v-else-if="filteredSandboxes.length === 0" class="flex flex-col items-center justify-center h-full gap-3">
<a-empty :image="Empty.PRESENTED_IMAGE_SIMPLE" class="!my-0">
<template #description>
<div class="text-nc-content-gray-muted mt-1">{{ t('msg.info.noSandboxesFound') }}</div>
</template>
</a-empty>
<!-- Empty State -->
<div v-else-if="filteredSandboxes.length === 0" class="nc-app-market-empty">
<div class="nc-empty-icon">
<GeneralIcon icon="ncBox" class="h-10 w-10 text-nc-content-gray-muted" />
</div>
<div class="text-base font-semibold text-nc-content-gray mb-2">No applications found</div>
<div class="text-sm text-nc-content-gray-subtle text-center max-w-md">
{{
searchQuery || selectedCategory
? "Try adjusting your search or filters to find what you're looking for."
: 'No managed applications are available yet. Be the first to publish one!'
}}
</div>
</div>
<div v-else class="flex flex-col gap-3 overflow-y-auto pr-2">
<!-- App List -->
<div v-else class="nc-app-market-list">
<div
v-for="sandbox in filteredSandboxes"
:key="sandbox.id"
class="nc-sandbox-card border-1 border-nc-border-gray-medium rounded-lg p-4 hover:shadow-md transition-all cursor-pointer"
@click="installSandbox(sandbox)"
class="nc-app-item"
>
<div class="flex gap-4">
<div class="flex-1">
<div class="flex items-start justify-between gap-3 mb-2">
<div class="flex-1">
<h3 class="text-base font-semibold text-nc-content-gray mb-1">{{ sandbox.title }}</h3>
<p class="text-sm text-nc-content-gray-subtle line-clamp-2">{{ sandbox.description }}</p>
<div class="nc-app-item-content">
<!-- App Icon & Info -->
<div class="nc-app-info">
<div class="nc-app-icon">
<GeneralIcon icon="ncBox" class="h-6 w-6" />
</div>
<div class="nc-app-details">
<div class="nc-app-title-row">
<h3 class="nc-app-title">{{ sandbox.title }}</h3>
<div
v-if="sandbox.category"
class="nc-app-category"
>
<GeneralIcon icon="ncHash" class="h-3 w-3" />
<span>{{ sandbox.category }}</span>
</div>
</div>
<p class="nc-app-description">
{{ sandbox.description || 'No description available' }}
</p>
<div class="nc-app-meta">
<span class="nc-app-meta-item">
<GeneralIcon icon="download" class="h-3.5 w-3.5" />
<span class="font-medium">{{ formatInstallCount(sandbox.install_count || 0) }}</span>
<span class="text-nc-content-gray-muted">installs</span>
</span>
<span v-if="sandbox.version" class="nc-app-meta-item">
<GeneralIcon icon="gitCommit" class="h-3.5 w-3.5" />
<span>v{{ sandbox.version }}</span>
</span>
</div>
<NcButton
:loading="installing === sandbox.id"
:disabled="!!installing"
size="small"
type="primary"
@click.stop="installSandbox(sandbox)"
>
<template #icon>
<GeneralIcon icon="download" />
</template>
{{ t('general.install') }}
</NcButton>
</div>
</div>
<div class="flex items-center gap-4 text-xs text-nc-content-gray-muted mt-3">
<span v-if="sandbox.category" class="flex items-center gap-1">
<GeneralIcon icon="tag" class="h-3 w-3" />
{{ sandbox.category }}
</span>
<span class="flex items-center gap-1">
<GeneralIcon icon="download" class="h-3 w-3" />
{{ sandbox.install_count || 0 }} {{ t('labels.installs') }}
</span>
</div>
<!-- Install Button -->
<div class="nc-app-action">
<NcButton
:loading="installing === sandbox.id"
:disabled="!!installing"
size="small"
type="primary"
@click="installSandbox(sandbox)"
>
<template #icon>
<GeneralIcon icon="download" class="h-4 w-4" />
</template>
{{ installing === sandbox.id ? 'Installing...' : t('general.install') }}
</NcButton>
</div>
</div>
</div>
@@ -210,16 +264,128 @@ watch(
</template>
<style lang="scss" scoped>
.nc-sandbox-card {
.nc-app-market {
@apply bg-nc-bg-gray-extralight;
}
.nc-app-market-header {
@apply px-6 py-4 bg-nc-bg-default border-b-1 border-nc-border-gray-light;
}
.nc-app-market-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);
}
.nc-app-market-filters {
@apply px-6 py-4 bg-nc-bg-default border-b-1 border-nc-border-gray-light;
}
.nc-app-market-empty {
@apply flex flex-col items-center justify-center h-full p-8;
}
.nc-empty-icon {
@apply w-20 h-20 rounded-full bg-nc-bg-gray-light flex items-center justify-center mb-4;
}
.nc-app-market-list {
@apply p-6;
}
.nc-app-item {
@apply bg-nc-bg-default border-1 border-nc-border-gray-light rounded-xl mb-3 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: '';
}
&:hover {
@apply border-brand-500;
@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-app-icon {
@apply transform scale-105;
box-shadow: 0 4px 8px rgba(51, 102, 255, 0.15);
}
}
&:last-child {
@apply mb-0;
}
}
.line-clamp-2 {
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
.nc-app-item-content {
@apply flex items-center gap-6 p-5;
}
.nc-app-info {
@apply flex gap-4 flex-1 min-w-0;
}
.nc-app-icon {
@apply w-12 h-12 rounded-xl border-1 border-nc-border-gray-light flex items-center justify-center flex-shrink-0 text-nc-content-brand;
@apply transition-all duration-200 ease-in-out;
background: linear-gradient(135deg, var(--nc-bg-brand) 0%, var(--nc-bg-blue-light) 100%);
}
.nc-app-details {
@apply flex-1 min-w-0;
}
.nc-app-title-row {
@apply flex items-center gap-3 mb-2;
}
.nc-app-title {
@apply text-base font-semibold text-nc-content-gray-emphasis m-0 truncate;
}
.nc-app-category {
@apply inline-flex items-center gap-1 px-2.5 py-0.5 bg-nc-bg-gray-light border-1 border-nc-border-gray-light;
@apply rounded-full text-xs text-nc-content-gray-subtle whitespace-nowrap flex-shrink-0;
}
.nc-app-description {
@apply text-sm text-nc-content-gray-subtle m-0 mb-3 leading-relaxed line-clamp-2;
}
.nc-app-meta {
@apply flex items-center gap-6 text-xs text-nc-content-gray-subtle2;
}
.nc-app-meta-item {
@apply flex items-center gap-1.5;
}
.nc-app-action {
@apply flex-shrink-0;
}
// Responsive adjustments
@media (max-width: 768px) {
.nc-app-item-content {
@apply flex-col items-start gap-4;
}
.nc-app-action {
@apply w-full;
:deep(button) {
@apply w-full;
}
}
.nc-app-title-row {
@apply flex-wrap;
}
}
</style>