Files

660 lines
15 KiB
JavaScript

import { defineAsyncComponent, h } from 'vue';
import { Button, Badge } from 'frappe-ui';
import { toast } from 'vue-sonner';
import ChangeAppBranchDialog from '../components/marketplace/ChangeAppBranchDialog.vue';
import { confirmDialog, icon, renderDialog } from '../utils/components';
import PlansDialog from '../components/marketplace/PlansDialog.vue';
import CodeReview from '../components/marketplace/CodeReview.vue';
import GenericDialog from '../components/GenericDialog.vue';
import ObjectList from '../components/ObjectList.vue';
import { userCurrency, currency } from '../utils/format';
import { getToastErrorMessage } from '../utils/toast';
import { isMobile } from '../utils/device';
import router from '../router';
export default {
doctype: 'Marketplace App',
whitelistedMethods: {
removeVersion: 'remove_version',
addVersion: 'add_version',
siteInstalls: 'site_installs',
createApprovalRequest: 'create_approval_request',
cancelApprovalRequest: 'cancel_approval_request',
updateListing: 'update_listing',
markAppReadyForReview: 'mark_app_ready_for_review'
},
list: {
route: '/apps',
title: 'Marketplace',
fields: ['image', 'title', 'status', 'description'],
columns: [
{
label: 'App',
fieldname: 'title',
class: 'font-medium',
width: 0.3,
prefix(row) {
return row.image
? h('img', {
src: row.image,
class: 'w-6 h-6 rounded-sm',
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: 'Status',
type: 'Badge',
fieldname: 'status',
width: 0.3
},
{
label: 'Description',
fieldname: 'description',
width: 1.0
}
],
primaryAction() {
return {
label: 'New App',
variant: 'solid',
slots: {
prefix: icon('plus')
},
onClick() {
const NewMarketplaceAppDialog = defineAsyncComponent(() =>
import('../components/marketplace/NewMarketplaceAppDialog.vue')
);
renderDialog(h(NewMarketplaceAppDialog));
}
};
}
},
detail: {
titleField: 'name',
route: '/apps/:name',
statusBadge({ documentResource: app }) {
return { label: app.doc.status };
},
breadcrumbs({ items, documentResource: app }) {
return [
items[0],
{
label: app.doc.title,
route: `/apps/${app.doc.name}`
}
];
},
tabs: [
{
label: 'Analytics',
icon: icon('bar-chart-2'),
route: 'analytics',
type: 'Component',
component: defineAsyncComponent(() =>
import('../components/marketplace/MarketplaceAppAnalytics.vue')
),
props: app => {
return { app: app.doc.name };
}
},
{
label: 'Listing',
icon: icon('shopping-cart'),
route: 'listing',
type: 'Component',
component: defineAsyncComponent(() =>
import('../components/MarketplaceAppListing.vue')
),
props: app => {
return { app: app };
}
},
{
label: 'Versions',
icon: icon('package'),
route: 'versions',
type: 'list',
list: {
doctype: 'Marketplace App Version',
filters: app => {
return { parent: app.doc.name, parenttype: 'Marketplace App' };
},
onRowClick: (row, context) => {
const { listResource: versions, documentResource: app } = context;
showReleases(row, app);
},
fields: [
'source.repository_owner as repository_owner',
'source.repository as repository',
'source.branch as branch'
],
columns: [
{
label: 'Version',
fieldname: 'version',
width: 0.5
},
{
label: 'Source',
fieldname: 'source',
width: 0.5
},
{
label: 'Repository',
width: 0.5,
format: (value, row) => {
return `${row.repository_owner}/${row.repository}`;
}
},
{
label: 'Branch',
fieldname: 'branch',
type: 'Badge',
width: 0.5
}
],
primaryAction({ listResource: versions, documentResource: app }) {
return {
label: 'New Version',
slots: {
prefix: icon('plus')
},
onClick() {
renderDialog(
h(
GenericDialog,
{
options: {
title: `Add version support for ${app.doc.title}`,
size: '4xl'
}
},
{
default: () =>
h(ObjectList, {
options: {
label: 'Version',
fieldname: 'version',
fieldtype: 'ListSelection',
columns: [
{
label: 'Version',
fieldname: 'version'
},
{
label: 'Branch',
type: 'Select',
fieldname: 'branch',
format: (value, row) => {
row.selectedOption = value[0];
return value.map(v => ({
label: v,
value: v,
onClick: () => {
row.selectedOption = v;
}
}));
}
},
{
label: '',
fieldname: '',
align: 'right',
type: 'Button',
width: '5rem',
Button({ row, listResource: versionsOptions }) {
return {
label: 'Add',
onClick() {
if (app.addVersion.loading) return;
toast.promise(
app.addVersion.submit({
version: row.version,
branch: row.selectedOption
}),
{
loading: 'Adding new version...',
success: () => {
versions.reload();
versionsOptions.reload();
return 'New version added';
},
error: e => getToastErrorMessage(e)
}
);
}
};
}
}
],
resource() {
return {
url: 'press.api.marketplace.options_for_version',
params: {
name: app.doc.name
},
auto: true
};
}
}
})
}
)
);
}
};
},
rowActions({ row, listResource: versions, documentResource: app }) {
return [
{
label: 'Show Releases',
slots: {
prefix: icon('plus')
},
onClick() {
showReleases(row, app);
}
},
{
label: 'Change Branch',
onClick() {
renderDialog(
h(ChangeAppBranchDialog, {
app: app.doc.name,
source: row.source,
version: row.version,
activeBranch: row.branch,
onBranchChanged() {
versions.reload();
}
})
);
}
},
{
label: 'Remove Version',
onClick() {
toast.promise(
app.removeVersion.submit({ version: row.version }),
{
loading: 'Removing version...',
success: () => {
versions.reload();
return 'Version removed successfully';
},
error: e => getToastErrorMessage(e)
}
);
}
}
];
}
}
},
{
label: 'Pricing',
icon: icon('dollar-sign'),
route: 'pricing',
type: 'list',
list: {
doctype: 'Marketplace App Plan',
filters: app => {
return { app: app.doc.name };
},
fields: ['name', 'title', 'price_inr', 'price_usd', 'enabled'],
columns: [
{
label: 'Title',
fieldname: 'title'
},
{
label: 'Enabled',
type: 'Badge',
fieldname: 'enabled',
format: value => {
return value == 1 ? 'Enabled' : 'Disabled';
}
},
{
label: 'Price (INR)',
fieldname: 'price_inr',
format: value => {
return currency(value, 'INR');
}
},
{
label: 'Price (USD)',
fieldname: 'price_usd',
format: value => {
return currency(value, 'USD');
}
}
],
primaryAction({ listResource: plans, documentResource: app }) {
return {
label: 'New Plan',
slots: {
prefix: icon('plus')
},
onClick() {
renderDialog(
h(PlansDialog, {
app: app.doc.name,
onPlanCreated() {
plans.reload();
}
})
);
}
};
},
rowActions({ row, listResource: plans, documentResource: app }) {
return [
{
label: 'Edit',
onClick() {
renderDialog(
h(PlansDialog, {
app: app.doc.name,
plan: row,
onPlanUpdated() {
plans.reload();
}
})
);
}
}
];
}
}
},
{
label: 'Subscriptions',
icon: icon('users'),
route: 'subscription',
type: 'list',
list: {
doctype: 'Subscription',
filters: app => {
return {
document_type: 'Marketplace App',
document_name: app.name
};
},
fields: ['site', 'enabled', 'team'],
filterControls() {
return [
{
type: 'select',
label: 'Status',
class: !isMobile() ? 'w-24' : '',
fieldname: 'enabled',
options: ['', 'Active', 'Disabled']
}
];
},
columns: [
{
label: 'Site',
fieldname: 'site',
width: 0.6
},
{
label: 'Status',
type: 'Badge',
fieldname: 'enabled',
width: 0.3,
format: value => {
return value ? 'Active' : 'Disabled';
}
},
{
label: 'Price',
fieldname: 'price',
width: 0.3,
format: value => {
return userCurrency(value);
}
},
{
label: 'Active For',
fieldname: 'active_for',
width: 0.3,
format: value => {
return value + (value == 1 ? ' day' : ' days');
}
}
]
}
}
],
actions({ documentResource: app }) {
return [
{
label: 'View in Marketplace',
slots: {
prefix: icon('external-link')
},
condition: () => app.doc.status === 'Published',
onClick() {
window.open(
`${window.location.origin}/marketplace/apps/${app.name}`,
'_blank'
);
}
},
{
label: 'Guidelines',
slots: {
icon: icon('help-circle')
},
condition: () => app.doc.status === 'Draft',
onClick() {
window.open(
'https://frappecloud.com/docs/marketplace/marketplace-guidelines',
'_blank'
);
}
},
{
label: 'Complete Listing',
variant: 'solid',
slots: {
prefix: icon('alert-circle')
},
condition: () => app.doc.status === 'Draft',
onClick() {
let AppListingStepsDialog = defineAsyncComponent(() =>
import('../components/marketplace/AppListingStepsDialog.vue')
);
renderDialog(
h(AppListingStepsDialog, {
app: app.doc.name
})
);
}
},
,
{
label: 'Options',
condition: () => app.doc.status === 'Draft',
options: [
{
label: 'Delete',
icon: icon('trash-2'),
condition: () => app.doc.status === 'Draft',
onClick() {
confirmDialog({
title: `Delete App ${app.doc.title}`,
message: 'Are you sure you want to delete this app?',
onSuccess({ hide }) {
toast.promise(app.delete.submit(), {
loading: 'Deleting app...',
success: () => {
hide();
router.push({ name: 'Marketplace App List' });
return 'App deleted successfully';
},
error: e => getToastErrorMessage(e)
});
}
});
}
}
]
}
];
}
}
};
function showReleases(row, app) {
renderDialog(
h(
GenericDialog,
{
options: {
title: `Releases for ${app.doc.name} on ${row.branch} branch`,
size: '6xl'
}
},
{
default: () =>
h(ObjectList, {
options: {
label: 'Version',
type: 'list',
doctype: 'App Release',
filters: {
app: app.doc.name,
source: row.source
},
fields: ['message', 'tag', 'author', 'status'],
orderBy: 'creation desc',
columns: [
{
label: 'Commit Message',
fieldname: 'message',
class: 'w-64',
width: 0.5
},
{
label: 'Hash',
fieldname: 'hash',
class: 'w-24',
type: 'Badge',
width: 0.2,
format: value => {
return value.slice(0, 7);
}
},
{
label: 'Author',
fieldname: 'author',
width: 0.2
},
{
label: 'Status',
fieldname: 'status',
type: 'Badge',
width: 0.3
},
{
label: 'Code Screening',
type: 'Component',
width: 0.2,
component: ({ row, listResource: releases, app }) => {
if (
(row.status === 'Awaiting Approval' ||
row.status === 'Rejected') &&
row.screening_status === 'Complete'
) {
return h(Button, {
label: 'Code Review',
variant: 'subtle',
theme: 'blue',
size: 'sm',
onClick: () =>
codeReview(row, app, window.is_system_user)
});
}
return h(Badge, {
label: row.screening_status || 'Not Started'
});
}
},
{
label: '',
fieldname: '',
align: 'right',
type: 'Button',
width: 0.2,
Button({ row, listResource: releases }) {
let label = '';
let successMessage = '';
let loadingMessage = '';
if (row.status === 'Awaiting Approval') {
label = 'Cancel';
successMessage = 'The release has been cancelled';
loadingMessage = 'Cancelling release...';
} else if (row.status === 'Draft') {
label = 'Submit';
loadingMessage = 'Submitting release for approval...';
successMessage =
'The release has been submitted for approval';
}
return {
label: label,
onClick() {
toast.promise(
row.status === 'Awaiting Approval'
? app.cancelApprovalRequest.submit({
app_release: row.name
})
: app.createApprovalRequest.submit({
app_release: row.name
}),
{
loading: loadingMessage,
success: () => {
releases.reload();
return successMessage;
},
error: e => getToastErrorMessage(e)
}
);
}
};
}
}
]
}
})
}
)
);
}
function codeReview(row, app, isSystemUser) {
renderDialog(
h(CodeReview, {
row: row,
app: app,
isSystemUser: isSystemUser
})
);
}