Regression: Apps and Marketplace UI issues (#15045)
* Alert admins about apps on invalid state * Implement ui for warning and error alerts in apps * Open detail modal on viewing subscription info instead of the subscribe one. Check license after close * Implement ui for failed state of apps in detail screen * Add failure alert support into appManage * Show validation erros/warnings on app detail page * Add status column to apps template * Update uninstall modal * Notify admins of disabled apps with valid licensespull/14848/head^2
parent
34f24f407b
commit
ee2ed86482
@ -1,3 +1,4 @@ |
||||
app/theme/client/vendor/fontello/css/fontello.css |
||||
packages/meteor-autocomplete/client/autocomplete.css |
||||
app/katex/katex.min.css |
||||
app/emoji-emojione/client/*.css |
||||
|
File diff suppressed because it is too large
Load Diff
@ -1,206 +1,284 @@ |
||||
import toastr from 'toastr'; |
||||
import { ReactiveVar } from 'meteor/reactive-var'; |
||||
import { FlowRouter } from 'meteor/kadira:flow-router'; |
||||
import { ReactiveDict } from 'meteor/reactive-dict'; |
||||
import { Template } from 'meteor/templating'; |
||||
import { Tracker } from 'meteor/tracker'; |
||||
|
||||
import { settings } from '../../../settings'; |
||||
import { t, APIClient } from '../../../utils'; |
||||
import { AppEvents } from '../communication'; |
||||
import { Apps } from '../orchestrator'; |
||||
import { SideNav } from '../../../ui-utils/client'; |
||||
import { |
||||
appButtonProps, |
||||
appStatusSpanProps, |
||||
checkCloudLogin, |
||||
handleAPIError, |
||||
promptSubscription, |
||||
triggerAppPopoverMenu, |
||||
warnStatusChange, |
||||
} from './helpers'; |
||||
|
||||
const ENABLED_STATUS = ['auto_enabled', 'manually_enabled']; |
||||
const enabled = ({ status }) => ENABLED_STATUS.includes(status); |
||||
import './apps.html'; |
||||
|
||||
const sortByColumn = (array, column, inverted) => |
||||
array.sort((a, b) => { |
||||
if (a.latest[column] < b.latest[column] && !inverted) { |
||||
return -1; |
||||
} |
||||
return 1; |
||||
|
||||
Template.apps.onCreated(function() { |
||||
this.state = new ReactiveDict({ |
||||
apps: [], // TODO: maybe use another ReactiveDict here
|
||||
isLoading: true, |
||||
searchText: '', |
||||
sortedColumn: 'name', |
||||
isAscendingOrder: true, |
||||
|
||||
// TODO: to use these fields
|
||||
page: 0, |
||||
itemsPerPage: 0, |
||||
wasEndReached: false, |
||||
}); |
||||
|
||||
const getInstalledApps = async (instance) => { |
||||
try { |
||||
const data = await APIClient.get('apps'); |
||||
const apps = data.apps.map((app) => ({ latest: app })); |
||||
(async () => { |
||||
try { |
||||
const appsFromMarketplace = await Apps.getAppsFromMarketplace(); |
||||
const installedApps = await Apps.getApps(); |
||||
|
||||
instance.apps.set(apps); |
||||
} catch (e) { |
||||
toastr.error((e.xhr.responseJSON && e.xhr.responseJSON.error) || e.message); |
||||
} |
||||
const apps = installedApps.map((app) => { |
||||
const appFromMarketplace = appsFromMarketplace.find(({ id }) => id === app.id); |
||||
|
||||
instance.isLoading.set(false); |
||||
instance.ready.set(true); |
||||
}; |
||||
if (!appFromMarketplace) { |
||||
return { |
||||
...app, |
||||
installed: true, |
||||
}; |
||||
} |
||||
|
||||
Template.apps.onCreated(function() { |
||||
const instance = this; |
||||
this.ready = new ReactiveVar(false); |
||||
this.apps = new ReactiveVar([]); |
||||
this.categories = new ReactiveVar([]); |
||||
this.searchText = new ReactiveVar(''); |
||||
this.searchSortBy = new ReactiveVar('name'); |
||||
this.sortDirection = new ReactiveVar('asc'); |
||||
this.limit = new ReactiveVar(0); |
||||
this.page = new ReactiveVar(0); |
||||
this.end = new ReactiveVar(false); |
||||
this.isLoading = new ReactiveVar(true); |
||||
|
||||
getInstalledApps(instance); |
||||
|
||||
try { |
||||
APIClient.get('apps?categories=true').then((data) => instance.categories.set(data)); |
||||
} catch (e) { |
||||
toastr.error((e.xhr.responseJSON && e.xhr.responseJSON.error) || e.message); |
||||
} |
||||
|
||||
instance.onAppAdded = function _appOnAppAdded() { |
||||
// ToDo: fix this formatting data to add an app to installedApps array without to fetch all
|
||||
|
||||
// fetch(`${ HOST }/v1/apps/${ appId }`).then((result) => {
|
||||
// const installedApps = instance.installedApps.get();
|
||||
|
||||
// installedApps.push({
|
||||
// latest: result.app,
|
||||
// });
|
||||
// instance.installedApps.set(installedApps);
|
||||
// });
|
||||
return { |
||||
...app, |
||||
installed: true, |
||||
categories: appFromMarketplace.categories, |
||||
marketplaceVersion: appFromMarketplace.version, |
||||
}; |
||||
}); |
||||
|
||||
this.state.set('apps', apps); |
||||
} catch (error) { |
||||
handleAPIError(error); |
||||
} finally { |
||||
this.state.set('isLoading', false); |
||||
} |
||||
})(); |
||||
|
||||
this.startAppWorking = (appId) => { |
||||
const apps = this.state.get('apps'); |
||||
const app = apps.find(({ id }) => id === appId); |
||||
app.working = true; |
||||
this.state.set('apps', apps); |
||||
}; |
||||
|
||||
this.stopAppWorking = (appId) => { |
||||
const apps = this.state.get('apps'); |
||||
const app = apps.find(({ id }) => id === appId); |
||||
delete app.working; |
||||
this.state.set('apps', apps); |
||||
}; |
||||
|
||||
instance.onAppRemoved = function _appOnAppRemoved(appId) { |
||||
const apps = instance.apps.get(); |
||||
this.handleAppAddedOrUpdated = async (appId) => { |
||||
try { |
||||
const app = await Apps.getApp(appId); |
||||
const { categories, version: marketplaceVersion } = await Apps.getAppFromMarketplace(appId, app.version) || {}; |
||||
const apps = [ |
||||
...this.state.get('apps').filter(({ id }) => id !== appId), |
||||
{ |
||||
...app, |
||||
installed: true, |
||||
categories, |
||||
marketplaceVersion, |
||||
}, |
||||
]; |
||||
this.state.set('apps', apps); |
||||
} catch (error) { |
||||
handleAPIError(error); |
||||
} |
||||
}; |
||||
|
||||
let index = -1; |
||||
apps.find((item, i) => { |
||||
if (item.id === appId) { |
||||
index = i; |
||||
return true; |
||||
} |
||||
return false; |
||||
}); |
||||
this.handleAppRemoved = (appId) => { |
||||
this.state.set('apps', this.state.get('apps').filter(({ id }) => id !== appId)); |
||||
}; |
||||
|
||||
apps.splice(index, 1); |
||||
instance.apps.set(apps); |
||||
this.handleAppStatusChange = ({ appId, status }) => { |
||||
const apps = this.state.get('apps'); |
||||
const app = apps.find(({ id }) => id === appId); |
||||
if (!app) { |
||||
return; |
||||
} |
||||
|
||||
app.status = status; |
||||
this.state.set('apps', apps); |
||||
}; |
||||
|
||||
Apps.getWsListener().registerListener(AppEvents.APP_ADDED, instance.onAppAdded); |
||||
Apps.getWsListener().registerListener(AppEvents.APP_REMOVED, instance.onAppAdded); |
||||
Apps.getWsListener().registerListener(AppEvents.APP_ADDED, this.handleAppAddedOrUpdated); |
||||
Apps.getWsListener().registerListener(AppEvents.APP_UPDATED, this.handleAppAddedOrUpdated); |
||||
Apps.getWsListener().registerListener(AppEvents.APP_REMOVED, this.handleAppRemoved); |
||||
Apps.getWsListener().registerListener(AppEvents.APP_STATUS_CHANGE, this.handleAppStatusChange); |
||||
}); |
||||
|
||||
Template.apps.onDestroyed(function() { |
||||
const instance = this; |
||||
Apps.getWsListener().unregisterListener(AppEvents.APP_ADDED, this.handleAppAddedOrUpdated); |
||||
Apps.getWsListener().unregisterListener(AppEvents.APP_UPDATED, this.handleAppAddedOrUpdated); |
||||
Apps.getWsListener().unregisterListener(AppEvents.APP_REMOVED, this.handleAppRemoved); |
||||
Apps.getWsListener().unregisterListener(AppEvents.APP_STATUS_CHANGE, this.handleAppStatusChange); |
||||
}); |
||||
|
||||
Apps.getWsListener().unregisterListener(AppEvents.APP_ADDED, instance.onAppAdded); |
||||
Apps.getWsListener().unregisterListener(AppEvents.APP_REMOVED, instance.onAppAdded); |
||||
Template.apps.onRendered(() => { |
||||
Tracker.afterFlush(() => { |
||||
SideNav.setFlex('adminFlex'); |
||||
SideNav.openFlex(); |
||||
}); |
||||
}); |
||||
|
||||
Template.apps.helpers({ |
||||
isReady() { |
||||
if (Template.instance().ready != null) { |
||||
return Template.instance().ready.get(); |
||||
} |
||||
|
||||
return false; |
||||
}, |
||||
apps() { |
||||
const instance = Template.instance(); |
||||
const searchText = instance.searchText.get().toLowerCase(); |
||||
const sortColumn = instance.searchSortBy.get(); |
||||
const inverted = instance.sortDirection.get() === 'desc'; |
||||
return sortByColumn(instance.apps.get().filter((app) => app.latest.name.toLowerCase().includes(searchText)), sortColumn, inverted); |
||||
}, |
||||
categories() { |
||||
return Template.instance().categories.get(); |
||||
}, |
||||
appsDevelopmentMode() { |
||||
isDevelopmentModeEnabled() { |
||||
return settings.get('Apps_Framework_Development_Mode') === true; |
||||
}, |
||||
parseStatus(status) { |
||||
return t(`App_status_${ status }`); |
||||
}, |
||||
isActive(status) { |
||||
return enabled({ status }); |
||||
}, |
||||
sortIcon(key) { |
||||
const { |
||||
sortDirection, |
||||
searchSortBy, |
||||
} = Template.instance(); |
||||
|
||||
return key === searchSortBy.get() && sortDirection.get() !== 'asc' ? 'sort-up' : 'sort-down'; |
||||
}, |
||||
searchSortBy(key) { |
||||
return Template.instance().searchSortBy.get() === key; |
||||
}, |
||||
isLoading() { |
||||
return Template.instance().isLoading.get(); |
||||
return Template.instance().state.get('isLoading'); |
||||
}, |
||||
onTableScroll() { |
||||
const instance = Template.instance(); |
||||
if (instance.loading || instance.end.get()) { |
||||
handleTableScroll() { |
||||
const { state } = Template.instance(); |
||||
if (state.get('isLoading') || state.get('wasEndReached')) { |
||||
return; |
||||
} |
||||
return function(currentTarget) { |
||||
if (currentTarget.offsetHeight + currentTarget.scrollTop >= currentTarget.scrollHeight - 100) { |
||||
return instance.page.set(instance.page.get() + 1); |
||||
|
||||
return ({ offsetHeight, scrollTop, scrollHeight }) => { |
||||
const shouldGoToNextPage = offsetHeight + scrollTop >= scrollHeight - 100; |
||||
if (shouldGoToNextPage) { |
||||
return state.set('page', state.get('page') + 1); |
||||
} |
||||
}; |
||||
}, |
||||
onTableResize() { |
||||
const { limit } = Template.instance(); |
||||
handleTableResize() { |
||||
const { state } = Template.instance(); |
||||
|
||||
return function() { |
||||
limit.set(Math.ceil((this.$('.table-scroll').height() / 40) + 5)); |
||||
const $table = this.$('.table-scroll'); |
||||
state.set('itemsPerPage', Math.ceil(($table.height() / 40) + 5)); |
||||
}; |
||||
}, |
||||
onTableSort() { |
||||
const { end, page, sortDirection, searchSortBy } = Template.instance(); |
||||
return function(type) { |
||||
end.set(false); |
||||
page.set(0); |
||||
handleTableSort() { |
||||
const { state } = Template.instance(); |
||||
|
||||
return (sortedColumn) => { |
||||
state.set({ |
||||
page: 0, |
||||
wasEndReached: false, |
||||
}); |
||||
|
||||
if (searchSortBy.get() === type) { |
||||
sortDirection.set(sortDirection.get() === 'asc' ? 'desc' : 'asc'); |
||||
if (state.get('sortedColumn') === sortedColumn) { |
||||
state.set('isAscendingOrder', !state.get('isAscendingOrder')); |
||||
return; |
||||
} |
||||
|
||||
searchSortBy.set(type); |
||||
sortDirection.set('asc'); |
||||
state.set({ |
||||
sortedColumn, |
||||
isAscendingOrder: true, |
||||
}); |
||||
}; |
||||
}, |
||||
formatCategories(categories = []) { |
||||
return categories.join(', '); |
||||
isSortingBy(column) { |
||||
return Template.instance().state.get('sortedColumn') === column; |
||||
}, |
||||
}); |
||||
sortIcon(column) { |
||||
const { state } = Template.instance(); |
||||
|
||||
Template.apps.events({ |
||||
'click .manage'() { |
||||
const rl = this; |
||||
return column === state.get('sortedColumn') && state.get('isAscendingOrder') ? 'sort-down' : 'sort-up'; |
||||
}, |
||||
apps() { |
||||
const { state } = Template.instance(); |
||||
const apps = state.get('apps'); |
||||
const searchText = state.get('searchText').toLocaleLowerCase(); |
||||
const sortedColumn = state.get('sortedColumn'); |
||||
const isAscendingOrder = state.get('isAscendingOrder'); |
||||
const sortingFactor = isAscendingOrder ? 1 : -1; |
||||
|
||||
if (rl && rl.latest && rl.latest.id) { |
||||
FlowRouter.go(`/admin/apps/${ rl.latest.id }?version=${ rl.latest.version }`); |
||||
} |
||||
return apps |
||||
.filter(({ name }) => name.toLocaleLowerCase().includes(searchText)) |
||||
.sort(({ [sortedColumn]: a }, { [sortedColumn]: b }) => sortingFactor * String(a).localeCompare(String(b))); |
||||
}, |
||||
'click [data-button="install_app"]'() { |
||||
appButtonProps, |
||||
appStatusSpanProps, |
||||
}); |
||||
|
||||
Template.apps.events({ |
||||
'click .js-marketplace'() { |
||||
FlowRouter.go('marketplace'); |
||||
}, |
||||
'click [data-button="upload_app"]'() { |
||||
'click .js-upload'() { |
||||
FlowRouter.go('app-install'); |
||||
}, |
||||
'keyup .js-search'(e, t) { |
||||
t.searchText.set(e.currentTarget.value); |
||||
'submit .js-search-form'(event) { |
||||
event.stopPropagation(); |
||||
return false; |
||||
}, |
||||
'submit .js-search-form'(e) { |
||||
e.preventDefault(); |
||||
e.stopPropagation(); |
||||
'input .js-search'(event, instance) { |
||||
instance.state.set('searchText', event.currentTarget.value); |
||||
}, |
||||
}); |
||||
'click .js-manage'(event, instance) { |
||||
event.stopPropagation(); |
||||
const { currentTarget } = event; |
||||
const { |
||||
id: appId, |
||||
version, |
||||
} = instance.state.get('apps').find(({ id }) => id === currentTarget.dataset.id); |
||||
FlowRouter.go('app-manage', { appId }, { version }); |
||||
}, |
||||
async 'click .js-install, click .js-update'(event, instance) { |
||||
event.preventDefault(); |
||||
event.stopPropagation(); |
||||
|
||||
Template.apps.onRendered(() => { |
||||
Tracker.afterFlush(() => { |
||||
SideNav.setFlex('adminFlex'); |
||||
SideNav.openFlex(); |
||||
}); |
||||
if (!await checkCloudLogin()) { |
||||
return; |
||||
} |
||||
|
||||
const { currentTarget: button } = event; |
||||
const app = instance.state.get('apps').find(({ id }) => id === button.dataset.id); |
||||
|
||||
instance.startAppWorking(app.id); |
||||
|
||||
try { |
||||
const { status } = await Apps.installApp(app.id, app.marketplaceVersion); |
||||
warnStatusChange(app.name, status); |
||||
} catch (error) { |
||||
handleAPIError(error); |
||||
} finally { |
||||
instance.stopAppWorking(app.id); |
||||
} |
||||
}, |
||||
async 'click .js-purchase'(event, instance) { |
||||
event.preventDefault(); |
||||
event.stopPropagation(); |
||||
|
||||
if (!await checkCloudLogin()) { |
||||
return; |
||||
} |
||||
|
||||
const { currentTarget: button } = event; |
||||
const app = instance.state.get('apps').find(({ id }) => id === button.dataset.id); |
||||
|
||||
instance.startAppWorking(app.id); |
||||
|
||||
await promptSubscription(app, async () => { |
||||
try { |
||||
const { status } = await Apps.installApp(app.id, app.marketplaceVersion); |
||||
warnStatusChange(app.name, status); |
||||
} catch (error) { |
||||
handleAPIError(error); |
||||
} finally { |
||||
instance.stopAppWorking(app.id); |
||||
} |
||||
}, instance.stopAppWorking.bind(instance, app.id)); |
||||
}, |
||||
'click .js-menu'(event, instance) { |
||||
event.stopPropagation(); |
||||
const { currentTarget } = event; |
||||
|
||||
const app = instance.state.get('apps').find(({ id }) => id === currentTarget.dataset.id); |
||||
triggerAppPopoverMenu(app, currentTarget, instance); |
||||
}, |
||||
}); |
||||
|
@ -0,0 +1,371 @@ |
||||
import { AppStatus } from '@rocket.chat/apps-engine/definition/AppStatus'; |
||||
import { FlowRouter } from 'meteor/kadira:flow-router'; |
||||
import semver from 'semver'; |
||||
import toastr from 'toastr'; |
||||
|
||||
import { modal, popover, call } from '../../../ui-utils/client'; |
||||
import { t } from '../../../utils/client'; |
||||
import { Apps } from '../orchestrator'; |
||||
|
||||
const appEnabledStatuses = [ |
||||
AppStatus.AUTO_ENABLED, |
||||
AppStatus.MANUALLY_ENABLED, |
||||
]; |
||||
|
||||
const appErroredStatuses = [ |
||||
AppStatus.COMPILER_ERROR_DISABLED, |
||||
AppStatus.ERROR_DISABLED, |
||||
AppStatus.INVALID_SETTINGS_DISABLED, |
||||
AppStatus.INVALID_LICENSE_DISABLED, |
||||
]; |
||||
|
||||
export const handleAPIError = (error) => { |
||||
console.error(error); |
||||
const message = (error.xhr && error.xhr.responseJSON && error.xhr.responseJSON.error) || error.message; |
||||
toastr.error(message); |
||||
}; |
||||
|
||||
export const warnStatusChange = (appName, status) => { |
||||
if (appErroredStatuses.includes(status)) { |
||||
toastr.error(t(`App_status_${ status }`), appName); |
||||
return; |
||||
} |
||||
|
||||
toastr.info(t(`App_status_${ status }`), appName); |
||||
}; |
||||
|
||||
const promptCloudLogin = () => { |
||||
modal.open({ |
||||
title: t('Apps_Marketplace_Login_Required_Title'), |
||||
text: t('Apps_Marketplace_Login_Required_Description'), |
||||
type: 'info', |
||||
showCancelButton: true, |
||||
confirmButtonColor: '#DD6B55', |
||||
confirmButtonText: t('Login'), |
||||
cancelButtonText: t('Cancel'), |
||||
closeOnConfirm: true, |
||||
html: false, |
||||
}, (confirmed) => { |
||||
if (confirmed) { |
||||
FlowRouter.go('cloud-config'); |
||||
} |
||||
}); |
||||
}; |
||||
|
||||
export const checkCloudLogin = async () => { |
||||
try { |
||||
const isLoggedIn = await call('cloud:checkUserLoggedIn'); |
||||
|
||||
if (!isLoggedIn) { |
||||
promptCloudLogin(); |
||||
} |
||||
|
||||
return isLoggedIn; |
||||
} catch (error) { |
||||
handleAPIError(error); |
||||
return false; |
||||
} |
||||
}; |
||||
|
||||
export const promptSubscription = async (app, callback, cancelCallback) => { |
||||
let data = null; |
||||
try { |
||||
data = await Apps.buildExternalUrl(app.id, app.purchaseType, false); |
||||
} catch (error) { |
||||
handleAPIError(error); |
||||
cancelCallback(); |
||||
return; |
||||
} |
||||
|
||||
modal.open({ |
||||
allowOutsideClick: false, |
||||
data, |
||||
template: 'iframeModal', |
||||
}, callback, cancelCallback); |
||||
}; |
||||
|
||||
const promptModifySubscription = async ({ id, purchaseType }) => { |
||||
if (!await checkCloudLogin()) { |
||||
return; |
||||
} |
||||
|
||||
let data = null; |
||||
try { |
||||
data = await Apps.buildExternalUrl(id, purchaseType, true); |
||||
} catch (error) { |
||||
handleAPIError(error); |
||||
return; |
||||
} |
||||
|
||||
await new Promise((resolve) => { |
||||
modal.open({ |
||||
allowOutsideClick: false, |
||||
data, |
||||
template: 'iframeModal', |
||||
}, resolve); |
||||
}); |
||||
}; |
||||
|
||||
const promptAppDeactivation = () => new Promise((resolve) => { |
||||
modal.open({ |
||||
text: t('Apps_Marketplace_Deactivate_App_Prompt'), |
||||
type: 'warning', |
||||
showCancelButton: true, |
||||
confirmButtonColor: '#DD6B55', |
||||
confirmButtonText: t('Yes'), |
||||
cancelButtonText: t('No'), |
||||
closeOnConfirm: true, |
||||
html: false, |
||||
}, resolve, () => resolve(false)); |
||||
}); |
||||
|
||||
const promptAppUninstall = () => new Promise((resolve) => { |
||||
modal.open({ |
||||
text: t('Apps_Marketplace_Uninstall_App_Prompt'), |
||||
type: 'warning', |
||||
showCancelButton: true, |
||||
confirmButtonColor: '#DD6B55', |
||||
confirmButtonText: t('Yes'), |
||||
cancelButtonText: t('No'), |
||||
closeOnConfirm: true, |
||||
html: false, |
||||
}, resolve, () => resolve(false)); |
||||
}); |
||||
|
||||
const promptSubscribedAppUninstall = () => new Promise((resolve) => { |
||||
modal.open({ |
||||
text: t('Apps_Marketplace_Uninstall_Subscribed_App_Prompt'), |
||||
type: 'info', |
||||
showCancelButton: true, |
||||
confirmButtonText: t('Apps_Marketplace_Modify_App_Subscription'), |
||||
cancelButtonText: t('Apps_Marketplace_Uninstall_Subscribed_App_Anyway'), |
||||
closeOnConfirm: true, |
||||
html: false, |
||||
}, resolve, () => resolve(false)); |
||||
}); |
||||
|
||||
export const triggerAppPopoverMenu = (app, currentTarget, instance) => { |
||||
if (!app) { |
||||
return; |
||||
} |
||||
|
||||
const canAppBeSubscribed = app.purchaseType === 'subscription'; |
||||
const isSubscribed = app.subscriptionInfo && ['active', 'trialing'].includes(app.subscriptionInfo.status); |
||||
const isAppEnabled = appEnabledStatuses.includes(app.status); |
||||
|
||||
const handleSubscription = async () => { |
||||
await promptModifySubscription(app); |
||||
try { |
||||
await Apps.syncApp(app.id); |
||||
} catch (error) { |
||||
handleAPIError(error); |
||||
} |
||||
}; |
||||
|
||||
const handleViewLogs = () => { |
||||
FlowRouter.go('app-logs', { appId: app.id }, { version: app.version }); |
||||
}; |
||||
|
||||
const handleDisable = async () => { |
||||
if (!await promptAppDeactivation()) { |
||||
return; |
||||
} |
||||
|
||||
try { |
||||
const effectiveStatus = await Apps.disableApp(app.id); |
||||
warnStatusChange(app.name, effectiveStatus); |
||||
} catch (error) { |
||||
handleAPIError(error); |
||||
} |
||||
}; |
||||
|
||||
const handleEnable = async () => { |
||||
try { |
||||
const effectiveStatus = await Apps.enableApp(app.id); |
||||
warnStatusChange(app.name, effectiveStatus); |
||||
} catch (error) { |
||||
handleAPIError(error); |
||||
} |
||||
}; |
||||
|
||||
const handleUninstall = async () => { |
||||
if (isSubscribed) { |
||||
const modifySubscription = await promptSubscribedAppUninstall(); |
||||
if (modifySubscription) { |
||||
await promptModifySubscription(app); |
||||
try { |
||||
await Apps.syncApp(app.id); |
||||
} catch (error) { |
||||
handleAPIError(error); |
||||
} |
||||
return; |
||||
} |
||||
} |
||||
|
||||
if (!await promptAppUninstall()) { |
||||
return; |
||||
} |
||||
try { |
||||
await Apps.uninstallApp(app.id); |
||||
} catch (error) { |
||||
handleAPIError(error); |
||||
} |
||||
}; |
||||
|
||||
popover.open({ |
||||
currentTarget, |
||||
instance, |
||||
columns: [{ |
||||
groups: [ |
||||
{ |
||||
items: [ |
||||
...canAppBeSubscribed ? [{ |
||||
icon: 'card', |
||||
name: t('Subscription'), |
||||
action: handleSubscription, |
||||
}] : [], |
||||
{ |
||||
icon: 'list-alt', |
||||
name: t('View_Logs'), |
||||
action: handleViewLogs, |
||||
}, |
||||
], |
||||
}, |
||||
{ |
||||
items: [ |
||||
isAppEnabled |
||||
? { |
||||
icon: 'ban', |
||||
name: t('Disable'), |
||||
modifier: 'alert', |
||||
action: handleDisable, |
||||
} |
||||
: { |
||||
icon: 'check', |
||||
name: t('Enable'), |
||||
action: handleEnable, |
||||
}, |
||||
{ |
||||
icon: 'trash', |
||||
name: t('Uninstall'), |
||||
modifier: 'alert', |
||||
action: handleUninstall, |
||||
}, |
||||
], |
||||
}, |
||||
], |
||||
}], |
||||
}); |
||||
}; |
||||
|
||||
export const appButtonProps = ({ |
||||
installed, |
||||
version, |
||||
marketplaceVersion, |
||||
isPurchased, |
||||
price, |
||||
purchaseType, |
||||
subscriptionInfo, |
||||
}) => { |
||||
const canUpdate = installed |
||||
&& version && marketplaceVersion |
||||
&& semver.lt(version, marketplaceVersion) |
||||
&& isPurchased; |
||||
if (canUpdate) { |
||||
return { |
||||
action: 'update', |
||||
icon: 'reload', |
||||
label: 'Update', |
||||
}; |
||||
} |
||||
|
||||
if (installed) { |
||||
return; |
||||
} |
||||
|
||||
const canDownload = isPurchased; |
||||
if (canDownload) { |
||||
return { |
||||
action: 'install', |
||||
label: 'Install', |
||||
}; |
||||
} |
||||
|
||||
const canTrial = purchaseType === 'subscription' && !subscriptionInfo.status; |
||||
if (canTrial) { |
||||
return { |
||||
action: 'purchase', |
||||
label: 'Trial', |
||||
}; |
||||
} |
||||
|
||||
const canBuy = price > 0; |
||||
if (canBuy) { |
||||
return { |
||||
action: 'purchase', |
||||
label: 'Buy', |
||||
}; |
||||
} |
||||
|
||||
return { |
||||
action: 'purchase', |
||||
label: 'Install', |
||||
}; |
||||
}; |
||||
|
||||
export const appStatusSpanProps = ({ |
||||
installed, |
||||
status, |
||||
subscriptionInfo, |
||||
}) => { |
||||
if (!installed) { |
||||
return; |
||||
} |
||||
|
||||
const isFailed = appErroredStatuses.includes(status); |
||||
if (isFailed) { |
||||
return { |
||||
type: 'failed', |
||||
icon: 'warning', |
||||
label: 'Failed', |
||||
}; |
||||
} |
||||
|
||||
const isEnabled = appEnabledStatuses.includes(status); |
||||
if (!isEnabled) { |
||||
return { |
||||
type: 'warning', |
||||
icon: 'warning', |
||||
label: 'Disabled', |
||||
}; |
||||
} |
||||
|
||||
const isOnTrialPeriod = subscriptionInfo && subscriptionInfo.status === 'trialing'; |
||||
if (isOnTrialPeriod) { |
||||
return { |
||||
icon: 'checkmark-circled', |
||||
label: 'Trial period', |
||||
}; |
||||
} |
||||
|
||||
return { |
||||
icon: 'checkmark-circled', |
||||
label: 'Enabled', |
||||
}; |
||||
}; |
||||
|
||||
export const formatPrice = (price) => `\$${ Number.parseFloat(price).toFixed(2) }`; |
||||
|
||||
export const formatPricingPlan = (pricingPlan) => { |
||||
const perUser = pricingPlan.isPerSeat && pricingPlan.tiers && pricingPlan.tiers.length; |
||||
|
||||
const pricingPlanTranslationString = [ |
||||
'Apps_Marketplace_pricingPlan', |
||||
pricingPlan.strategy, |
||||
perUser && 'perUser', |
||||
].filter(Boolean).join('_'); |
||||
|
||||
return t(pricingPlanTranslationString, { |
||||
price: formatPrice(pricingPlan.price), |
||||
}); |
||||
}; |
@ -1,68 +0,0 @@ |
||||
.rc-apps-marketplace { |
||||
&__app-status, |
||||
&__app-button, |
||||
&__app-menu-trigger { |
||||
display: flex; |
||||
flex: 1; |
||||
|
||||
padding: 0; |
||||
|
||||
font-size: 0.875rem; |
||||
line-height: 1.25rem; |
||||
align-items: center; |
||||
appearance: none; |
||||
} |
||||
|
||||
&__app-menu-trigger, |
||||
&__app-button { |
||||
position: relative; |
||||
|
||||
&:active { |
||||
transform: translateY(2px); |
||||
|
||||
opacity: 0.9; |
||||
} |
||||
|
||||
&:active::before { |
||||
top: -2px; |
||||
} |
||||
|
||||
&::before { |
||||
position: absolute; |
||||
top: 0; |
||||
right: 0; |
||||
bottom: 0; |
||||
left: 0; |
||||
|
||||
content: ""; |
||||
cursor: pointer; |
||||
} |
||||
} |
||||
|
||||
&__app-button { |
||||
color: var(--rc-color-button-primary); |
||||
|
||||
&.loading { |
||||
opacity: 0.6; |
||||
|
||||
& > .rc-icon { |
||||
animation: spin 1s linear infinite; |
||||
} |
||||
} |
||||
} |
||||
|
||||
&__app-menu-trigger { |
||||
visibility: hidden; |
||||
flex: 0 0 auto; |
||||
} |
||||
|
||||
.rc-table-content .rc-table { |
||||
.rc-table-tr:hover .rc-apps-marketplace__app-menu-trigger { |
||||
visibility: visible; |
||||
} |
||||
|
||||
.rc-table-td--small:last-child { |
||||
width: 150px; |
||||
} |
||||
} |
||||
} |
@ -1,540 +1,333 @@ |
||||
import toastr from 'toastr'; |
||||
import { Meteor } from 'meteor/meteor'; |
||||
import { ReactiveVar } from 'meteor/reactive-var'; |
||||
import { FlowRouter } from 'meteor/kadira:flow-router'; |
||||
import { ReactiveDict } from 'meteor/reactive-dict'; |
||||
import { Template } from 'meteor/templating'; |
||||
import { Tracker } from 'meteor/tracker'; |
||||
import semver from 'semver'; |
||||
|
||||
import { settings } from '../../../settings'; |
||||
import { t, APIClient } from '../../../utils'; |
||||
import { modal } from '../../../ui-utils'; |
||||
import { SideNav, call } from '../../../ui-utils/client'; |
||||
import { t } from '../../../utils'; |
||||
import { AppEvents } from '../communication'; |
||||
import { Apps } from '../orchestrator'; |
||||
import { SideNav, popover } from '../../../ui-utils/client'; |
||||
import { |
||||
appButtonProps, |
||||
appStatusSpanProps, |
||||
checkCloudLogin, |
||||
formatPrice, |
||||
formatPricingPlan, |
||||
handleAPIError, |
||||
promptSubscription, |
||||
triggerAppPopoverMenu, |
||||
warnStatusChange, |
||||
} from './helpers'; |
||||
|
||||
import './marketplace.html'; |
||||
import './marketplace.css'; |
||||
|
||||
const ENABLED_STATUS = ['auto_enabled', 'manually_enabled']; |
||||
const enabled = ({ status }) => ENABLED_STATUS.includes(status); |
||||
|
||||
const sortByColumn = (array, column, inverted) => |
||||
array.sort((a, b) => { |
||||
if (a.latest[column] < b.latest[column] && !inverted) { |
||||
return -1; |
||||
} |
||||
return 1; |
||||
Template.marketplace.onCreated(function() { |
||||
this.state = new ReactiveDict({ |
||||
isLoggedInCloud: true, |
||||
apps: [], // TODO: maybe use another ReactiveDict here
|
||||
isLoading: true, |
||||
searchText: '', |
||||
sortedColumn: 'name', |
||||
isAscendingOrder: true, |
||||
|
||||
// TODO: to use these fields
|
||||
page: 0, |
||||
itemsPerPage: 0, |
||||
wasEndReached: false, |
||||
}); |
||||
|
||||
const getCloudLoggedIn = async (instance) => { |
||||
Meteor.call('cloud:checkUserLoggedIn', (error, result) => { |
||||
if (error) { |
||||
console.warn(error); |
||||
return; |
||||
} |
||||
|
||||
instance.cloudLoggedIn.set(result); |
||||
}); |
||||
}; |
||||
|
||||
const handleAPIError = (e, instance) => { |
||||
console.error(e); |
||||
const errMsg = (e.xhr.responseJSON && e.xhr.responseJSON.error) || e.message; |
||||
toastr.error(errMsg); |
||||
|
||||
if (errMsg === 'Unauthorized') { |
||||
getCloudLoggedIn(instance); |
||||
} |
||||
}; |
||||
|
||||
const getApps = async (instance) => { |
||||
instance.isLoading.set(true); |
||||
|
||||
try { |
||||
const data = await APIClient.get('apps?marketplace=true'); |
||||
|
||||
instance.apps.set(data); |
||||
} catch (e) { |
||||
handleAPIError(e, instance); |
||||
} |
||||
|
||||
instance.isLoading.set(false); |
||||
instance.ready.set(true); |
||||
}; |
||||
|
||||
const getInstalledApps = async (instance) => { |
||||
try { |
||||
const data = await APIClient.get('apps'); |
||||
const apps = data.apps.map((app) => ({ latest: app })); |
||||
instance.installedApps.set(apps); |
||||
} catch (e) { |
||||
handleAPIError(e, instance); |
||||
} |
||||
}; |
||||
|
||||
const formatPrice = (price) => `\$${ Number.parseFloat(price).toFixed(2) }`; |
||||
|
||||
const formatPricingPlan = (pricingPlan) => { |
||||
const perUser = pricingPlan.isPerSeat && pricingPlan.tiers && pricingPlan.tiers.length; |
||||
|
||||
const pricingPlanTranslationString = [ |
||||
'Apps_Marketplace_pricingPlan', |
||||
pricingPlan.strategy, |
||||
perUser && 'perUser', |
||||
].filter(Boolean).join('_'); |
||||
|
||||
return t(pricingPlanTranslationString, { |
||||
price: formatPrice(pricingPlan.price), |
||||
}); |
||||
}; |
||||
|
||||
const isLoggedInCloud = (instance) => { |
||||
if (instance.cloudLoggedIn.get()) { |
||||
return true; |
||||
} |
||||
|
||||
modal.open({ |
||||
title: t('Apps_Marketplace_Login_Required_Title'), |
||||
text: t('Apps_Marketplace_Login_Required_Description'), |
||||
type: 'info', |
||||
showCancelButton: true, |
||||
confirmButtonColor: '#DD6B55', |
||||
confirmButtonText: t('Login'), |
||||
cancelButtonText: t('Cancel'), |
||||
closeOnConfirm: true, |
||||
html: false, |
||||
}, (confirmed) => { |
||||
if (confirmed) { |
||||
FlowRouter.go('/admin/cloud'); |
||||
(async () => { |
||||
try { |
||||
this.state.set('isLoggedInCloud', await call('cloud:checkUserLoggedIn')); |
||||
} catch (error) { |
||||
handleAPIError(error); |
||||
} |
||||
}); |
||||
|
||||
return false; |
||||
}; |
||||
try { |
||||
const appsFromMarketplace = await Apps.getAppsFromMarketplace(); |
||||
const installedApps = await Apps.getApps(); |
||||
|
||||
const apps = appsFromMarketplace.map((app) => { |
||||
const installedApp = installedApps.find(({ id }) => id === app.id); |
||||
|
||||
if (!installedApp) { |
||||
return { |
||||
...app, |
||||
status: undefined, |
||||
marketplaceVersion: app.version, |
||||
}; |
||||
} |
||||
|
||||
return { |
||||
...app, |
||||
installed: true, |
||||
status: installedApp.status, |
||||
version: installedApp.version, |
||||
marketplaceVersion: app.version, |
||||
}; |
||||
}); |
||||
|
||||
const triggerButtonLoadingState = (button) => { |
||||
const icon = button.querySelector('.rc-icon use'); |
||||
const iconHref = icon.getAttribute('href'); |
||||
this.state.set('apps', apps); |
||||
} catch (error) { |
||||
handleAPIError(error); |
||||
} finally { |
||||
this.state.set('isLoading', false); |
||||
} |
||||
})(); |
||||
|
||||
button.classList.add('loading'); |
||||
button.disabled = true; |
||||
icon.setAttribute('href', '#icon-loading'); |
||||
this.startAppWorking = (appId) => { |
||||
const apps = this.state.get('apps'); |
||||
const app = apps.find(({ id }) => id === appId); |
||||
app.working = true; |
||||
this.state.set('apps', apps); |
||||
}; |
||||
|
||||
return () => { |
||||
button.classList.remove('loading'); |
||||
button.disabled = false; |
||||
icon.setAttribute('href', iconHref); |
||||
this.stopAppWorking = (appId) => { |
||||
const apps = this.state.get('apps'); |
||||
const app = apps.find(({ id }) => id === appId); |
||||
delete app.working; |
||||
this.state.set('apps', apps); |
||||
}; |
||||
}; |
||||
|
||||
const promptSubscription = async ({ latest, purchaseType = 'buy' }, instance) => { |
||||
let data = null; |
||||
try { |
||||
data = await APIClient.get(`apps?buildExternalUrl=true&appId=${ latest.id }&purchaseType=${ purchaseType }`); |
||||
} catch (e) { |
||||
handleAPIError(e, instance); |
||||
return; |
||||
} |
||||
|
||||
modal.open({ |
||||
allowOutsideClick: false, |
||||
data, |
||||
template: 'iframeModal', |
||||
}, async () => { |
||||
try { |
||||
await APIClient.post('apps/', { |
||||
appId: latest.id, |
||||
marketplace: true, |
||||
version: latest.version, |
||||
}); |
||||
await Promise.all([ |
||||
getInstalledApps(instance), |
||||
getApps(instance), |
||||
]); |
||||
} catch (e) { |
||||
handleAPIError(e, instance); |
||||
} |
||||
}); |
||||
}; |
||||
|
||||
const setAppStatus = async (installedApp, status, instance) => { |
||||
try { |
||||
const result = await APIClient.post(`apps/${ installedApp.latest.id }/status`, { status }); |
||||
installedApp.latest.status = result.status; |
||||
instance.installedApps.set(instance.installedApps.get()); |
||||
} catch (e) { |
||||
handleAPIError(e, instance); |
||||
} |
||||
}; |
||||
|
||||
const activateApp = (installedApp, instance) => { |
||||
if (!isLoggedInCloud(instance)) { |
||||
return; |
||||
} |
||||
|
||||
setAppStatus(installedApp, 'manually_enabled', instance); |
||||
}; |
||||
|
||||
const promptAppDeactivation = (installedApp, instance) => { |
||||
if (!isLoggedInCloud(instance)) { |
||||
return; |
||||
} |
||||
|
||||
modal.open({ |
||||
text: t('Apps_Marketplace_Deactivate_App_Prompt'), |
||||
type: 'warning', |
||||
showCancelButton: true, |
||||
confirmButtonColor: '#DD6B55', |
||||
confirmButtonText: t('Yes'), |
||||
cancelButtonText: t('No'), |
||||
closeOnConfirm: true, |
||||
html: false, |
||||
}, (confirmed) => { |
||||
if (!confirmed) { |
||||
return; |
||||
} |
||||
setAppStatus(installedApp, 'manually_disabled', instance); |
||||
}); |
||||
}; |
||||
|
||||
const uninstallApp = async (installedApp, instance) => { |
||||
try { |
||||
await APIClient.delete(`apps/${ installedApp.latest.id }`); |
||||
const installedApps = instance.installedApps.get().filter((app) => app.latest.id !== installedApp.latest.id); |
||||
instance.installedApps.set(installedApps); |
||||
} catch (e) { |
||||
handleAPIError(e, instance); |
||||
} |
||||
}; |
||||
|
||||
const promptAppUninstall = (installedApp, instance) => { |
||||
if (!isLoggedInCloud(instance)) { |
||||
return; |
||||
} |
||||
|
||||
modal.open({ |
||||
text: t('Apps_Marketplace_Uninstall_App_Prompt'), |
||||
type: 'warning', |
||||
showCancelButton: true, |
||||
confirmButtonColor: '#DD6B55', |
||||
confirmButtonText: t('Yes'), |
||||
cancelButtonText: t('No'), |
||||
closeOnConfirm: true, |
||||
html: false, |
||||
}, (confirmed) => { |
||||
if (!confirmed) { |
||||
return; |
||||
} |
||||
uninstallApp(installedApp, instance); |
||||
}); |
||||
}; |
||||
|
||||
Template.marketplace.onCreated(function() { |
||||
this.ready = new ReactiveVar(false); |
||||
this.apps = new ReactiveVar([]); |
||||
this.installedApps = new ReactiveVar([]); |
||||
this.searchText = new ReactiveVar(''); |
||||
this.searchSortBy = new ReactiveVar('name'); |
||||
this.sortDirection = new ReactiveVar('asc'); |
||||
this.limit = new ReactiveVar(0); |
||||
this.page = new ReactiveVar(0); |
||||
this.end = new ReactiveVar(false); |
||||
this.isLoading = new ReactiveVar(true); |
||||
this.cloudLoggedIn = new ReactiveVar(false); |
||||
|
||||
getInstalledApps(this); |
||||
getApps(this); |
||||
getCloudLoggedIn(this); |
||||
|
||||
this.onAppAdded = async (appId) => { |
||||
const installedApps = this.installedApps.get().filter((installedApp) => installedApp.appId !== appId); |
||||
this.handleAppAddedOrUpdated = async (appId) => { |
||||
try { |
||||
const { app } = await APIClient.get(`apps/${ appId }`); |
||||
installedApps.push({ latest: app }); |
||||
this.installedApps.set(installedApps); |
||||
} catch (e) { |
||||
handleAPIError(e, this); |
||||
const { status, version } = await Apps.getApp(appId); |
||||
const app = await Apps.getAppFromMarketplace(appId, version); |
||||
const apps = [ |
||||
...this.state.get('apps').filter(({ id }) => id !== appId), |
||||
{ |
||||
...app, |
||||
installed: true, |
||||
status, |
||||
version, |
||||
marketplaceVersion: app.version, |
||||
}, |
||||
]; |
||||
this.state.set('apps', apps); |
||||
} catch (error) { |
||||
handleAPIError(error); |
||||
} |
||||
}; |
||||
|
||||
this.onAppRemoved = (appId) => { |
||||
const apps = this.apps.get().filter(({ id }) => id !== appId); |
||||
this.apps.set(apps); |
||||
this.handleAppRemoved = (appId) => { |
||||
const apps = this.state.get('apps').map((app) => { |
||||
if (app.id === appId) { |
||||
delete app.installed; |
||||
delete app.status; |
||||
app.version = app.marketplaceVersion; |
||||
} |
||||
|
||||
return app; |
||||
}); |
||||
this.state.set('apps', apps); |
||||
}; |
||||
|
||||
this.handleAppStatusChange = ({ appId, status }) => { |
||||
const apps = this.state.get('apps'); |
||||
const app = apps.find(({ id }) => id === appId); |
||||
if (!app) { |
||||
return; |
||||
} |
||||
|
||||
app.status = status; |
||||
this.state.set('apps', apps); |
||||
}; |
||||
|
||||
Apps.getWsListener().registerListener(AppEvents.APP_ADDED, this.onAppAdded); |
||||
Apps.getWsListener().registerListener(AppEvents.APP_REMOVED, this.onAppRemoved); |
||||
Apps.getWsListener().registerListener(AppEvents.APP_ADDED, this.handleAppAddedOrUpdated); |
||||
Apps.getWsListener().registerListener(AppEvents.APP_UPDATED, this.handleAppAddedOrUpdated); |
||||
Apps.getWsListener().registerListener(AppEvents.APP_REMOVED, this.handleAppRemoved); |
||||
Apps.getWsListener().registerListener(AppEvents.APP_STATUS_CHANGE, this.handleAppStatusChange); |
||||
}); |
||||
|
||||
Template.marketplace.onDestroyed(function() { |
||||
Apps.getWsListener().unregisterListener(AppEvents.APP_ADDED, this.onAppAdded); |
||||
Apps.getWsListener().unregisterListener(AppEvents.APP_REMOVED, this.onAppRemoved); |
||||
Apps.getWsListener().unregisterListener(AppEvents.APP_ADDED, this.handleAppAddedOrUpdated); |
||||
Apps.getWsListener().unregisterListener(AppEvents.APP_UPDATED, this.handleAppAddedOrUpdated); |
||||
Apps.getWsListener().unregisterListener(AppEvents.APP_REMOVED, this.handleAppRemoved); |
||||
Apps.getWsListener().unregisterListener(AppEvents.APP_STATUS_CHANGE, this.handleAppStatusChange); |
||||
}); |
||||
|
||||
Template.marketplace.helpers({ |
||||
isReady() { |
||||
if (Template.instance().ready != null) { |
||||
return Template.instance().ready.get(); |
||||
} |
||||
|
||||
return false; |
||||
}, |
||||
apps() { |
||||
const instance = Template.instance(); |
||||
const searchText = instance.searchText.get().toLowerCase(); |
||||
const sortColumn = instance.searchSortBy.get(); |
||||
const inverted = instance.sortDirection.get() === 'desc'; |
||||
const apps = instance.apps.get().filter((app) => app.latest.name.toLowerCase().includes(searchText)); |
||||
return sortByColumn(apps, sortColumn, inverted); |
||||
}, |
||||
appsDevelopmentMode() { |
||||
return settings.get('Apps_Framework_Development_Mode') === true; |
||||
}, |
||||
cloudLoggedIn() { |
||||
return Template.instance().cloudLoggedIn.get(); |
||||
}, |
||||
parseStatus(status) { |
||||
return t(`App_status_${ status }`); |
||||
}, |
||||
isActive(status) { |
||||
return enabled({ status }); |
||||
}, |
||||
sortIcon(key) { |
||||
const { |
||||
sortDirection, |
||||
searchSortBy, |
||||
} = Template.instance(); |
||||
Template.marketplace.onRendered(() => { |
||||
Tracker.afterFlush(() => { |
||||
SideNav.setFlex('adminFlex'); |
||||
SideNav.openFlex(); |
||||
}); |
||||
}); |
||||
|
||||
return key === searchSortBy.get() && sortDirection.get() !== 'asc' ? 'sort-up' : 'sort-down'; |
||||
}, |
||||
searchSortBy(key) { |
||||
return Template.instance().searchSortBy.get() === key; |
||||
Template.marketplace.helpers({ |
||||
isLoggedInCloud() { |
||||
return Template.instance().state.get('isLoggedInCloud'); |
||||
}, |
||||
isLoading() { |
||||
return Template.instance().isLoading.get(); |
||||
return Template.instance().state.get('isLoading'); |
||||
}, |
||||
onTableScroll() { |
||||
const instance = Template.instance(); |
||||
if (instance.loading || instance.end.get()) { |
||||
handleTableScroll() { |
||||
const { state } = Template.instance(); |
||||
if (state.get('isLoading') || state.get('wasEndReached')) { |
||||
return; |
||||
} |
||||
return function(currentTarget) { |
||||
if (currentTarget.offsetHeight + currentTarget.scrollTop >= currentTarget.scrollHeight - 100) { |
||||
return instance.page.set(instance.page.get() + 1); |
||||
|
||||
return ({ offsetHeight, scrollTop, scrollHeight }) => { |
||||
const shouldGoToNextPage = offsetHeight + scrollTop >= scrollHeight - 100; |
||||
if (shouldGoToNextPage) { |
||||
return state.set('page', state.get('page') + 1); |
||||
} |
||||
}; |
||||
}, |
||||
onTableResize() { |
||||
const { limit } = Template.instance(); |
||||
handleTableResize() { |
||||
const { state } = Template.instance(); |
||||
|
||||
return function() { |
||||
limit.set(Math.ceil((this.$('.table-scroll').height() / 40) + 5)); |
||||
const $table = this.$('.table-scroll'); |
||||
state.set('itemsPerPage', Math.ceil(($table.height() / 40) + 5)); |
||||
}; |
||||
}, |
||||
onTableSort() { |
||||
const { end, page, sortDirection, searchSortBy } = Template.instance(); |
||||
return function(type) { |
||||
end.set(false); |
||||
page.set(0); |
||||
|
||||
if (searchSortBy.get() === type) { |
||||
sortDirection.set(sortDirection.get() === 'asc' ? 'desc' : 'asc'); |
||||
handleTableSort() { |
||||
const { state } = Template.instance(); |
||||
|
||||
return (sortedColumn) => { |
||||
state.set({ |
||||
page: 0, |
||||
wasEndReached: false, |
||||
}); |
||||
|
||||
if (state.get('sortedColumn') === sortedColumn) { |
||||
state.set('isAscendingOrder', !state.get('isAscendingOrder')); |
||||
return; |
||||
} |
||||
|
||||
searchSortBy.set(type); |
||||
sortDirection.set('asc'); |
||||
state.set({ |
||||
sortedColumn, |
||||
isAscendingOrder: true, |
||||
}); |
||||
}; |
||||
}, |
||||
purchaseTypeDisplay(app) { |
||||
if (app.purchaseType === 'subscription') { |
||||
isSortingBy(column) { |
||||
return Template.instance().state.get('sortedColumn') === column; |
||||
}, |
||||
sortIcon(column) { |
||||
const { state } = Template.instance(); |
||||
|
||||
return column === state.get('sortedColumn') && state.get('isAscendingOrder') ? 'sort-down' : 'sort-up'; |
||||
}, |
||||
apps() { |
||||
const { state } = Template.instance(); |
||||
const apps = state.get('apps'); |
||||
const searchText = state.get('searchText').toLocaleLowerCase(); |
||||
const sortedColumn = state.get('sortedColumn'); |
||||
const isAscendingOrder = state.get('isAscendingOrder'); |
||||
const sortingFactor = isAscendingOrder ? 1 : -1; |
||||
|
||||
return apps |
||||
.filter(({ name }) => name.toLocaleLowerCase().includes(searchText)) |
||||
.sort(({ [sortedColumn]: a }, { [sortedColumn]: b }) => sortingFactor * String(a).localeCompare(String(b))); |
||||
}, |
||||
purchaseTypeDisplay({ purchaseType, price }) { |
||||
if (purchaseType === 'subscription') { |
||||
return t('Subscription'); |
||||
} |
||||
|
||||
if (app.price > 0) { |
||||
if (price > 0) { |
||||
return t('Paid'); |
||||
} |
||||
|
||||
return t('Free'); |
||||
}, |
||||
priceDisplay(app) { |
||||
if (app.purchaseType === 'subscription') { |
||||
if (!app.pricingPlans || !Array.isArray(app.pricingPlans) || app.pricingPlans.length === 0) { |
||||
priceDisplay({ purchaseType, pricingPlans, price }) { |
||||
if (purchaseType === 'subscription') { |
||||
if (!pricingPlans || !Array.isArray(pricingPlans) || pricingPlans.length === 0) { |
||||
return '-'; |
||||
} |
||||
|
||||
return formatPricingPlan(app.pricingPlans[0]); |
||||
return formatPricingPlan(pricingPlans[0]); |
||||
} |
||||
|
||||
if (app.price > 0) { |
||||
return formatPrice(app.price); |
||||
if (price > 0) { |
||||
return formatPrice(price); |
||||
} |
||||
|
||||
return '-'; |
||||
}, |
||||
isInstalled(app) { |
||||
const { installedApps } = Template.instance(); |
||||
const installedApp = installedApps.get().find(({ latest: { id } }) => id === app.latest.id); |
||||
return !!installedApp; |
||||
}, |
||||
isOnTrialPeriod(app) { |
||||
return app.subscriptionInfo.status === 'trialing'; |
||||
}, |
||||
canUpdate(app) { |
||||
const { installedApps } = Template.instance(); |
||||
const installedApp = installedApps.get().find(({ latest: { id } }) => id === app.latest.id); |
||||
return !!installedApp && semver.lt(installedApp.latest.version, app.latest.version); |
||||
}, |
||||
canTrial(app) { |
||||
return app.purchaseType === 'subscription' && !app.subscriptionInfo.status; |
||||
}, |
||||
canBuy(app) { |
||||
return app.price > 0; |
||||
}, |
||||
appButtonProps, |
||||
appStatusSpanProps, |
||||
}); |
||||
|
||||
Template.marketplace.events({ |
||||
'click [data-button="install"]'() { |
||||
FlowRouter.go('/admin/app/install'); |
||||
'click .js-cloud-login'() { |
||||
FlowRouter.go('cloud-config'); |
||||
}, |
||||
'click [data-button="login"]'() { |
||||
FlowRouter.go('/admin/cloud'); |
||||
'submit .js-search-form'(event) { |
||||
event.stopPropagation(); |
||||
return false; |
||||
}, |
||||
'keyup .js-search'(event, instance) { |
||||
instance.state.set('searchText', event.currentTarget.value); |
||||
}, |
||||
'click .js-open'(e) { |
||||
e.stopPropagation(); |
||||
const { latest: { id, version } } = this; |
||||
FlowRouter.go(`/admin/apps/${ id }?version=${ version }`); |
||||
'click .js-open'(event, instance) { |
||||
event.stopPropagation(); |
||||
const { currentTarget } = event; |
||||
const { |
||||
id: appId, |
||||
version, |
||||
marketplaceVersion, |
||||
} = instance.state.get('apps').find(({ id }) => id === currentTarget.dataset.id); |
||||
FlowRouter.go('marketplace-app', { appId }, { version: version || marketplaceVersion }); |
||||
}, |
||||
async 'click .js-install'(e, instance) { |
||||
e.stopPropagation(); |
||||
async 'click .js-install, click .js-update'(event, instance) { |
||||
event.preventDefault(); |
||||
event.stopPropagation(); |
||||
|
||||
if (!isLoggedInCloud(instance)) { |
||||
const isLoggedInCloud = await checkCloudLogin(); |
||||
instance.state.set('isLoggedInCloud', isLoggedInCloud); |
||||
if (!isLoggedInCloud) { |
||||
return; |
||||
} |
||||
|
||||
const { currentTarget: button } = e; |
||||
const stopLoading = triggerButtonLoadingState(button); |
||||
const { currentTarget: button } = event; |
||||
const app = instance.state.get('apps').find(({ id }) => id === button.dataset.id); |
||||
|
||||
const { latest } = this; |
||||
instance.startAppWorking(app.id); |
||||
|
||||
try { |
||||
await APIClient.post('apps/', { |
||||
appId: latest.id, |
||||
marketplace: true, |
||||
version: latest.version, |
||||
}); |
||||
await Promise.all([ |
||||
getInstalledApps(instance), |
||||
getApps(instance), |
||||
]); |
||||
} catch (e) { |
||||
handleAPIError(e, instance); |
||||
const { status } = await Apps.installApp(app.id, app.marketplaceVersion); |
||||
warnStatusChange(app.name, status); |
||||
} catch (error) { |
||||
handleAPIError(error); |
||||
} finally { |
||||
stopLoading(); |
||||
instance.stopAppWorking(app.id); |
||||
} |
||||
}, |
||||
async 'click .js-purchase'(e, instance) { |
||||
e.stopPropagation(); |
||||
async 'click .js-purchase'(event, instance) { |
||||
event.preventDefault(); |
||||
event.stopPropagation(); |
||||
|
||||
if (!isLoggedInCloud(instance)) { |
||||
const isLoggedInCloud = await checkCloudLogin(); |
||||
instance.state.set('isLoggedInCloud', isLoggedInCloud); |
||||
if (!isLoggedInCloud) { |
||||
return; |
||||
} |
||||
|
||||
const { latest, purchaseType = 'buy' } = this; |
||||
const { currentTarget: button } = e; |
||||
const stopLoading = triggerButtonLoadingState(button); |
||||
const { currentTarget: button } = event; |
||||
const app = instance.state.get('apps').find(({ id }) => id === button.dataset.id); |
||||
|
||||
let data = null; |
||||
try { |
||||
data = await APIClient.get(`apps?buildExternalUrl=true&appId=${ latest.id }&purchaseType=${ purchaseType }`); |
||||
} catch (e) { |
||||
handleAPIError(e, instance); |
||||
stopLoading(); |
||||
return; |
||||
} |
||||
instance.startAppWorking(app.id); |
||||
|
||||
modal.open({ |
||||
allowOutsideClick: false, |
||||
data, |
||||
template: 'iframeModal', |
||||
}, async () => { |
||||
await promptSubscription(app, async () => { |
||||
try { |
||||
await APIClient.post('apps/', { |
||||
appId: latest.id, |
||||
marketplace: true, |
||||
version: latest.version, |
||||
}); |
||||
await Promise.all([ |
||||
getInstalledApps(instance), |
||||
getApps(instance), |
||||
]); |
||||
} catch (e) { |
||||
handleAPIError(e, instance); |
||||
const { status } = await Apps.installApp(app.id, app.marketplaceVersion); |
||||
warnStatusChange(app.name, status); |
||||
} catch (error) { |
||||
handleAPIError(error); |
||||
} finally { |
||||
stopLoading(); |
||||
instance.stopAppWorking(app.id); |
||||
} |
||||
}, stopLoading); |
||||
}, instance.stopAppWorking.bind(instance, app.id)); |
||||
}, |
||||
'click .js-menu'(e, instance) { |
||||
e.stopPropagation(); |
||||
const { currentTarget } = e; |
||||
|
||||
const installedApp = instance.installedApps.get().find(({ latest: { id } }) => id === this.latest.id); |
||||
const isActive = installedApp && ['auto_enabled', 'manually_enabled'].includes(installedApp.latest.status); |
||||
|
||||
popover.open({ |
||||
currentTarget, |
||||
instance, |
||||
columns: [{ |
||||
groups: [ |
||||
...this.purchaseType === 'subscription' ? [{ |
||||
items: [ |
||||
{ |
||||
icon: 'card', |
||||
name: t('Subscription'), |
||||
action: () => promptSubscription(this, instance), |
||||
}, |
||||
], |
||||
}] : [], |
||||
{ |
||||
items: [ |
||||
isActive |
||||
? { |
||||
icon: 'ban', |
||||
name: t('Deactivate'), |
||||
modifier: 'alert', |
||||
action: () => promptAppDeactivation(installedApp, instance), |
||||
} |
||||
: { |
||||
icon: 'check', |
||||
name: t('Activate'), |
||||
action: () => activateApp(installedApp, instance), |
||||
}, |
||||
{ |
||||
icon: 'trash', |
||||
name: t('Uninstall'), |
||||
modifier: 'alert', |
||||
action: () => promptAppUninstall(installedApp, instance), |
||||
}, |
||||
], |
||||
}, |
||||
], |
||||
}], |
||||
}); |
||||
}, |
||||
'keyup .js-search'(e, t) { |
||||
t.searchText.set(e.currentTarget.value); |
||||
}, |
||||
'submit .js-search-form'(e) { |
||||
e.preventDefault(); |
||||
e.stopPropagation(); |
||||
}, |
||||
}); |
||||
'click .js-menu'(event, instance) { |
||||
event.stopPropagation(); |
||||
const { currentTarget } = event; |
||||
|
||||
Template.marketplace.onRendered(() => { |
||||
Tracker.afterFlush(() => { |
||||
SideNav.setFlex('adminFlex'); |
||||
SideNav.openFlex(); |
||||
}); |
||||
const app = instance.state.get('apps').find(({ id }) => id === currentTarget.dataset.id); |
||||
triggerAppPopoverMenu(app, currentTarget, instance); |
||||
}, |
||||
}); |
||||
|
@ -1,3 +1 @@ |
||||
import { AppWebsocketReceiver, AppEvents } from './websockets'; |
||||
|
||||
export { AppWebsocketReceiver, AppEvents }; |
||||
export { AppWebsocketReceiver, AppEvents } from './websockets'; |
||||
|
@ -0,0 +1,38 @@ |
||||
import { TAPi18next } from 'meteor/tap:i18n'; |
||||
|
||||
import { Apps } from './orchestrator'; |
||||
import { Utilities } from '../lib/misc/Utilities'; |
||||
import { AppEvents } from './communication'; |
||||
|
||||
|
||||
export const loadAppI18nResources = (appId, languages) => { |
||||
Object.entries(languages).forEach(([language, translations]) => { |
||||
try { |
||||
// Translations keys must be scoped under app id
|
||||
const scopedTranslations = Object.entries(translations) |
||||
.reduce((translations, [key, value]) => { |
||||
translations[Utilities.getI18nKeyForApp(key, appId)] = value; |
||||
return translations; |
||||
}, {}); |
||||
|
||||
TAPi18next.addResourceBundle(language, 'project', scopedTranslations); |
||||
} catch (error) { |
||||
Apps.handleError(error); |
||||
} |
||||
}); |
||||
}; |
||||
|
||||
const handleAppAdded = async (appId) => { |
||||
const languages = await Apps.getAppLanguages(appId); |
||||
loadAppI18nResources(appId, languages); |
||||
}; |
||||
|
||||
export const handleI18nResources = async () => { |
||||
const apps = await Apps.getAppsLanguages(); |
||||
apps.forEach(({ id, languages }) => { |
||||
loadAppI18nResources(id, languages); |
||||
}); |
||||
|
||||
Apps.getWsListener().unregisterListener(AppEvents.APP_ADDED, handleAppAdded); |
||||
Apps.getWsListener().registerListener(AppEvents.APP_ADDED, handleAppAdded); |
||||
}; |
@ -1,189 +1,188 @@ |
||||
import { Meteor } from 'meteor/meteor'; |
||||
import { FlowRouter } from 'meteor/kadira:flow-router'; |
||||
import { BlazeLayout } from 'meteor/kadira:blaze-layout'; |
||||
import { TAPi18next } from 'meteor/tap:i18n'; |
||||
import toastr from 'toastr'; |
||||
|
||||
import { AppWebsocketReceiver } from './communication'; |
||||
import { Utilities } from '../lib/misc/Utilities'; |
||||
import { APIClient } from '../../utils'; |
||||
import { AdminBox } from '../../ui-utils'; |
||||
import { CachedCollectionManager } from '../../ui-cached-collection'; |
||||
import { hasAtLeastOnePermission } from '../../authorization'; |
||||
import { handleI18nResources } from './i18n'; |
||||
|
||||
const createDeferredValue = () => { |
||||
let resolve; |
||||
let reject; |
||||
const promise = new Promise((_resolve, _reject) => { |
||||
resolve = _resolve; |
||||
reject = _reject; |
||||
}); |
||||
|
||||
export let Apps; |
||||
return [promise, resolve, reject]; |
||||
}; |
||||
|
||||
class AppClientOrchestrator { |
||||
constructor() { |
||||
this._isLoaded = false; |
||||
this._isEnabled = false; |
||||
this._loadingResolve; |
||||
this._refreshLoading(); |
||||
} |
||||
|
||||
isLoaded() { |
||||
return this._isLoaded; |
||||
this.isLoaded = false; |
||||
[this.deferredIsEnabled, this.setEnabled] = createDeferredValue(); |
||||
} |
||||
|
||||
isEnabled() { |
||||
return this._isEnabled; |
||||
} |
||||
|
||||
getLoadingPromise() { |
||||
if (this._isLoaded) { |
||||
return Promise.resolve(this._isEnabled); |
||||
load = async (isEnabled) => { |
||||
if (!this.isLoaded) { |
||||
this.ws = new AppWebsocketReceiver(); |
||||
this.registerAdminMenuItems(); |
||||
this.isLoaded = true; |
||||
} |
||||
|
||||
return this._loadingPromise; |
||||
} |
||||
|
||||
load(isEnabled) { |
||||
this._isEnabled = isEnabled; |
||||
this.setEnabled(isEnabled); |
||||
|
||||
// It was already loaded, so let's load it again
|
||||
if (this._isLoaded) { |
||||
this._refreshLoading(); |
||||
} else { |
||||
this.ws = new AppWebsocketReceiver(this); |
||||
this._addAdminMenuOption(); |
||||
} |
||||
|
||||
Meteor.defer(() => { |
||||
this._loadLanguages().then(() => { |
||||
this._loadingResolve(this._isEnabled); |
||||
this._isLoaded = true; |
||||
}); |
||||
}); |
||||
} |
||||
// Since the deferred value (a promise) is immutable after resolved,
|
||||
// it need to be recreated to resolve a new value
|
||||
[this.deferredIsEnabled, this.setEnabled] = createDeferredValue(); |
||||
|
||||
getWsListener() { |
||||
return this.ws; |
||||
await handleI18nResources(); |
||||
this.setEnabled(isEnabled); |
||||
} |
||||
|
||||
_refreshLoading() { |
||||
this._loadingPromise = new Promise((resolve) => { |
||||
this._loadingResolve = resolve; |
||||
}); |
||||
} |
||||
getWsListener = () => this.ws |
||||
|
||||
_addAdminMenuOption() { |
||||
registerAdminMenuItems = () => { |
||||
AdminBox.addOption({ |
||||
icon: 'cube', |
||||
href: 'apps', |
||||
i18nLabel: 'Apps', |
||||
permissionGranted() { |
||||
return hasAtLeastOnePermission(['manage-apps']); |
||||
}, |
||||
permissionGranted: () => hasAtLeastOnePermission(['manage-apps']), |
||||
}); |
||||
|
||||
AdminBox.addOption({ |
||||
icon: 'cube', |
||||
href: 'marketplace', |
||||
i18nLabel: 'Marketplace', |
||||
permissionGranted() { |
||||
return hasAtLeastOnePermission(['manage-apps']); |
||||
}, |
||||
permissionGranted: () => hasAtLeastOnePermission(['manage-apps']), |
||||
}); |
||||
} |
||||
|
||||
_loadLanguages() { |
||||
return APIClient.get('apps/languages').then((info) => { |
||||
info.apps.forEach((rlInfo) => this.parseAndLoadLanguages(rlInfo.languages, rlInfo.id)); |
||||
}); |
||||
handleError = (error) => { |
||||
console.error(error); |
||||
if (hasAtLeastOnePermission(['manage-apps'])) { |
||||
toastr.error(error.message); |
||||
} |
||||
} |
||||
|
||||
parseAndLoadLanguages(languages, id) { |
||||
Object.entries(languages).forEach(([language, translations]) => { |
||||
try { |
||||
translations = Object.entries(translations).reduce((newTranslations, [key, value]) => { |
||||
newTranslations[Utilities.getI18nKeyForApp(key, id)] = value; |
||||
return newTranslations; |
||||
}, {}); |
||||
isEnabled = () => this.deferredIsEnabled |
||||
|
||||
TAPi18next.addResourceBundle(language, 'project', translations); |
||||
} catch (e) { |
||||
// Failed to parse the json
|
||||
} |
||||
getApps = async () => { |
||||
const { apps } = await APIClient.get('apps'); |
||||
return apps; |
||||
} |
||||
|
||||
getAppsFromMarketplace = async () => { |
||||
const appsOverviews = await APIClient.get('apps', { marketplace: 'true' }); |
||||
return appsOverviews.map(({ latest, price, pricingPlans, purchaseType }) => ({ |
||||
...latest, |
||||
price, |
||||
pricingPlans, |
||||
purchaseType, |
||||
})); |
||||
} |
||||
|
||||
getAppsOnBundle = async (bundleId) => { |
||||
const { apps } = await APIClient.get(`apps/bundles/${ bundleId }/apps`); |
||||
return apps; |
||||
} |
||||
|
||||
getAppsLanguages = async () => { |
||||
const { apps } = await APIClient.get('apps/languages'); |
||||
return apps; |
||||
} |
||||
|
||||
getApp = async (appId) => { |
||||
const { app } = await APIClient.get(`apps/${ appId }`); |
||||
return app; |
||||
} |
||||
|
||||
getAppFromMarketplace = async (appId, version) => { |
||||
const { app } = await APIClient.get(`apps/${ appId }`, { |
||||
marketplace: 'true', |
||||
version, |
||||
}); |
||||
return app; |
||||
} |
||||
|
||||
async getAppApis(appId) { |
||||
const result = await APIClient.get(`apps/${ appId }/apis`); |
||||
return result.apis; |
||||
getLatestAppFromMarketplace = async (appId, version) => { |
||||
const { app } = await APIClient.get(`apps/${ appId }`, { |
||||
marketplace: 'true', |
||||
update: 'true', |
||||
appVersion: version, |
||||
}); |
||||
return app; |
||||
} |
||||
|
||||
getAppSettings = async (appId) => { |
||||
const { settings } = await APIClient.get(`apps/${ appId }/settings`); |
||||
return settings; |
||||
} |
||||
} |
||||
|
||||
Meteor.startup(function _rlClientOrch() { |
||||
Apps = new AppClientOrchestrator(); |
||||
setAppSettings = async (appId, settings) => { |
||||
const { updated } = await APIClient.post(`apps/${ appId }/settings`, undefined, { settings }); |
||||
return updated; |
||||
} |
||||
|
||||
CachedCollectionManager.onLogin(() => { |
||||
Meteor.call('apps/is-enabled', (error, isEnabled) => { |
||||
Apps.load(isEnabled); |
||||
getAppApis = async (appId) => { |
||||
const { apis } = await APIClient.get(`apps/${ appId }/apis`); |
||||
return apis; |
||||
} |
||||
|
||||
getAppLanguages = async (appId) => { |
||||
const { languages } = await APIClient.get(`apps/${ appId }/languages`); |
||||
return languages; |
||||
} |
||||
|
||||
installApp = async (appId, version) => { |
||||
const { app } = await APIClient.post('apps/', { |
||||
appId, |
||||
marketplace: true, |
||||
version, |
||||
}); |
||||
}); |
||||
}); |
||||
return app; |
||||
} |
||||
|
||||
const appsRouteAction = function _theRealAction(whichCenter) { |
||||
Meteor.defer(() => Apps.getLoadingPromise().then((isEnabled) => { |
||||
if (isEnabled) { |
||||
BlazeLayout.render('main', { center: whichCenter, old: true }); // TODO remove old
|
||||
} else { |
||||
FlowRouter.go('app-what-is-it'); |
||||
} |
||||
})); |
||||
}; |
||||
uninstallApp = (appId) => APIClient.delete(`apps/${ appId }`) |
||||
|
||||
// Bah, this has to be done *before* `Meteor.startup`
|
||||
FlowRouter.route('/admin/marketplace', { |
||||
name: 'marketplace', |
||||
action() { |
||||
appsRouteAction('marketplace'); |
||||
}, |
||||
}); |
||||
syncApp = (appId) => APIClient.post(`apps/${ appId }/sync`) |
||||
|
||||
FlowRouter.route('/admin/marketplace/:itemId', { |
||||
name: 'app-manage', |
||||
action() { |
||||
appsRouteAction('appManage'); |
||||
}, |
||||
}); |
||||
setAppStatus = async (appId, status) => { |
||||
const { status: effectiveStatus } = await APIClient.post(`apps/${ appId }/status`, { status }); |
||||
return effectiveStatus; |
||||
} |
||||
|
||||
FlowRouter.route('/admin/apps', { |
||||
name: 'apps', |
||||
action() { |
||||
appsRouteAction('apps'); |
||||
}, |
||||
}); |
||||
enableApp = (appId) => this.setAppStatus(appId, 'manually_enabled') |
||||
|
||||
FlowRouter.route('/admin/app/install', { |
||||
name: 'app-install', |
||||
action() { |
||||
appsRouteAction('appInstall'); |
||||
}, |
||||
}); |
||||
disableApp = (appId) => this.setAppStatus(appId, 'manually_disabled') |
||||
|
||||
FlowRouter.route('/admin/apps/:appId', { |
||||
name: 'app-manage', |
||||
action() { |
||||
appsRouteAction('appManage'); |
||||
}, |
||||
}); |
||||
buildExternalUrl = (appId, purchaseType = 'buy', details = false) => |
||||
APIClient.get('apps', { |
||||
buildExternalUrl: 'true', |
||||
appId, |
||||
purchaseType, |
||||
details, |
||||
}) |
||||
|
||||
FlowRouter.route('/admin/apps/:appId/logs', { |
||||
name: 'app-logs', |
||||
action() { |
||||
appsRouteAction('appLogs'); |
||||
}, |
||||
}); |
||||
getCategories = async () => { |
||||
const categories = await APIClient.get('apps', { categories: 'true' }); |
||||
return categories; |
||||
} |
||||
} |
||||
|
||||
export const Apps = new AppClientOrchestrator(); |
||||
|
||||
FlowRouter.route('/admin/app/what-is-it', { |
||||
name: 'app-what-is-it', |
||||
action() { |
||||
Meteor.defer(() => Apps.getLoadingPromise().then((isEnabled) => { |
||||
if (isEnabled) { |
||||
FlowRouter.go('apps'); |
||||
} else { |
||||
BlazeLayout.render('main', { center: 'appWhatIsIt' }); |
||||
Meteor.startup(() => { |
||||
CachedCollectionManager.onLogin(() => { |
||||
Meteor.call('apps/is-enabled', (error, isEnabled) => { |
||||
if (error) { |
||||
Apps.handleError(error); |
||||
return; |
||||
} |
||||
})); |
||||
}, |
||||
|
||||
Apps.load(isEnabled); |
||||
}); |
||||
}); |
||||
}); |
||||
|
@ -0,0 +1,55 @@ |
||||
import { FlowRouter } from 'meteor/kadira:flow-router'; |
||||
import { BlazeLayout } from 'meteor/kadira:blaze-layout'; |
||||
|
||||
import { Apps } from './orchestrator'; |
||||
|
||||
FlowRouter.route('/admin/apps/what-is-it', { |
||||
name: 'apps-what-is-it', |
||||
action: async () => { |
||||
// TODO: render loading indicator
|
||||
if (await Apps.isEnabled()) { |
||||
FlowRouter.go('apps'); |
||||
} else { |
||||
BlazeLayout.render('main', { center: 'appWhatIsIt' }); |
||||
} |
||||
}, |
||||
}); |
||||
|
||||
const createAppsRouteAction = (centerTemplate) => async () => { |
||||
// TODO: render loading indicator
|
||||
if (await Apps.isEnabled()) { |
||||
BlazeLayout.render('main', { center: centerTemplate, old: true }); // TODO remove old
|
||||
} else { |
||||
FlowRouter.go('apps-what-is-it'); |
||||
} |
||||
}; |
||||
|
||||
FlowRouter.route('/admin/apps', { |
||||
name: 'apps', |
||||
action: createAppsRouteAction('apps'), |
||||
}); |
||||
|
||||
FlowRouter.route('/admin/apps/install', { |
||||
name: 'app-install', |
||||
action: createAppsRouteAction('appInstall'), |
||||
}); |
||||
|
||||
FlowRouter.route('/admin/apps/:appId', { |
||||
name: 'app-manage', |
||||
action: createAppsRouteAction('appManage'), |
||||
}); |
||||
|
||||
FlowRouter.route('/admin/apps/:appId/logs', { |
||||
name: 'app-logs', |
||||
action: createAppsRouteAction('appLogs'), |
||||
}); |
||||
|
||||
FlowRouter.route('/admin/marketplace', { |
||||
name: 'marketplace', |
||||
action: createAppsRouteAction('marketplace'), |
||||
}); |
||||
|
||||
FlowRouter.route('/admin/marketplace/:appId', { |
||||
name: 'marketplace-app', |
||||
action: createAppsRouteAction('appManage'), |
||||
}); |
Loading…
Reference in new issue