380 lines
8.4 KiB
Vue
380 lines
8.4 KiB
Vue
<template>
|
|
<Dialog
|
|
:options="{
|
|
title: 'Add Marketplace App',
|
|
size: '6xl',
|
|
}"
|
|
v-model="showDialog"
|
|
>
|
|
<template #body-content>
|
|
<div class="flex">
|
|
<div>
|
|
<TextInput
|
|
placeholder="Search"
|
|
class="w-[20rem]"
|
|
:debounce="500"
|
|
v-model="searchQuery"
|
|
>
|
|
<template #prefix>
|
|
<lucide-search class="h-4 w-4 text-gray-500" />
|
|
</template>
|
|
<template #suffix>
|
|
<span class="text-sm text-gray-500" v-if="searchQuery">
|
|
{{
|
|
filteredRows.length === 0
|
|
? 'No results'
|
|
: `${filteredRows.length} of ${rows.length}`
|
|
}}
|
|
</span>
|
|
</template>
|
|
</TextInput>
|
|
</div>
|
|
<div class="ml-auto flex items-center space-x-2">
|
|
<Button
|
|
@click="$resources.installableApps.reload()"
|
|
:loading="isLoading"
|
|
>
|
|
<template #icon>
|
|
<lucide-refresh-ccw class="h-4 w-4" />
|
|
</template>
|
|
</Button>
|
|
<Button
|
|
@click="
|
|
showDialog = false;
|
|
showNewAppDialog = true;
|
|
"
|
|
>
|
|
<template #prefix>
|
|
<lucide-github class="h-4 w-4" />
|
|
Add from GitHub
|
|
</template>
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
<div class="mt-3 min-h-0 flex-1 overflow-y-auto">
|
|
<ListView
|
|
:columns="columns"
|
|
:rows="filteredRows"
|
|
:options="{
|
|
selectable: false,
|
|
onRowClick: () => {},
|
|
getRowRoute: null,
|
|
}"
|
|
row-key="name"
|
|
>
|
|
<ListHeader>
|
|
<ListHeaderItem
|
|
v-for="column in columns"
|
|
:key="column.key"
|
|
:item="column"
|
|
/>
|
|
</ListHeader>
|
|
<ListRows>
|
|
<ListRow
|
|
v-for="(row, i) in filteredRows"
|
|
:row="row"
|
|
:key="row.name"
|
|
>
|
|
<template v-slot="{ column, item }">
|
|
<div class="flex items-center">
|
|
<div v-if="column.prefix" class="mr-2">
|
|
<component :is="column.prefix(row)" />
|
|
</div>
|
|
<Dropdown
|
|
:options="dropdownItems(row)"
|
|
right
|
|
v-if="column.type === 'select'"
|
|
>
|
|
<template v-slot="{ open }">
|
|
<Button
|
|
v-if="row.source.branch"
|
|
type="white"
|
|
icon-right="chevron-down"
|
|
class="max-w-48 truncate"
|
|
>
|
|
<span class="truncate">{{ row.source.branch }}</span>
|
|
</Button>
|
|
</template>
|
|
</Dropdown>
|
|
<component
|
|
v-else-if="column.type === 'Component'"
|
|
:is="column.component(row)"
|
|
/>
|
|
<Badge
|
|
v-else-if="column.type === 'Badge'"
|
|
v-bind="formattedValue(column, item, row)"
|
|
/>
|
|
<div v-else class="truncate text-base" :class="column.class">
|
|
{{ formattedValue(column, item, row) }}
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</ListRow>
|
|
</ListRows>
|
|
</ListView>
|
|
<div class="px-5" v-if="filteredRows.length === 0">
|
|
<div
|
|
class="text-center text-sm leading-10 text-gray-500"
|
|
v-if="isLoading"
|
|
>
|
|
Loading...
|
|
</div>
|
|
<div v-else class="text-center text-sm leading-10 text-gray-500">
|
|
No apps available to add
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</Dialog>
|
|
<NewAppDialog
|
|
v-if="showNewAppDialog"
|
|
@app-added="addAppFromGithub"
|
|
:group="group"
|
|
/>
|
|
</template>
|
|
|
|
<script>
|
|
import {
|
|
ListView,
|
|
ListHeader,
|
|
ListHeaderItem,
|
|
ListRow,
|
|
ListRows,
|
|
ListRowItem,
|
|
TextInput,
|
|
Badge,
|
|
Button,
|
|
} from 'frappe-ui';
|
|
import { toast } from 'vue-sonner';
|
|
import { h } from 'vue';
|
|
import { getToastErrorMessage } from '../../utils/toast';
|
|
import NewAppDialog from '../NewAppDialog.vue';
|
|
|
|
export default {
|
|
name: 'AddAppDialog',
|
|
props: {
|
|
group: {
|
|
type: Object,
|
|
required: true,
|
|
},
|
|
},
|
|
components: {
|
|
ListView,
|
|
ListHeader,
|
|
ListHeaderItem,
|
|
ListRow,
|
|
ListRows,
|
|
ListRowItem,
|
|
TextInput,
|
|
NewAppDialog,
|
|
},
|
|
emits: ['appAdd', 'newApp'],
|
|
data() {
|
|
return {
|
|
searchQuery: '',
|
|
showNewAppDialog: false,
|
|
selectedAppSources: [],
|
|
showDialog: true,
|
|
addedApps: [],
|
|
};
|
|
},
|
|
resources: {
|
|
addApp: {
|
|
url: 'press.api.bench.add_app',
|
|
onSuccess() {
|
|
this.$emit('appAdd');
|
|
},
|
|
onError(e) {
|
|
toast.error(getToastErrorMessage(e));
|
|
},
|
|
},
|
|
installableApps() {
|
|
return {
|
|
url: 'press.api.bench.all_apps',
|
|
params: {
|
|
name: this.group.name,
|
|
},
|
|
transform(data) {
|
|
return data.map((app) => {
|
|
app.compatible = app.sources.length > 0;
|
|
app.source = app.sources.length > 0 ? app.sources[0] : {};
|
|
return app;
|
|
});
|
|
},
|
|
auto: true,
|
|
cache: ['benchInstallableApps', this.group.version],
|
|
initialData: [],
|
|
};
|
|
},
|
|
},
|
|
computed: {
|
|
rows() {
|
|
return this.$resources.installableApps.data;
|
|
},
|
|
columns() {
|
|
return [
|
|
{
|
|
label: 'Title',
|
|
key: 'title',
|
|
class: 'font-medium',
|
|
prefix(row) {
|
|
return row.image
|
|
? h('img', {
|
|
src: row.image,
|
|
class: 'w-6 h-6 rounded',
|
|
alt: row.title,
|
|
})
|
|
: h(
|
|
'div',
|
|
{
|
|
class:
|
|
'w-6 h-6 rounded bg-gray-300 text-gray-600 flex items-center justify-center',
|
|
},
|
|
row.title[0].toUpperCase(),
|
|
);
|
|
},
|
|
},
|
|
{
|
|
label: 'Repository',
|
|
key: 'repo',
|
|
class: 'text-gray-600',
|
|
width: '15rem',
|
|
format(value, row) {
|
|
if (!row.sources.length) return value;
|
|
return `${row.source.repository_owner}/${row.source.repository}`;
|
|
},
|
|
},
|
|
{
|
|
label: 'Branch',
|
|
type: 'select',
|
|
key: 'sources',
|
|
width: '15rem',
|
|
format(value, row) {
|
|
return row.sources.map((s) => {
|
|
return {
|
|
label: s.branch,
|
|
value: s.name,
|
|
};
|
|
});
|
|
},
|
|
},
|
|
{
|
|
label: '',
|
|
type: 'Component',
|
|
width: '8rem',
|
|
component: (row) => {
|
|
if (row.compatible)
|
|
return h(Button, {
|
|
label: 'Add',
|
|
iconLeft: this.addedApps.includes(row) ? 'check' : 'plus',
|
|
|
|
disabled: !row.compatible,
|
|
class: {
|
|
'ml-auto': true,
|
|
'pointer-events-none': this.addedApps.includes(row),
|
|
},
|
|
onClick: () => this.addApp(row),
|
|
});
|
|
else
|
|
return h(Badge, {
|
|
class: 'ml-auto',
|
|
label: 'Not Compatible',
|
|
theme: 'red',
|
|
});
|
|
},
|
|
},
|
|
];
|
|
},
|
|
filteredRows() {
|
|
let rows = this.rows.sort((a, b) => {
|
|
// Sort by compatible first, then by total installs
|
|
if (a.compatible && !b.compatible) {
|
|
return -1;
|
|
} else if (!a.compatible && b.compatible) {
|
|
return 1;
|
|
} else if (a.total_installs != b.total_installs) {
|
|
return b.total_installs - a.total_installs;
|
|
} else {
|
|
return a.title.localeCompare(b.title);
|
|
}
|
|
});
|
|
|
|
if (!this.searchQuery) return rows;
|
|
let query = this.searchQuery.toLowerCase();
|
|
|
|
return rows.filter((row) => {
|
|
let values = this.columns.map((column) => {
|
|
let value = row[column.key];
|
|
if (column.format) {
|
|
value = column.format(value, row);
|
|
}
|
|
return value;
|
|
});
|
|
for (let value of values) {
|
|
if (value && value.toLowerCase?.().includes(query)) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
});
|
|
},
|
|
isLoading() {
|
|
return (
|
|
this.$resources.addApp.loading ||
|
|
this.$resources.installableApps.loading
|
|
);
|
|
},
|
|
},
|
|
methods: {
|
|
addAppFromGithub(app, isUpdate) {
|
|
app.group = this.group.name;
|
|
this.$emit('newApp', app, isUpdate);
|
|
},
|
|
addApp(row) {
|
|
if (!this.selectedAppSources.includes(row))
|
|
this.selectedAppSources.push(row);
|
|
|
|
let app = this.selectedAppSources.find((app) => app.name === row.name);
|
|
|
|
this.$resources.addApp
|
|
.submit({
|
|
name: this.group.name,
|
|
source: app.source.name,
|
|
app: app.name,
|
|
})
|
|
.then(() => {
|
|
this.addedApps.push(app);
|
|
});
|
|
},
|
|
dropdownItems(row) {
|
|
return row.sources.map((source) => ({
|
|
label: `${source.repository_owner}/${source.repository}:${source.branch}`,
|
|
icon: row.source.name === source.name ? 'check' : null,
|
|
onClick: () => this.selectSource(row, source),
|
|
}));
|
|
},
|
|
selectSource(app, source) {
|
|
app.source = source;
|
|
this.selectedAppSources = this.filteredRows.map((_app) => {
|
|
if (app.name === _app.app) {
|
|
return {
|
|
app: app.name,
|
|
source,
|
|
};
|
|
}
|
|
return _app;
|
|
});
|
|
},
|
|
formattedValue(column, value, row) {
|
|
let formattedValue = column.format ? column.format(value, row) : value;
|
|
if (formattedValue == null) {
|
|
formattedValue = '';
|
|
}
|
|
return typeof formattedValue === 'object'
|
|
? formattedValue
|
|
: String(formattedValue);
|
|
},
|
|
},
|
|
};
|
|
</script>
|