mirror of
https://github.com/hanzo-io/press.git
synced 2026-10-30 18:56:50 +00:00
660 lines
15 KiB
JavaScript
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
|
|
})
|
|
);
|
|
}
|