Fix empty server order modal by restoring serverOrderForm JS.
Deploy Ladill Servers / deploy (push) Successful in 20s
Deploy Ladill Servers / deploy (push) Successful in 20s
The modal Alpine component was never copied during extraction, so step content never rendered. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -180,5 +180,365 @@ Alpine.data('topbarSearch', (config = {}) => ({
|
||||
},
|
||||
}));
|
||||
|
||||
window.serverOrderForm = (products = {}, billingCycleLabels = {}, initial = {}) => ({
|
||||
products,
|
||||
billingCycleLabels,
|
||||
steps: [{ label: 'Select Plan' }, { label: 'Configure' }, { label: 'Review' }],
|
||||
step: Number(initial.step ?? 1),
|
||||
selectedProductId: initial.selectedProductId ? String(initial.selectedProductId) : '',
|
||||
selectedProduct: null,
|
||||
serverName: String(initial.serverName ?? ''),
|
||||
billingCycle: String(initial.billingCycle ?? 'monthly'),
|
||||
selections: {
|
||||
region: '',
|
||||
image: '',
|
||||
license: '',
|
||||
application: '',
|
||||
default_user: '',
|
||||
additional_ip: '',
|
||||
private_networking: '',
|
||||
storage_type: '',
|
||||
object_storage: '',
|
||||
},
|
||||
serverPassword: '',
|
||||
serverPasswordConfirmation: '',
|
||||
submitting: false,
|
||||
error: '',
|
||||
|
||||
clone(value) {
|
||||
return JSON.parse(JSON.stringify(value ?? null));
|
||||
},
|
||||
|
||||
init() {
|
||||
if (this.selectedProductId) {
|
||||
this.applyProduct(this.selectedProductId, true);
|
||||
}
|
||||
},
|
||||
|
||||
applyProduct(productId, preserveState = false) {
|
||||
const product = this.products[String(productId)] || null;
|
||||
|
||||
if (!product) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.selectedProductId = String(productId);
|
||||
this.selectedProduct = this.clone(product);
|
||||
|
||||
const defaults = this.clone(product.defaults || {});
|
||||
const initialSelections = preserveState ? this.clone(initial.selections || {}) : {};
|
||||
|
||||
this.selections = {
|
||||
...defaults,
|
||||
...initialSelections,
|
||||
};
|
||||
|
||||
if (!preserveState) {
|
||||
this.serverName = '';
|
||||
this.billingCycle = 'monthly';
|
||||
this.serverPassword = '';
|
||||
this.serverPasswordConfirmation = '';
|
||||
}
|
||||
|
||||
if (!Object.prototype.hasOwnProperty.call(this.billingCycleLabels, this.billingCycle)) {
|
||||
this.billingCycle = 'monthly';
|
||||
}
|
||||
|
||||
this.syncDefaultUser();
|
||||
this.syncLicenseSelection();
|
||||
this.error = '';
|
||||
},
|
||||
|
||||
selectProduct(productId) {
|
||||
this.applyProduct(productId, false);
|
||||
},
|
||||
|
||||
nextStep() {
|
||||
if (this.step === 1 && !this.selectedProductId) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.step === 2 && !this.serverName.trim()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.step < this.steps.length) {
|
||||
this.step += 1;
|
||||
}
|
||||
},
|
||||
|
||||
catalog(key) {
|
||||
return this.selectedProduct?.catalog?.[key] || [];
|
||||
},
|
||||
|
||||
findOption(options, value) {
|
||||
return (options || []).find((option) => option.value === value) || null;
|
||||
},
|
||||
|
||||
optionAmount(option) {
|
||||
return Number(option?.prices?.[this.billingCycle] || 0);
|
||||
},
|
||||
|
||||
optionLabel(option) {
|
||||
if (!option) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const amount = this.optionAmount(option);
|
||||
|
||||
return amount > 0
|
||||
? `${option.label} (+${this.formatMoney(amount)})`
|
||||
: option.label;
|
||||
},
|
||||
|
||||
selectionLabel(catalogKey, value) {
|
||||
const option = this.findOption(this.catalog(catalogKey), value);
|
||||
|
||||
return option?.label || value || '-';
|
||||
},
|
||||
|
||||
currentImage() {
|
||||
return this.findOption(this.catalog('images'), this.selections.image);
|
||||
},
|
||||
|
||||
imageSupportsManagedStack(image = null) {
|
||||
const currentImage = image || this.currentImage();
|
||||
const osFamily = String(currentImage?.os_family || this.selectedProduct?.catalog?.default_image_os_family || 'linux').toLowerCase();
|
||||
const imageLabel = String(currentImage?.label || '').toLowerCase();
|
||||
const installLater = Boolean(currentImage?.is_install_later);
|
||||
|
||||
if (installLater || osFamily.startsWith('win')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (['cpanel', 'plesk', 'whm'].some((needle) => imageLabel.includes(needle))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return ['ubuntu 22.04', 'ubuntu 24.04', 'debian 12', 'bookworm', 'almalinux', 'alma linux', 'alma', 'rocky linux', 'rocky', 'fedora', 'opensuse leap', 'open suse leap', 'opensuse', 'leap', 'centos']
|
||||
.some((needle) => imageLabel.includes(needle));
|
||||
},
|
||||
|
||||
bundledPanelFromImage(image = null) {
|
||||
const imageLabel = String((image || this.currentImage())?.label || '').toLowerCase();
|
||||
|
||||
if (imageLabel.includes('cpanel') || imageLabel.includes('whm')) {
|
||||
return 'cpanel';
|
||||
}
|
||||
|
||||
if (imageLabel.includes('plesk')) {
|
||||
return 'plesk';
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
|
||||
availableLicenseOptions() {
|
||||
const image = this.currentImage();
|
||||
const installLater = Boolean(image?.is_install_later);
|
||||
const bundledPanel = this.bundledPanelFromImage(image);
|
||||
|
||||
return this.catalog('licenses').filter((option) => {
|
||||
if (installLater) {
|
||||
return option.value === 'none';
|
||||
}
|
||||
|
||||
if (bundledPanel && option.value === 'none') {
|
||||
return false;
|
||||
}
|
||||
|
||||
const optionLicense = String(option.license || '').toLowerCase();
|
||||
|
||||
if (bundledPanel === 'cpanel' && optionLicense.startsWith('plesk')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (bundledPanel === 'plesk' && optionLicense.startsWith('cpanel')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (option.panel !== 'ladill') {
|
||||
return true;
|
||||
}
|
||||
|
||||
return this.imageSupportsManagedStack(image);
|
||||
});
|
||||
},
|
||||
|
||||
currentLicenseOption() {
|
||||
return this.findOption(this.availableLicenseOptions(), this.selections.license)
|
||||
|| this.findOption(this.catalog('licenses'), this.selections.license);
|
||||
},
|
||||
|
||||
currentPanelSnapshot() {
|
||||
const license = this.currentLicenseOption();
|
||||
const image = this.currentImage();
|
||||
const managedCandidate = this.imageSupportsManagedStack(image);
|
||||
|
||||
if (license?.panel === 'ladill') {
|
||||
return {
|
||||
label: license.label || 'Ladill Server Manager',
|
||||
primary: managedCandidate
|
||||
? 'Ladill Server Manager will provision this server with Ladill-managed controls on the selected supported Linux image.'
|
||||
: 'Ladill Server Manager is not available for the selected image.',
|
||||
secondary: managedCandidate
|
||||
? 'After provisioning finishes, Server Manager can handle power, status, files, databases, domains, PHP, SSL, cron, and logs.'
|
||||
: 'Choose a plain supported Linux image without cPanel or Plesk to use Ladill-managed hosting features.',
|
||||
futureCapabilities: managedCandidate ? ['Files', 'Databases', 'Domains', 'PHP', 'SSL', 'Cron', 'Logs'] : [],
|
||||
toneClasses: managedCandidate
|
||||
? 'border-amber-200 bg-amber-50 text-amber-900'
|
||||
: 'border-slate-200 bg-slate-50 text-slate-700',
|
||||
};
|
||||
}
|
||||
|
||||
if (license?.license) {
|
||||
return {
|
||||
label: license.label || 'Server Panel',
|
||||
primary: `${license.label} will run inside the server as the primary control panel.`,
|
||||
secondary: 'Ladill will keep the order and infrastructure lifecycle, but site and service management will happen in the installed panel.',
|
||||
futureCapabilities: [],
|
||||
toneClasses: 'border-blue-200 bg-blue-50 text-blue-900',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
label: license?.label || 'Remote Login Only',
|
||||
primary: 'This selection provisions the server without a Ladill-managed or commercial control panel.',
|
||||
secondary: 'You will manage the machine directly over SSH or RDP until a panel or managed runtime is installed separately.',
|
||||
futureCapabilities: [],
|
||||
toneClasses: 'border-gray-200 bg-gray-50 text-gray-700',
|
||||
};
|
||||
},
|
||||
|
||||
defaultUserOptions() {
|
||||
const defaultFamily = this.selectedProduct?.catalog?.default_image_os_family || 'linux';
|
||||
const osFamily = this.currentImage()?.os_family || defaultFamily;
|
||||
|
||||
return osFamily.startsWith('win')
|
||||
? this.catalog('windows_default_users')
|
||||
: this.catalog('linux_default_users');
|
||||
},
|
||||
|
||||
syncDefaultUser() {
|
||||
const options = this.defaultUserOptions();
|
||||
|
||||
if (!options.some((option) => option.value === this.selections.default_user)) {
|
||||
this.selections.default_user = options[0]?.value || '';
|
||||
}
|
||||
},
|
||||
|
||||
syncLicenseSelection() {
|
||||
const options = this.availableLicenseOptions();
|
||||
|
||||
if (!options.some((option) => option.value === this.selections.license)) {
|
||||
this.selections.license = options[0]?.value || 'none';
|
||||
}
|
||||
},
|
||||
|
||||
handleImageChange() {
|
||||
this.syncDefaultUser();
|
||||
this.syncLicenseSelection();
|
||||
},
|
||||
|
||||
basePrice() {
|
||||
return Number(this.selectedProduct?.prices?.[this.billingCycle] || 0);
|
||||
},
|
||||
|
||||
setupFee() {
|
||||
return Number(this.selectedProduct?.setup_fees?.[this.billingCycle] || 0);
|
||||
},
|
||||
|
||||
addOnLineItems() {
|
||||
const items = [];
|
||||
const setupFee = this.setupFee();
|
||||
|
||||
if (setupFee > 0) {
|
||||
items.push({
|
||||
key: 'setup_fee',
|
||||
label: 'One-time setup fee',
|
||||
amount: setupFee,
|
||||
});
|
||||
}
|
||||
|
||||
const keys = ['image', 'region', 'license', 'application', 'additional_ip', 'private_networking', 'storage_type', 'object_storage'];
|
||||
const catalogKeys = {
|
||||
image: 'images',
|
||||
region: 'regions',
|
||||
license: 'licenses',
|
||||
application: 'applications',
|
||||
additional_ip: 'additional_ip',
|
||||
private_networking: 'private_networking',
|
||||
storage_type: 'storage_types',
|
||||
object_storage: 'object_storage',
|
||||
};
|
||||
|
||||
keys.forEach((key) => {
|
||||
const option = this.findOption(this.catalog(catalogKeys[key]), this.selections[key]);
|
||||
const amount = this.optionAmount(option);
|
||||
|
||||
if (amount > 0) {
|
||||
items.push({
|
||||
key,
|
||||
label: option.label,
|
||||
amount,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return items;
|
||||
},
|
||||
|
||||
currentTotal() {
|
||||
return this.basePrice() + this.addOnLineItems().reduce((sum, item) => sum + item.amount, 0);
|
||||
},
|
||||
|
||||
cycleMonths() {
|
||||
return {
|
||||
monthly: 1,
|
||||
quarterly: 3,
|
||||
semiannual: 6,
|
||||
yearly: 12,
|
||||
}[this.billingCycle] || 1;
|
||||
},
|
||||
|
||||
monthlyEquivalent() {
|
||||
return this.currentTotal() / this.cycleMonths();
|
||||
},
|
||||
|
||||
formatMoney(amount, currency = null) {
|
||||
if (amount === null || amount === undefined) {
|
||||
return '-';
|
||||
}
|
||||
|
||||
const code = currency || this.selectedProduct?.currency || 'GHS';
|
||||
|
||||
return `${code} ${Number(amount).toLocaleString('en-US', {
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2,
|
||||
})}`;
|
||||
},
|
||||
|
||||
canSubmit() {
|
||||
return Boolean(
|
||||
this.selectedProductId
|
||||
&& this.serverName.trim()
|
||||
&& this.serverPassword
|
||||
&& this.serverPassword === this.serverPasswordConfirmation
|
||||
&& this.serverPassword.length >= 8
|
||||
);
|
||||
},
|
||||
|
||||
submitOrder() {
|
||||
if (!this.canSubmit()) {
|
||||
this.error = 'Complete the required fields and make sure the passwords match.';
|
||||
return;
|
||||
}
|
||||
|
||||
this.submitting = true;
|
||||
this.error = '';
|
||||
this.$refs.form.submit();
|
||||
},
|
||||
});
|
||||
|
||||
window.Alpine = Alpine;
|
||||
Alpine.start();
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
@click.stop>
|
||||
<form x-ref="form"
|
||||
method="POST"
|
||||
action="{{ route('hosting.servers.store') }}"
|
||||
action="{{ route('servers.orders.store') }}"
|
||||
x-data="serverOrderForm(@js($serverOrderPayloads), @js($billingCycles), @js($initialServerOrderState))"
|
||||
@submit.prevent="submitOrder"
|
||||
class="max-h-[90vh] overflow-y-auto">
|
||||
|
||||
Reference in New Issue
Block a user