迁移合并dashboard里面的src到src2
This commit is contained in:
parent
c381a476c5
commit
0fbc2b3944
@ -1,9 +1,9 @@
|
||||
{
|
||||
"include": ["./src/**/*", "src2/components/AddressableErrorDialog.vue"],
|
||||
"include": ["./src2/**/*"],
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["src/*"]
|
||||
"@/*": ["src2/*"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -10,7 +10,7 @@
|
||||
"generate-theme-config": "node ./generateThemeConfig.cjs",
|
||||
"test": "vitest",
|
||||
"coverage": "vitest run --coverage",
|
||||
"lint": "eslint src"
|
||||
"lint": "eslint src2"
|
||||
},
|
||||
"dependencies": {
|
||||
"@codemirror/autocomplete": "^6.18.1",
|
||||
|
||||
@ -1,74 +0,0 @@
|
||||
<template>
|
||||
<div class="text-gray-900 antialiased">
|
||||
<div class="flex h-screen overflow-hidden">
|
||||
<div
|
||||
class="flex flex-1 overflow-y-auto"
|
||||
:class="{
|
||||
'sm:bg-gray-50':
|
||||
$route.meta.isLoginPage && $route.fullPath.indexOf('/checkout') < 0
|
||||
}"
|
||||
>
|
||||
<div class="flex-1">
|
||||
<Navbar class="sm:hidden" v-if="!$route.meta.isLoginPage" />
|
||||
<div class="mx-auto flex flex-row justify-start">
|
||||
<Sidebar
|
||||
class="sticky top-0 hidden w-64 flex-shrink-0 sm:flex"
|
||||
v-if="$auth.isLoggedIn && !$route.meta.hideSidebar"
|
||||
/>
|
||||
<router-view v-slot="{ Component }" class="w-full sm:mr-0">
|
||||
<keep-alive
|
||||
:include="[
|
||||
'Sites',
|
||||
'Benches',
|
||||
'Servers',
|
||||
'Site',
|
||||
'Bench',
|
||||
'Server',
|
||||
'Marketplace',
|
||||
'Account',
|
||||
'MarketplaceApp'
|
||||
]"
|
||||
>
|
||||
<component :is="Component" />
|
||||
</keep-alive>
|
||||
</router-view>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<NotificationToasts />
|
||||
<UserPrompts v-if="$auth.isLoggedIn" />
|
||||
<ConfirmDialogs />
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import Sidebar from '@/components/Sidebar.vue';
|
||||
import Navbar from '@/components/Navbar.vue';
|
||||
import UserPrompts from '@/views/onboarding/UserPrompts.vue';
|
||||
import ConfirmDialogs from '@/components/ConfirmDialogs.vue';
|
||||
import NotificationToasts from '@/components/NotificationToasts.vue';
|
||||
|
||||
export default {
|
||||
name: 'App',
|
||||
components: {
|
||||
Sidebar,
|
||||
Navbar,
|
||||
UserPrompts,
|
||||
ConfirmDialogs,
|
||||
NotificationToasts
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
viewportWidth: 0
|
||||
};
|
||||
},
|
||||
provide: {
|
||||
viewportWidth: Math.max(
|
||||
document.documentElement.clientWidth || 0,
|
||||
window.innerWidth || 0
|
||||
)
|
||||
}
|
||||
};
|
||||
</script>
|
||||
<style src="./assets/style.css"></style>
|
||||
@ -1,65 +0,0 @@
|
||||
<template>
|
||||
<Alert title="Account Setup" v-if="!$account.hasBillingInfo">
|
||||
{{ message }}
|
||||
<template #actions>
|
||||
<Button
|
||||
variant="solid"
|
||||
@click="
|
||||
isDefaultPaymentModeCard
|
||||
? (showPrepaidCreditsDialog = true)
|
||||
: (showCardDialog = true)
|
||||
"
|
||||
class="whitespace-nowrap"
|
||||
>
|
||||
{{
|
||||
isDefaultPaymentModeCard ? 'Add Balance' : 'Add Billing Information'
|
||||
}}
|
||||
</Button>
|
||||
</template>
|
||||
<BillingInformationDialog v-model="showCardDialog" v-if="showCardDialog" />
|
||||
<PrepaidCreditsDialog
|
||||
v-if="showPrepaidCreditsDialog"
|
||||
v-model:show="showPrepaidCreditsDialog"
|
||||
:minimum-amount="$account.team.currency === 'CNY' ? 0.01 : 0.01"
|
||||
@success="handleAddPrepaidCreditsSuccess"
|
||||
/>
|
||||
</Alert>
|
||||
</template>
|
||||
<script>
|
||||
import { defineAsyncComponent } from 'vue';
|
||||
|
||||
export default {
|
||||
name: 'AlertBillingInformation',
|
||||
components: {
|
||||
BillingInformationDialog: defineAsyncComponent(() =>
|
||||
import('./BillingInformationDialog.vue')
|
||||
),
|
||||
PrepaidCreditsDialog: defineAsyncComponent(() =>
|
||||
import('./PrepaidCreditsDialog.vue')
|
||||
)
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showCardDialog: false,
|
||||
showPrepaidCreditsDialog: false
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
handleAddPrepaidCreditsSuccess() {
|
||||
this.showPrepaidCreditsDialog = false;
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
isDefaultPaymentModeCard() {
|
||||
return this.$account.team.payment_mode == 'Card';
|
||||
},
|
||||
message() {
|
||||
if (this.isDefaultPaymentModeCard) {
|
||||
return "We couldn't verify your card with micro charge. Please add some balance to your account to start creating sites.";
|
||||
} else {
|
||||
return "You haven't added your billing information yet. Add it to start creating sites.";
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@ -1,32 +0,0 @@
|
||||
<template>
|
||||
<Alert
|
||||
title="Site Activation"
|
||||
v-if="site.status == 'Active' && !site.setup_wizard_complete"
|
||||
>
|
||||
<span>
|
||||
Please login and complete the setup wizard on your site. Analytics will be
|
||||
collected only after setup is complete.
|
||||
</span>
|
||||
<template #actions>
|
||||
<Button
|
||||
variant="solid"
|
||||
@click="$resources.loginAsAdmin.submit()"
|
||||
:loading="$resources.loginAsAdmin.loading"
|
||||
>
|
||||
Login
|
||||
</Button>
|
||||
</template>
|
||||
</Alert>
|
||||
</template>
|
||||
<script>
|
||||
import { loginAsAdmin } from '@/controllers/loginAsAdmin';
|
||||
export default {
|
||||
name: 'AlertSiteActivation',
|
||||
props: ['site'],
|
||||
resources: {
|
||||
loginAsAdmin() {
|
||||
return loginAsAdmin(this.site?.name);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@ -1,165 +0,0 @@
|
||||
<template>
|
||||
<Alert title="Update Available" v-if="show">
|
||||
<span>
|
||||
A new update is available for your site. Would you like to update your
|
||||
site now?
|
||||
</span>
|
||||
<template #actions>
|
||||
<Tooltip
|
||||
:text="
|
||||
!permissions.update
|
||||
? `You don't have enough permissions to perform this action`
|
||||
: ''
|
||||
"
|
||||
>
|
||||
<Button
|
||||
:disabled="!permissions.update"
|
||||
variant="solid"
|
||||
@click="showUpdatesDialog = true"
|
||||
>
|
||||
Show updates
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</template>
|
||||
<Dialog
|
||||
:options="{
|
||||
title: 'Updates available',
|
||||
actions: [
|
||||
{
|
||||
label: 'Update Now',
|
||||
variant: 'solid',
|
||||
onClick: () => $resources.scheduleUpdate.fetch()
|
||||
}
|
||||
]
|
||||
}"
|
||||
v-model="showUpdatesDialog"
|
||||
>
|
||||
<template v-slot:body-content>
|
||||
<SiteAppUpdates :apps="updateAvailableApps" />
|
||||
<div class="mt-4" v-if="updateAvailableApps.length">
|
||||
<!-- Skip Failing Checkbox -->
|
||||
<input
|
||||
id="skip-failing"
|
||||
type="checkbox"
|
||||
class="h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
|
||||
v-model="wantToSkipFailingPatches"
|
||||
/>
|
||||
<label for="skip-failing" class="ml-1 text-sm text-gray-900">
|
||||
Skip failing patches if any?
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="mt-2" v-if="skip_backups">
|
||||
<!-- Skip Site Backup -->
|
||||
<input
|
||||
id="skip-backup"
|
||||
type="checkbox"
|
||||
class="h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
|
||||
v-model="wantToSkipBackups"
|
||||
/>
|
||||
<label for="skip-backup" class="ml-1 text-sm text-gray-900">
|
||||
Update without site backup?
|
||||
</label>
|
||||
<div class="mt-1 text-sm text-red-600" v-if="wantToSkipBackups">
|
||||
In case of failure, you won't be able to restore the site.
|
||||
</div>
|
||||
</div>
|
||||
<ErrorMessage class="mt-1" :message="$resources.scheduleUpdate.error" />
|
||||
</template>
|
||||
</Dialog>
|
||||
</Alert>
|
||||
</template>
|
||||
<script>
|
||||
import SiteAppUpdates from './SiteAppUpdates.vue';
|
||||
import { notify } from '@/utils/toast';
|
||||
export default {
|
||||
name: 'AlertSiteUpdate',
|
||||
props: ['site'],
|
||||
components: {
|
||||
SiteAppUpdates
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showUpdatesDialog: false,
|
||||
wantToSkipFailingPatches: false,
|
||||
wantToSkipBackups: false
|
||||
};
|
||||
},
|
||||
resources: {
|
||||
updateInformation() {
|
||||
return {
|
||||
url: 'jcloud.api.site.check_for_updates',
|
||||
params: {
|
||||
name: this.site?.name
|
||||
},
|
||||
auto: true
|
||||
};
|
||||
},
|
||||
lastMigrateFailed() {
|
||||
return {
|
||||
url: 'jcloud.api.site.last_migrate_failed',
|
||||
params: {
|
||||
name: this.site?.name
|
||||
},
|
||||
auto: true
|
||||
};
|
||||
},
|
||||
scheduleUpdate() {
|
||||
return {
|
||||
url: 'jcloud.api.site.update',
|
||||
params: {
|
||||
name: this.site?.name,
|
||||
skip_failing_patches: this.wantToSkipFailingPatches,
|
||||
skip_backups: this.wantToSkipBackups
|
||||
},
|
||||
onSuccess() {
|
||||
this.showUpdatesDialog = false;
|
||||
notify({
|
||||
title: 'Site update scheduled successfully',
|
||||
icon: 'check',
|
||||
color: 'green'
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
permissions() {
|
||||
return {
|
||||
update: this.$account.hasPermission(
|
||||
this.site.name,
|
||||
'jcloud.api.site.update'
|
||||
)
|
||||
};
|
||||
},
|
||||
show() {
|
||||
if (this.updateInformation) {
|
||||
return (
|
||||
this.site.setup_wizard_complete &&
|
||||
this.updateInformation.update_available &&
|
||||
['Active', 'Inactive', 'Suspended', 'Broken'].includes(
|
||||
this.site.status
|
||||
)
|
||||
);
|
||||
}
|
||||
},
|
||||
updateInformation() {
|
||||
return this.$resources.updateInformation.data;
|
||||
},
|
||||
updateAvailableApps() {
|
||||
const installedApps = this.updateInformation.installed_apps;
|
||||
const updateAvailableApps = this.updateInformation.apps;
|
||||
|
||||
return updateAvailableApps.filter(app =>
|
||||
installedApps.find(installedApp => installedApp.app === app.app)
|
||||
);
|
||||
},
|
||||
lastMigrateFailed() {
|
||||
return this.$resources.lastMigrateFailed.data;
|
||||
},
|
||||
skip_backups() {
|
||||
return this.$account.team?.skip_backups;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@ -1,172 +0,0 @@
|
||||
<template>
|
||||
<Alert :title="alertTitle" v-if="show">
|
||||
<span v-if="deployInformation.deploy_in_progress"
|
||||
>A deploy for this bench is in progress</span
|
||||
>
|
||||
<span v-else-if="bench.status == 'Active'">
|
||||
A new update is available for your bench. Would you like to deploy the
|
||||
update now?
|
||||
</span>
|
||||
<span v-else>
|
||||
Your bench is not deployed yet. You can add more apps to your bench before
|
||||
deploying. If you want to deploy now, click on the Show Updates button.
|
||||
</span>
|
||||
<template #actions>
|
||||
<Button
|
||||
v-if="deployInformation.deploy_in_progress"
|
||||
variant="solid"
|
||||
:route="`/groups/${bench.name}/deploys/${deployInformation.last_deploy.name}`"
|
||||
>View Progress</Button
|
||||
>
|
||||
<Button
|
||||
v-else
|
||||
variant="solid"
|
||||
@click="
|
||||
() => {
|
||||
showDeployDialog = true;
|
||||
step = 'Apps';
|
||||
}
|
||||
"
|
||||
>
|
||||
Show Updates
|
||||
</Button>
|
||||
</template>
|
||||
|
||||
<Dialog
|
||||
:options="{
|
||||
title:
|
||||
step == 'Apps'
|
||||
? 'Select the apps you want to update'
|
||||
: 'Select the sites you want to update'
|
||||
}"
|
||||
v-model="showDeployDialog"
|
||||
>
|
||||
<template v-slot:body-content>
|
||||
<BenchAppUpdates
|
||||
v-if="step == 'Apps'"
|
||||
:apps="deployInformation.apps"
|
||||
v-model:selectedApps="selectedApps"
|
||||
:removedApps="deployInformation.removed_apps"
|
||||
/>
|
||||
<BenchSiteUpdates
|
||||
class="p-1"
|
||||
v-if="step == 'Sites'"
|
||||
:sites="deployInformation.sites"
|
||||
v-model:selectedSites="selectedSites"
|
||||
/>
|
||||
<ErrorMessage class="mt-2" :message="errorMessage" />
|
||||
</template>
|
||||
<template v-slot:actions>
|
||||
<Button v-if="step == 'Sites'" class="w-full" @click="step = 'Apps'">
|
||||
Back
|
||||
</Button>
|
||||
<Button
|
||||
v-if="step == 'Sites'"
|
||||
variant="solid"
|
||||
class="mt-2 w-full"
|
||||
@click="$resources.deploy.submit()"
|
||||
:loading="$resources.deploy.loading"
|
||||
>
|
||||
{{ selectedSites.length > 0 ? 'Update' : 'Skip and Deploy' }}
|
||||
</Button>
|
||||
<Button v-else variant="solid" class="w-full" @click="step = 'Sites'">
|
||||
Next
|
||||
</Button>
|
||||
</template>
|
||||
</Dialog>
|
||||
</Alert>
|
||||
</template>
|
||||
<script>
|
||||
import BenchAppUpdates from './BenchAppUpdates.vue';
|
||||
import BenchSiteUpdates from './BenchSiteUpdates.vue';
|
||||
import SwitchTeamDialog from './SwitchTeamDialog.vue';
|
||||
import { notify } from '@/utils/toast';
|
||||
|
||||
export default {
|
||||
name: 'AlertBenchUpdate',
|
||||
props: ['bench'],
|
||||
components: {
|
||||
BenchAppUpdates,
|
||||
BenchSiteUpdates,
|
||||
SwitchTeamDialog
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showDeployDialog: false,
|
||||
showTeamSwitcher: false,
|
||||
selectedApps: [],
|
||||
selectedSites: [],
|
||||
step: 'Apps'
|
||||
};
|
||||
},
|
||||
resources: {
|
||||
deployInformation() {
|
||||
return {
|
||||
url: 'jcloud.api.bench.deploy_information',
|
||||
params: {
|
||||
name: this.bench?.name
|
||||
},
|
||||
auto: true
|
||||
};
|
||||
},
|
||||
deploy() {
|
||||
return {
|
||||
url: 'jcloud.api.bench.deploy_and_update',
|
||||
params: {
|
||||
name: this.bench?.name,
|
||||
apps: this.selectedApps,
|
||||
sites: this.selectedSites
|
||||
},
|
||||
validate() {
|
||||
if (
|
||||
this.selectedApps.length === 0 &&
|
||||
this.deployInformation.removed_apps.length === 0
|
||||
) {
|
||||
return 'You must select atleast 1 app to proceed with update.';
|
||||
}
|
||||
},
|
||||
onSuccess(new_candidate_name) {
|
||||
this.showDeployDialog = false;
|
||||
this.$resources.deployInformation.setData({
|
||||
...this.$resources.deployInformation.data,
|
||||
deploy_in_progress: true,
|
||||
last_deploy: { name: new_candidate_name, status: 'Running' }
|
||||
});
|
||||
notify({
|
||||
title: 'Updates scheduled successfully',
|
||||
icon: 'check',
|
||||
color: 'green'
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
show() {
|
||||
if (this.deployInformation) {
|
||||
return (
|
||||
this.deployInformation.update_available &&
|
||||
['Awaiting Deploy', 'Active'].includes(this.bench.status)
|
||||
);
|
||||
}
|
||||
},
|
||||
errorMessage() {
|
||||
return (
|
||||
this.$resources.deploy.error ||
|
||||
(this.bench.team !== $account.team.name
|
||||
? "Current Team doesn't have enough permissions"
|
||||
: '')
|
||||
);
|
||||
},
|
||||
deployInformation() {
|
||||
return this.$resources.deployInformation.data;
|
||||
},
|
||||
alertTitle() {
|
||||
if (this.deployInformation && this.deployInformation.deploy_in_progress) {
|
||||
return 'Deploy in Progress';
|
||||
}
|
||||
return this.bench.status == 'Active' ? 'Update Available' : 'Deploy';
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@ -1,110 +0,0 @@
|
||||
<template>
|
||||
<div class="space-y-3 max-h-96 overflow-auto px-4">
|
||||
<button
|
||||
class="block w-full cursor-pointer rounded-md border px-4 py-3 text-left shadow ring-inset focus:outline-none"
|
||||
:class="
|
||||
isAppSelected(app)
|
||||
? 'bg-gray-50 ring-2 ring-gray-600'
|
||||
: 'cursor-pointer hover:border-gray-300'
|
||||
"
|
||||
v-for="app in apps"
|
||||
:key="app.name"
|
||||
@click="toggleApp(app.name)"
|
||||
>
|
||||
<div class="ml-1 flex items-center justify-between text-left text-base">
|
||||
<div>
|
||||
<div class="font-semibold">
|
||||
{{ app.title }}
|
||||
</div>
|
||||
<div class="text-gray-700">
|
||||
{{ app.source.repository_owner }}/{{ app.source.repository }}
|
||||
</div>
|
||||
</div>
|
||||
<Dropdown :options="dropdownItems(app)" right>
|
||||
<template v-slot="{ open }">
|
||||
<Button type="white" icon-right="chevron-down">
|
||||
<span>{{ app.source.branch }}</span>
|
||||
</Button>
|
||||
</template>
|
||||
</Dropdown>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
name: 'AppSourceSelector',
|
||||
props: ['apps', 'modelValue', 'multiple'],
|
||||
emits: ['update:modelValue'],
|
||||
methods: {
|
||||
toggleApp(appName) {
|
||||
let mapApp = app => ({ app: app.name, source: app.source });
|
||||
|
||||
if (!this.multiple) {
|
||||
let selectedApp = this.apps.find(app => app.name === appName);
|
||||
this.$emit('update:modelValue', mapApp(selectedApp));
|
||||
return;
|
||||
}
|
||||
|
||||
// multiple
|
||||
let selectedAppsMap = Object.assign({}, this.selectedAppsMap);
|
||||
if (selectedAppsMap[appName]) {
|
||||
// exists already, remove
|
||||
selectedAppsMap[appName] = false;
|
||||
} else {
|
||||
// add
|
||||
selectedAppsMap[appName] = true;
|
||||
}
|
||||
let selectedApps = this.apps
|
||||
.filter(app => selectedAppsMap[app.name])
|
||||
.map(mapApp);
|
||||
|
||||
this.$emit('update:modelValue', selectedApps);
|
||||
},
|
||||
isAppSelected(app) {
|
||||
if (this.multiple) {
|
||||
return this.selectedAppsMap[app.name];
|
||||
}
|
||||
return this.modelValue && this.modelValue.app === app.name;
|
||||
},
|
||||
dropdownItems(app) {
|
||||
return app.sources.map(source => ({
|
||||
label: `${source.repository_owner}/${source.repository}:${source.branch}`,
|
||||
onClick: () => this.selectSource(app, source)
|
||||
}));
|
||||
},
|
||||
selectSource(app, source) {
|
||||
app.source = source;
|
||||
if (this.multiple) {
|
||||
let selectedApps = this.modelValue.map(_app => {
|
||||
if (app.name === _app.app) {
|
||||
return {
|
||||
app: app.name,
|
||||
source
|
||||
};
|
||||
}
|
||||
return _app;
|
||||
});
|
||||
this.$emit('update:modelValue', selectedApps);
|
||||
} else {
|
||||
this.$emit('update:modelValue', {
|
||||
app: app.name,
|
||||
source
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
selectedAppsMap() {
|
||||
if (!this.multiple) return {};
|
||||
|
||||
let out = {};
|
||||
let selectedAppNames = this.modelValue.map(app => app.app);
|
||||
for (let app of this.apps) {
|
||||
out[app.name] = selectedAppNames.includes(app.name);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@ -1,75 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<h3>All apps (3)</h3>
|
||||
|
||||
<ul class="mt-5 divide-y divide-gray-300">
|
||||
<li class="flex flex-row items-start justify-between pb-3">
|
||||
<div class="flex flex-row self-end">
|
||||
<!-- Replace with app icon -->
|
||||
<div class="mr-3 h-10 w-10 self-center rounded-lg bg-red-400"></div>
|
||||
<div class="flex flex-col">
|
||||
<h4 class="text-lg font-medium text-gray-900">Jingrow Mail</h4>
|
||||
<p class="mt-1 text-base text-gray-600">Best open source ERP.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col">
|
||||
<div
|
||||
class="rounded-md bg-blue-100 py-1 px-2 text-xs uppercase text-blue-500"
|
||||
>
|
||||
Most popular
|
||||
</div>
|
||||
<p
|
||||
class="mt-1 self-end text-right text-xl font-semibold text-gray-900"
|
||||
>
|
||||
$10<span class="text-base font-normal text-gray-600">/ Month</span>
|
||||
</p>
|
||||
</div>
|
||||
</li>
|
||||
<li class="flex flex-row items-center justify-between py-3">
|
||||
<div class="flex flex-row self-end">
|
||||
<!-- Replace with app icon -->
|
||||
<div class="mr-3 h-10 w-10 self-center rounded-lg bg-green-400"></div>
|
||||
<div class="flex flex-col">
|
||||
<h4 class="text-lg font-medium text-gray-900">Darkify</h4>
|
||||
<p class="mt-1 text-base text-gray-600">Best open source ERP.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col">
|
||||
<p
|
||||
class="mt-1 self-center text-right text-xl font-semibold text-gray-900"
|
||||
>
|
||||
$49<span class="text-base font-normal text-gray-600">/ Month</span>
|
||||
</p>
|
||||
</div>
|
||||
</li>
|
||||
<li class="flex flex-row items-center justify-between py-3">
|
||||
<div class="flex flex-row self-end">
|
||||
<!-- Replace with app icon -->
|
||||
<div
|
||||
class="mr-3 h-10 w-10 self-center rounded-lg bg-indigo-400"
|
||||
></div>
|
||||
<div class="flex flex-col">
|
||||
<h4 class="text-lg font-medium text-gray-900">Jingrow Healthcare</h4>
|
||||
<p class="mt-1 text-base text-gray-600">Best open source ERP.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col">
|
||||
<p
|
||||
class="mt-1 self-end text-right text-xl font-semibold text-gray-900"
|
||||
>
|
||||
$129<span class="text-base font-normal text-gray-600">/ Month</span>
|
||||
</p>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'AppSubscriptionSummary',
|
||||
data() {
|
||||
return {};
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@ -1,147 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="mt-2 space-y-2">
|
||||
<FileUploader
|
||||
v-for="file in files"
|
||||
:fileTypes="file.ext"
|
||||
:key="file.type"
|
||||
:type="file.type"
|
||||
@success="onFileUpload(file, $event)"
|
||||
:fileValidator="f => databaseBackupChecker(f, file.type)"
|
||||
:s3="true"
|
||||
>
|
||||
<template
|
||||
v-slot="{
|
||||
file: fileObj,
|
||||
uploading,
|
||||
progress,
|
||||
error,
|
||||
success,
|
||||
openFileSelector
|
||||
}"
|
||||
>
|
||||
<ListItem
|
||||
class="border-b"
|
||||
:title="fileObj ? fileObj.name : file.title"
|
||||
>
|
||||
<template #subtitle>
|
||||
<span
|
||||
class="text-base"
|
||||
:class="error ? 'text-red-500' : 'text-gray-600'"
|
||||
>
|
||||
{{
|
||||
uploading
|
||||
? `上传中 ${progress}%`
|
||||
: success
|
||||
? formatBytes(fileObj.size)
|
||||
: error
|
||||
? error
|
||||
: file.description
|
||||
}}
|
||||
</span>
|
||||
</template>
|
||||
<template #actions>
|
||||
<Button
|
||||
:loading="uploading"
|
||||
loadingText="上传中..."
|
||||
@click="openFileSelector()"
|
||||
v-if="!success"
|
||||
>
|
||||
上传
|
||||
</Button>
|
||||
<GreenCheckIcon class="w-5" v-if="success" />
|
||||
</template>
|
||||
</ListItem>
|
||||
</template>
|
||||
</FileUploader>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import FileUploader from './FileUploader.vue';
|
||||
|
||||
export default {
|
||||
name: 'BackupFilesUploader',
|
||||
components: { FileUploader },
|
||||
emits: ['update:backupFiles'],
|
||||
props: ['backupFiles'],
|
||||
data() {
|
||||
return {
|
||||
files: [
|
||||
{
|
||||
icon: '<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M5.33325 9.33333V22.6667C5.33325 25.6133 10.1093 28 15.9999 28C21.8906 28 26.6666 25.6133 26.6666 22.6667V9.33333M5.33325 9.33333C5.33325 12.28 10.1093 14.6667 15.9999 14.6667C21.8906 14.6667 26.6666 12.28 26.6666 9.33333M5.33325 9.33333C5.33325 6.38667 10.1093 4 15.9999 4C21.8906 4 26.6666 6.38667 26.6666 9.33333M26.6666 16C26.6666 18.9467 21.8906 21.3333 15.9999 21.3333C10.1093 21.3333 5.33325 18.9467 5.33325 16" stroke="#1F272E" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>',
|
||||
type: 'database',
|
||||
ext: 'application/x-gzip,application/sql,.sql',
|
||||
title: '数据库备份',
|
||||
description:
|
||||
'上传数据库备份文件。通常文件名以 .sql.gz 或 .sql 结尾',
|
||||
file: null
|
||||
},
|
||||
{
|
||||
icon: '<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M9.39111 6.3913H26.3476V22.2174C26.3476 25.9478 23.2955 29 19.565 29H9.39111V6.3913Z" stroke="#1F272E" stroke-width="1.5" stroke-miterlimit="10"/><path d="M13.9131 13.1739H21.8261" stroke="#1F272E" stroke-width="1.5" stroke-miterlimit="10"/><path d="M13.9131 17.6957H21.8261" stroke="#1F272E" stroke-width="1.5" stroke-miterlimit="10"/><path d="M13.9131 22.2173H19.8479" stroke="#1F272E" stroke-width="1.5" stroke-miterlimit="10"/><path d="M22.9565 6.3913V3H6V25.6087H9.3913" stroke="#1F272E" stroke-width="1.5" stroke-miterlimit="10"/></svg>',
|
||||
type: 'public',
|
||||
ext: 'application/x-tar',
|
||||
title: '公共文件',
|
||||
description:
|
||||
'上传公共文件备份。通常文件名以 -files.tar 结尾',
|
||||
file: null
|
||||
},
|
||||
{
|
||||
icon: '<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M8.39111 6.3913H25.3476V22.2174C25.3476 25.9478 22.2955 29 18.565 29H8.39111V6.3913Z" stroke="#1F272E" stroke-width="1.5" stroke-miterlimit="10"/><path d="M21.9565 6.3913V3H5V25.6087H8.3913" stroke="#1F272E" stroke-width="1.5" stroke-miterlimit="10"/></svg>',
|
||||
type: 'private',
|
||||
ext: 'application/x-tar',
|
||||
title: '私有文件',
|
||||
description:
|
||||
'上传私有文件备份。通常文件名以 -private-files.tar 结尾',
|
||||
file: null
|
||||
},
|
||||
{
|
||||
icon: '<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M8.39111 6.3913H25.3476V22.2174C25.3476 25.9478 22.2955 29 18.565 29H8.39111V6.3913Z" stroke="#1F272E" stroke-width="1.5" stroke-miterlimit="10"/><path d="M21.9565 6.3913V3H5V25.6087H8.3913" stroke="#1F272E" stroke-width="1.5" stroke-miterlimit="10"/></svg>',
|
||||
type: 'config',
|
||||
ext: 'application/json',
|
||||
title: '站点配置(如备份已加密则必需)',
|
||||
description:
|
||||
'上传站点配置文件。通常文件名以 -site_config_backup.json 结尾',
|
||||
file: null
|
||||
}
|
||||
]
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
onFileUpload(file, data) {
|
||||
let backupFiles = Object.assign({}, this.backupFiles);
|
||||
backupFiles[file.type] = data;
|
||||
this.$emit('update:backupFiles', backupFiles);
|
||||
},
|
||||
async databaseBackupChecker(file, type) {
|
||||
if (type === 'database') {
|
||||
// valid strings are "database.sql.gz", "database.sql", "database.sql (1).gz", "database.sql (2).gz"
|
||||
if (!/\.sql( \(\d\))?\.gz$|\.sql$/.test(file.name)) {
|
||||
throw new Error(
|
||||
'数据库备份文件应以"database.sql.gz"或"database.sql"结尾'
|
||||
);
|
||||
}
|
||||
if (
|
||||
![
|
||||
'application/x-gzip',
|
||||
'application/gzip',
|
||||
'application/sql'
|
||||
].includes(file.type)
|
||||
) {
|
||||
throw new Error('无效的数据库备份文件');
|
||||
}
|
||||
}
|
||||
if (['public', 'private'].includes(type)) {
|
||||
if (file.type != 'application/x-tar') {
|
||||
throw new Error(`无效的${type === 'public' ? '公共' : '私有'}文件备份文件`);
|
||||
}
|
||||
}
|
||||
if (type === 'config') {
|
||||
if (file.type != 'application/json') {
|
||||
throw new Error(`无效的站点配置文件`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@ -1,105 +0,0 @@
|
||||
<template>
|
||||
<div class="mt-2 space-y-2 divide-y">
|
||||
<AppUpdateCard
|
||||
v-for="(app, index) in appsWithUpdates"
|
||||
:key="app.app"
|
||||
@click.native.self="toggleApp(app)"
|
||||
v-model:app="appsWithUpdates[index]"
|
||||
:selected="selectedApps.map(a => a.app).includes(app.app)"
|
||||
:uninstall="false"
|
||||
:selectable="true"
|
||||
/>
|
||||
<AppUpdateCard
|
||||
v-for="(app, index) in removedApps"
|
||||
:key="app.name"
|
||||
@click.native.self="toggleApp(app)"
|
||||
v-model:app="removedApps[index]"
|
||||
:selected="selectedApps.map(a => a.app).includes(app.app)"
|
||||
:uninstall="true"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import AppUpdateCard from './AppUpdateCard.vue';
|
||||
|
||||
export default {
|
||||
name: 'BenchAppUpdates',
|
||||
props: ['apps', 'removedApps'],
|
||||
components: {
|
||||
AppUpdateCard
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
selectedApps: []
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
// Select all apps by default
|
||||
this.selectedApps = this.appsWithUpdates.map(a => {
|
||||
return { app: a.app, release: a.next_release };
|
||||
});
|
||||
this.$emit('update:selectedApps', this.selectedApps);
|
||||
},
|
||||
methods: {
|
||||
toggleApp(app) {
|
||||
if (!this.selectedApps.map(a => a.app).includes(app.app)) {
|
||||
this.selectedApps.push({ app: app.app, release: app.next_release });
|
||||
this.$emit('update:selectedApps', this.selectedApps);
|
||||
} else {
|
||||
this.selectedApps = this.selectedApps.filter(a => a.app !== app.app);
|
||||
this.$emit('update:selectedApps', this.selectedApps);
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
appsWithUpdates() {
|
||||
return this.apps.filter(app => app.update_available);
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
appsWithUpdates: {
|
||||
handler(apps) {
|
||||
apps.map(app => {
|
||||
this.selectedApps
|
||||
.filter(a => a.app == app.app)
|
||||
.map(a => (a.release = app.next_release));
|
||||
});
|
||||
},
|
||||
deep: true,
|
||||
immediate: true
|
||||
},
|
||||
removedApps: {
|
||||
handler(apps) {
|
||||
apps.map(app => {
|
||||
this.selectedApps
|
||||
.filter(a => a.app == app.app)
|
||||
.map(a => (a.release = app.next_release));
|
||||
});
|
||||
},
|
||||
deep: true,
|
||||
immediate: true
|
||||
},
|
||||
selectedApps: {
|
||||
handler(apps) {
|
||||
// Hardcoded for now, need a better way
|
||||
// to manage such dependencies (#TODO)
|
||||
// If updating JERP, must update Jingrow with it
|
||||
|
||||
let jingrowApp = this.apps.filter(a => a.app == 'jingrow');
|
||||
let jingrowUpdateAvailable =
|
||||
jingrowApp.length == 1 && jingrowApp[0].update_available;
|
||||
|
||||
if (
|
||||
apps.map(a => a.app).includes('jerp') &&
|
||||
!apps.map(a => a.app).includes('jingrow') &&
|
||||
jingrowUpdateAvailable
|
||||
) {
|
||||
apps.push({ app: 'jingrow', release: jingrowApp[0].next_release });
|
||||
}
|
||||
},
|
||||
deep: true,
|
||||
immediate: true
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@ -1,35 +0,0 @@
|
||||
<template>
|
||||
<Dialog :options="{ title: 'Add card to create sites' }" v-model="showDialog">
|
||||
<template v-slot:body-content>
|
||||
<StripeCard
|
||||
class="mb-1"
|
||||
@complete="
|
||||
showDialog = false;
|
||||
$emit('success');
|
||||
"
|
||||
/>
|
||||
</template>
|
||||
</Dialog>
|
||||
</template>
|
||||
<script>
|
||||
import StripeCard from '@/components/StripeCard.vue';
|
||||
|
||||
export default {
|
||||
name: 'BillingInformationDialog',
|
||||
props: ['modelValue'],
|
||||
emits: ['update:modelValue', 'success'],
|
||||
components: {
|
||||
StripeCard
|
||||
},
|
||||
computed: {
|
||||
showDialog: {
|
||||
get() {
|
||||
return this.modelValue;
|
||||
},
|
||||
set(value) {
|
||||
this.$emit('update:modelValue', value);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@ -1,201 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<FormControl
|
||||
v-if="step == 'Get Amount'"
|
||||
class="mb-2"
|
||||
label="Credits"
|
||||
v-model.number="creditsToBuy"
|
||||
name="amount"
|
||||
autocomplete="off"
|
||||
type="number"
|
||||
:min="minimumAmount"
|
||||
/>
|
||||
<label
|
||||
class="block"
|
||||
:class="{
|
||||
'h-0.5 opacity-0': step != 'Add Card Details',
|
||||
'mt-4': step == 'Add Card Details'
|
||||
}"
|
||||
>
|
||||
<span class="text-sm leading-4 text-gray-700">
|
||||
Credit or Debit Card
|
||||
</span>
|
||||
<div
|
||||
class="form-input mt-2 block w-full py-2 pl-3"
|
||||
ref="card-element"
|
||||
></div>
|
||||
<ErrorMessage class="mt-1" :message="cardErrorMessage" />
|
||||
</label>
|
||||
|
||||
<FormControl
|
||||
v-if="step == 'Get Amount'"
|
||||
label="Total Amount"
|
||||
disabled
|
||||
v-model="total"
|
||||
name="total"
|
||||
autocomplete="off"
|
||||
type="number"
|
||||
/>
|
||||
|
||||
<div v-if="step == 'Setting up Stripe'" class="mt-8 flex justify-center">
|
||||
<Spinner class="h-4 w-4 text-gray-600" />
|
||||
</div>
|
||||
<ErrorMessage
|
||||
class="mt-2"
|
||||
:message="$resources.createPaymentIntent.error || errorMessage"
|
||||
/>
|
||||
<div class="mt-4 flex w-full justify-between">
|
||||
<StripeLogo />
|
||||
<div v-if="step == 'Get Amount'">
|
||||
<Button
|
||||
variant="solid"
|
||||
@click="$resources.createPaymentIntent.submit()"
|
||||
:loading="$resources.createPaymentIntent.loading"
|
||||
>
|
||||
Next
|
||||
</Button>
|
||||
</div>
|
||||
<div v-if="step == 'Add Card Details'">
|
||||
<Button @click="$emit('cancel')"> Cancel </Button>
|
||||
<Button
|
||||
class="ml-2"
|
||||
variant="solid"
|
||||
@click="onBuyClick"
|
||||
:loading="paymentInProgress"
|
||||
>
|
||||
Buy Credits
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import StripeLogo from '@/components/StripeLogo.vue';
|
||||
import { loadStripe } from '@stripe/stripe-js';
|
||||
|
||||
export default {
|
||||
name: 'BuyPrepaidCredits',
|
||||
components: {
|
||||
StripeLogo
|
||||
},
|
||||
props: {
|
||||
minimumAmount: {
|
||||
default: 0
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.updateTotal();
|
||||
},
|
||||
watch: {
|
||||
creditsToBuy() {
|
||||
this.updateTotal();
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
step: 'Get Amount', // Get Amount / Add Card Details
|
||||
clientSecret: null,
|
||||
creditsToBuy: this.minimumAmount || null,
|
||||
total: this.minimumAmount,
|
||||
cardErrorMessage: null,
|
||||
errorMessage: null,
|
||||
paymentInProgress: false
|
||||
};
|
||||
},
|
||||
resources: {
|
||||
createPaymentIntent() {
|
||||
return {
|
||||
url: 'jcloud.api.billing.create_payment_intent_for_buying_credits',
|
||||
params: {
|
||||
amount: this.creditsToBuy
|
||||
},
|
||||
validate() {
|
||||
if (
|
||||
this.creditsToBuy < this.minimumAmount &&
|
||||
!this.$account.team.jerp_partner
|
||||
) {
|
||||
return `Amount must be greater than ${this.minimumAmount}`;
|
||||
}
|
||||
},
|
||||
async onSuccess(data) {
|
||||
this.step = 'Setting up Stripe';
|
||||
let { publishable_key, client_secret } = data;
|
||||
this.clientSecret = client_secret;
|
||||
this.stripe = await loadStripe(publishable_key);
|
||||
this.elements = this.stripe.elements();
|
||||
let theme = this.$theme;
|
||||
let style = {
|
||||
base: {
|
||||
color: theme.colors.black,
|
||||
fontFamily: theme.fontFamily.sans.join(', '),
|
||||
fontSmoothing: 'antialiased',
|
||||
fontSize: '13px',
|
||||
'::placeholder': {
|
||||
color: theme.colors.gray['400']
|
||||
}
|
||||
},
|
||||
invalid: {
|
||||
color: theme.colors.red['600'],
|
||||
iconColor: theme.colors.red['600']
|
||||
}
|
||||
};
|
||||
this.card = this.elements.create('card', {
|
||||
hidePostalCode: true,
|
||||
style: style,
|
||||
classes: {
|
||||
complete: '',
|
||||
focus: 'bg-gray-100'
|
||||
}
|
||||
});
|
||||
|
||||
this.step = 'Add Card Details';
|
||||
this.$nextTick(() => {
|
||||
this.card.mount(this.$refs['card-element']);
|
||||
});
|
||||
|
||||
this.card.addEventListener('change', event => {
|
||||
this.cardErrorMessage = event.error?.message || null;
|
||||
});
|
||||
this.card.addEventListener('ready', () => {
|
||||
this.ready = true;
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
updateTotal() {
|
||||
if (this.$account.team.currency === 'CNY') {
|
||||
this.total = Number(
|
||||
(
|
||||
this.creditsToBuy +
|
||||
this.creditsToBuy * this.$account.billing_info.gst_percentage
|
||||
).toFixed(2)
|
||||
);
|
||||
} else {
|
||||
this.total = this.creditsToBuy;
|
||||
}
|
||||
},
|
||||
setupStripe() {
|
||||
this.$resources.createPaymentIntent.submit();
|
||||
},
|
||||
async onBuyClick() {
|
||||
this.paymentInProgress = true;
|
||||
let payload = await this.stripe.confirmCardPayment(this.clientSecret, {
|
||||
payment_method: {
|
||||
card: this.card
|
||||
}
|
||||
});
|
||||
|
||||
this.paymentInProgress = false;
|
||||
if (payload.error) {
|
||||
this.errorMessage = payload.error.message;
|
||||
} else {
|
||||
this.$emit('success');
|
||||
this.errorMessage = null;
|
||||
this.creditsToBuy = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@ -1,115 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<ErrorMessage :message="$resources.getAppPlans.error" />
|
||||
|
||||
<Button
|
||||
v-if="
|
||||
$resources.getAppPlans.loading ||
|
||||
$resources.getMarketplaceAppInfo.loading
|
||||
"
|
||||
:loading="true"
|
||||
loadingText="Loading Plans..."
|
||||
></Button>
|
||||
|
||||
<div v-if="plans && appInfo" class="mb-6 flex flex-row items-center">
|
||||
<Avatar
|
||||
class="mr-2"
|
||||
size="lg"
|
||||
shape="square"
|
||||
:image="appInfo.image"
|
||||
:label="appInfo.title"
|
||||
/>
|
||||
|
||||
<div class="flex flex-col">
|
||||
<h4 class="text-xl font-semibold text-gray-900">{{ appInfo.title }}</h4>
|
||||
<p class="text-base text-gray-600">Choose your plans</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="plans" class="mx-auto grid grid-cols-1 gap-2 md:grid-cols-3">
|
||||
<AppPlanCard
|
||||
v-for="plan in plans"
|
||||
:editable="editable"
|
||||
:plan="plan"
|
||||
:key="plan.name"
|
||||
:selected="selectedPlan == plan"
|
||||
@click.native="handleCardClick(plan)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import AppPlanCard from '@/components/AppPlanCard.vue';
|
||||
|
||||
export default {
|
||||
name: 'ChangeAppPlanSelector',
|
||||
components: {
|
||||
AppPlanCard
|
||||
},
|
||||
props: ['app', 'group', 'jingrowVersion', 'currentPlan', 'editable'],
|
||||
emits: ['change'],
|
||||
data() {
|
||||
return {
|
||||
selectedPlan: null
|
||||
};
|
||||
},
|
||||
resources: {
|
||||
getAppPlans() {
|
||||
return {
|
||||
url: 'jcloud.api.marketplace.get_app_plans',
|
||||
params: {
|
||||
app: this.app,
|
||||
include_disabled: false,
|
||||
release_group: this.group,
|
||||
jingrow_version: this.jingrowVersion
|
||||
},
|
||||
onSuccess(plans) {
|
||||
if (this.currentPlan) {
|
||||
for (let plan of plans) {
|
||||
if (plan.name === this.currentPlan) {
|
||||
this.selectedPlan = plan;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
auto: true
|
||||
};
|
||||
},
|
||||
getMarketplaceAppInfo() {
|
||||
return {
|
||||
url: 'jcloud.api.marketplace.get_app_info',
|
||||
params: {
|
||||
app: this.app
|
||||
},
|
||||
auto: true
|
||||
};
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleCardClick(plan) {
|
||||
this.selectedPlan = plan;
|
||||
this.$emit('change', plan);
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
plans() {
|
||||
if (
|
||||
this.$resources.getAppPlans.data &&
|
||||
!this.$resources.getAppPlans.loading
|
||||
) {
|
||||
return this.$resources.getAppPlans.data;
|
||||
}
|
||||
},
|
||||
appInfo() {
|
||||
if (
|
||||
!this.$resources.getMarketplaceAppInfo.loading &&
|
||||
this.$resources.getMarketplaceAppInfo.data
|
||||
) {
|
||||
return this.$resources.getMarketplaceAppInfo.data;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@ -1,135 +0,0 @@
|
||||
<template>
|
||||
<Dialog
|
||||
:options="{
|
||||
title: 'Change Payment Mode',
|
||||
actions: [
|
||||
{
|
||||
label: 'Change',
|
||||
variant: 'solid',
|
||||
loading: $resources.changePaymentMode.loading,
|
||||
onClick: () => $resources.changePaymentMode.submit()
|
||||
}
|
||||
]
|
||||
}"
|
||||
:modelValue="modelValue"
|
||||
@update:modelValue="$emit('update:modelValue', $event)"
|
||||
>
|
||||
<template v-slot:body-content>
|
||||
<FormControl
|
||||
label="Select Payment Mode"
|
||||
type="select"
|
||||
:options="paymentModeOptions"
|
||||
v-model="paymentMode"
|
||||
/>
|
||||
<p class="mt-2 text-base text-gray-600 mb-5">
|
||||
{{ paymentModeDescription }}
|
||||
</p>
|
||||
<ErrorMessage
|
||||
class="mt-2"
|
||||
:message="$resources.changePaymentMode.error"
|
||||
/>
|
||||
</template>
|
||||
</Dialog>
|
||||
<BillingInformationDialog
|
||||
v-model="showBillingInformationDialog"
|
||||
v-if="showBillingInformationDialog"
|
||||
/>
|
||||
<PrepaidCreditsDialog
|
||||
v-if="showPrepaidCreditsDialog"
|
||||
v-model:show="showPrepaidCreditsDialog"
|
||||
:minimumAmount="$account.team.currency == 'CNY' ? 0.01 : 0.01"
|
||||
@success="
|
||||
() => {
|
||||
$resources.upcomingInvoice.reload();
|
||||
showPrepaidCreditsDialog = false;
|
||||
}
|
||||
"
|
||||
/>
|
||||
</template>
|
||||
<script>
|
||||
import { defineAsyncComponent } from 'vue';
|
||||
|
||||
export default {
|
||||
name: 'ChangePaymentModeDialog',
|
||||
props: ['modelValue'],
|
||||
emits: ['update:modelValue'],
|
||||
components: {
|
||||
BillingInformationDialog: defineAsyncComponent(() =>
|
||||
import('./BillingInformationDialog.vue')
|
||||
),
|
||||
PrepaidCreditsDialog: defineAsyncComponent(() =>
|
||||
import('@/components/PrepaidCreditsDialog.vue')
|
||||
)
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showBillingInformationDialog: false,
|
||||
showPrepaidCreditsDialog: false,
|
||||
paymentMode: this.$account.team.payment_mode
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
show(value) {
|
||||
if (!value) {
|
||||
this.paymentMode = this.$account.team.payment_mode;
|
||||
}
|
||||
}
|
||||
},
|
||||
resources: {
|
||||
changePaymentMode() {
|
||||
return {
|
||||
url: 'jcloud.api.billing.change_payment_mode',
|
||||
params: {
|
||||
mode: this.paymentMode
|
||||
},
|
||||
onSuccess() {
|
||||
this.$emit('update:modelValue', false);
|
||||
this.$resources.changePaymentMode.reset();
|
||||
},
|
||||
validate() {
|
||||
if (
|
||||
this.paymentMode == 'Card' &&
|
||||
!this.$account.team.default_payment_method
|
||||
) {
|
||||
this.$emit('update:modelValue', false);
|
||||
this.showBillingInformationDialog = true;
|
||||
}
|
||||
|
||||
if (
|
||||
this.paymentMode == 'Prepaid Credits' &&
|
||||
this.$account.balance === 0
|
||||
) {
|
||||
this.$emit('update:modelValue', false);
|
||||
this.showPrepaidCreditsDialog = true;
|
||||
}
|
||||
|
||||
if (
|
||||
this.paymentMode == 'Paid By Partner' &&
|
||||
!this.$account.team.partner_email
|
||||
) {
|
||||
return 'Please add a partner first from Partner section';
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
paymentModeDescription() {
|
||||
return {
|
||||
Card: `Your card will be charged for monthly subscription`,
|
||||
'Prepaid Credits': `You will be charged from your account balance for monthly subscription`,
|
||||
'Paid By Partner': `Your partner will be charged for monthly subscription`
|
||||
}[this.paymentMode];
|
||||
},
|
||||
paymentModeOptions() {
|
||||
if (
|
||||
this.$account.team.jerp_partner ||
|
||||
!this.$account.team.partner_email
|
||||
) {
|
||||
return ['Card', 'Prepaid Credits'];
|
||||
}
|
||||
return ['Card', 'Prepaid Credits', 'Paid By Partner'];
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@ -1,59 +0,0 @@
|
||||
<template>
|
||||
<div class="relative rounded-lg border-2 border-gray-200 bg-gray-100 p-3">
|
||||
<div class="select-all break-all text-xs text-gray-800">
|
||||
<pre
|
||||
:class="{
|
||||
'whitespace-pre-wrap': breakLines,
|
||||
'overflow-x-auto': !breakLines
|
||||
}"
|
||||
:style="
|
||||
!breakLines
|
||||
? 'scrollbar-width: none; -ms-overflow-style: none; -webkit-scrollbar: none;'
|
||||
: ''
|
||||
"
|
||||
>{{ textContent }}</pre
|
||||
>
|
||||
</div>
|
||||
<button
|
||||
class="absolute right-2 top-2 rounded-sm border border-gray-200 bg-white p-1 text-xs text-gray-600"
|
||||
variant="outline"
|
||||
@click="copyTextContentToClipboard"
|
||||
>
|
||||
{{ copied ? 'copied' : 'copy' }}
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { toast } from 'vue-sonner';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
textContent: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
breakLines: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
copied: false
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
copyTextContentToClipboard() {
|
||||
const clipboard = window.navigator.clipboard;
|
||||
clipboard.writeText(this.textContent).then(() => {
|
||||
this.copied = true;
|
||||
setTimeout(() => {
|
||||
this.copied = false;
|
||||
}, 4000);
|
||||
toast.success('Copied to clipboard!');
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@ -1,341 +0,0 @@
|
||||
<template>
|
||||
<Card :title="title || 'Site Config'">
|
||||
<template #actions>
|
||||
<Button
|
||||
class="mr-2"
|
||||
:loading="$resources.configData.loading"
|
||||
v-if="isDirty"
|
||||
@click="
|
||||
() => {
|
||||
$resources.configData.reload().then(() => {
|
||||
isDirty = false;
|
||||
});
|
||||
}
|
||||
"
|
||||
>
|
||||
Discard changes
|
||||
</Button>
|
||||
<Button
|
||||
variant="solid"
|
||||
v-if="isDirty"
|
||||
@click="updateConfig"
|
||||
:loading="$resources.updateConfig.loading"
|
||||
>
|
||||
Save changes
|
||||
</Button>
|
||||
</template>
|
||||
<div class="flex space-x-4">
|
||||
<div class="w-full shrink-0 space-y-4 md:w-1/2">
|
||||
<div class="ml-2">
|
||||
<ErrorMessage :message="$resources.updateConfig.error" />
|
||||
<div
|
||||
v-if="$resources.configData?.data?.length"
|
||||
v-for="config in $resources.configData.data"
|
||||
:key="config.key"
|
||||
class="mt-2 flex"
|
||||
>
|
||||
<FormControl
|
||||
:label="getStandardConfigTitle(config.key)"
|
||||
v-model="config.value"
|
||||
@click="
|
||||
config.type === 'Password' ? (config.value = '') : null;
|
||||
config.type === 'Password' ? (isDirty = true) : null;
|
||||
"
|
||||
@input="isDirty = true"
|
||||
class="flex-1"
|
||||
/>
|
||||
<Button
|
||||
class="ml-2 mt-5"
|
||||
icon="x"
|
||||
variant="ghost"
|
||||
@click="removeConfig(config)"
|
||||
/>
|
||||
</div>
|
||||
<p v-else class="my-2 text-base text-gray-600">
|
||||
No keys added. Click on Add Key to add one.
|
||||
</p>
|
||||
<Button class="mt-4" @click="showAddConfigKeyDialog = true"
|
||||
>Add Key</Button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="hidden h-fit max-w-full flex-1 overflow-x-scroll whitespace-pre-line rounded bg-gray-100 p-4 font-mono text-base md:block"
|
||||
>
|
||||
<div v-if="configName" class="mb-4">{{ configName }}</div>
|
||||
<div v-html="configPreview"></div>
|
||||
</div>
|
||||
<Dialog
|
||||
:options="{
|
||||
title: 'Add Config Key',
|
||||
actions: [
|
||||
{
|
||||
label: 'Add Key',
|
||||
variant: 'solid',
|
||||
onClick: addConfig
|
||||
}
|
||||
]
|
||||
}"
|
||||
v-model="showAddConfigKeyDialog"
|
||||
>
|
||||
<template v-slot:body-content>
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<span class="mb-1 block text-xs text-gray-600">Key</span>
|
||||
<Autocomplete
|
||||
placeholder="Key"
|
||||
:options="getStandardConfigKeys"
|
||||
v-model="chosenStandardConfig"
|
||||
@update:modelValue="handleAutocompleteSelection"
|
||||
/>
|
||||
</div>
|
||||
<FormControl
|
||||
v-if="showCustomKeyInput"
|
||||
v-model="newConfig.key"
|
||||
label="Custom Key"
|
||||
class="w-full"
|
||||
@change="isDirty = true"
|
||||
/>
|
||||
<FormControl
|
||||
label="Type"
|
||||
v-model="newConfig.type"
|
||||
type="select"
|
||||
:disabled="chosenStandardConfig && !showCustomKeyInput"
|
||||
:options="[
|
||||
'String',
|
||||
'Number',
|
||||
'JSON',
|
||||
'Boolean',
|
||||
chosenStandardConfig?.value !== 'custom_key' ? 'Password' : null
|
||||
]"
|
||||
@change="isDirty = true"
|
||||
/>
|
||||
<FormControl
|
||||
v-bind="configInputProps()"
|
||||
v-model="newConfig.value"
|
||||
label="Value"
|
||||
class="w-full"
|
||||
@change="isDirty = true"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</Dialog>
|
||||
</div>
|
||||
</Card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { Autocomplete } from 'jingrow-ui';
|
||||
|
||||
export default {
|
||||
name: 'ConfigEditor',
|
||||
components: {
|
||||
Autocomplete
|
||||
},
|
||||
props: [
|
||||
'title',
|
||||
'subtitle',
|
||||
'configName',
|
||||
'configData',
|
||||
'updateConfigMethod'
|
||||
],
|
||||
data() {
|
||||
return {
|
||||
isDirty: false,
|
||||
showCustomKeyInput: false,
|
||||
showAddConfigKeyDialog: false,
|
||||
chosenStandardConfig: {
|
||||
title: '',
|
||||
key: ''
|
||||
},
|
||||
newConfig: {
|
||||
key: '',
|
||||
value: '',
|
||||
type: 'String'
|
||||
}
|
||||
};
|
||||
},
|
||||
resources: {
|
||||
configData() {
|
||||
return this.configData();
|
||||
},
|
||||
updateConfig() {
|
||||
function isValidJSON(str) {
|
||||
try {
|
||||
JSON.parse(str);
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
const updatedConfig = this.$resources.configData.data.map(d => {
|
||||
const value = d.value;
|
||||
if (!isNaN(value)) d.type = 'Number';
|
||||
else if (isValidJSON(value)) d.type = 'JSON';
|
||||
else if (d.type === 'Password') d.type = 'Password';
|
||||
else d.type = 'String';
|
||||
|
||||
return {
|
||||
key: d.key,
|
||||
value,
|
||||
type: d.type
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
...this.updateConfigMethod(updatedConfig),
|
||||
async validate() {
|
||||
let keys = updatedConfig.map(d => d.key);
|
||||
if (keys.length !== [...new Set(keys)].length) {
|
||||
return 'Duplicate key';
|
||||
}
|
||||
this.$resources.validateKeys.submit({
|
||||
keys: JSON.stringify(keys)
|
||||
});
|
||||
let invalidKeys = this.$resources.validateKeys.data;
|
||||
if (invalidKeys?.length > 0) {
|
||||
return `Invalid key: ${invalidKeys.join(', ')}`;
|
||||
}
|
||||
for (let config of updatedConfig) {
|
||||
if (config.type === 'JSON') {
|
||||
try {
|
||||
JSON.parse(JSON.stringify(config.value));
|
||||
} catch (error) {
|
||||
return `Invalid JSON -- ${error}`;
|
||||
}
|
||||
} else if (config.type === 'Number') {
|
||||
try {
|
||||
Number(config.value);
|
||||
} catch (error) {
|
||||
return 'Invalid Number';
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
onSuccess() {
|
||||
this.isDirty = false;
|
||||
this.$resources.configData.reload();
|
||||
}
|
||||
};
|
||||
},
|
||||
standardConfigKeys: {
|
||||
url: 'jcloud.api.config.standard_keys',
|
||||
cache: 'standardConfigKeys',
|
||||
auto: true
|
||||
},
|
||||
validateKeys: {
|
||||
url: 'jcloud.api.config.is_valid'
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
configPreview() {
|
||||
let obj = {};
|
||||
|
||||
for (let d of this.$resources.configData.data) {
|
||||
let value = d.value;
|
||||
if (['Boolean', 'Number'].includes(d.type)) {
|
||||
value = Number(d.value);
|
||||
} else if (d.type === 'JSON') {
|
||||
try {
|
||||
value = JSON.parse(d.value);
|
||||
} catch (error) {
|
||||
value = {};
|
||||
}
|
||||
}
|
||||
obj[d.key] = value;
|
||||
}
|
||||
return JSON.stringify(obj, null, ' ');
|
||||
},
|
||||
getStandardConfigKeys() {
|
||||
return [
|
||||
{
|
||||
group: 'Custom',
|
||||
items: [{ label: 'Create a custom key', value: 'custom_key' }]
|
||||
},
|
||||
{
|
||||
group: 'Standard',
|
||||
items: this.$resources.standardConfigKeys.data.map(d => ({
|
||||
label: d.title,
|
||||
value: d.key
|
||||
}))
|
||||
}
|
||||
];
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
configInputProps() {
|
||||
let type = {
|
||||
String: 'text',
|
||||
Password: 'text',
|
||||
Number: 'number',
|
||||
JSON: 'textarea',
|
||||
Boolean: 'select'
|
||||
}[this.newConfig.type];
|
||||
|
||||
return {
|
||||
type,
|
||||
options: this.newConfig.type === 'Boolean' ? ['1', '0'] : null
|
||||
};
|
||||
},
|
||||
addConfig() {
|
||||
this.$resources.configData.data.push({
|
||||
key: this.getStandardConfigKey(this.newConfig.key),
|
||||
value: this.newConfig.value,
|
||||
type: this.newConfig.type
|
||||
});
|
||||
this.isDirty = true;
|
||||
this.showAddConfigKeyDialog = false;
|
||||
},
|
||||
handleAutocompleteSelection() {
|
||||
if (this.chosenStandardConfig?.value === 'custom_key') {
|
||||
this.showCustomKeyInput = true;
|
||||
} else {
|
||||
this.showCustomKeyInput = false;
|
||||
this.newConfig.type = this.getStandardConfigType(
|
||||
this.chosenStandardConfig?.value
|
||||
);
|
||||
}
|
||||
|
||||
if (this.newConfig.type === 'Boolean') {
|
||||
this.newConfig.value = 0;
|
||||
} else if (this.newConfig.type === 'JSON') {
|
||||
this.newConfig.value = '{}';
|
||||
} else {
|
||||
this.newConfig.value = '';
|
||||
}
|
||||
|
||||
this.newConfig.key = this.chosenStandardConfig?.value || '';
|
||||
},
|
||||
getStandardConfigType(key) {
|
||||
return (
|
||||
this.$resources.standardConfigKeys.data.find(d => d.key === key)
|
||||
?.type || 'String'
|
||||
);
|
||||
},
|
||||
getStandardConfigKey(key) {
|
||||
return (
|
||||
this.$resources.standardConfigKeys.data.find(d => d.title === key)
|
||||
?.key || key
|
||||
);
|
||||
},
|
||||
getStandardConfigTitle(key) {
|
||||
return (
|
||||
this.$resources.standardConfigKeys.data.find(d => d.key === key)
|
||||
?.title || key
|
||||
);
|
||||
},
|
||||
removeConfig(config) {
|
||||
const index = this.$resources.configData.data.indexOf(config);
|
||||
if (index > -1) this.$resources.configData.data.splice(index, 1);
|
||||
this.isDirty = true;
|
||||
},
|
||||
updateConfig() {
|
||||
if (this.isDirty) {
|
||||
this.$resources.updateConfig.submit();
|
||||
} else {
|
||||
this.isDirty = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@ -1,63 +0,0 @@
|
||||
<script setup>
|
||||
import { getCurrentInstance, ref } from 'vue';
|
||||
|
||||
const app = getCurrentInstance();
|
||||
const confirmDialogs = ref([]);
|
||||
|
||||
function confirm(dialog) {
|
||||
dialog.id = confirmDialogs.value.length;
|
||||
dialog.show = true;
|
||||
confirmDialogs.value.push(dialog);
|
||||
}
|
||||
|
||||
function removeConfirmDialog(dialog) {
|
||||
confirmDialogs.value = confirmDialogs.value.filter(
|
||||
_dialog => dialog !== _dialog
|
||||
);
|
||||
}
|
||||
|
||||
function onDialogAction(dialog) {
|
||||
let closeDialog = () => removeConfirmDialog(dialog);
|
||||
dialog.action(closeDialog);
|
||||
}
|
||||
|
||||
app.appContext.config.globalProperties.$confirm = confirm;
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<Dialog
|
||||
v-for="dialog in confirmDialogs"
|
||||
v-model="dialog.show"
|
||||
@close="removeConfirmDialog(dialog)"
|
||||
:key="dialog.id"
|
||||
:options="{
|
||||
title: dialog.title,
|
||||
actions: [
|
||||
{
|
||||
label: dialog.actionLabel || 'Submit',
|
||||
theme: dialog.actionColor,
|
||||
variant: dialog.actionVariant || 'solid',
|
||||
onClick: () => onDialogAction(dialog),
|
||||
loading: dialog.resource?.loading
|
||||
},
|
||||
{
|
||||
label: 'Cancel',
|
||||
onClick: () => removeConfirmDialog(dialog)
|
||||
}
|
||||
]
|
||||
}"
|
||||
>
|
||||
<template v-slot:body-content>
|
||||
<div class="prose">
|
||||
<p class="text-base" v-html="dialog.message"></p>
|
||||
</div>
|
||||
<ErrorMessage
|
||||
class="mt-2"
|
||||
v-if="dialog.resource"
|
||||
:message="dialog.resource.error"
|
||||
/>
|
||||
</template>
|
||||
</Dialog>
|
||||
</div>
|
||||
</template>
|
||||
@ -1,17 +0,0 @@
|
||||
<template>
|
||||
<div class="space-y-4">
|
||||
<div v-for="item in items" :key="item.label">
|
||||
<div class="text-sm text-gray-700">{{ item.label }}</div>
|
||||
<div class="mt-1 rounded bg-gray-100 px-3 py-1 text-base text-gray-900">
|
||||
{{ item.value }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'DescriptionList',
|
||||
props: ['items']
|
||||
};
|
||||
</script>
|
||||
@ -1,19 +0,0 @@
|
||||
<template>
|
||||
<ul class="space-y-2 text-sm text-gray-700">
|
||||
<li
|
||||
class="flex flex-row justify-items-center"
|
||||
v-for="feature in features"
|
||||
:key="feature"
|
||||
>
|
||||
<CircularCheckIcon class="mr-2" />
|
||||
{{ feature }}
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'FeatureList',
|
||||
props: ['features']
|
||||
};
|
||||
</script>
|
||||
@ -1,130 +0,0 @@
|
||||
<template>
|
||||
<div class="space-y-4">
|
||||
<div
|
||||
v-for="field in fields"
|
||||
:key="field.fieldname"
|
||||
v-show="field.condition ? field.condition() : true"
|
||||
>
|
||||
<div class="flex space-x-4" v-if="Array.isArray(field)">
|
||||
<FormControl
|
||||
v-bind="getBindProps(subfield)"
|
||||
:key="subfield.fieldname"
|
||||
class="w-full"
|
||||
v-for="subfield in field"
|
||||
/>
|
||||
</div>
|
||||
<FormControl v-else v-bind="getBindProps(field)" />
|
||||
<ErrorMessage
|
||||
class="mt-1"
|
||||
v-if="requiredFieldNotSet.includes(field)"
|
||||
error="This field is required"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
// https://github.com/eggert/tz/blob/main/backward add more if required.
|
||||
const TZ_BACKWARD_COMPATBILITY_MAP = {
|
||||
'Asia/Calcutta': 'Asia/Kolkata'
|
||||
};
|
||||
|
||||
export default {
|
||||
name: 'Form',
|
||||
props: ['fields', 'modelValue'],
|
||||
emits: ['update:modelValue'],
|
||||
data() {
|
||||
return {
|
||||
requiredFieldNotSet: [],
|
||||
guessedTimezone: ''
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.guessedTimezone = this.guessTimezone();
|
||||
},
|
||||
watch: {
|
||||
fields: {
|
||||
handler(new_fields) {
|
||||
let timezoneFields = new_fields.filter(
|
||||
f => f.fieldtype === 'Select' && f.fieldname.endsWith('_tz')
|
||||
);
|
||||
for (let field of timezoneFields) {
|
||||
if (!field.options) {
|
||||
field.options = [];
|
||||
}
|
||||
if (
|
||||
this.guessedTimezone &&
|
||||
field.options.includes(this.guessedTimezone)
|
||||
) {
|
||||
this.onChange(this.guessedTimezone, field);
|
||||
}
|
||||
}
|
||||
},
|
||||
deep: true
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onChange(value, field) {
|
||||
this.checkRequired(field, value);
|
||||
this.updateValue(field.fieldname, value);
|
||||
},
|
||||
updateValue(fieldname, value) {
|
||||
let values = Object.assign({}, this.modelValue, {
|
||||
[fieldname]: value
|
||||
});
|
||||
this.$emit('update:modelValue', values);
|
||||
},
|
||||
checkRequired(field, value) {
|
||||
if (field.required) {
|
||||
if (!value) {
|
||||
this.requiredFieldNotSet.push(field);
|
||||
return false;
|
||||
} else {
|
||||
this.requiredFieldNotSet = this.requiredFieldNotSet.filter(
|
||||
f => f !== field
|
||||
);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
},
|
||||
getBindProps(field) {
|
||||
return {
|
||||
label: field.label || field.fieldname,
|
||||
type: this.getInputType(field),
|
||||
options: field.options,
|
||||
name: field.fieldname,
|
||||
modelValue: this.modelValue[field.fieldname],
|
||||
disabled: field.disabled,
|
||||
required: field.required || false,
|
||||
rows: field.rows,
|
||||
placeholder: field.placeholder,
|
||||
'onUpdate:modelValue': value => this.onChange(value, field),
|
||||
onBlur: e => this.checkRequired(field, e)
|
||||
};
|
||||
},
|
||||
getInputType(field) {
|
||||
return {
|
||||
Data: 'text',
|
||||
Int: 'number',
|
||||
Select: 'select',
|
||||
Check: 'checkbox',
|
||||
Password: 'password',
|
||||
Text: 'textarea',
|
||||
Date: 'date'
|
||||
}[field.fieldtype || 'Data'];
|
||||
},
|
||||
guessTimezone() {
|
||||
try {
|
||||
let tz = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
||||
if (TZ_BACKWARD_COMPATBILITY_MAP[tz]) {
|
||||
return TZ_BACKWARD_COMPATBILITY_MAP[tz];
|
||||
}
|
||||
return tz;
|
||||
} catch (e) {
|
||||
console.error("Couldn't guess timezone", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@ -1,95 +0,0 @@
|
||||
<template>
|
||||
<svg
|
||||
width="48"
|
||||
height="48"
|
||||
viewBox="0 0 48 48"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<g filter="url(#filter0_ddd_28_966)">
|
||||
<rect x="4" y="3" width="40" height="40" rx="10" fill="#590EFA" />
|
||||
</g>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M25.415 13.5C27.411 13.5 29.2176 14.3109 30.5234 15.6212C29.7284 15.7907 28.9722 16.0399 28.2642 16.4062C26.1108 17.5202 24.5787 19.6053 23.494 23.173C23.44 23.3504 23.3876 23.5261 23.3365 23.7L12.0706 23.7C12.6494 20.3849 15.4531 17.8345 18.8863 17.6441C20.039 15.1952 22.529 13.5 25.415 13.5ZM12.0005 25.7375C12.3752 29.3476 15.3672 32.1803 19.046 32.3051V32.3104H19.0698L19.0998 32.2983C19.9271 31.9628 20.5108 31.43 20.9884 30.6753C21.4839 29.8923 21.8606 28.8779 22.2438 27.5888C22.4117 27.024 22.5758 26.4246 22.752 25.7811L22.7521 25.7808L22.7521 25.7807L22.764 25.7375L12.0005 25.7375ZM29.2004 18.2158C29.9705 17.8175 30.8419 17.5899 31.8545 17.4615C32.2821 18.3068 32.548 19.2477 32.6118 20.2437C34.6288 21.3236 36.0007 23.451 36.0007 25.8988C36.0007 29.383 33.2211 32.218 29.7583 32.3071V32.3104H22.329C22.4637 32.1358 22.5905 31.9538 22.7101 31.7648C23.3634 30.7323 23.8038 29.4915 24.1968 28.1692C24.3715 27.5817 24.5419 26.9594 24.7172 26.3189L24.7173 26.3187L24.7173 26.3186L24.7877 26.0615C24.9895 25.3258 25.2014 24.5613 25.4433 23.7656C26.4384 20.4927 27.7142 18.9847 29.2004 18.2158Z"
|
||||
fill="white"
|
||||
/>
|
||||
<defs>
|
||||
<filter
|
||||
id="filter0_ddd_28_966"
|
||||
x="0.233733"
|
||||
y="0.04079"
|
||||
width="47.5325"
|
||||
height="47.5325"
|
||||
filterUnits="userSpaceOnUse"
|
||||
color-interpolation-filters="sRGB"
|
||||
>
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix" />
|
||||
<feColorMatrix
|
||||
in="SourceAlpha"
|
||||
type="matrix"
|
||||
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
|
||||
result="hardAlpha"
|
||||
/>
|
||||
<feMorphology
|
||||
radius="0.269019"
|
||||
operator="dilate"
|
||||
in="SourceAlpha"
|
||||
result="effect1_dropShadow_28_966"
|
||||
/>
|
||||
<feOffset dy="0.807057" />
|
||||
<feGaussianBlur stdDeviation="1.74862" />
|
||||
<feColorMatrix
|
||||
type="matrix"
|
||||
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.08 0"
|
||||
/>
|
||||
<feBlend
|
||||
mode="normal"
|
||||
in2="BackgroundImageFix"
|
||||
result="effect1_dropShadow_28_966"
|
||||
/>
|
||||
<feColorMatrix
|
||||
in="SourceAlpha"
|
||||
type="matrix"
|
||||
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
|
||||
result="hardAlpha"
|
||||
/>
|
||||
<feOffset dy="0.13451" />
|
||||
<feGaussianBlur stdDeviation="0.13451" />
|
||||
<feColorMatrix
|
||||
type="matrix"
|
||||
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.07 0"
|
||||
/>
|
||||
<feBlend
|
||||
mode="normal"
|
||||
in2="effect1_dropShadow_28_966"
|
||||
result="effect2_dropShadow_28_966"
|
||||
/>
|
||||
<feColorMatrix
|
||||
in="SourceAlpha"
|
||||
type="matrix"
|
||||
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
|
||||
result="hardAlpha"
|
||||
/>
|
||||
<feOffset />
|
||||
<feGaussianBlur stdDeviation="0.0672548" />
|
||||
<feColorMatrix
|
||||
type="matrix"
|
||||
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.19 0"
|
||||
/>
|
||||
<feBlend
|
||||
mode="normal"
|
||||
in2="effect2_dropShadow_28_966"
|
||||
result="effect3_dropShadow_28_966"
|
||||
/>
|
||||
<feBlend
|
||||
mode="normal"
|
||||
in="SourceGraphic"
|
||||
in2="effect3_dropShadow_28_966"
|
||||
result="shape"
|
||||
/>
|
||||
</filter>
|
||||
</defs>
|
||||
</svg>
|
||||
</template>
|
||||
@ -1,39 +0,0 @@
|
||||
<template>
|
||||
<Card v-if="invoice" :title="title">
|
||||
<template #actions-left>
|
||||
<Button route="/billing/invoices"> ← Back </Button>
|
||||
</template>
|
||||
<InvoiceUsageTable :invoice="invoice" @pg="pg = $event" />
|
||||
</Card>
|
||||
</template>
|
||||
<script>
|
||||
import InvoiceUsageTable from './InvoiceUsageTable.vue';
|
||||
export default {
|
||||
name: 'InvoiceUsageCard',
|
||||
props: ['invoice'],
|
||||
components: {
|
||||
InvoiceUsageTable
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
pg: null
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
title() {
|
||||
let pg = this.pg;
|
||||
if (!pg) {
|
||||
return '';
|
||||
}
|
||||
if (!pg.period_start || !pg.period_end) {
|
||||
return `Invoice Details for ${this.invoice}`;
|
||||
}
|
||||
let periodStart = this.$date(pg.period_start);
|
||||
let periodEnd = this.$date(pg.period_end);
|
||||
let start = periodStart.toLocaleString({ month: 'long', day: 'numeric' });
|
||||
let end = periodEnd.toLocaleString({ month: 'short', day: 'numeric' });
|
||||
return `Invoice for ${start} - ${end} ${periodEnd.year}`;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@ -1,150 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<div v-if="pg" class="overflow-x-auto">
|
||||
<table class="text w-full text-sm">
|
||||
<thead>
|
||||
<tr class="text-gray-600">
|
||||
<th class="border-b py-3 pr-2 text-left font-normal">
|
||||
Description
|
||||
</th>
|
||||
<th class="border-b py-3 pr-2 text-left font-normal">Site</th>
|
||||
<th
|
||||
class="whitespace-nowrap border-b py-3 pr-2 text-right font-normal"
|
||||
>
|
||||
Rate
|
||||
</th>
|
||||
<th class="border-b py-3 pr-2 text-right font-normal">Amount</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(row, i) in pg.items" :key="row.idx">
|
||||
<td class="border-b py-3 pr-2">
|
||||
{{ row.description || row.document_name }}
|
||||
</td>
|
||||
<td class="border-b py-3 pr-2">
|
||||
{{ row.site || '-' }}
|
||||
</td>
|
||||
<td class="border-b py-3 pr-2 text-right">
|
||||
{{ row.rate }} x {{ row.quantity }}
|
||||
</td>
|
||||
<td class="border-b py-3 pr-2 text-right font-semibold">
|
||||
{{ pg.formatted.items[i].amount }}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr v-if="pg.total_discount_amount > 0">
|
||||
<td></td>
|
||||
<td class="pb-2 pr-2 pt-4 text-right font-semibold">
|
||||
Total Without Discount
|
||||
</td>
|
||||
<td
|
||||
class="whitespace-nowrap pb-2 pr-2 pt-4 text-right font-semibold"
|
||||
>
|
||||
{{ pg.formatted.total_before_discount }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="pg.total_discount_amount > 0">
|
||||
<td></td>
|
||||
<td class="pb-2 pr-2 pt-4 text-right font-semibold">
|
||||
Total Discount Amount
|
||||
</td>
|
||||
<td
|
||||
class="whitespace-nowrap pb-2 pr-2 pt-4 text-right font-semibold"
|
||||
>
|
||||
{{
|
||||
pg.partner_email && pg.partner_email != $account.team.user
|
||||
? 0
|
||||
: pg.formatted.total_discount_amount
|
||||
}}
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="pg.gst > 0">
|
||||
<td></td>
|
||||
<td class="pb-2 pr-2 pt-4 text-right font-semibold">
|
||||
Total (Without Tax)
|
||||
</td>
|
||||
<td
|
||||
class="whitespace-nowrap pb-2 pr-2 pt-4 text-right font-semibold"
|
||||
>
|
||||
{{ pg.formatted.total_before_tax }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="pg.gst > 0">
|
||||
<td></td>
|
||||
<td class="pb-2 pr-2 pt-4 text-right font-semibold">
|
||||
IGST @ {{ Number($account.billing_info.gst_percentage * 100) }}%
|
||||
</td>
|
||||
<td
|
||||
class="whitespace-nowrap pb-2 pr-2 pt-4 text-right font-semibold"
|
||||
>
|
||||
{{ pg.formatted.gst }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td></td>
|
||||
<td class="pb-2 pr-2 pt-4 text-right font-semibold">Grand Total</td>
|
||||
<td
|
||||
class="whitespace-nowrap pb-2 pr-2 pt-4 text-right font-semibold"
|
||||
>
|
||||
{{
|
||||
pg.partner_email && pg.partner_email != $account.team.user
|
||||
? pg.formatted.total_before_discount
|
||||
: pg.formatted.total
|
||||
}}
|
||||
</td>
|
||||
</tr>
|
||||
<template v-if="pg.total !== pg.amount_due && pg.pagestatus == 1">
|
||||
<tr>
|
||||
<td></td>
|
||||
<td class="pr-2 text-right">Applied Balance:</td>
|
||||
<td class="whitespace-nowrap py-3 pr-2 text-right font-semibold">
|
||||
- {{ pg.formatted.applied_credits }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td></td>
|
||||
<td class="pr-2 text-right">Amount Due:</td>
|
||||
<td class="whitespace-nowrap py-3 pr-2 text-right font-semibold">
|
||||
{{ pg.formatted.amount_due }}
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
<div class="py-20 text-center" v-if="$resources.pg.loading">
|
||||
<Button :loading="true">Loading</Button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
name: 'InvoiceUsageTable',
|
||||
props: ['invoice', 'invoiceDoc'],
|
||||
resources: {
|
||||
pg() {
|
||||
return {
|
||||
url: 'jcloud.api.billing.get_invoice_usage',
|
||||
params: { invoice: this.invoice },
|
||||
auto: this.invoice,
|
||||
onSuccess(pg) {
|
||||
this.$emit('pg', pg);
|
||||
}
|
||||
};
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
invoice(value) {
|
||||
if (value) {
|
||||
this.$resources.pg.fetch();
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
pg() {
|
||||
return this.invoiceDoc || this.$resources.pg.data;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@ -1,30 +0,0 @@
|
||||
<template>
|
||||
<button
|
||||
class="flex self-stretch rounded-lg border bg-white p-3 shadow hover:border-gray-300 focus:outline-none"
|
||||
>
|
||||
<Avatar
|
||||
class="shrink-0"
|
||||
size="2xl"
|
||||
shape="square"
|
||||
:image="app.image"
|
||||
:label="app.title"
|
||||
/>
|
||||
<div class="ml-3 w-full">
|
||||
<div class="flex items-center justify-between">
|
||||
<h2 class="text-left text-xl font-bold">
|
||||
{{ app.title }}
|
||||
</h2>
|
||||
<Badge :label="app.status" />
|
||||
</div>
|
||||
<p class="pt-1 text-left text-base text-gray-600">
|
||||
{{ app.description }}
|
||||
</p>
|
||||
</div>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: ['app']
|
||||
};
|
||||
</script>
|
||||
@ -1,184 +0,0 @@
|
||||
<template>
|
||||
<Card
|
||||
class="md:col-span-2"
|
||||
title="App Descriptions"
|
||||
subtitle="Details about your app"
|
||||
>
|
||||
<div class="divide-y" v-if="app">
|
||||
<ListItem title="Summary" :description="$sanitize(app.description)">
|
||||
<template #actions>
|
||||
<Button icon-left="edit" @click="showEditSummaryDialog = true">
|
||||
Edit
|
||||
</Button>
|
||||
</template>
|
||||
</ListItem>
|
||||
<Dialog
|
||||
:options="{
|
||||
title: 'Update App Summary',
|
||||
actions: [
|
||||
{
|
||||
label: 'Save Changes',
|
||||
variant: 'solid',
|
||||
loading: $resources.updateAppSummary.loading,
|
||||
onClick: () => $resources.updateAppSummary.submit()
|
||||
}
|
||||
]
|
||||
}"
|
||||
v-model="showEditSummaryDialog"
|
||||
>
|
||||
<template v-slot:body-content>
|
||||
<FormControl
|
||||
label="Summary of the app"
|
||||
type="textarea"
|
||||
v-model="app.description"
|
||||
/>
|
||||
<ErrorMessage
|
||||
class="mt-4"
|
||||
:message="$resources.updateAppSummary.error"
|
||||
/>
|
||||
</template>
|
||||
</Dialog>
|
||||
<div class="py-3">
|
||||
<ListItem title="Long Description">
|
||||
<template #actions>
|
||||
<Button icon-left="edit" @click="showEditDescriptionDialog = true">
|
||||
Edit
|
||||
</Button>
|
||||
</template>
|
||||
</ListItem>
|
||||
<div
|
||||
class="prose mt-1 text-gray-600"
|
||||
v-if="app.description"
|
||||
v-html="descriptionHTML"
|
||||
></div>
|
||||
<Dialog
|
||||
:options="{
|
||||
title: 'Update App Description',
|
||||
size: '5xl',
|
||||
actions: [
|
||||
{
|
||||
label: 'Save Changes',
|
||||
variant: 'solid',
|
||||
loading: $resources.updateAppDescription.loading,
|
||||
onClick: () => $resources.updateAppDescription.submit()
|
||||
}
|
||||
]
|
||||
}"
|
||||
:dismissable="true"
|
||||
v-model="showEditDescriptionDialog"
|
||||
width="full"
|
||||
>
|
||||
<template v-slot:body-content>
|
||||
<div class="grid grid-cols-1 gap-5 md:grid-cols-2">
|
||||
<FormControl
|
||||
:rows="30"
|
||||
type="textarea"
|
||||
v-model="app.long_description"
|
||||
/>
|
||||
<div class="prose" v-html="descriptionHTML"></div>
|
||||
</div>
|
||||
|
||||
<ErrorMessage
|
||||
class="mt-4"
|
||||
:message="$resources.updateAppDescription.error"
|
||||
/>
|
||||
</template>
|
||||
</Dialog>
|
||||
</div>
|
||||
</div>
|
||||
<template #actions>
|
||||
<Button
|
||||
:loading="$resources.fetchReadme.loading"
|
||||
@click="$resources.fetchReadme.submit()"
|
||||
>
|
||||
Fetch Readme
|
||||
</Button>
|
||||
</template>
|
||||
</Card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import MarkdownIt from 'markdown-it';
|
||||
import { notify } from '@/utils/toast';
|
||||
|
||||
export default {
|
||||
name: 'MarketplaceAppDescriptions',
|
||||
props: {
|
||||
app: Object
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showEditSummaryDialog: false,
|
||||
showEditDescriptionDialog: false
|
||||
};
|
||||
},
|
||||
resources: {
|
||||
updateAppSummary() {
|
||||
let { name, description } = this.app;
|
||||
return {
|
||||
url: 'jcloud.api.marketplace.update_app_summary',
|
||||
params: {
|
||||
name,
|
||||
summary: description
|
||||
},
|
||||
onSuccess() {
|
||||
this.notifySuccess('App Summary Updated!');
|
||||
this.showEditSummaryDialog = false;
|
||||
}
|
||||
};
|
||||
},
|
||||
updateAppDescription() {
|
||||
let { name, long_description } = this.app;
|
||||
return {
|
||||
url: 'jcloud.api.marketplace.update_app_description',
|
||||
params: {
|
||||
name,
|
||||
description: long_description
|
||||
},
|
||||
onSuccess() {
|
||||
this.notifySuccess('App Description Updated!');
|
||||
this.showEditDescriptionDialog = false;
|
||||
}
|
||||
};
|
||||
},
|
||||
fetchReadme() {
|
||||
return {
|
||||
url: 'jcloud.api.marketplace.fetch_readme',
|
||||
params: { name: this.app.name },
|
||||
onSuccess() {
|
||||
notify({
|
||||
title: 'Successfully fetched latest readme',
|
||||
message: 'Long description updated!',
|
||||
icon: 'check',
|
||||
color: 'green'
|
||||
});
|
||||
},
|
||||
onError(e) {
|
||||
notify({
|
||||
title: e,
|
||||
color: 'red',
|
||||
icon: 'x'
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
descriptionHTML() {
|
||||
if (this.app && this.app.long_description) {
|
||||
return MarkdownIt().render(this.app.long_description);
|
||||
}
|
||||
return '';
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
notifySuccess(message) {
|
||||
notify({
|
||||
title: message,
|
||||
icon: 'check',
|
||||
color: 'green'
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@ -1,313 +0,0 @@
|
||||
<template>
|
||||
<Card title="App Releases">
|
||||
<div v-if="sources.length">
|
||||
<div class="flex flex-row items-baseline">
|
||||
<FormControl
|
||||
type="select"
|
||||
v-if="sources.length > 1"
|
||||
v-model="selectedSource"
|
||||
:options="
|
||||
sources.map(s => ({
|
||||
label: `${s.source_information.repository}:${s.source_information.branch}`,
|
||||
value: s.source
|
||||
}))
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!sources.length">
|
||||
<p class="mt-3 text-center text-lg text-gray-600">
|
||||
No published source exist for this app. Please contact support to
|
||||
publish a version of this app.
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="releasesList.length === 0 && !$resources.releases.list.loading"
|
||||
>
|
||||
<p class="mt-3 text-center text-lg text-gray-600">
|
||||
No app releases have been created for this version.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div v-else>
|
||||
<div class="divide-y">
|
||||
<div
|
||||
class="grid grid-cols-3 items-center gap-x-8 py-4 text-base text-gray-600 md:grid-cols-6"
|
||||
>
|
||||
<span class="md:col-span-2">Commit Message</span>
|
||||
<span class="hidden md:inline">Tag</span>
|
||||
<span class="hidden md:inline">Author</span>
|
||||
<span>Status</span>
|
||||
<span></span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-for="release in releasesList"
|
||||
:key="release.name"
|
||||
class="grid grid-cols-3 items-center gap-x-8 py-4 text-base text-gray-900 md:grid-cols-6"
|
||||
>
|
||||
<p
|
||||
class="max-w-md truncate text-base font-medium text-gray-700 md:col-span-2"
|
||||
>
|
||||
{{ release.message }}
|
||||
</p>
|
||||
<CommitTag
|
||||
class="hidden md:flex"
|
||||
:tag="release.tag || release.hash.slice(0, 6)"
|
||||
:link="getCommitUrl(release.hash)"
|
||||
/>
|
||||
<span class="hidden text-gray-600 md:inline">
|
||||
{{ release.author }}
|
||||
</span>
|
||||
<span>
|
||||
<Badge v-if="release.status != 'Draft'" :label="release.status" />
|
||||
</span>
|
||||
<span class="text-right">
|
||||
<Button
|
||||
v-if="isPublishable(release)"
|
||||
:loading="
|
||||
$resources.createApprovalRequest.loading ||
|
||||
$resources.latestApproved.loading
|
||||
"
|
||||
@click="confirmApprovalRequest(release.name)"
|
||||
>
|
||||
Publish
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
v-else-if="release.status == 'Awaiting Approval'"
|
||||
@click="confirmCancelRequest(release.name)"
|
||||
>Cancel</Button
|
||||
>
|
||||
|
||||
<Button
|
||||
v-else-if="release.status == 'Rejected'"
|
||||
@click="showFeedback(release)"
|
||||
>View Feedback</Button
|
||||
>
|
||||
</span>
|
||||
</div>
|
||||
<Dialog
|
||||
:options="{ title: 'Reason for Rejection' }"
|
||||
v-model="showRejectionFeedbackDialog"
|
||||
>
|
||||
<template v-slot:body-content>
|
||||
<div class="prose text-lg" v-html="rejectionFeedback"></div>
|
||||
</template>
|
||||
</Dialog>
|
||||
|
||||
<div class="py-3">
|
||||
<Button
|
||||
@click="$resources.releases.next()"
|
||||
v-if="$resources.releases.hasNextPage"
|
||||
:loading="$resources.releases.list.loading"
|
||||
loadingText="Loading..."
|
||||
>Load More</Button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import CommitTag from './utils/CommitTag.vue';
|
||||
import { notify } from '@/utils/toast';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
app: {
|
||||
type: Object
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showRejectionFeedbackDialog: false,
|
||||
rejectionFeedback: '',
|
||||
selectedSource: null
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.$socket.emit('pagetype_subscribe', 'App Release');
|
||||
this.$socket.on('list_update', this.releaseStateUpdate);
|
||||
if (this.sources.length > 0) {
|
||||
this.selectedSource = this.sources[0].source;
|
||||
}
|
||||
},
|
||||
beforeMount() {
|
||||
this.$socket.emit('pagetype_unsubscribe', 'App Release');
|
||||
this.$socket.off('list_update', this.releaseStateUpdate);
|
||||
},
|
||||
resources: {
|
||||
releases() {
|
||||
return {
|
||||
type: 'list',
|
||||
pagetype: 'App Release',
|
||||
url: 'jcloud.api.marketplace.releases',
|
||||
filters: {
|
||||
app: this.app.app,
|
||||
source: this.selectedSource
|
||||
},
|
||||
start: 0,
|
||||
pageLength: 15,
|
||||
auto: true
|
||||
};
|
||||
},
|
||||
appSource() {
|
||||
return {
|
||||
url: 'jcloud.api.marketplace.get_app_source',
|
||||
params: {
|
||||
name: this.selectedSource
|
||||
}
|
||||
};
|
||||
},
|
||||
latestApproved() {
|
||||
return {
|
||||
url: 'jcloud.api.marketplace.latest_approved_release',
|
||||
params: {
|
||||
source: this.selectedSource
|
||||
},
|
||||
auto: true
|
||||
};
|
||||
},
|
||||
createApprovalRequest() {
|
||||
return {
|
||||
url: 'jcloud.api.marketplace.create_approval_request',
|
||||
onSuccess() {
|
||||
this.resetReleaseListState();
|
||||
},
|
||||
onError(err) {
|
||||
const requestAlreadyExists = err.messages.some(msg =>
|
||||
msg.includes('already awaiting approval')
|
||||
);
|
||||
|
||||
if (requestAlreadyExists)
|
||||
notify({
|
||||
title: 'Request already exists',
|
||||
message: err.messages.join('\n'),
|
||||
color: 'red',
|
||||
icon: 'x'
|
||||
});
|
||||
else
|
||||
notify({
|
||||
title: 'Error',
|
||||
message: err.messages.join('\n'),
|
||||
color: 'red',
|
||||
icon: 'x'
|
||||
});
|
||||
}
|
||||
};
|
||||
},
|
||||
cancelApprovalRequest() {
|
||||
return {
|
||||
url: 'jcloud.api.marketplace.cancel_approval_request',
|
||||
onSuccess() {
|
||||
this.resetReleaseListState();
|
||||
}
|
||||
};
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
isPublishable(release) {
|
||||
return (
|
||||
release.status == 'Draft' &&
|
||||
(!this.latestApprovedOn ||
|
||||
this.$date(release.creation) > this.latestApprovedOn)
|
||||
);
|
||||
},
|
||||
createApprovalRequest(appRelease) {
|
||||
let { app } = this.app;
|
||||
this.$resources.createApprovalRequest.submit({
|
||||
name: app,
|
||||
app_release: appRelease
|
||||
});
|
||||
},
|
||||
cancelApprovalRequest(appRelease) {
|
||||
let { app } = this.app;
|
||||
this.$resources.cancelApprovalRequest.submit({
|
||||
marketplace_app: app,
|
||||
app_release: appRelease
|
||||
});
|
||||
},
|
||||
resetReleaseListState() {
|
||||
this.$resources.releases.reload();
|
||||
this.$resources.latestApproved.reload();
|
||||
},
|
||||
showFeedback(appRelease) {
|
||||
this.showRejectionFeedbackDialog = true;
|
||||
this.rejectionFeedback = appRelease.reason_for_rejection;
|
||||
},
|
||||
confirmApprovalRequest(appRelease) {
|
||||
this.$confirm({
|
||||
title: 'Publish Release',
|
||||
message:
|
||||
'Are you sure you want to publish this release to marketplace? Upon confirmation, the release will be sent for approval by the review team.',
|
||||
actionLabel: 'Publish',
|
||||
action: closeDialog => {
|
||||
closeDialog();
|
||||
this.createApprovalRequest(appRelease);
|
||||
}
|
||||
});
|
||||
},
|
||||
confirmCancelRequest(appRelease) {
|
||||
this.$confirm({
|
||||
title: 'Cancel Release Approval Request',
|
||||
message:
|
||||
'Are you sure you want to <strong>cancel</strong> the publish request for this release?',
|
||||
actionLabel: 'Proceed',
|
||||
actionColor: 'red',
|
||||
action: closeDialog => {
|
||||
closeDialog();
|
||||
this.cancelApprovalRequest(appRelease);
|
||||
}
|
||||
});
|
||||
},
|
||||
getCommitUrl(releaseHash) {
|
||||
return this.repoUrl ? `${this.repoUrl}/commit/${releaseHash}` : '';
|
||||
},
|
||||
releaseStateUpdate(data) {
|
||||
if (this.selectedSource && data.source == this.selectedSource) {
|
||||
this.resetReleaseListState();
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
releasesList() {
|
||||
if (!this.$resources.releases.data) {
|
||||
return [];
|
||||
}
|
||||
return this.$resources.releases.data;
|
||||
},
|
||||
latestApprovedOn() {
|
||||
if (
|
||||
this.$resources.latestApproved.data &&
|
||||
!this.$resources.latestApproved.loading
|
||||
) {
|
||||
return this.$date(this.$resources.latestApproved.data.creation);
|
||||
}
|
||||
},
|
||||
sources() {
|
||||
// Return only the unique sources
|
||||
let tempArray = [];
|
||||
for (let source of this.app.sources) {
|
||||
if (!tempArray.find(x => x.source === source.source)) {
|
||||
tempArray.push(source);
|
||||
}
|
||||
}
|
||||
return tempArray;
|
||||
},
|
||||
repoUrl() {
|
||||
return this.$resources.appSource?.data?.repository_url;
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
selectedSource(value) {
|
||||
if (value) {
|
||||
this.resetReleaseListState();
|
||||
this.$resources.appSource.submit({ name: value });
|
||||
}
|
||||
}
|
||||
},
|
||||
components: { CommitTag }
|
||||
};
|
||||
</script>
|
||||
@ -1,86 +0,0 @@
|
||||
<template>
|
||||
<Card
|
||||
class="md:col-span-2"
|
||||
title="Screenshots"
|
||||
subtitle="Add screenshots to show on the marketplace website"
|
||||
>
|
||||
<div>
|
||||
<div class="flex flex-row">
|
||||
<Avatar
|
||||
size="lg"
|
||||
class="mx-1 cursor-pointer hover:bg-red-100 hover:opacity-20"
|
||||
shape="square"
|
||||
:image="image.image"
|
||||
v-for="(image, index) in app.screenshots"
|
||||
@click="removeScreenshot(image.image, index)"
|
||||
>
|
||||
</Avatar>
|
||||
<FileUploader
|
||||
@success="onAppImageAdd"
|
||||
@failure="onAppImageUploadError"
|
||||
fileTypes="image/*"
|
||||
:upload-args="{
|
||||
pagetype: 'Marketplace App',
|
||||
docname: app.name,
|
||||
method: 'jcloud.api.marketplace.add_app_screenshot'
|
||||
}"
|
||||
>
|
||||
<template v-slot="{ openFileSelector, uploading, progress, error }">
|
||||
<Button
|
||||
class="ml-1 h-12 w-12"
|
||||
@click="openFileSelector()"
|
||||
icon="plus"
|
||||
>
|
||||
</Button>
|
||||
</template>
|
||||
</FileUploader>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import FileUploader from '@/components/FileUploader.vue';
|
||||
import { notify } from '@/utils/toast';
|
||||
|
||||
export default {
|
||||
name: 'MarketplaceAppScreenshots',
|
||||
props: {
|
||||
app: Object
|
||||
},
|
||||
components: {
|
||||
FileUploader
|
||||
},
|
||||
methods: {
|
||||
onAppImageAdd(file) {
|
||||
this.app.screenshots.push({ image: file });
|
||||
notify({
|
||||
title: 'Screenshot was added successfully!',
|
||||
icon: 'check',
|
||||
color: 'green'
|
||||
});
|
||||
},
|
||||
removeScreenshot(file, index) {
|
||||
this.$resources.removeScreenshot.submit({
|
||||
name: this.app.name,
|
||||
file: file
|
||||
});
|
||||
this.app.screenshots.splice(index, 1);
|
||||
},
|
||||
onAppImageUploadError(errorMessage) {
|
||||
notify({
|
||||
title: errorMessage,
|
||||
color: 'red',
|
||||
icon: 'x'
|
||||
});
|
||||
}
|
||||
},
|
||||
resources: {
|
||||
removeScreenshot(file) {
|
||||
return {
|
||||
url: 'jcloud.api.marketplace.remove_app_screenshot'
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@ -1,79 +0,0 @@
|
||||
<template>
|
||||
<teleport to="#modals">
|
||||
<div
|
||||
v-show="show"
|
||||
class="fixed inset-0 flex items-center justify-center px-4 py-4"
|
||||
>
|
||||
<div
|
||||
v-show="show"
|
||||
class="fixed inset-0 transition-opacity"
|
||||
@click="onBackdropClick"
|
||||
>
|
||||
<div class="absolute inset-0 bg-gray-900 opacity-75"></div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-show="show"
|
||||
class="w-full transform overflow-auto rounded-lg bg-white shadow-xl transition-all"
|
||||
:class="widthClasses"
|
||||
style="max-height: 95vh"
|
||||
>
|
||||
<slot></slot>
|
||||
</div>
|
||||
</div>
|
||||
</teleport>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'Modal',
|
||||
emits: ['update:show'],
|
||||
props: {
|
||||
show: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
dismissable: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
width: {
|
||||
default: 'auto'
|
||||
}
|
||||
},
|
||||
created() {
|
||||
if (!this.dismissable) return;
|
||||
this.escapeListener = e => {
|
||||
if (e.key === 'Escape') {
|
||||
this.hide();
|
||||
}
|
||||
};
|
||||
document.addEventListener('keydown', this.escapeListener);
|
||||
},
|
||||
unmounted() {
|
||||
document.removeEventListener('keydown', this.escapeListener);
|
||||
},
|
||||
methods: {
|
||||
onBackdropClick() {
|
||||
if (!this.dismissable) return;
|
||||
this.hide();
|
||||
},
|
||||
hide() {
|
||||
this.$emit('update:show', false);
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
widthClasses() {
|
||||
if (this.width === 'auto') {
|
||||
return ['sm:max-w-lg'];
|
||||
} else if (this.width === 'half') {
|
||||
return ['sm:max-w-2xl'];
|
||||
} else if (this.width === 'w-2/6') {
|
||||
return ['w-2/6'];
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@ -1,143 +0,0 @@
|
||||
<template>
|
||||
<nav class="border-b bg-gray-50 px-4">
|
||||
<div class="z-10 mx-auto md:container">
|
||||
<div class="flex h-16 items-center justify-between">
|
||||
<div class="flex items-center">
|
||||
<div class="shrink-0">
|
||||
<router-link to="/">
|
||||
<JingrowCloudLogo class="h-6" />
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
<div class="-mr-2 flex md:hidden">
|
||||
<button
|
||||
class="focus:shadow-outline-gray inline-flex items-center justify-center rounded-md p-2 text-gray-700 focus:outline-none"
|
||||
@click="mobileMenuOpen = !mobileMenuOpen"
|
||||
>
|
||||
<FeatherIcon v-if="!mobileMenuOpen" name="menu" class="h-6 w-6" />
|
||||
<FeatherIcon v-else name="x" class="h-6 w-6" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="md:hidden" :class="mobileMenuOpen ? 'block' : 'hidden'">
|
||||
<div class="px-4 pb-2">
|
||||
<router-link
|
||||
v-for="item in items"
|
||||
:key="item.label"
|
||||
:to="item.route"
|
||||
v-slot="{ href, route, navigate, isActive, isExactActive }"
|
||||
>
|
||||
<a
|
||||
:class="[
|
||||
(item.route == '/' ? isExactActive : isActive)
|
||||
? 'bg-gray-200'
|
||||
: 'text-gray-900 hover:bg-gray-50'
|
||||
]"
|
||||
:href="href"
|
||||
@click="navigate"
|
||||
class="block rounded-md px-3 py-2 text-sm font-medium focus:outline-none"
|
||||
>
|
||||
{{ item.label }}
|
||||
</a>
|
||||
</router-link>
|
||||
</div>
|
||||
<div class="border-t pb-3 pt-4">
|
||||
<div class="flex items-center px-4">
|
||||
<div class="shrink-0">
|
||||
<Avatar
|
||||
v-if="$account.user"
|
||||
:label="$account.user.first_name"
|
||||
:image="$account.user.user_image"
|
||||
/>
|
||||
</div>
|
||||
<div class="ml-3" v-if="$account.user">
|
||||
<div class="text-base font-medium leading-none">
|
||||
{{ $account.user.first_name }} {{ $account.user.last_name }}
|
||||
</div>
|
||||
<div class="mt-1 text-sm font-medium leading-none text-gray-400">
|
||||
{{ $account.user.email }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-3 space-y-3 px-2">
|
||||
<a
|
||||
href="/support/tickets"
|
||||
target="_blank"
|
||||
class="block rounded-md px-3 pt-4 text-base font-medium focus:outline-none"
|
||||
>
|
||||
Support
|
||||
</a>
|
||||
<a
|
||||
href="/docs"
|
||||
target="_blank"
|
||||
class="block rounded-md px-3 text-base font-medium focus:outline-none"
|
||||
>
|
||||
Docs
|
||||
</a>
|
||||
<a
|
||||
href="#"
|
||||
class="block rounded-md px-3 text-base font-medium focus:outline-none"
|
||||
@click.prevent="$auth.logout"
|
||||
>
|
||||
Logout
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import JingrowCloudLogo from '@/components/icons/JingrowCloudLogo.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
JingrowCloudLogo
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
mobileMenuOpen: false
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
items() {
|
||||
return [
|
||||
{
|
||||
label: 'Sites',
|
||||
route: '/sites',
|
||||
highlight: () => {
|
||||
return this.$route.fullPath.endsWith('/sites');
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'Benches',
|
||||
route: '/groups',
|
||||
highlight: () => {
|
||||
return this.$route.fullPath.endsWith('/sites');
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'Apps',
|
||||
route: '/marketplace',
|
||||
highlight: () => {
|
||||
return this.$route.fullPath.includes('/marketplace');
|
||||
},
|
||||
condition: () => this.$account.team?.is_developer
|
||||
},
|
||||
{
|
||||
label: 'Billing',
|
||||
route: '/billing',
|
||||
highlight: () => {
|
||||
return this.$route.fullPath.endsWith('/sites');
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'Settings',
|
||||
route: '/settings/profile'
|
||||
}
|
||||
].filter(d => (d.condition ? d.condition() : true));
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@ -1,108 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<div v-for="installation in options.installations" :key="installation.id">
|
||||
<details class="cursor-pointer">
|
||||
<summary
|
||||
class="flex w-full select-none items-center space-x-2 rounded px-2 py-3 text-base hover:bg-gray-50"
|
||||
>
|
||||
<img
|
||||
class="h-6 w-6 rounded bg-blue-100 object-cover"
|
||||
:src="installation.image"
|
||||
:alt="`${installation.login} image`"
|
||||
/>
|
||||
<span>
|
||||
{{ installation.login }}
|
||||
</span>
|
||||
</summary>
|
||||
<div class="mb-4 ml-4">
|
||||
<button
|
||||
class="w-full rounded-md border-2 px-3 py-2 text-base text-gray-900 focus:outline-none"
|
||||
:class="
|
||||
selectedRepo === repo
|
||||
? 'border-blue-500 bg-blue-50'
|
||||
: 'border-transparent hover:bg-gray-50'
|
||||
"
|
||||
v-for="repo in installation.repos"
|
||||
:key="repo.id"
|
||||
@click="selectRepo(repo, installation)"
|
||||
>
|
||||
<div class="flex w-full items-center">
|
||||
<FeatherIcon
|
||||
:name="repo.private ? 'lock' : 'book'"
|
||||
class="mr-2 h-4 w-4"
|
||||
/>
|
||||
<span class="text-lg font-medium">
|
||||
{{ repo.name }}
|
||||
</span>
|
||||
<button
|
||||
class="ml-auto"
|
||||
v-if="selectedRepo === repo"
|
||||
@click.stop="selectRepo(null, null)"
|
||||
>
|
||||
<FeatherIcon name="x" class="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
<div v-show="selectedRepo === repo">
|
||||
<Dropdown class="ml-6 mt-2 text-left" :options="branchOptions">
|
||||
<template v-slot="{ open }">
|
||||
<Button
|
||||
type="white"
|
||||
:loading="repositoryResource.loading"
|
||||
loadingText="Loading branches..."
|
||||
icon-right="chevron-down"
|
||||
>
|
||||
{{ selectedBranch || 'Select branch' }}
|
||||
</Button>
|
||||
</template>
|
||||
</Dropdown>
|
||||
</div>
|
||||
</button>
|
||||
<p class="mt-4 text-sm text-gray-700">
|
||||
Don't see your repository here?
|
||||
<Link :href="installation.url" class="font-medium">
|
||||
Add from GitHub
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
name: 'NewAppRepositories',
|
||||
emits: [
|
||||
'update:selectRepo',
|
||||
'update:selectedBranch',
|
||||
'update:selectedInstallation'
|
||||
],
|
||||
props: [
|
||||
'options',
|
||||
'repositoryResource',
|
||||
'selectedRepo',
|
||||
'selectedInstallation',
|
||||
'selectedBranch'
|
||||
],
|
||||
methods: {
|
||||
selectRepo(repo, installation) {
|
||||
if (repo === this.selectedRepo) return;
|
||||
this.$emit('update:selectedRepo', repo);
|
||||
this.$emit('update:selectedInstallation', installation);
|
||||
this.$emit('update:selectedBranch', null);
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
branchOptions() {
|
||||
if (this.repositoryResource.loading || !this.repositoryResource.data) {
|
||||
return [];
|
||||
}
|
||||
return (this.repositoryResource.data.branches || []).map(d => {
|
||||
return {
|
||||
label: d.name,
|
||||
onClick: () => this.$emit('update:selectedBranch', d.name)
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@ -1,59 +0,0 @@
|
||||
<template>
|
||||
<div
|
||||
class="pointer-events-auto w-full max-w-sm rounded-lg bg-white shadow-lg"
|
||||
>
|
||||
<div class="shadow-xs overflow-hidden rounded-lg">
|
||||
<div class="p-4">
|
||||
<div class="flex items-start">
|
||||
<div class="mr-3 shrink-0" v-if="icon">
|
||||
<div class="h-6 w-6 rounded-full p-1" :class="iconContainerClass">
|
||||
<FeatherIcon :name="icon" :class="iconClass" class="h-4 w-4" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<p class="text-sm font-medium leading-6 text-gray-900">
|
||||
{{ title }}
|
||||
</p>
|
||||
<p
|
||||
v-if="message"
|
||||
v-html="message"
|
||||
class="mt-1 text-sm leading-5 text-gray-700"
|
||||
/>
|
||||
</div>
|
||||
<div class="ml-3 flex h-6 shrink-0 items-center">
|
||||
<button @click="$emit('dismiss', id)" class="focus:outline-none">
|
||||
<FeatherIcon name="x" class="h-5 w-5 text-gray-800" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'Notification',
|
||||
props: ['id', 'title', 'message', 'color', 'icon'],
|
||||
computed: {
|
||||
iconClass() {
|
||||
return {
|
||||
red: 'text-red-500',
|
||||
green: 'text-green-500',
|
||||
yellow: 'text-yellow-500',
|
||||
blue: 'text-blue-500',
|
||||
gray: 'text-gray-500'
|
||||
}[this.color || 'gray'];
|
||||
},
|
||||
iconContainerClass() {
|
||||
return {
|
||||
red: 'bg-red-100',
|
||||
green: 'bg-green-100',
|
||||
yellow: 'bg-yellow-100',
|
||||
blue: 'bg-blue-100',
|
||||
gray: 'bg-gray-100'
|
||||
}[this.color || 'gray'];
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@ -1,20 +0,0 @@
|
||||
<script setup>
|
||||
import Notification from './Notification.vue';
|
||||
import { hideNotification, notifications } from '@/utils/toast';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="pointer-events-none fixed inset-0 flex items-start justify-end px-4 py-6 sm:p-6"
|
||||
>
|
||||
<div class="fixed top-15">
|
||||
<Notification
|
||||
v-bind="props"
|
||||
class="mb-4"
|
||||
:key="i"
|
||||
v-for="(props, i) in notifications"
|
||||
@dismiss="hideNotification"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@ -1,25 +0,0 @@
|
||||
<template>
|
||||
<div
|
||||
class="m-1 flex h-12 w-12 items-center justify-center rounded-full bg-white shadow"
|
||||
>
|
||||
<svg
|
||||
width="21"
|
||||
height="25"
|
||||
viewBox="0 0 21 25"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
class="text-blue-500"
|
||||
d="M0 8.9V15.5C0 16.6324 1.20672 17.3564 2.20588 16.8235L14.2059 10.4235C14.6947 10.1628 15 9.65397 15 9.1V2.5C15 1.36762 13.7933 0.643586 12.7941 1.17647L0.794118 7.57647C0.305322 7.83716 0 8.34603 0 8.9Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
class="text-blue-400"
|
||||
opacity="0.3"
|
||||
d="M6 15.7V22.3C6 23.4324 7.20672 24.1564 8.20588 23.6235L20.2059 17.2235C20.6947 16.9628 21 16.454 21 15.9V9.29999C21 8.16761 19.7933 7.44357 18.7941 7.97646L6.79412 14.3765C6.30532 14.6371 6 15.146 6 15.7Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</template>
|
||||
@ -1,194 +0,0 @@
|
||||
<template>
|
||||
<div ref="reference">
|
||||
<div class="h-full">
|
||||
<slot name="target" :togglePopover="togglePopover"></slot>
|
||||
</div>
|
||||
<teleport to="#popovers">
|
||||
<div
|
||||
ref="popover"
|
||||
:class="popoverClass"
|
||||
class="popover-container relative z-50 rounded-md border bg-white shadow-md"
|
||||
v-show="isOpen"
|
||||
>
|
||||
<div v-if="!hideArrow" class="popover-arrow" ref="popover-arrow"></div>
|
||||
<slot name="content" :togglePopover="togglePopover"></slot>
|
||||
</div>
|
||||
</teleport>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { createPopper } from '@popperjs/core';
|
||||
|
||||
export default {
|
||||
name: 'Popover',
|
||||
props: {
|
||||
hideArrow: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
showPopup: {
|
||||
default: null
|
||||
},
|
||||
right: Boolean,
|
||||
placement: {
|
||||
type: String,
|
||||
default: 'bottom-start'
|
||||
},
|
||||
popoverClass: [String, Object, Array]
|
||||
},
|
||||
emits: ['init', 'open', 'close'],
|
||||
watch: {
|
||||
showPopup(value) {
|
||||
if (value === true) {
|
||||
this.open();
|
||||
}
|
||||
if (value === false) {
|
||||
this.close();
|
||||
}
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isOpen: false,
|
||||
listener: null
|
||||
};
|
||||
},
|
||||
activated() {
|
||||
this.setupListener();
|
||||
},
|
||||
mounted() {
|
||||
this.setupListener();
|
||||
},
|
||||
deactivated() {
|
||||
this.close();
|
||||
},
|
||||
unmounted() {
|
||||
this.destroyPopperAndRemoveListener();
|
||||
},
|
||||
methods: {
|
||||
setupListener() {
|
||||
if (this.listener) {
|
||||
return;
|
||||
}
|
||||
|
||||
let listener = e => {
|
||||
let $els = [this.$refs.reference, this.$refs.popover];
|
||||
let insideClick = $els.some(
|
||||
$el => $el && (e.target === $el || $el.contains(e.target))
|
||||
);
|
||||
if (insideClick) {
|
||||
return;
|
||||
}
|
||||
this.close();
|
||||
};
|
||||
|
||||
if (this.show == null) {
|
||||
document.addEventListener('click', listener);
|
||||
}
|
||||
|
||||
this.listener = listener;
|
||||
},
|
||||
destroyPopperAndRemoveListener() {
|
||||
if (this.isOpen) {
|
||||
this.close();
|
||||
}
|
||||
|
||||
this.popper && this.popper.destroy();
|
||||
|
||||
if (this.listener) {
|
||||
document.removeEventListener('click', this.listener);
|
||||
this.listener = null;
|
||||
}
|
||||
},
|
||||
setupPopper() {
|
||||
if (!this.popper) {
|
||||
this.popper = createPopper(this.$refs.reference, this.$refs.popover, {
|
||||
placement: this.placement,
|
||||
modifiers: !this.hideArrow
|
||||
? [
|
||||
{
|
||||
name: 'arrow',
|
||||
options: {
|
||||
element: this.$refs['popover-arrow']
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'offset',
|
||||
options: {
|
||||
offset: [0, 10]
|
||||
}
|
||||
}
|
||||
]
|
||||
: []
|
||||
});
|
||||
} else {
|
||||
this.popper.update();
|
||||
}
|
||||
this.$emit('init');
|
||||
},
|
||||
togglePopover(flag) {
|
||||
if (flag == null) {
|
||||
flag = !this.isOpen;
|
||||
}
|
||||
flag = Boolean(flag);
|
||||
if (flag) {
|
||||
this.open();
|
||||
} else {
|
||||
this.close();
|
||||
}
|
||||
},
|
||||
open() {
|
||||
if (this.isOpen) {
|
||||
return;
|
||||
}
|
||||
this.isOpen = true;
|
||||
this.$nextTick(() => {
|
||||
this.setupPopper();
|
||||
});
|
||||
this.$emit('open');
|
||||
},
|
||||
close() {
|
||||
if (!this.isOpen) {
|
||||
return;
|
||||
}
|
||||
this.isOpen = false;
|
||||
this.$emit('close');
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
<style scoped>
|
||||
.popover-arrow,
|
||||
.popover-arrow::after {
|
||||
position: absolute;
|
||||
width: theme('spacing.4');
|
||||
height: theme('spacing.4');
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.popover-arrow::after {
|
||||
content: '';
|
||||
background: white;
|
||||
transform: rotate(45deg);
|
||||
border-top: 1px solid theme('borderColor.gray.400');
|
||||
border-left: 1px solid theme('borderColor.gray.400');
|
||||
border-top-left-radius: 6px;
|
||||
}
|
||||
|
||||
.popover-container[data-popper-placement^='top'] > .popover-arrow {
|
||||
bottom: calc(theme('spacing.2') * -1);
|
||||
}
|
||||
|
||||
.popover-container[data-popper-placement^='bottom'] > .popover-arrow {
|
||||
top: calc(theme('spacing.2') * -1);
|
||||
}
|
||||
|
||||
.popover-container[data-popper-placement^='left'] > .popover-arrow {
|
||||
right: calc(theme('spacing.2') * -1);
|
||||
}
|
||||
|
||||
.popover-container[data-popper-placement^='right'] > .popover-arrow {
|
||||
left: calc(theme('spacing.2') * -1);
|
||||
}
|
||||
</style>
|
||||
@ -1,61 +0,0 @@
|
||||
<template>
|
||||
<svg
|
||||
viewBox="0 0 48 24"
|
||||
preserveAspectRatio="xMidYMin slice"
|
||||
class="h-12 w-12"
|
||||
>
|
||||
<circle cx="24" cy="24" r="9" fill="#fff"></circle>
|
||||
<circle
|
||||
class="stroke-current text-gray-200"
|
||||
cx="24"
|
||||
cy="24"
|
||||
r="9"
|
||||
fill="transparent"
|
||||
stroke-width="4"
|
||||
></circle>
|
||||
<circle
|
||||
class="stroke-current"
|
||||
:class="colorClass"
|
||||
cx="24"
|
||||
cy="24"
|
||||
r="9"
|
||||
fill="transparent"
|
||||
stroke-width="4"
|
||||
:stroke-dasharray="circumference"
|
||||
:stroke-dashoffset="dashOffset"
|
||||
></circle>
|
||||
</svg>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
name: 'ProgressArc',
|
||||
props: ['percentage'],
|
||||
computed: {
|
||||
circumference() {
|
||||
return 2 * Math.PI * 9;
|
||||
},
|
||||
dashOffset() {
|
||||
let halfCircumference = this.circumference / 2;
|
||||
if (isNaN(this.percentage)) {
|
||||
return halfCircumference;
|
||||
}
|
||||
let percentage = this.percentage;
|
||||
if (percentage > 100) {
|
||||
percentage = 100;
|
||||
}
|
||||
return halfCircumference - (percentage / 100) * halfCircumference;
|
||||
},
|
||||
colorClass() {
|
||||
if (this.percentage < 60) {
|
||||
return 'text-green-500';
|
||||
}
|
||||
if (this.percentage < 80) {
|
||||
return 'text-yellow-500';
|
||||
}
|
||||
if (this.percentage >= 80) {
|
||||
return 'text-red-500';
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@ -1,60 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="flex flex-row justify-between pb-3">
|
||||
<h1 class="text-2xl font-bold">{{ title }}</h1>
|
||||
<!-- generic report filters -->
|
||||
<div class="flex gap-2 px-2">
|
||||
<div v-for="filter in filters" :key="filter.name">
|
||||
<p class="text-sm text-gray-600">{{ filter.label }}</p>
|
||||
<FormControl
|
||||
:type="filter.type"
|
||||
:options="filter.options"
|
||||
v-model="filter.value"
|
||||
/>
|
||||
</div>
|
||||
<slot name="actions"> </slot>
|
||||
</div>
|
||||
</div>
|
||||
<div class="divide-y">
|
||||
<div class="flex items-center gap-2 py-2 text-base text-gray-600">
|
||||
<!-- generic report columns -->
|
||||
<div v-for="column in columns" :key="column.name" :class="column.class">
|
||||
{{ column.label }}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="flex items-center gap-2 py-2 text-base"
|
||||
v-for="(row, i) in data"
|
||||
:key="i"
|
||||
>
|
||||
<!-- loop through data -->
|
||||
<div v-for="column in row" :key="column.name" :class="column['class']">
|
||||
{{ column['value'] }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
name: 'Report',
|
||||
props: {
|
||||
filters: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
columns: {
|
||||
type: Array,
|
||||
required: true
|
||||
},
|
||||
data: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@ -1,253 +0,0 @@
|
||||
<template>
|
||||
<div class="flex h-screen flex-col justify-between bg-gray-50 p-2">
|
||||
<div>
|
||||
<Dropdown :options="dropdownItems">
|
||||
<template v-slot="{ open }">
|
||||
<button
|
||||
class="flex w-[15rem] items-center rounded-md px-2 py-2 text-left"
|
||||
:class="open ? 'bg-white shadow-sm' : 'hover:bg-gray-200'"
|
||||
>
|
||||
<JLogo class="h-8 w-8 rounded" />
|
||||
<div class="ml-2 flex flex-col">
|
||||
<div class="text-base font-medium leading-none text-gray-900">
|
||||
今果 Jingrow
|
||||
</div>
|
||||
<div
|
||||
v-if="$account.user"
|
||||
class="mt-1 hidden text-sm leading-none text-gray-700 sm:inline"
|
||||
>
|
||||
{{ $account.user.full_name }}
|
||||
</div>
|
||||
</div>
|
||||
<FeatherIcon
|
||||
name="chevron-down"
|
||||
class="ml-auto h-5 w-5 text-gray-700"
|
||||
/>
|
||||
</button>
|
||||
</template>
|
||||
</Dropdown>
|
||||
<div class="mt-2 flex flex-col space-y-0.5">
|
||||
<div class="mb-2 flex flex-col space-y-0.5">
|
||||
<button
|
||||
v-if="$account.number_of_sites > 3"
|
||||
class="rounded text-gray-900 hover:bg-gray-100"
|
||||
@click="show = true"
|
||||
>
|
||||
<div class="flex w-full items-center px-2 py-1">
|
||||
<span class="mr-1.5">
|
||||
<FeatherIcon name="search" class="h-5 w-5 text-gray-700" />
|
||||
</span>
|
||||
<span class="text-sm">Search</span>
|
||||
<span class="ml-auto text-sm text-gray-500">
|
||||
<template v-if="$platform === 'mac'">⌘K</template>
|
||||
<template v-else>Ctrl+K</template>
|
||||
</span>
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
class="rounded text-gray-900 hover:bg-gray-100"
|
||||
@click="this.$router.push({ name: 'Notifications' })"
|
||||
>
|
||||
<div
|
||||
class="flex w-full items-center rounded-md px-2 py-1"
|
||||
:class="{
|
||||
'bg-white shadow-sm':
|
||||
this.$route.fullPath.startsWith('/notifications')
|
||||
}"
|
||||
>
|
||||
<span class="mr-1.5">
|
||||
<FeatherIcon name="inbox" class="h-4.5 w-4.5 text-gray-700" />
|
||||
</span>
|
||||
<span class="text-sm">Notifications </span>
|
||||
<span
|
||||
v-if="unreadNotificationsCount > 0"
|
||||
class="ml-auto rounded bg-gray-400 px-1.5 py-0.5 text-xs text-white"
|
||||
>
|
||||
{{
|
||||
unreadNotificationsCount > 99
|
||||
? '99+'
|
||||
: unreadNotificationsCount
|
||||
}}
|
||||
</span>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<CommandPalette
|
||||
:show="showCommandPalette"
|
||||
@close="showCommandPalette = false"
|
||||
/>
|
||||
<router-link
|
||||
v-for="item in items"
|
||||
:key="item.label"
|
||||
:to="item.route"
|
||||
v-slot="{ href, route, navigate }"
|
||||
>
|
||||
<a
|
||||
:class="[
|
||||
(
|
||||
Boolean(item.highlight)
|
||||
? item.highlight(route)
|
||||
: item.route == '/'
|
||||
)
|
||||
? 'bg-white shadow-sm'
|
||||
: 'text-gray-900 hover:bg-gray-100'
|
||||
]"
|
||||
:href="href"
|
||||
@click="navigate"
|
||||
class="flex items-center rounded-md px-2 py-1 pr-10 text-start text-sm focus:outline-none"
|
||||
>
|
||||
<Component class="mr-1.5 text-gray-700" :is="item.icon" />
|
||||
{{ item.label }}
|
||||
</a>
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<SwitchTeamDialog v-model="showTeamSwitcher" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { FCIcons } from '@/components/icons';
|
||||
import SwitchTeamDialog from './SwitchTeamDialog.vue';
|
||||
import JLogo from '@/components/icons/JLogo.vue';
|
||||
import CommandPalette from '@/components/CommandPalette.vue';
|
||||
import { unreadNotificationsCount } from '@/data/notifications';
|
||||
|
||||
export default {
|
||||
name: 'Sidebar',
|
||||
components: {
|
||||
JLogo,
|
||||
SwitchTeamDialog,
|
||||
CommandPalette
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showCommandPalette: false,
|
||||
showTeamSwitcher: false,
|
||||
dropdownItems: [
|
||||
{
|
||||
label: 'Switch Team',
|
||||
icon: 'command',
|
||||
onClick: () => (this.showTeamSwitcher = true)
|
||||
},
|
||||
{
|
||||
label: 'Support & Docs',
|
||||
icon: 'help-circle',
|
||||
onClick: () => (window.location.href = '/support')
|
||||
},
|
||||
{
|
||||
label: 'Logout',
|
||||
icon: 'log-out',
|
||||
onClick: () => this.$auth.logout()
|
||||
}
|
||||
]
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
window.addEventListener('keydown', e => {
|
||||
if (e.key === 'k' && (e.ctrlKey || e.metaKey)) {
|
||||
this.showCommandPalette = !this.showCommandPalette;
|
||||
e.preventDefault();
|
||||
}
|
||||
if (e.key === 'Escape') {
|
||||
this.showCommandPalette = false;
|
||||
}
|
||||
});
|
||||
|
||||
this.$socket.emit('pagetype_subscribe', 'Jcloud Notification');
|
||||
this.$socket.on('jcloud_notification', data => {
|
||||
if (data.team === this.$account.team.name) {
|
||||
unreadNotificationsCount.setData(data => data + 1);
|
||||
}
|
||||
});
|
||||
|
||||
unreadNotificationsCount.fetch();
|
||||
},
|
||||
beforeUnmount() {
|
||||
this.$socket.emit('pagetype_unsubscribe', 'Jcloud Notification');
|
||||
this.$socket.off('jcloud_notification');
|
||||
},
|
||||
computed: {
|
||||
unreadNotificationsCount() {
|
||||
return unreadNotificationsCount.data;
|
||||
},
|
||||
items() {
|
||||
return [
|
||||
{
|
||||
label: 'Sites',
|
||||
route: '/sites',
|
||||
highlight: () => {
|
||||
return this.$route.fullPath.startsWith('/sites');
|
||||
},
|
||||
icon: FCIcons.SiteIcon
|
||||
},
|
||||
{
|
||||
label: 'Bench Groups',
|
||||
route: '/groups',
|
||||
highlight: () => {
|
||||
return this.$route.fullPath.startsWith('/groups');
|
||||
},
|
||||
icon: FCIcons.BenchIcon
|
||||
//condition: () => this.$account.team?.benches_enabled
|
||||
},
|
||||
{
|
||||
label: 'Servers',
|
||||
route: '/servers',
|
||||
highlight: () => {
|
||||
return this.$route.fullPath.startsWith('/servers');
|
||||
},
|
||||
icon: FCIcons.ServerIcon,
|
||||
condition: () => this.$account.team?.servers_enabled
|
||||
},
|
||||
{
|
||||
label: 'Spaces',
|
||||
route: '/spaces',
|
||||
highlight: () => {
|
||||
return this.$route.fullPath.startsWith('/spaces');
|
||||
},
|
||||
icon: FCIcons.SpacesIcon,
|
||||
condition: () => this.$account.team?.code_servers_enabled
|
||||
},
|
||||
{
|
||||
label: 'Apps',
|
||||
route: '/marketplace/apps',
|
||||
highlight: () => {
|
||||
return this.$route.fullPath.startsWith('/marketplace');
|
||||
},
|
||||
icon: FCIcons.AppsIcon,
|
||||
condition: () => this.$account.team?.is_developer
|
||||
},
|
||||
{
|
||||
label: 'Security',
|
||||
route: '/security',
|
||||
highlight: () => {
|
||||
return this.$route.fullPath.startsWith('/security');
|
||||
},
|
||||
icon: FCIcons.SecurityIcon,
|
||||
condition: () => this.$account.team?.security_portal_enabled
|
||||
},
|
||||
{
|
||||
label: 'Billing',
|
||||
route: '/billing',
|
||||
highlight: () => {
|
||||
return this.$route.fullPath.startsWith('/billing');
|
||||
},
|
||||
icon: FCIcons.BillingIcon,
|
||||
condition: () =>
|
||||
$account.user?.name === $account.team?.user ||
|
||||
$account.user?.user_type === 'System User'
|
||||
},
|
||||
{
|
||||
label: 'Settings',
|
||||
route: '/settings',
|
||||
highlight: () => {
|
||||
return this.$route.fullPath.startsWith('/settings');
|
||||
},
|
||||
icon: FCIcons.SettingsIcon
|
||||
}
|
||||
].filter(d => (d.condition ? d.condition() : true));
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@ -1,74 +0,0 @@
|
||||
<template>
|
||||
<button
|
||||
class="flex w-full flex-row items-center justify-between rounded-lg border border-gray-100 px-4 py-2 shadow focus:outline-none"
|
||||
:class="[
|
||||
selected || uninstall ? 'ring-2 ring-inset ring-gray-600' : '',
|
||||
selectable ? 'hover:border-gray-300' : 'cursor-default'
|
||||
]"
|
||||
ref="card"
|
||||
>
|
||||
<div class="flex flex-row items-center gap-2">
|
||||
<input
|
||||
v-if="selectable"
|
||||
@click.self="$refs['card'].click()"
|
||||
:checked="selected"
|
||||
type="checkbox"
|
||||
class="h-4 w-4 cursor-pointer rounded border-gray-300 text-gray-600 focus:ring-transparent"
|
||||
/>
|
||||
<h3 class="text-left text-lg font-medium text-gray-900">
|
||||
{{ app.title }}
|
||||
</h3>
|
||||
</div>
|
||||
<Badge v-if="uninstall" theme="red" label="Will Be Uninstalled " />
|
||||
<div v-else class="ml-2 flex flex-row space-x-2">
|
||||
<CommitTag
|
||||
v-if="deployFrom(app)"
|
||||
:tag="deployFrom(app)"
|
||||
:link="`${app.repository_url}/commit/${app.current_hash}`"
|
||||
/>
|
||||
<a
|
||||
v-if="deployFrom(app)"
|
||||
class="flex cursor-pointer flex-col justify-center"
|
||||
:href="`${app.repository_url}/compare/${app.current_hash}...${app.next_hash}`"
|
||||
target="_blank"
|
||||
>
|
||||
<FeatherIcon name="arrow-right" class="w-4" />
|
||||
</a>
|
||||
<Badge
|
||||
v-else
|
||||
label="First Deploy"
|
||||
theme="green"
|
||||
class="whitespace-nowrap"
|
||||
/>
|
||||
<CommitTag
|
||||
:tag="deployTo(app)"
|
||||
:link="`${app.repository_url}/commit/${app.next_hash}`"
|
||||
/>
|
||||
</div>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import CommitTag from './utils/CommitTag.vue';
|
||||
export default {
|
||||
name: 'SiteAppUpdateCard',
|
||||
props: ['app', 'selectable', 'selected', 'uninstall'],
|
||||
methods: {
|
||||
deployFrom(app) {
|
||||
if (app.will_branch_change) {
|
||||
return app.current_branch;
|
||||
}
|
||||
return app.current_hash
|
||||
? app.current_tag || app.current_hash.slice(0, 7)
|
||||
: null;
|
||||
},
|
||||
deployTo(app) {
|
||||
if (app.will_branch_change) {
|
||||
return app.branch;
|
||||
}
|
||||
return app.next_tag || app.next_hash.slice(0, 7);
|
||||
}
|
||||
},
|
||||
components: { CommitTag }
|
||||
};
|
||||
</script>
|
||||
@ -1,75 +0,0 @@
|
||||
<template>
|
||||
<div v-if="plans.length">
|
||||
<div
|
||||
class="bg-gray-0 flex rounded-t-md border border-b-0 px-4 py-3 text-base text-gray-800"
|
||||
>
|
||||
<div class="w-10"></div>
|
||||
<div class="w-1/4">Plan</div>
|
||||
<div class="w-1/4">Compute</div>
|
||||
<div class="w-1/4">Database</div>
|
||||
<div class="w-1/4">Disk</div>
|
||||
<div class="w-1/4">Support</div>
|
||||
</div>
|
||||
<div
|
||||
class="focus-within:shadow-outline flex cursor-pointer border px-4 py-3 text-left text-base"
|
||||
:class="[
|
||||
selectedPlan === plan ? 'bg-gray-100' : 'hover:bg-gray-50',
|
||||
{
|
||||
'border-b-0': i !== plans.length - 1,
|
||||
'rounded-b-md border-b': i === plans.length - 1,
|
||||
'pointer-events-none': plan.disabled
|
||||
}
|
||||
]"
|
||||
v-for="(plan, i) in plans"
|
||||
:key="plan.name"
|
||||
@click="$emit('update:selectedPlan', plan)"
|
||||
>
|
||||
<div class="flex w-10 items-center">
|
||||
<input
|
||||
type="radio"
|
||||
class="form-radio text-gray-900"
|
||||
:checked="selectedPlan === plan"
|
||||
@change="e => (selectedPlan = e.target.checked ? plan : null)"
|
||||
/>
|
||||
</div>
|
||||
<div class="w-1/4 text-gray-900" :class="{ 'opacity-25': plan.disabled }">
|
||||
<span class="font-semibold">
|
||||
{{ $planTitle(plan) }}
|
||||
</span>
|
||||
<span v-if="plan.price_usd > 0"> /mo</span>
|
||||
</div>
|
||||
<div class="w-1/4 text-gray-700" :class="{ 'opacity-25': plan.disabled }">
|
||||
{{ plan.cpu_time_per_day }}
|
||||
{{ $plural(plan.cpu_time_per_day, 'hour', 'hours') }} / day
|
||||
</div>
|
||||
<div class="w-1/4 text-gray-700" :class="{ 'opacity-25': plan.disabled }">
|
||||
{{ formatBytes(plan.max_database_usage, 0, 2) }}
|
||||
</div>
|
||||
<div class="w-1/4 text-gray-700" :class="{ 'opacity-25': plan.disabled }">
|
||||
{{ formatBytes(plan.max_storage_usage, 0, 2) }}
|
||||
</div>
|
||||
<a
|
||||
v-if="plan.support_included"
|
||||
href="https://support.jingrow.com"
|
||||
target="_blank"
|
||||
class="w-1/4"
|
||||
>
|
||||
<Tooltip text="JERP/HR product warranty and support" placement="top">
|
||||
<Badge class="hover:cursor-pointer" color="blue" label="Included" />
|
||||
</Tooltip>
|
||||
</a>
|
||||
<div v-else class="w-1/4"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-center" v-else>
|
||||
<Button :loading="true">Loading</Button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'SitePlansTable',
|
||||
props: ['plans', 'selectedPlan'],
|
||||
emits: ['update:selectedPlan']
|
||||
};
|
||||
</script>
|
||||
@ -1,60 +0,0 @@
|
||||
<template>
|
||||
<button
|
||||
class="flex w-full flex-row items-center justify-between rounded-lg border border-gray-100 px-4 py-2 shadow focus:outline-none"
|
||||
:class="[
|
||||
selected ? 'ring-2 ring-inset ring-gray-600' : '',
|
||||
selectable ? 'hover:border-gray-300' : 'cursor-default'
|
||||
]"
|
||||
ref="card"
|
||||
>
|
||||
<div class="flex flex-col">
|
||||
<div
|
||||
@click.self="$refs['card'].click()"
|
||||
class="flex flex-row items-center gap-2"
|
||||
>
|
||||
<input
|
||||
@click.self="$refs['card'].click()"
|
||||
v-if="selectable"
|
||||
:checked="selected"
|
||||
type="checkbox"
|
||||
class="h-4 w-4 rounded border-gray-300 text-gray-600 focus:ring-transparent"
|
||||
/>
|
||||
<h3 class="group-select text-lg font-medium text-gray-900">
|
||||
{{ site }}
|
||||
</h3>
|
||||
</div>
|
||||
<div v-if="selected" class="mt-2 flex flex-row">
|
||||
<FormControl
|
||||
@change="toggleProperty($event, 'skip_failing_patches', site)"
|
||||
type="checkbox"
|
||||
label="Skip failing patches"
|
||||
class="h-4 rounded border-gray-300 text-gray-600 focus:ring-transparent"
|
||||
/>
|
||||
<FormControl
|
||||
v-if="$account.team?.skip_backups"
|
||||
@change="toggleProperty($event, 'skip_backups', site)"
|
||||
type="checkbox"
|
||||
label="Skip backup"
|
||||
class="ml-2 h-4 rounded border-gray-300 text-gray-600 focus:ring-transparent"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'SiteUpdateCard',
|
||||
props: ['site', 'selectable', 'selected', 'selectedSites'],
|
||||
methods: {
|
||||
toggleProperty(value, prop, site) {
|
||||
this.selectedSites.map(selectedSite => {
|
||||
if (site == selectedSite.name) {
|
||||
selectedSite[prop] = value.target.checked;
|
||||
}
|
||||
});
|
||||
this.$emit('update:selectedSites', this.selectedSites);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@ -1,90 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="flex items-center justify-center">
|
||||
<div class="flex space-x-8">
|
||||
<div class="relative" v-for="(step, index) in steps" :key="step.name">
|
||||
<div
|
||||
class="z-10 flex h-5 w-5 items-center justify-center rounded-full border border-gray-400 bg-white"
|
||||
:class="{
|
||||
'bg-gray-700 text-white': isStepCompleted(step),
|
||||
'border-gray-500': isStepCurrent(step) || isStepCompleted(step)
|
||||
}"
|
||||
>
|
||||
<FeatherIcon
|
||||
v-if="isStepCompleted(step)"
|
||||
name="check"
|
||||
class="h-3 w-3"
|
||||
:stroke-width="3"
|
||||
/>
|
||||
<div
|
||||
class="h-1.5 w-1.5 rounded-full bg-gray-700"
|
||||
v-if="isStepCurrent(step)"
|
||||
></div>
|
||||
</div>
|
||||
<div
|
||||
class="absolute top-1/2 w-8 -translate-x-8 -translate-y-1/2 transform border-t border-gray-400"
|
||||
:class="{
|
||||
'border-gray-500': isStepCompleted(step) || isStepCurrent(step)
|
||||
}"
|
||||
v-show="index !== 0"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<template v-if="active">
|
||||
<slot v-bind="{ active, next, previous, hasNext, hasPrevious }"></slot>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
name: 'Steps',
|
||||
props: ['steps'],
|
||||
data() {
|
||||
return {
|
||||
active: null
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.active = this.steps[0];
|
||||
},
|
||||
methods: {
|
||||
next() {
|
||||
if (this.active.validate && !this.active.validate()) {
|
||||
return;
|
||||
}
|
||||
let currentIndex = this.steps.indexOf(this.active);
|
||||
let nextIndex = currentIndex + 1;
|
||||
if (nextIndex == this.steps.length) {
|
||||
nextIndex = this.steps.length - 1;
|
||||
}
|
||||
let nextStep = this.steps[nextIndex];
|
||||
this.active = nextStep;
|
||||
},
|
||||
previous() {
|
||||
let currentIndex = this.steps.indexOf(this.active);
|
||||
let prevIndex = currentIndex - 1;
|
||||
if (prevIndex == -1) {
|
||||
prevIndex = 0;
|
||||
}
|
||||
this.active = this.steps[prevIndex];
|
||||
},
|
||||
isStepCompleted(step) {
|
||||
let currentIndex = this.steps.indexOf(this.active);
|
||||
let stepIndex = this.steps.indexOf(step);
|
||||
return stepIndex < currentIndex;
|
||||
},
|
||||
isStepCurrent(step) {
|
||||
return this.active === step;
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
hasNext() {
|
||||
return this.steps.indexOf(this.active) < this.steps.length - 1;
|
||||
},
|
||||
hasPrevious() {
|
||||
return this.steps.indexOf(this.active) > 0;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@ -1,281 +0,0 @@
|
||||
<template>
|
||||
<div class="relative">
|
||||
<div
|
||||
v-if="!ready"
|
||||
class="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-8 transform"
|
||||
>
|
||||
<Spinner class="h-5 w-5 text-gray-600" />
|
||||
</div>
|
||||
<div :class="{ 'opacity-0': !ready }">
|
||||
<div v-show="!tryingMicroCharge">
|
||||
<label class="block">
|
||||
<span class="block text-xs text-gray-600">
|
||||
Credit or Debit Card
|
||||
</span>
|
||||
<div
|
||||
class="form-input mt-2 block h-[unset] w-full py-2 pl-3"
|
||||
ref="card-element"
|
||||
></div>
|
||||
<ErrorMessage class="mt-1" :message="cardErrorMessage" />
|
||||
</label>
|
||||
<FormControl
|
||||
class="mt-4"
|
||||
label="Name on Card"
|
||||
type="text"
|
||||
v-model="billingInformation.cardHolderName"
|
||||
/>
|
||||
<AddressForm
|
||||
v-if="!withoutAddress"
|
||||
class="mt-4"
|
||||
v-model:address="billingInformation"
|
||||
ref="address-form"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="mt-3" v-show="tryingMicroCharge">
|
||||
<p class="text-lg text-gray-800">
|
||||
We are attempting to charge your card with
|
||||
<strong>{{ formattedMicroChargeAmount }}</strong> to make sure the
|
||||
card works. This amount will be <strong>refunded</strong> back to your
|
||||
account.
|
||||
</p>
|
||||
|
||||
<Button class="mt-2" :loading="true">Attempting Test Charge</Button>
|
||||
</div>
|
||||
|
||||
<ErrorMessage class="mt-2" :message="errorMessage" />
|
||||
|
||||
<div class="mt-6 flex items-center justify-between">
|
||||
<StripeLogo />
|
||||
<Button variant="solid" @click="submit" :loading="addingCard">
|
||||
Save Card
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import AddressForm from '../../src2/components/AddressForm.vue';
|
||||
import StripeLogo from '@/components/StripeLogo.vue';
|
||||
import { loadStripe } from '@stripe/stripe-js';
|
||||
|
||||
export default {
|
||||
name: 'StripeCard',
|
||||
props: ['withoutAddress'],
|
||||
emits: ['complete'],
|
||||
components: {
|
||||
AddressForm,
|
||||
StripeLogo
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
errorMessage: null,
|
||||
cardErrorMessage: null,
|
||||
ready: false,
|
||||
setupIntent: null,
|
||||
billingInformation: {
|
||||
cardHolderName: '',
|
||||
country: '',
|
||||
gstin: ''
|
||||
},
|
||||
gstNotApplicable: false,
|
||||
addingCard: false,
|
||||
tryingMicroCharge: false
|
||||
};
|
||||
},
|
||||
async mounted() {
|
||||
this.setupCard();
|
||||
|
||||
let { first_name, last_name = '' } = this.$account.user;
|
||||
let fullname = first_name + ' ' + last_name;
|
||||
this.billingInformation.cardHolderName = fullname.trimEnd();
|
||||
},
|
||||
resources: {
|
||||
countryList: 'jcloud.api.account.country_list',
|
||||
billingAddress() {
|
||||
return {
|
||||
url: 'jcloud.api.account.get_billing_information',
|
||||
params: {
|
||||
timezone: this.browserTimezone
|
||||
},
|
||||
auto: true,
|
||||
onSuccess(data) {
|
||||
this.billingInformation.country = data?.country;
|
||||
this.billingInformation.address = data?.address_line1;
|
||||
this.billingInformation.city = data?.city;
|
||||
this.billingInformation.state = data?.state;
|
||||
this.billingInformation.postal_code = data?.pincode;
|
||||
}
|
||||
};
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async setupCard() {
|
||||
let result = await this.$call(
|
||||
'jcloud.api.billing.get_publishable_key_and_setup_intent'
|
||||
);
|
||||
//window.posthog.capture('init_client_add_card', 'fc_signup');
|
||||
let { publishable_key, setup_intent } = result;
|
||||
this.setupIntent = setup_intent;
|
||||
this.stripe = await loadStripe(publishable_key);
|
||||
this.elements = this.stripe.elements();
|
||||
let theme = this.$theme;
|
||||
let style = {
|
||||
base: {
|
||||
color: theme.colors.black,
|
||||
fontFamily: theme.fontFamily.sans.join(', '),
|
||||
fontSmoothing: 'antialiased',
|
||||
fontSize: '13px',
|
||||
'::placeholder': {
|
||||
color: theme.colors.gray['400']
|
||||
}
|
||||
},
|
||||
invalid: {
|
||||
color: theme.colors.red['600'],
|
||||
iconColor: theme.colors.red['600']
|
||||
}
|
||||
};
|
||||
this.card = this.elements.create('card', {
|
||||
hidePostalCode: true,
|
||||
style: style,
|
||||
classes: {
|
||||
complete: '',
|
||||
focus: 'bg-gray-100'
|
||||
}
|
||||
});
|
||||
this.card.mount(this.$refs['card-element']);
|
||||
|
||||
this.card.addEventListener('change', event => {
|
||||
this.cardErrorMessage = event.error?.message || null;
|
||||
});
|
||||
this.card.addEventListener('ready', () => {
|
||||
this.ready = true;
|
||||
});
|
||||
},
|
||||
async submit() {
|
||||
this.addingCard = true;
|
||||
|
||||
let message;
|
||||
if (!this.withoutAddress) {
|
||||
message = await this.$refs['address-form'].validateValues();
|
||||
}
|
||||
if (message) {
|
||||
this.errorMessage = message;
|
||||
this.addingCard = false;
|
||||
return;
|
||||
} else {
|
||||
this.errorMessage = null;
|
||||
}
|
||||
|
||||
const { setupIntent, error } = await this.stripe.confirmCardSetup(
|
||||
this.setupIntent.client_secret,
|
||||
{
|
||||
payment_method: {
|
||||
card: this.card,
|
||||
billing_details: {
|
||||
name: this.billingInformation.cardHolderName,
|
||||
address: {
|
||||
line1: this.billingInformation.address,
|
||||
city: this.billingInformation.city,
|
||||
state: this.billingInformation.state,
|
||||
postal_code: this.billingInformation.postal_code,
|
||||
country: this.getCountryCode(this.billingInformation.country)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
if (error) {
|
||||
this.addingCard = false;
|
||||
let errorMessage = error.message;
|
||||
// fix for duplicate error message
|
||||
if (errorMessage != 'Your card number is incomplete.') {
|
||||
this.errorMessage = errorMessage;
|
||||
}
|
||||
} else {
|
||||
if (setupIntent.status === 'succeeded') {
|
||||
try {
|
||||
const { payment_method_name } = await this.$call(
|
||||
'jcloud.api.billing.setup_intent_success',
|
||||
{
|
||||
setup_intent: setupIntent,
|
||||
address: this.withoutAddress ? null : this.billingInformation
|
||||
}
|
||||
);
|
||||
//window.posthog.capture('completed_client_add_card', 'fc_signup');
|
||||
|
||||
await this.verifyWithMicroChargeIfApplicable(payment_method_name);
|
||||
|
||||
this.addingCard = false;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
this.addingCard = false;
|
||||
this.errorMessage = error.messages.join('\n');
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
async verifyWithMicroChargeIfApplicable(paymentMethodName) {
|
||||
const teamCurrency = this.$account.team.currency;
|
||||
const verifyCardsWithMicroCharge =
|
||||
this.$account.feature_flags.verify_cards_with_micro_charge;
|
||||
|
||||
const isMicroChargeApplicable =
|
||||
verifyCardsWithMicroCharge === 'Both CNY and USD' ||
|
||||
(verifyCardsWithMicroCharge == 'Only CNY' && teamCurrency === 'CNY') ||
|
||||
(verifyCardsWithMicroCharge === 'Only USD' && teamCurrency === 'USD');
|
||||
|
||||
if (isMicroChargeApplicable) {
|
||||
await this._verifyWithMicroCharge(paymentMethodName);
|
||||
} else {
|
||||
this.$emit('complete');
|
||||
}
|
||||
},
|
||||
|
||||
async _verifyWithMicroCharge(paymentMethodName) {
|
||||
this.tryingMicroCharge = true;
|
||||
|
||||
const paymentIntent = await this.$call(
|
||||
'jcloud.api.billing.create_payment_intent_for_micro_debit',
|
||||
{
|
||||
payment_method_name: paymentMethodName
|
||||
}
|
||||
);
|
||||
|
||||
let { client_secret: clientSecret } = paymentIntent;
|
||||
|
||||
let payload = await this.stripe.confirmCardPayment(clientSecret, {
|
||||
payment_method: {
|
||||
card: this.card
|
||||
}
|
||||
});
|
||||
|
||||
if (payload.paymentIntent.status === 'succeeded') {
|
||||
this.$emit('complete');
|
||||
}
|
||||
|
||||
this.tryingMicroCharge = false;
|
||||
},
|
||||
getCountryCode(country) {
|
||||
let code = this.$resources.countryList.data.find(
|
||||
d => d.name === country
|
||||
).code;
|
||||
return code.toUpperCase();
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
formattedMicroChargeAmount() {
|
||||
const isCNY = this.$account.team.currency === 'CNY';
|
||||
return isCNY ? '¥100' : '$1';
|
||||
},
|
||||
browserTimezone() {
|
||||
if (!window.Intl) {
|
||||
return null;
|
||||
}
|
||||
return Intl.DateTimeFormat().resolvedOptions().timeZone;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@ -1,32 +0,0 @@
|
||||
<template>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
width="119px"
|
||||
height="26px"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
opacity="0.349"
|
||||
fill="rgb(66, 71, 112)"
|
||||
d="M113.000,26.000 L6.000,26.000 C2.686,26.000 -0.000,23.314 -0.000,20.000 L-0.000,6.000 C-0.000,2.686 2.686,-0.000 6.000,-0.000 L113.000,-0.000 C116.314,-0.000 119.000,2.686 119.000,6.000 L119.000,20.000 C119.000,23.314 116.314,26.000 113.000,26.000 ZM118.000,6.000 C118.000,3.239 115.761,1.000 113.000,1.000 L6.000,1.000 C3.239,1.000 1.000,3.239 1.000,6.000 L1.000,20.000 C1.000,22.761 3.239,25.000 6.000,25.000 L113.000,25.000 C115.761,25.000 118.000,22.761 118.000,20.000 L118.000,6.000 Z"
|
||||
/>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
opacity="0.502"
|
||||
fill="rgb(66, 71, 112)"
|
||||
d="M60.700,18.437 L59.395,18.437 L60.405,15.943 L58.395,10.871 L59.774,10.871 L61.037,14.323 L62.310,10.871 L63.689,10.871 L60.700,18.437 ZM55.690,16.259 C55.238,16.259 54.774,16.091 54.354,15.764 L54.354,16.133 L53.007,16.133 L53.007,8.566 L54.354,8.566 L54.354,11.229 C54.774,10.913 55.238,10.745 55.690,10.745 C57.100,10.745 58.068,11.881 58.068,13.502 C58.068,15.122 57.100,16.259 55.690,16.259 ZM55.406,11.902 C55.038,11.902 54.669,12.060 54.354,12.376 L54.354,14.628 C54.669,14.943 55.038,15.101 55.406,15.101 C56.164,15.101 56.690,14.449 56.690,13.502 C56.690,12.555 56.164,11.902 55.406,11.902 ZM47.554,15.764 C47.144,16.091 46.681,16.259 46.218,16.259 C44.818,16.259 43.840,15.122 43.840,13.502 C43.840,11.881 44.818,10.745 46.218,10.745 C46.681,10.745 47.144,10.913 47.554,11.229 L47.554,8.566 L48.912,8.566 L48.912,16.133 L47.554,16.133 L47.554,15.764 ZM47.554,12.376 C47.249,12.060 46.881,11.902 46.513,11.902 C45.744,11.902 45.218,12.555 45.218,13.502 C45.218,14.449 45.744,15.101 46.513,15.101 C46.881,15.101 47.249,14.943 47.554,14.628 L47.554,12.376 ZM39.535,13.870 C39.619,14.670 40.251,15.217 41.134,15.217 C41.619,15.217 42.155,15.038 42.702,14.722 L42.702,15.849 C42.103,16.122 41.503,16.259 40.913,16.259 C39.324,16.259 38.209,15.101 38.209,13.460 C38.209,11.871 39.303,10.745 40.808,10.745 C42.187,10.745 43.123,11.829 43.123,13.375 C43.123,13.523 43.123,13.691 43.102,13.870 L39.535,13.870 ZM40.756,11.786 C40.103,11.786 39.598,12.271 39.535,12.997 L41.829,12.997 C41.787,12.281 41.356,11.786 40.756,11.786 ZM35.988,12.618 L35.988,16.133 L34.641,16.133 L34.641,10.871 L35.988,10.871 L35.988,11.397 C36.367,10.976 36.830,10.745 37.282,10.745 C37.430,10.745 37.577,10.755 37.724,10.797 L37.724,11.997 C37.577,11.955 37.409,11.934 37.251,11.934 C36.809,11.934 36.335,12.176 35.988,12.618 ZM29.979,13.870 C30.063,14.670 30.694,15.217 31.578,15.217 C32.062,15.217 32.599,15.038 33.146,14.722 L33.146,15.849 C32.546,16.122 31.946,16.259 31.357,16.259 C29.768,16.259 28.653,15.101 28.653,13.460 C28.653,11.871 29.747,10.745 31.252,10.745 C32.630,10.745 33.567,11.829 33.567,13.375 C33.567,13.523 33.567,13.691 33.546,13.870 L29.979,13.870 ZM31.199,11.786 C30.547,11.786 30.042,12.271 29.979,12.997 L32.273,12.997 C32.231,12.281 31.799,11.786 31.199,11.786 ZM25.274,16.133 L24.200,12.555 L23.137,16.133 L21.927,16.133 L20.117,10.871 L21.464,10.871 L22.527,14.449 L23.590,10.871 L24.810,10.871 L25.873,14.449 L26.936,10.871 L28.283,10.871 L26.484,16.133 L25.274,16.133 ZM17.043,16.259 C15.454,16.259 14.328,15.112 14.328,13.502 C14.328,11.881 15.454,10.745 17.043,10.745 C18.632,10.745 19.748,11.881 19.748,13.502 C19.748,15.112 18.632,16.259 17.043,16.259 ZM17.043,11.871 C16.254,11.871 15.707,12.534 15.707,13.502 C15.707,14.470 16.254,15.133 17.043,15.133 C17.822,15.133 18.369,14.470 18.369,13.502 C18.369,12.534 17.822,11.871 17.043,11.871 ZM11.128,13.533 L9.918,13.533 L9.918,16.133 L8.571,16.133 L8.571,8.892 L11.128,8.892 C12.602,8.892 13.654,9.850 13.654,11.218 C13.654,12.586 12.602,13.533 11.128,13.533 ZM10.939,9.987 L9.918,9.987 L9.918,12.439 L10.939,12.439 C11.718,12.439 12.265,11.944 12.265,11.218 C12.265,10.482 11.718,9.987 10.939,9.987 Z"
|
||||
/>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
opacity="0.502"
|
||||
fill="rgb(66, 71, 112)"
|
||||
d="M111.116,14.051 L105.557,14.051 C105.684,15.382 106.659,15.774 107.766,15.774 C108.893,15.774 109.781,15.536 110.555,15.146 L110.555,17.433 C109.784,17.861 108.765,18.169 107.408,18.169 C104.642,18.169 102.704,16.437 102.704,13.013 C102.704,10.121 104.348,7.825 107.049,7.825 C109.746,7.825 111.154,10.120 111.154,13.028 C111.154,13.303 111.129,13.898 111.116,14.051 ZM107.031,10.140 C106.321,10.140 105.532,10.676 105.532,11.955 L108.468,11.955 C108.468,10.677 107.728,10.140 107.031,10.140 ZM98.108,18.169 C97.114,18.169 96.507,17.750 96.099,17.451 L96.093,20.664 L93.254,21.268 L93.253,8.014 L95.753,8.014 L95.901,8.715 C96.293,8.349 97.012,7.825 98.125,7.825 C100.119,7.825 101.997,9.621 101.997,12.927 C101.997,16.535 100.139,18.169 98.108,18.169 ZM97.446,10.340 C96.795,10.340 96.386,10.578 96.090,10.903 L96.107,15.122 C96.383,15.421 96.780,15.661 97.446,15.661 C98.496,15.661 99.200,14.518 99.200,12.989 C99.200,11.504 98.485,10.340 97.446,10.340 ZM89.149,8.014 L91.999,8.014 L91.999,17.966 L89.149,17.966 L89.149,8.014 ZM89.149,4.836 L91.999,4.230 L91.999,6.543 L89.149,7.149 L89.149,4.836 ZM86.110,11.219 L86.110,17.966 L83.272,17.966 L83.272,8.014 L85.727,8.014 L85.905,8.853 C86.570,7.631 87.897,7.879 88.275,8.015 L88.275,10.625 C87.914,10.508 86.781,10.338 86.110,11.219 ZM80.024,14.475 C80.024,16.148 81.816,15.627 82.179,15.482 L82.179,17.793 C81.801,18.001 81.115,18.169 80.187,18.169 C78.502,18.169 77.237,16.928 77.237,15.247 L77.250,6.138 L80.022,5.548 L80.024,8.014 L82.180,8.014 L82.180,10.435 L80.024,10.435 L80.024,14.475 ZM76.485,14.959 C76.485,17.003 74.858,18.169 72.497,18.169 C71.518,18.169 70.448,17.979 69.392,17.525 L69.392,14.814 C70.345,15.332 71.559,15.721 72.500,15.721 C73.133,15.721 73.589,15.551 73.589,15.026 C73.589,13.671 69.273,14.181 69.273,11.038 C69.273,9.028 70.808,7.825 73.111,7.825 C74.052,7.825 74.992,7.969 75.933,8.344 L75.933,11.019 C75.069,10.552 73.972,10.288 73.109,10.288 C72.514,10.288 72.144,10.460 72.144,10.903 C72.144,12.181 76.485,11.573 76.485,14.959 Z"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
name: 'StripeLogo'
|
||||
};
|
||||
</script>
|
||||
@ -1,34 +0,0 @@
|
||||
<template>
|
||||
<Dialog :options="{ title: 'Switch Team' }">
|
||||
<template v-slot:body-content>
|
||||
<span class="text-xs text-gray-600"
|
||||
>Current Team: {{ $account.team.name }}</span
|
||||
>
|
||||
<div class="border-t my-2">
|
||||
<ListItem
|
||||
v-for="team in $account.teams"
|
||||
:title="`${team.team_title}`"
|
||||
:description="team.user"
|
||||
:key="team"
|
||||
>
|
||||
<template #actions>
|
||||
<div v-if="$account.team.name === team.name">
|
||||
<Badge label="Active" theme="blue" />
|
||||
</div>
|
||||
<div v-else class="flex flex-row justify-end">
|
||||
<Button @click="$account.switchToTeam(team.name)">
|
||||
Switch
|
||||
</Button>
|
||||
</div>
|
||||
</template>
|
||||
</ListItem>
|
||||
</div>
|
||||
</template>
|
||||
</Dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'SwitchTeamDialog'
|
||||
};
|
||||
</script>
|
||||
@ -1,66 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<div>
|
||||
<ul class="hidden border-b text-base sm:flex">
|
||||
<router-link
|
||||
v-for="tab in tabs"
|
||||
:key="tab.label"
|
||||
:to="tab.route"
|
||||
v-slot="{ href, navigate, isActive }"
|
||||
>
|
||||
<li>
|
||||
<a
|
||||
class="font-base relative mr-4 block truncate border-b py-2 leading-none focus:outline-none"
|
||||
:class="[
|
||||
isTabSelected(tab)
|
||||
? 'border-brand border-gray-900 text-gray-900'
|
||||
: 'border-transparent text-gray-600 hover:text-gray-900'
|
||||
]"
|
||||
:href="href"
|
||||
@click="navigate"
|
||||
>
|
||||
<span>
|
||||
{{ tab.label }}
|
||||
</span>
|
||||
<div
|
||||
class="absolute right-0 top-1 h-1.5 w-1.5 rounded-full bg-red-500"
|
||||
v-if="tab.showRedDot && !isActive"
|
||||
></div>
|
||||
</a>
|
||||
</li>
|
||||
</router-link>
|
||||
</ul>
|
||||
<select
|
||||
class="form-select block w-full sm:hidden"
|
||||
@change="e => changeTab(e.target.value)"
|
||||
>
|
||||
<option
|
||||
v-for="tab in tabs"
|
||||
:selected="isTabSelected(tab)"
|
||||
:value="tab.route"
|
||||
:key="tab.label"
|
||||
>
|
||||
{{ tab.label }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="w-full pt-5" v-if="$slots.default">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'Tabs',
|
||||
props: ['tabs'],
|
||||
methods: {
|
||||
changeTab(route) {
|
||||
this.$router.replace(route);
|
||||
},
|
||||
isTabSelected(tab) {
|
||||
return this.$route.path.startsWith(tab.route);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@ -1,158 +0,0 @@
|
||||
<template>
|
||||
<Card title="Tags">
|
||||
<template #actions>
|
||||
<Button label="添加标签 Tag" @click="showAddDialog = true" />
|
||||
</template>
|
||||
<div class="divide-y" v-if="addedTags?.length">
|
||||
<ListItem v-for="tag in addedTags" :key="tag.name" :title="tag.tag">
|
||||
<template #actions>
|
||||
<Button icon="x" @click="removeTag(tag.name)" />
|
||||
</template>
|
||||
</ListItem>
|
||||
</div>
|
||||
<div v-else class="m-4 text-center">
|
||||
<p class="text-base text-gray-500">No tags added yet</p>
|
||||
</div>
|
||||
<ErrorMessage
|
||||
:message="
|
||||
$resources.addTag.error ||
|
||||
$resources.createTag.error ||
|
||||
$resources.removeTag.error
|
||||
"
|
||||
/>
|
||||
</Card>
|
||||
<Dialog
|
||||
:options="{ title: `Add a New Tag for ${pagetype}` }"
|
||||
v-model="showAddDialog"
|
||||
>
|
||||
<template #body-content>
|
||||
<Autocomplete
|
||||
placeholder="Tags"
|
||||
:options="getAutocompleteOptions"
|
||||
v-model="chosenTag"
|
||||
@update:modelValue="handleAutocompleteSelection"
|
||||
/>
|
||||
<FormControl
|
||||
v-if="showNewTagInput"
|
||||
v-model="newTag"
|
||||
class="mt-4"
|
||||
placeholder="Enter New Tag's name"
|
||||
/>
|
||||
</template>
|
||||
<template #actions>
|
||||
<Button variant="solid" class="w-full" @click="addTag()">{{
|
||||
showNewTagInput ? '创建一个新标签' : '添加标签'
|
||||
}}</Button>
|
||||
</template>
|
||||
</Dialog>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
name: 'Tags',
|
||||
props: ['name', 'pagetype', 'resourceTags', 'tags'],
|
||||
data() {
|
||||
return {
|
||||
showAddDialog: false,
|
||||
showNewTagInput: false,
|
||||
chosenTag: '',
|
||||
newTag: '',
|
||||
addedTags: [],
|
||||
createErrorMessage: ''
|
||||
};
|
||||
},
|
||||
resources: {
|
||||
addTag() {
|
||||
return {
|
||||
url: 'jcloud.api.dashboard.add_tag',
|
||||
params: {
|
||||
name: this.name,
|
||||
pagetype: this.pagetype,
|
||||
tag: this.newTag
|
||||
},
|
||||
validate() {
|
||||
if (this.addedTags.find(t => t.name == this.newTag)) {
|
||||
return 'Tag already added';
|
||||
}
|
||||
},
|
||||
onSuccess(d) {
|
||||
this.addedTags.push(this.tags.find(t => t.name == d));
|
||||
this.showAddDialog = false;
|
||||
this.newTag = '';
|
||||
this.chosenTag = '';
|
||||
}
|
||||
};
|
||||
},
|
||||
removeTag() {
|
||||
return {
|
||||
url: 'jcloud.api.dashboard.remove_tag',
|
||||
onSuccess(d) {
|
||||
this.addedTags = this.addedTags.filter(t => t.name != d);
|
||||
}
|
||||
};
|
||||
},
|
||||
createTag() {
|
||||
return {
|
||||
url: 'jcloud.api.dashboard.create_new_tag',
|
||||
params: {
|
||||
name: this.name,
|
||||
pagetype: this.pagetype,
|
||||
tag: this.newTag
|
||||
},
|
||||
validate() {
|
||||
if (this.tags.find(t => t.tag === this.newTag)) {
|
||||
return 'Tag already exists';
|
||||
}
|
||||
},
|
||||
onSuccess(d) {
|
||||
this.addedTags.push({ name: d.name, tag: d.tag });
|
||||
this.showNewTagInput = false;
|
||||
this.newTag = '';
|
||||
this.chosenTag = '';
|
||||
}
|
||||
};
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
addTag() {
|
||||
if (this.showNewTagInput) {
|
||||
this.$resources.createTag.submit();
|
||||
} else {
|
||||
this.$resources.addTag.submit();
|
||||
}
|
||||
this.showAddDialog = false;
|
||||
},
|
||||
removeTag(tagName) {
|
||||
this.$resources.removeTag.submit({
|
||||
name: this.name,
|
||||
pagetype: this.pagetype,
|
||||
tag: tagName
|
||||
});
|
||||
},
|
||||
handleAutocompleteSelection() {
|
||||
if (this.chosenTag.value === 'new_tag') {
|
||||
this.showNewTagInput = true;
|
||||
} else {
|
||||
this.newTag = this.chosenTag.value;
|
||||
this.showNewTagInput = false;
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.addedTags = this.resourceTags;
|
||||
},
|
||||
computed: {
|
||||
getAutocompleteOptions() {
|
||||
return [
|
||||
{
|
||||
group: 'New Tag',
|
||||
items: [{ label: 'Create a New Tag', value: 'new_tag' }]
|
||||
},
|
||||
{
|
||||
group: 'Existing Tags',
|
||||
items: this.tags.map(t => ({ label: t.tag, value: t.name }))
|
||||
}
|
||||
];
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@ -1,16 +0,0 @@
|
||||
<template>
|
||||
<div class="pb-10 sm:mt-5">
|
||||
<div class="sm:px-8">
|
||||
<div
|
||||
class="mx-auto mb-20 w-auto space-y-4 rounded-xl p-4 sm:w-wizard sm:border sm:p-8 sm:shadow-xl"
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
name: 'WizardCard'
|
||||
};
|
||||
</script>
|
||||
@ -1,78 +0,0 @@
|
||||
<template>
|
||||
<Dialog
|
||||
v-if="source"
|
||||
:options="{
|
||||
title: 'Add New App Release',
|
||||
actions: [
|
||||
{
|
||||
label: 'Change Branch',
|
||||
variant: 'solid',
|
||||
loading: $resources.changeBranch.loading,
|
||||
onClick: () => $resources.changeBranch.submit()
|
||||
}
|
||||
]
|
||||
}"
|
||||
:modelValue="show"
|
||||
>
|
||||
<template v-slot:body-content>
|
||||
<select class="form-select block w-full" v-model="selectedBranch">
|
||||
<option v-for="branch in branchList()" :key="branch">
|
||||
{{ branch }}
|
||||
</option>
|
||||
</select>
|
||||
</template>
|
||||
</Dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'ChangeAppBranchDialog',
|
||||
data() {
|
||||
return {
|
||||
selectedBranch: null
|
||||
};
|
||||
},
|
||||
props: ['show', 'app', 'source', 'version', 'activeBranch'],
|
||||
resources: {
|
||||
branches() {
|
||||
return {
|
||||
url: 'jcloud.api.marketplace.branches',
|
||||
params: {
|
||||
name: this.source
|
||||
},
|
||||
auto: true
|
||||
};
|
||||
},
|
||||
changeBranch() {
|
||||
return {
|
||||
url: 'jcloud.api.marketplace.change_branch',
|
||||
params: {
|
||||
name: this.app,
|
||||
source: this.source,
|
||||
version: this.version,
|
||||
to_branch: this.selectedBranch
|
||||
},
|
||||
onSuccess() {
|
||||
window.location.reload();
|
||||
},
|
||||
validate() {
|
||||
if (this.selectedBranch == this.app.branch) {
|
||||
return 'Please select a different branch';
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
branchList() {
|
||||
if (this.$resources.branches.loading || !this.$resources.branches.data) {
|
||||
return [];
|
||||
}
|
||||
return this.$resources.branches.data.map(d => d.name);
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.selectedBranch = this.activeBranch;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@ -1,104 +0,0 @@
|
||||
<template>
|
||||
<Dialog
|
||||
:modelValue="show"
|
||||
:options="{
|
||||
title: 'Add a New App Version',
|
||||
actions: [
|
||||
{
|
||||
label: 'Add New Version',
|
||||
variant: 'solid',
|
||||
loading: $resources.addVersion.loading,
|
||||
onClick: () => $resources.addVersion.submit()
|
||||
}
|
||||
]
|
||||
}"
|
||||
@close="
|
||||
() => {
|
||||
$emit('close', true);
|
||||
}
|
||||
"
|
||||
>
|
||||
<template v-slot:body-content>
|
||||
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||
<div>
|
||||
<span class="mb-2 block text-sm leading-4 text-gray-700">
|
||||
Version
|
||||
</span>
|
||||
<select class="form-select block w-full" v-model="selectedVersion">
|
||||
<option v-for="version in versions" :key="version">
|
||||
{{ version }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<span class="mb-2 block text-sm leading-4 text-gray-700">
|
||||
Branch
|
||||
</span>
|
||||
<select class="form-select block w-full" v-model="selectedBranch">
|
||||
<option v-for="branch in branchList()" :key="branch">
|
||||
{{ branch }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</Dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { notify } from '@/utils/toast';
|
||||
|
||||
export default {
|
||||
name: 'CreateMarketplaceAppVersion.vue',
|
||||
data() {
|
||||
return {
|
||||
versions: [],
|
||||
branches: [],
|
||||
selectedBranch: null,
|
||||
selectedVersion: null
|
||||
};
|
||||
},
|
||||
props: ['app', 'show'],
|
||||
resources: {
|
||||
options() {
|
||||
return {
|
||||
url: 'jcloud.api.marketplace.options_for_version',
|
||||
auto: true,
|
||||
params: {
|
||||
name: this.app.name,
|
||||
source: this.app.sources[0]?.source
|
||||
},
|
||||
onSuccess(d) {
|
||||
this.versions = d.versions;
|
||||
this.branches = d.branches;
|
||||
}
|
||||
};
|
||||
},
|
||||
addVersion() {
|
||||
return {
|
||||
url: 'jcloud.api.marketplace.add_version',
|
||||
params: {
|
||||
name: this.app.name,
|
||||
branch: this.selectedBranch,
|
||||
version: this.selectedVersion
|
||||
},
|
||||
onSuccess() {
|
||||
window.location.reload();
|
||||
},
|
||||
onError(e) {
|
||||
notify({
|
||||
title: e,
|
||||
color: 'red',
|
||||
icon: 'x'
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
branchList() {
|
||||
return this.branches.map(d => d.name);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@ -1,161 +0,0 @@
|
||||
<template>
|
||||
<CardWithDetails
|
||||
v-if="reviewStages"
|
||||
title="Review Steps"
|
||||
:subtitle="
|
||||
app.review_stage === 'Not Started'
|
||||
? 'Complete all the steps before submitting for a review'
|
||||
: 'App is sent for review'
|
||||
"
|
||||
>
|
||||
<ListItem v-for="step in reviewStages" :key="step.step" :title="step.step">
|
||||
<template #actions>
|
||||
<GreenCheckIcon v-if="step.completed" class="h-5 w-5" />
|
||||
<GrayCheckIcon v-else class="h-5 w-5" />
|
||||
</template>
|
||||
</ListItem>
|
||||
<template #actions>
|
||||
<Button
|
||||
v-if="app.status === 'Draft' && app.review_stage === 'Not Started'"
|
||||
:disabled="reviewStages.some(step => !step.completed)"
|
||||
:loading="$resources.startReview.isLoading"
|
||||
@click="$resources.startReview.submit()"
|
||||
class="py-5"
|
||||
>
|
||||
Submit for Review
|
||||
</Button>
|
||||
</template>
|
||||
<template #details>
|
||||
<CardDetails>
|
||||
<div class="h-full px-6 py-5">
|
||||
<div class="flex justify-between">
|
||||
<div>
|
||||
<h2 class="text-xl font-semibold">Review Communication</h2>
|
||||
<p class="mt-1.5 text-base text-gray-600">
|
||||
Chat with the developer assigned for review
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Button @click="showReplyDialog = true"> Reply </Button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4 h-full overflow-auto py-5">
|
||||
<div
|
||||
v-for="message in communication"
|
||||
class="mb-4 overflow-auto rounded-lg bg-gray-50 p-4"
|
||||
>
|
||||
<div class="flex justify-between">
|
||||
<div class="flex pb-4">
|
||||
<Avatar
|
||||
class="mr-2"
|
||||
:label="message.sender"
|
||||
:image="message.user_image"
|
||||
/>
|
||||
<span class="self-center text-lg font-semibold">{{
|
||||
message.sender
|
||||
}}</span>
|
||||
</div>
|
||||
<span class="text-base text-gray-600">{{
|
||||
getFormattedDateTime(message.communication_date)
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="text-base" v-html="message.content"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardDetails>
|
||||
</template>
|
||||
</CardWithDetails>
|
||||
<Dialog
|
||||
v-model="showReplyDialog"
|
||||
:options="{
|
||||
title: 'Reply',
|
||||
actions: [
|
||||
{
|
||||
label: 'Send',
|
||||
variant: 'solid',
|
||||
onClick: () => $resources.addReply.submit()
|
||||
}
|
||||
]
|
||||
}"
|
||||
>
|
||||
<template v-slot:body-content>
|
||||
<FormControl label="Message" v-model="message" type="textarea" required />
|
||||
</template>
|
||||
</Dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import CardWithDetails from '@/components/CardWithDetails.vue';
|
||||
import CardDetails from '@/components/CardDetails.vue';
|
||||
import { notify } from '@/utils/toast';
|
||||
|
||||
export default {
|
||||
name: 'MarketplaceAppReviewStages',
|
||||
components: { CardWithDetails, CardDetails },
|
||||
props: ['appName', 'app', 'reviewStages'],
|
||||
data() {
|
||||
return {
|
||||
showReplyDialog: false,
|
||||
message: null
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
getFormattedDateTime(time) {
|
||||
const date = new Date(time);
|
||||
return date.toLocaleString(undefined, {
|
||||
dateStyle: 'medium',
|
||||
timeStyle: 'short'
|
||||
});
|
||||
}
|
||||
},
|
||||
resources: {
|
||||
startReview() {
|
||||
return {
|
||||
url: 'jcloud.api.marketplace.mark_app_ready_for_review',
|
||||
params: {
|
||||
name: this.appName
|
||||
},
|
||||
onSuccess() {
|
||||
window.location.reload();
|
||||
}
|
||||
};
|
||||
},
|
||||
communication() {
|
||||
return {
|
||||
url: 'jcloud.api.marketplace.communication',
|
||||
params: {
|
||||
name: this.appName
|
||||
}
|
||||
};
|
||||
},
|
||||
addReply() {
|
||||
return {
|
||||
url: 'jcloud.api.marketplace.add_reply',
|
||||
params: {
|
||||
name: this.appName,
|
||||
message: this.message
|
||||
},
|
||||
onSuccess() {
|
||||
this.showReplyDialog = false;
|
||||
notify({
|
||||
title: 'Reply Queued',
|
||||
message: 'Message reply is queued for sending',
|
||||
icon: 'check',
|
||||
color: 'green'
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.$resources.communication.submit();
|
||||
},
|
||||
computed: {
|
||||
communication() {
|
||||
return this.$resources.communication.data;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@ -1,154 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<Card
|
||||
v-if="profileData && profileData.profile_created"
|
||||
title="Payout Preferences"
|
||||
subtitle="Used for payouts for your premium apps"
|
||||
>
|
||||
<div class="divide-y-2">
|
||||
<ListItem
|
||||
title="Payout Method"
|
||||
:description="payoutMethod || 'Not Set'"
|
||||
/>
|
||||
|
||||
<ListItem
|
||||
v-if="payoutMethod == 'PayPal'"
|
||||
title="PayPal ID"
|
||||
:description="payPalId || 'Not Set'"
|
||||
/>
|
||||
|
||||
<ListItem
|
||||
v-if="payoutMethod == 'Bank Transfer'"
|
||||
title="Account Holder Name"
|
||||
:description="acName || 'Not Set'"
|
||||
/>
|
||||
|
||||
<ListItem
|
||||
v-if="payoutMethod == 'Bank Transfer'"
|
||||
title="Account Number"
|
||||
:description="acNumber || 'Not Set'"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<template #actions>
|
||||
<Button icon-left="edit" @click="showEditProfileDialog = true"
|
||||
>Edit</Button
|
||||
>
|
||||
</template>
|
||||
</Card>
|
||||
|
||||
<Dialog
|
||||
:options="{
|
||||
title: 'Edit Publisher Profile',
|
||||
actions: [
|
||||
{
|
||||
variant: 'solid',
|
||||
label: 'Save Changes',
|
||||
loading: $resources.updatePublisherProfile.loading,
|
||||
onClick: () => $resources.updatePublisherProfile.submit()
|
||||
}
|
||||
]
|
||||
}"
|
||||
v-model="showEditProfileDialog"
|
||||
>
|
||||
<template v-slot:body-content>
|
||||
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||
<FormControl
|
||||
label="Preferred Payment Method"
|
||||
type="select"
|
||||
:options="['今果 Jingrow Credits', 'Bank Transfer', 'PayPal']"
|
||||
v-model="payoutMethod"
|
||||
/>
|
||||
|
||||
<FormControl
|
||||
v-if="payoutMethod == 'PayPal'"
|
||||
label="PayPal ID"
|
||||
v-model="payPalId"
|
||||
/>
|
||||
|
||||
<FormControl
|
||||
label="GSTIN (if applicable)"
|
||||
v-if="payoutMethod != '今果 Jingrow Credits'"
|
||||
v-model="gstin"
|
||||
/>
|
||||
|
||||
<FormControl
|
||||
v-if="payoutMethod == 'Bank Transfer'"
|
||||
label="Account Number"
|
||||
v-model="acNumber"
|
||||
/>
|
||||
|
||||
<FormControl
|
||||
v-if="payoutMethod == 'Bank Transfer'"
|
||||
label="Account Holder Name"
|
||||
v-model="acName"
|
||||
/>
|
||||
|
||||
<FormControl
|
||||
label="Bank Name, Branch, IFS Code"
|
||||
v-if="payoutMethod == 'Bank Transfer'"
|
||||
type="textarea"
|
||||
v-model="otherDetails"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<ErrorMessage
|
||||
class="mt-4"
|
||||
:message="$resources.updatePublisherProfile.error"
|
||||
/>
|
||||
</template>
|
||||
</Dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: ['profileData'],
|
||||
emits: ['profileUpdated'],
|
||||
data() {
|
||||
return {
|
||||
showEditProfileDialog: false,
|
||||
payoutMethod: '',
|
||||
payPalId: '',
|
||||
acNumber: '',
|
||||
acName: '',
|
||||
gstin: '',
|
||||
otherDetails: ''
|
||||
};
|
||||
},
|
||||
mounted() {},
|
||||
resources: {
|
||||
updatePublisherProfile() {
|
||||
return {
|
||||
url: 'jcloud.api.marketplace.update_publisher_profile',
|
||||
params: {
|
||||
profile_data: {
|
||||
preferred_payout_method: this.payoutMethod,
|
||||
paypal_id: this.payPalId,
|
||||
bank_account_number: this.acNumber,
|
||||
bank_account_holder_name: this.acName,
|
||||
gstin: this.gstin,
|
||||
other_bank_details: this.otherDetails
|
||||
}
|
||||
},
|
||||
onSuccess() {
|
||||
this.showEditProfileDialog = false;
|
||||
this.$emit('profileUpdated');
|
||||
}
|
||||
};
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
profileData(data) {
|
||||
if (data && data.profile_created) {
|
||||
this.payoutMethod = data.profile_info.preferred_payout_method;
|
||||
this.payPalId = data.profile_info.paypal_id;
|
||||
this.acNumber = data.profile_info.bank_account_number;
|
||||
this.acName = data.profile_info.bank_account_holder_name;
|
||||
this.gstin = data.profile_info.gstin;
|
||||
this.otherDetails = data.profile_info.other_bank_details;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@ -1,114 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<Card
|
||||
v-if="profileData && profileData.profile_created"
|
||||
title="Publisher Profile"
|
||||
subtitle="Visible on the marketplace website"
|
||||
>
|
||||
<div class="divide-y-2">
|
||||
<ListItem
|
||||
title="Display Name"
|
||||
:description="$sanitize(displayName || 'Not Set')"
|
||||
/>
|
||||
<ListItem
|
||||
title="Contact Email"
|
||||
:description="$sanitize(contactEmail || 'Not Set')"
|
||||
/>
|
||||
<ListItem
|
||||
title="Website"
|
||||
:description="$sanitize(website || 'Not Set')"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<template #actions>
|
||||
<Button icon-left="edit" @click="showEditProfileDialog = true"
|
||||
>Edit</Button
|
||||
>
|
||||
</template>
|
||||
</Card>
|
||||
|
||||
<Dialog
|
||||
:options="{
|
||||
title: 'Edit Publisher Profile',
|
||||
actions: [
|
||||
{
|
||||
variant: 'solid',
|
||||
label: 'Save Changes',
|
||||
loading: $resources.updatePublisherProfile.loading,
|
||||
onClick: () => $resources.updatePublisherProfile.submit()
|
||||
}
|
||||
]
|
||||
}"
|
||||
v-model="showEditProfileDialog"
|
||||
>
|
||||
<template v-slot:body-content>
|
||||
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||
<FormControl label="Display Name" v-model="displayName" />
|
||||
<FormControl
|
||||
label="Contact Email"
|
||||
type="email"
|
||||
v-model="contactEmail"
|
||||
/>
|
||||
<FormControl label="Website" v-model="website" />
|
||||
</div>
|
||||
|
||||
<ErrorMessage
|
||||
class="mt-4"
|
||||
:message="$resources.updatePublisherProfile.error"
|
||||
/>
|
||||
</template>
|
||||
</Dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: ['profileData', 'showEditDialog'],
|
||||
emits: ['profileUpdated'],
|
||||
data() {
|
||||
return {
|
||||
showEditProfileDialog: false,
|
||||
displayName: '',
|
||||
contactEmail: '',
|
||||
website: ''
|
||||
};
|
||||
},
|
||||
resources: {
|
||||
updatePublisherProfile() {
|
||||
return {
|
||||
url: 'jcloud.api.marketplace.update_publisher_profile',
|
||||
params: {
|
||||
profile_data: {
|
||||
display_name: this.displayName,
|
||||
contact_email: this.contactEmail,
|
||||
website: this.website
|
||||
}
|
||||
},
|
||||
validate() {
|
||||
if (!this.displayName) {
|
||||
return 'Display Name is required.';
|
||||
}
|
||||
},
|
||||
onSuccess() {
|
||||
this.showEditProfileDialog = false;
|
||||
this.$emit('profileUpdated');
|
||||
}
|
||||
};
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
profileData(data) {
|
||||
if (data && data.profile_created) {
|
||||
this.displayName = data.profile_info.display_name;
|
||||
this.contactEmail = data.profile_info.contact_email;
|
||||
this.website = data.profile_info.website;
|
||||
}
|
||||
},
|
||||
showEditDialog(value) {
|
||||
if (value) {
|
||||
this.showEditProfileDialog = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@ -1,7 +0,0 @@
|
||||
import { createResource } from 'jingrow-ui';
|
||||
|
||||
export const unreadNotificationsCount = createResource({
|
||||
cache: 'Unread Notifications Count',
|
||||
url: 'jcloud.api.notifications.get_unread_count',
|
||||
initialData: 0
|
||||
});
|
||||
@ -1,108 +0,0 @@
|
||||
import App from './App.vue';
|
||||
import { createApp } from 'vue';
|
||||
import registerPlugins from './plugins';
|
||||
import registerRouter from './router/register';
|
||||
import registerControllers from './controllers/register';
|
||||
import registerGlobalComponents from './components/global/register';
|
||||
import * as Sentry from '@sentry/vue';
|
||||
import posthog from 'posthog-js';
|
||||
import { BrowserTracing } from '@sentry/tracing';
|
||||
import router from './router/index';
|
||||
import dayjs from 'dayjs';
|
||||
import { notify } from '@/utils/toast';
|
||||
import {
|
||||
setConfig,
|
||||
jingrowRequest,
|
||||
pageMetaPlugin,
|
||||
resourcesPlugin
|
||||
} from 'jingrow-ui';
|
||||
|
||||
const app = createApp(App);
|
||||
let request = options => {
|
||||
let _options = options || {};
|
||||
_options.headers = options.headers || {};
|
||||
let currentTeam = localStorage.getItem('current_team');
|
||||
if (currentTeam) {
|
||||
_options.headers['X-Jcloud-Team'] = currentTeam;
|
||||
}
|
||||
return jingrowRequest(_options);
|
||||
};
|
||||
setConfig('resourceFetcher', request);
|
||||
app.use(resourcesPlugin);
|
||||
app.use(pageMetaPlugin);
|
||||
|
||||
registerPlugins(app);
|
||||
registerGlobalComponents(app);
|
||||
const { auth, account } = registerControllers(app);
|
||||
registerRouter(app, auth, account);
|
||||
|
||||
// sentry
|
||||
if (window.jcloud_frontend_sentry_dsn?.includes('https://')) {
|
||||
Sentry.init({
|
||||
app,
|
||||
dsn: window.jcloud_frontend_sentry_dsn,
|
||||
integrations: [
|
||||
new BrowserTracing({
|
||||
routingInstrumentation: Sentry.vueRouterInstrumentation(router),
|
||||
tracingOrigins: ['localhost', /^\//]
|
||||
})
|
||||
],
|
||||
beforeSend(event, hint) {
|
||||
const ignoreErrors = [
|
||||
/dynamically imported module/,
|
||||
/NetworkError when attempting to fetch resource/
|
||||
];
|
||||
const error = hint.originalException;
|
||||
|
||||
if (error?.message && ignoreErrors.some(re => re.test(error.message)))
|
||||
return null;
|
||||
|
||||
return event;
|
||||
},
|
||||
logErrors: true
|
||||
});
|
||||
}
|
||||
|
||||
// posthog
|
||||
if (window.jcloud_frontend_posthog_host?.includes('https://')) {
|
||||
try {
|
||||
posthog.init(window.jcloud_frontend_posthog_project_id, {
|
||||
api_host: window.jcloud_frontend_posthog_host,
|
||||
autocapture: false,
|
||||
capture_pageview: false,
|
||||
capture_pageleave: false,
|
||||
advanced_disable_decide: true
|
||||
});
|
||||
window.posthog = posthog;
|
||||
} catch (e) {
|
||||
console.trace('Failed to initialize telemetry', e);
|
||||
}
|
||||
}
|
||||
|
||||
if (import.meta.env.DEV) {
|
||||
request({
|
||||
url: '/api/action/jcloud.www.dashboard.get_context_for_dev'
|
||||
}).then(values => {
|
||||
for (let key in values) {
|
||||
window[key] = values[key];
|
||||
}
|
||||
app.mount('#app');
|
||||
});
|
||||
} else {
|
||||
app.mount('#app');
|
||||
}
|
||||
|
||||
app.config.globalProperties.$dayjs = dayjs;
|
||||
app.config.errorHandler = (error, instance) => {
|
||||
if (instance) {
|
||||
let errorMessage = error.message;
|
||||
if (error.messages) errorMessage = error.messages.join('\n');
|
||||
notify({
|
||||
icon: 'x',
|
||||
title: 'An error occurred',
|
||||
message: errorMessage,
|
||||
color: 'red'
|
||||
});
|
||||
}
|
||||
console.error(error);
|
||||
};
|
||||
@ -1,62 +0,0 @@
|
||||
import dayjs from 'dayjs';
|
||||
import relativeTime from 'dayjs/esm/plugin/relativeTime';
|
||||
import localizedFormat from 'dayjs/plugin/localizedFormat';
|
||||
import updateLocale from 'dayjs/plugin/updateLocale';
|
||||
import isToday from 'dayjs/plugin/isToday';
|
||||
import utc from 'dayjs/plugin/utc';
|
||||
import timezone from 'dayjs/plugin/timezone';
|
||||
|
||||
dayjs.extend(updateLocale);
|
||||
dayjs.extend(relativeTime);
|
||||
dayjs.extend(localizedFormat);
|
||||
dayjs.extend(isToday);
|
||||
dayjs.extend(utc);
|
||||
dayjs.extend(timezone);
|
||||
|
||||
dayjs.shortFormating = (s, ago = false) => {
|
||||
if (s === 'now' || s === 'now ago') {
|
||||
return 'now';
|
||||
}
|
||||
|
||||
const prefix = s.split(' ')[0];
|
||||
const posfix = s.split(' ')[1];
|
||||
const isPast = s.includes('ago');
|
||||
let newPostfix = '';
|
||||
switch (posfix) {
|
||||
case 'minute':
|
||||
newPostfix = 'm';
|
||||
break;
|
||||
case 'minutes':
|
||||
newPostfix = 'm';
|
||||
break;
|
||||
case 'hour':
|
||||
newPostfix = 'h';
|
||||
break;
|
||||
case 'hours':
|
||||
newPostfix = 'h';
|
||||
break;
|
||||
case 'day':
|
||||
newPostfix = 'd';
|
||||
break;
|
||||
case 'days':
|
||||
newPostfix = 'd';
|
||||
break;
|
||||
case 'month':
|
||||
newPostfix = 'M';
|
||||
break;
|
||||
case 'months':
|
||||
newPostfix = 'M';
|
||||
break;
|
||||
case 'year':
|
||||
newPostfix = 'Y';
|
||||
break;
|
||||
case 'years':
|
||||
newPostfix = 'Y';
|
||||
break;
|
||||
}
|
||||
return `${['a', 'an'].includes(prefix) ? '1' : prefix} ${newPostfix}${
|
||||
isPast ? (ago ? ' ago' : '') : ''
|
||||
}`;
|
||||
};
|
||||
|
||||
export default dayjs;
|
||||
@ -1,15 +0,0 @@
|
||||
import { ref } from 'vue';
|
||||
|
||||
export const notifications = ref([]);
|
||||
|
||||
export const hideNotification = id => {
|
||||
notifications.value = notifications.value.filter(props => props.id !== id);
|
||||
};
|
||||
|
||||
export const notify = props => {
|
||||
// TODO: remove the line below once the jingrow-ui bug (onError triggers twice) is fixed
|
||||
if (notifications.value.some(n => n.message === props.message)) return;
|
||||
props.id = Math.floor(Math.random() * 1000 + Date.now());
|
||||
notifications.value.push(props);
|
||||
setTimeout(() => hideNotification(props.id), props.timeout || 5000);
|
||||
};
|
||||
@ -272,7 +272,7 @@ provide('team', team);
|
||||
provide('session', session);
|
||||
</script>
|
||||
|
||||
<style src="../src/assets/style.css"></style>
|
||||
<style src="./assets/style.css"></style>
|
||||
|
||||
<style>
|
||||
.app-sidebar-sider {
|
||||
|
||||
|
Before Width: | Height: | Size: 94 KiB After Width: | Height: | Size: 94 KiB |
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user