Files
appwrite/app/views/install/installer/js/steps.js

474 lines
17 KiB
JavaScript

(() => {
const Context = window.InstallerStepsContext || {};
const State = window.InstallerStepsState || {};
const Validation = window.InstallerStepsValidation || {};
const UI = window.InstallerStepsUI || {};
const Progress = window.InstallerStepsProgress || {};
const Tooltips = window.InstallerTooltips || null;
const {
INSTALLATION_STEPS,
clampStep,
isUpgradeMode,
getEnabledDatabases
} = Context;
const {
formState,
dispatchStateChange,
applyBodyDefaults,
applyLockPayload,
clearInstallLock,
clearInstallId,
isInstallLocked,
syncInstallLockFlag,
getInstallLock,
getLockedDatabase
} = State;
const {
isValidEmail,
isValidPort,
isValidHostnameInput,
isValidPassword
} = Validation;
const {
clearFieldErrors,
setFieldError,
bindErrorClear,
updateDatabaseSelection,
setupResetButtons,
setupAccordion,
openAccordion,
disableControls,
generateSecretKey,
copyToClipboard,
setTooltipText,
resetTooltipText,
updateReviewSummary
} = UI;
let reviewListener = null;
const bindInputToState = (input, key) => {
if (!input) return;
const update = () => {
formState[key] = input.value;
dispatchStateChange?.(key);
};
input.addEventListener('input', update);
input.addEventListener('change', update);
update();
};
const lockDatabaseSelection = (root, lockedDatabase) => {
if (lockedDatabase) {
const radios = root.querySelectorAll('input[name="database"]');
radios.forEach((radio) => {
const isLockedChoice = radio.value === lockedDatabase;
const card = radio.closest('.selector-card');
radio.disabled = !isLockedChoice;
if (card) {
card.classList.toggle('is-disabled', !isLockedChoice);
}
if (isLockedChoice) {
radio.checked = true;
updateDatabaseSelection?.(radio, root);
}
});
}
};
const applyEnabledDatabases = (root) => {
const enabled = getEnabledDatabases?.() || [];
const radios = root.querySelectorAll('input[name="database"]');
radios.forEach((radio) => {
if (!enabled.includes(radio.value)) {
const card = radio.closest('.selector-card');
if (card) {
card.remove();
}
}
});
};
const bindDatabaseSelection = (root) => {
const radios = root.querySelectorAll('input[name="database"]');
radios.forEach((radio) => {
radio.addEventListener('change', () => {
formState.database = radio.value;
updateDatabaseSelection?.(radio, root);
});
});
};
const hydrateStep1State = (root) => {
State.setStateIfEmpty?.('appDomain', root.querySelector('#hostname')?.value);
State.setStateIfEmpty?.('database', root.querySelector('input[name="database"]:checked')?.value);
State.setStateIfEmpty?.('httpPort', root.querySelector('#http-port')?.value);
State.setStateIfEmpty?.('httpsPort', root.querySelector('#https-port')?.value);
State.setStateIfEmpty?.('emailCertificates', root.querySelector('#ssl-email')?.value);
State.setStateIfEmpty?.('assistantOpenAIKey', root.querySelector('#assistant-openai-key')?.value);
};
const applyStep1State = (root) => {
const hostname = root.querySelector('#hostname');
if (hostname && formState.appDomain) hostname.value = formState.appDomain;
const httpPort = root.querySelector('#http-port');
if (httpPort && formState.httpPort) httpPort.value = formState.httpPort;
const httpsPort = root.querySelector('#https-port');
if (httpsPort && formState.httpsPort) httpsPort.value = formState.httpsPort;
const sslEmail = root.querySelector('#ssl-email');
if (sslEmail && formState.emailCertificates) sslEmail.value = formState.emailCertificates;
const assistantKey = root.querySelector('#assistant-openai-key');
if (assistantKey && formState.assistantOpenAIKey) {
assistantKey.value = formState.assistantOpenAIKey;
}
if (formState.database) {
const radio = root.querySelector(`input[name="database"][value="${formState.database}"]`);
if (radio) {
radio.checked = true;
updateDatabaseSelection?.(radio, root);
}
}
};
const initStep1 = (root) => {
if (!root) return;
syncInstallLockFlag?.();
applyLockPayload?.();
applyBodyDefaults?.();
hydrateStep1State(root);
applyStep1State(root);
if (isInstallLocked?.()) {
openAccordion?.(root);
disableControls?.(root);
return;
}
applyEnabledDatabases(root);
const lockedDatabase = getLockedDatabase?.() || '';
if (lockedDatabase) {
lockDatabaseSelection(root, lockedDatabase);
} else {
bindDatabaseSelection(root);
}
const hostname = root.querySelector('#hostname');
const httpPort = root.querySelector('#http-port');
const httpsPort = root.querySelector('#https-port');
const sslEmail = root.querySelector('#ssl-email');
const assistantKey = root.querySelector('#assistant-openai-key');
bindInputToState(hostname, 'appDomain');
bindInputToState(httpPort, 'httpPort');
bindInputToState(httpsPort, 'httpsPort');
bindInputToState(sslEmail, 'emailCertificates');
bindInputToState(assistantKey, 'assistantOpenAIKey');
bindErrorClear?.(hostname);
bindErrorClear?.(httpPort);
bindErrorClear?.(httpsPort);
bindErrorClear?.(sslEmail);
bindErrorClear?.(assistantKey);
const checked = root.querySelector('input[name="database"]:checked');
if (checked) {
updateDatabaseSelection?.(checked, root);
}
setupResetButtons?.(root);
setupAccordion?.(root);
Tooltips?.setupTooltipPortals?.(root);
};
const hydrateStep2State = (root) => {
const value = root.querySelector('#secret-key')?.value;
if (formState.opensslKey) return;
if (value) {
formState.opensslKey = value;
}
};
const applyStep2State = (root) => {
const input = root.querySelector('#secret-key');
if (input && formState.opensslKey) {
input.value = formState.opensslKey;
}
};
const initStep2 = (root) => {
if (!root) return;
syncInstallLockFlag?.();
applyLockPayload?.();
applyBodyDefaults?.();
hydrateStep2State(root);
if (!isUpgradeMode?.() && (!formState.opensslKey || !formState.opensslKey.trim())) {
formState.opensslKey = generateSecretKey?.();
dispatchStateChange?.('opensslKey');
}
applyStep2State(root);
const input = root.querySelector('#secret-key');
if (input) {
bindInputToState(input, 'opensslKey');
bindErrorClear?.(input);
}
const copyButton = root.querySelector('[data-copy-target]');
const tooltipWrapper = copyButton?.closest('.tooltip-wrapper');
if (tooltipWrapper) {
tooltipWrapper.addEventListener('mouseenter', () => resetTooltipText?.(tooltipWrapper));
tooltipWrapper.addEventListener('focusin', () => resetTooltipText?.(tooltipWrapper));
}
if (copyButton) {
copyButton.addEventListener('click', () => {
const targetId = copyButton.getAttribute('data-copy-target');
const targetInput = targetId ? root.querySelector(`#${targetId}`) : null;
const value = targetInput?.value || '';
copyToClipboard?.(value, targetInput);
copyButton.blur();
if (tooltipWrapper) {
const successText = tooltipWrapper.dataset.tooltipSuccess || 'Copied';
setTooltipText?.(tooltipWrapper, successText);
}
});
}
const regenerateButton = root.querySelector('[data-regenerate-target]');
if (regenerateButton && !isInstallLocked?.()) {
regenerateButton.addEventListener('click', () => {
const targetId = regenerateButton.getAttribute('data-regenerate-target');
const targetInput = targetId ? root.querySelector(`#${targetId}`) : null;
if (!targetInput) return;
regenerateButton.classList.remove('is-rotating');
void regenerateButton.offsetWidth;
regenerateButton.classList.add('is-rotating');
const handleAnimationEnd = () => {
regenerateButton.classList.remove('is-rotating');
};
regenerateButton.addEventListener('animationend', handleAnimationEnd, { once: true });
targetInput.value = generateSecretKey?.();
targetInput.dispatchEvent(new Event('input', { bubbles: true }));
});
}
if (isInstallLocked?.()) {
disableControls?.(root);
}
};
const hydrateStep3State = (root) => {
State.setStateIfEmpty?.('accountEmail', root.querySelector('#account-email')?.value);
State.setStateIfEmpty?.('accountPassword', root.querySelector('#account-password')?.value);
};
const applyStep3State = (root) => {
const email = root.querySelector('#account-email');
if (email && formState.accountEmail) email.value = formState.accountEmail;
const password = root.querySelector('#account-password');
if (password && formState.accountPassword) password.value = formState.accountPassword;
};
const initStep3 = (root) => {
if (!root) return;
syncInstallLockFlag?.();
applyLockPayload?.();
applyBodyDefaults?.();
hydrateStep3State(root);
applyStep3State(root);
const email = root.querySelector('#account-email');
const password = root.querySelector('#account-password');
const passwordToggle = root.querySelector('[data-password-toggle="account-password"]');
bindInputToState(email, 'accountEmail');
bindInputToState(password, 'accountPassword');
bindErrorClear?.(email);
bindErrorClear?.(password);
if (password && passwordToggle) {
passwordToggle.addEventListener('click', () => {
const isVisible = passwordToggle.classList.toggle('is-visible');
password.type = isVisible ? 'text' : 'password';
passwordToggle.setAttribute('aria-label', isVisible ? 'Hide password' : 'Show password');
});
}
if (isInstallLocked?.()) {
disableControls?.(root);
}
};
const initStep4 = (root) => {
if (!root) return;
syncInstallLockFlag?.();
applyLockPayload?.();
applyBodyDefaults?.();
updateReviewSummary?.(root);
if (reviewListener) {
document.removeEventListener('installer:state-change', reviewListener);
}
reviewListener = () => updateReviewSummary?.(root);
document.addEventListener('installer:state-change', reviewListener);
if (isInstallLocked?.()) {
disableControls?.(root);
}
};
const initStep6 = (root) => {
if (!root) return;
syncInstallLockFlag?.();
applyLockPayload?.();
applyBodyDefaults?.();
const checkbox = root.querySelector('#run-migration');
if (checkbox) {
if (formState.migrate !== undefined) {
checkbox.checked = formState.migrate;
} else {
formState.migrate = checkbox.checked;
}
checkbox.addEventListener('change', () => {
formState.migrate = checkbox.checked;
dispatchStateChange?.('migrate');
});
}
if (isInstallLocked?.()) {
disableControls?.(root);
}
};
const initStep = (step, container) => {
if (!container) return;
const root = container.querySelector('.step-layout') || container;
const normalized = clampStep?.(step) ?? 1;
Tooltips?.cleanupTooltipPortals?.();
if (normalized !== 4 && reviewListener) {
document.removeEventListener('installer:state-change', reviewListener);
reviewListener = null;
}
if (normalized !== 5) {
Progress.cleanupInstallFlow?.();
}
if (normalized === 1) initStep1(root);
if (normalized === 2) initStep2(root);
if (normalized === 3) initStep3(root);
if (normalized === 4) initStep4(root);
if (normalized === 5) Progress.initStep5?.(root);
if (normalized === 6) initStep6(root);
};
window.InstallerSteps = {
initStep1,
initStep2,
initStep3,
initStep4,
initStep5: Progress.initStep5,
installationSteps: INSTALLATION_STEPS || [],
isInstallLocked,
getInstallLock,
clearInstallLock,
initStep,
validateStep: (step, container) => {
const root = container?.querySelector('.step-layout') || container;
const normalized = clampStep?.(step) ?? 1;
if (normalized === 1) {
clearFieldErrors?.(root);
let valid = true;
const hostname = root?.querySelector('#hostname');
const httpPort = root?.querySelector('#http-port');
const httpsPort = root?.querySelector('#https-port');
const sslEmail = root?.querySelector('#ssl-email');
if (!hostname || !hostname.value.trim()) {
setFieldError?.(hostname, 'Please enter your Appwrite hostname');
valid = false;
} else if (!isValidHostnameInput?.(hostname.value.trim())) {
setFieldError?.(hostname, 'Please enter a valid hostname');
valid = false;
}
const parsePort = (input, label) => {
const value = input?.value;
if (!value || !isValidPort?.(value)) {
setFieldError?.(input, `Please enter a valid ${label} port (1-65535)`);
return false;
}
return true;
};
if (!parsePort(httpPort, 'HTTP')) valid = false;
if (!parsePort(httpsPort, 'HTTPS')) valid = false;
if (sslEmail && sslEmail.value.trim() && !isValidEmail?.(sslEmail.value.trim())) {
setFieldError?.(sslEmail, 'Please enter a valid email address');
valid = false;
}
if (!valid) {
openAccordion?.(root);
}
return valid;
}
if (normalized === 2) {
clearFieldErrors?.(root);
const secretKey = root?.querySelector('#secret-key');
const secretValue = secretKey?.value.trim() || '';
if (!secretKey || !secretValue) {
setFieldError?.(secretKey, 'Please enter or generate a secret API key');
return false;
}
if (secretValue.length > 64) {
setFieldError?.(secretKey, 'Secret API key must be 1-64 characters');
return false;
}
}
if (normalized === 3) {
clearFieldErrors?.(root);
let valid = true;
const email = root?.querySelector('#account-email');
const password = root?.querySelector('#account-password');
if (!email || !email.value.trim()) {
setFieldError?.(email, 'This field is required');
valid = false;
} else if (!isValidEmail?.(email.value.trim())) {
setFieldError?.(email, 'Please enter a valid email address');
valid = false;
}
const passwordValue = password?.value ?? '';
if (!password || !/\S/.test(passwordValue)) {
setFieldError?.(password, 'This field is required');
valid = false;
} else if (!isValidPassword?.(passwordValue)) {
setFieldError?.(password, 'Password must be at least 8 characters long');
valid = false;
}
return valid;
}
return true;
}
};
})();