v206 - bagi

This commit is contained in:
Arseny Sazhin (via WeWeb) 2025-11-01 07:14:26 +03:00
parent 01f1703975
commit 9b6a03977f
53 changed files with 7888 additions and 4345 deletions

View File

@ -1 +1 @@
{"name":"weweb-front","version":"4.0.0","private":true,"type":"module","scripts":{"serve":"vite","build":"vite build","postbuild":"node ./postbuild.js"},"dependencies":{"@vueuse/head":"2.0.0","axios":"1.12.2","html-escaper":"3.0.3","lodash":"4.17.21","pinia":"3.0.2","short-unique-id":"5.3.2","tiny-emitter":"2.1.0","uuid":"11.1.0","vue":"3.5.13","vue-cookie-next":"1.3.0","vue-meta":"2.4.0","vue-router":"4.5.1","vuex":"4.1.0","dayjs":"1.11.0","qs":"^6.14.0","@supabase/supabase-js":"2.50.3","@vueform/multiselect":"2.6.2","vue-ellipse-progress":"^2.1.1","@vueuse/core":"^13.0.0","diff":"^5.1.0","animejs":"^3.2.2","marked":"^14.1.2","medium-zoom":"^1.0.6","prismjs":"^1.23.0","@vuepic/vue-datepicker":"3.6.8","date-fns":"4.1.0","@tiptap/extension-color":"2.8.0","@tiptap/extension-image":"2.8.0","@tiptap/extension-link":"2.8.0","@tiptap/extension-mathematics":"2.26.1","@tiptap/extension-mention":"2.8.0","@tiptap/extension-placeholder":"2.8.0","@tiptap/extension-table":"2.8.0","@tiptap/extension-table-cell":"2.8.0","@tiptap/extension-table-header":"2.8.0","@tiptap/extension-table-row":"2.8.0","@tiptap/extension-task-item":"2.8.0","@tiptap/extension-task-list":"2.8.0","@tiptap/extension-text-align":"2.8.0","@tiptap/extension-text-style":"2.8.0","@tiptap/extension-underline":"2.8.0","@tiptap/pm":"2.8.0","@tiptap/starter-kit":"2.8.0","@tiptap/suggestion":"2.8.0","@tiptap/vue-3":"2.8.0","katex":"^0.16.22","prosemirror-commands":"1.5.0","prosemirror-dropcursor":"1.5.0","prosemirror-gapcursor":"1.3.1","prosemirror-history":"1.3.0","prosemirror-keymap":"1.2.0","prosemirror-schema-list":"1.2.2","tiptap-markdown":"^0.8.10","lodash-es":"^4.17.21"},"devDependencies":{"@vitejs/plugin-vue":"5.2.4","autoprefixer":"10.4.21","handlebars":"4.7.8","sass-embedded":"1.89.0","vite":"6.3.5","vite-plugin-node-polyfills":"0.23.0"},"browserslist":["last 3 years"]} {"name":"weweb-front","version":"4.0.0","private":true,"type":"module","scripts":{"serve":"vite","build":"vite build","postbuild":"node ./postbuild.js"},"dependencies":{"@vueuse/head":"2.0.0","axios":"1.12.2","html-escaper":"3.0.3","lodash":"4.17.21","pinia":"3.0.2","short-unique-id":"5.3.2","tiny-emitter":"2.1.0","uuid":"11.1.0","vue":"3.5.13","vue-cookie-next":"1.3.0","vue-meta":"2.4.0","vue-router":"4.5.1","vuex":"4.1.0","dayjs":"1.11.0","qs":"^6.14.0","@supabase/supabase-js":"2.50.3","@vueform/multiselect":"2.6.2","vue-ellipse-progress":"^2.1.1","diff":"^5.1.0","animejs":"^3.2.2","marked":"^14.1.2","medium-zoom":"^1.0.6","prismjs":"^1.23.0","@tiptap/extension-color":"2.8.0","@tiptap/extension-image":"2.8.0","@tiptap/extension-link":"2.8.0","@tiptap/extension-mathematics":"2.26.1","@tiptap/extension-mention":"2.8.0","@tiptap/extension-placeholder":"2.8.0","@tiptap/extension-table":"2.8.0","@tiptap/extension-table-cell":"2.8.0","@tiptap/extension-table-header":"2.8.0","@tiptap/extension-table-row":"2.8.0","@tiptap/extension-task-item":"2.8.0","@tiptap/extension-task-list":"2.8.0","@tiptap/extension-text-align":"2.8.0","@tiptap/extension-text-style":"2.8.0","@tiptap/extension-underline":"2.8.0","@tiptap/pm":"2.8.0","@tiptap/starter-kit":"2.8.0","@tiptap/suggestion":"2.8.0","@tiptap/vue-3":"2.8.0","katex":"^0.16.22","prosemirror-commands":"1.5.0","prosemirror-dropcursor":"1.5.0","prosemirror-gapcursor":"1.3.1","prosemirror-history":"1.3.0","prosemirror-keymap":"1.2.0","prosemirror-schema-list":"1.2.2","tiptap-markdown":"^0.8.10","@vueuse/core":"^13.0.0","@vuepic/vue-datepicker":"3.6.8","date-fns":"4.1.0","lodash-es":"^4.17.21"},"devDependencies":{"@vitejs/plugin-vue":"5.2.4","autoprefixer":"10.4.21","handlebars":"4.7.8","sass-embedded":"1.89.0","vite":"6.3.5","vite-plugin-node-polyfills":"0.23.0"},"browserslist":["last 3 years"]}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1 +1 @@
{"cacheVersion":205,"page":{"id":"cf9f551f-e733-4934-a682-535575cb7c70","paths":{"en":"promo","default":"promo"},"cmsDataSetPath":null,"workflows":[]},"sections":{"4a2188f6-d98c-4150-9ba7-5918c72f3421":{"uid":"4a2188f6-d98c-4150-9ba7-5918c72f3421","linkId":"416695e7-e54e-4897-9f9a-befeb223f1b0","_state":{"style":{"default":{}}},"content":{"default":{"wwObjects":[],"_ww-layout_alignItems":"flex-start","_ww-layout_flexDirection":"column"}},"sectionBaseId":"99586bd3-2b15-4d6b-a025-6a50d07ca845","sectionTitle":"Section"}},"wwObjects":{},"collections":[],"variables":[],"workflows":[],"formulas":[],"libraryComponents":[]} {"cacheVersion":206,"page":{"id":"cf9f551f-e733-4934-a682-535575cb7c70","paths":{"en":"promo","default":"promo"},"cmsDataSetPath":null,"workflows":[]},"sections":{"4a2188f6-d98c-4150-9ba7-5918c72f3421":{"uid":"4a2188f6-d98c-4150-9ba7-5918c72f3421","linkId":"416695e7-e54e-4897-9f9a-befeb223f1b0","_state":{"style":{"default":{}}},"content":{"default":{"wwObjects":[],"_ww-layout_alignItems":"flex-start","_ww-layout_flexDirection":"column"}},"sectionBaseId":"99586bd3-2b15-4d6b-a025-6a50d07ca845","sectionTitle":"Section"}},"wwObjects":{},"collections":[],"variables":[],"workflows":[],"formulas":[],"libraryComponents":[]}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1 +1 @@
{"name":"Образовательная платформа Meetguru","short_name":"Образовательная платформа Meetguru","icons":[{"src":"images/48-favicon.png?_wwcv=205","type":"image/png","sizes":"48x48"},{"src":"images/72-favicon.png?_wwcv=205","type":"image/png","sizes":"72x72"},{"src":"images/96-favicon.png?_wwcv=205","type":"image/png","sizes":"96x96"},{"src":"images/128-favicon.png?_wwcv=205","type":"image/png","sizes":"128x128"},{"src":"images/144-favicon.png?_wwcv=205","type":"image/png","sizes":"144x144"},{"src":"images/152-favicon.png?_wwcv=205","type":"image/png","sizes":"152x152"},{"src":"images/192-favicon.png?_wwcv=205","type":"image/png","sizes":"192x192"},{"src":"images/256-favicon.png?_wwcv=205","type":"image/png","sizes":"256x256"},{"src":"images/384-favicon.png?_wwcv=205","type":"image/png","sizes":"384x384"},{"src":"images/512-favicon.png?_wwcv=205","type":"image/png","sizes":"512x512"}],"start_url":"/","display":"fullscreen","scope":"/","background_color":"#FFFFFF","theme_color":"#FFFFFF"} {"name":"Образовательная платформа Meetguru","short_name":"Образовательная платформа Meetguru","icons":[{"src":"images/48-favicon.png?_wwcv=206","type":"image/png","sizes":"48x48"},{"src":"images/72-favicon.png?_wwcv=206","type":"image/png","sizes":"72x72"},{"src":"images/96-favicon.png?_wwcv=206","type":"image/png","sizes":"96x96"},{"src":"images/128-favicon.png?_wwcv=206","type":"image/png","sizes":"128x128"},{"src":"images/144-favicon.png?_wwcv=206","type":"image/png","sizes":"144x144"},{"src":"images/152-favicon.png?_wwcv=206","type":"image/png","sizes":"152x152"},{"src":"images/192-favicon.png?_wwcv=206","type":"image/png","sizes":"192x192"},{"src":"images/256-favicon.png?_wwcv=206","type":"image/png","sizes":"256x256"},{"src":"images/384-favicon.png?_wwcv=206","type":"image/png","sizes":"384x384"},{"src":"images/512-favicon.png?_wwcv=206","type":"image/png","sizes":"512x512"}],"start_url":"/","display":"fullscreen","scope":"/","background_color":"#FFFFFF","theme_color":"#FFFFFF"}

View File

@ -1,4 +1,4 @@
const version = 205; const version = 206;
self.addEventListener('install', event => { self.addEventListener('install', event => {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.log(`Service worker v${version} installed`); console.log(`Service worker v${version} installed`);

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -224,7 +224,7 @@ export default {
}, },
modelType() { modelType() {
if (this.content.dateMode === "date") return "yyyy-MM-dd"; if (this.content.dateMode === "date") return "yyyy-MM-dd";
if (this.content.dateMode === "time") return "HH:mm:ss"; if (this.content.dateMode === "time") return "HH:mm:SS";
if (this.content.dateMode === "month") return "yyyy-MM"; if (this.content.dateMode === "month") return "yyyy-MM";
return null; return null;
}, },
@ -310,7 +310,7 @@ export default {
else if (this.content.selectionMode === "range") { else if (this.content.selectionMode === "range") {
if (!value.start && !value.end) return null; if (!value.start && !value.end) return null;
return [value.start || null, value.end || null].filter( return [value.start || null, value.end || null].filter(
(item) => item !== null && item !== "" (value) => value !== null && value !== ""
); );
} else if (this.content.selectionMode === "multi") return value; } else if (this.content.selectionMode === "multi") return value;
}, },

View File

@ -11,8 +11,10 @@ export default {
"lang", "lang",
"format", "format",
"customFormat", "customFormat",
[ {
"displayTitle", label: "Display",
isCollapsible: true,
properties: [
"orientation", "orientation",
"menuPosition", "menuPosition",
"enableCalendarOnly", "enableCalendarOnly",
@ -24,8 +26,11 @@ export default {
"multiCalendars", "multiCalendars",
"multiCalendarsSolo", "multiCalendarsSolo",
], ],
[ },
"stylesTitle", {
label: "Style",
isCollapsible: true,
properties: [
"themeFontFamily", "themeFontFamily",
"themeFontSize", "themeFontSize",
"themeTimeFontSize", "themeTimeFontSize",
@ -38,8 +43,11 @@ export default {
"themeCellPadding", "themeCellPadding",
"themeMenuMinWidth", "themeMenuMinWidth",
], ],
[ },
"colorsTitle", {
label: "Colors",
isCollapsible: true,
properties: [
"themePrimaryColor", "themePrimaryColor",
"themeSecondaryColor", "themeSecondaryColor",
"themeBackgroundColor", "themeBackgroundColor",
@ -62,12 +70,15 @@ export default {
"themeDangerColor", "themeDangerColor",
"themeHighlightColor", "themeHighlightColor",
], ],
},
], ],
customSettingsPropertiesOrder: [ customSettingsPropertiesOrder: [
"readonly", "readonly",
"required", "required",
[ {
"selectionTitle", label: "Selection",
isCollapsible: true,
properties: [
"selectionMode", "selectionMode",
"initValueSingle", "initValueSingle",
"initValueRangeStart", "initValueRangeStart",
@ -81,10 +92,21 @@ export default {
"maxRange", "maxRange",
"noDisabledRange", "noDisabledRange",
], ],
["behaviorTitle", "autoApply", "closeOnAutoApply"], },
["timeTitle", "dateMode", "timezone", "use24", "enableSeconds"], {
[ label: "Behavior",
"datesTitle", isCollapsible: true,
properties: ["autoApply", "closeOnAutoApply"],
},
{
label: "Time",
isCollapsible: true,
properties: ["dateMode", "timezone", "use24", "enableSeconds"],
},
{
label: "Dates",
isCollapsible: true,
properties: [
"startDate", "startDate",
"minDate", "minDate",
"maxDate", "maxDate",
@ -96,15 +118,22 @@ export default {
"disabledDates", "disabledDates",
"disabledWeekDays", "disabledWeekDays",
], ],
[ },
"weeksAndMonthsTitle", {
label: "Weeks / Months",
isCollapsible: true,
properties: [
"weekStart", "weekStart",
"weekNumbers", "weekNumbers",
"hideOffsetDates", "hideOffsetDates",
"disableMonthYearSelect", "disableMonthYearSelect",
], ],
},
["flowTitle", "enableFlow", "flowHint", "flowSteps"], {
label: "Flow",
isCollapsible: true,
properties: ["enableFlow", "flowHint", "flowSteps"],
},
], ],
}, },
triggerEvents: [ triggerEvents: [
@ -183,81 +212,6 @@ export default {
hidden: (content) => content.selectionMode !== "multi", hidden: (content) => content.selectionMode !== "multi",
}, },
displayTitle: {
label: {
en: "Display",
},
type: "Title",
section: "style",
editorOnly: true,
},
stylesTitle: {
label: {
en: "Style",
fr: "Style",
},
type: "Title",
section: "style",
editorOnly: true,
},
colorsTitle: {
label: {
en: "Colors",
fr: "Colors",
},
type: "Title",
section: "style",
editorOnly: true,
},
selectionTitle: {
label: {
en: "Selection",
},
type: "Title",
section: "settings",
editorOnly: true,
},
behaviorTitle: {
label: {
en: "Behavior",
},
type: "Title",
section: "settings",
editorOnly: true,
hidden: (content) => content.dateMode === "time",
},
timeTitle: {
label: {
en: "Time",
},
type: "Title",
section: "settings",
editorOnly: true,
},
datesTitle: {
label: {
en: "Dates",
},
type: "Title",
section: "settings",
editorOnly: true,
},
weeksAndMonthsTitle: {
label: {
en: "Weeks / Months",
},
type: "Title",
section: "settings",
editorOnly: true,
},
flowTitle: {
label: {
en: "Flow",
},
type: "Title",
section: "settings",
editorOnly: true,
},
advancedColors: { advancedColors: {
label: { en: "Advanced" }, label: { en: "Advanced" },
section: "style", section: "style",
@ -1089,3 +1043,4 @@ function generateThemeSizingConfig(label, defaultValue, max, advanced) {
hidden: advanced ? (content) => !content.advancedStyles : undefined, hidden: advanced ? (content) => !content.advancedStyles : undefined,
}; };
} }

View File

@ -8,8 +8,10 @@ export default {
}, },
icon: "history", icon: "history",
customStylePropertiesOrder: [ customStylePropertiesOrder: [
[ {
"markerStyle", label: "Marker style",
isCollapsible: true,
properties: [
"markerShape", "markerShape",
"markerSize", "markerSize",
"markerBackgroundColor", "markerBackgroundColor",
@ -18,24 +20,22 @@ export default {
"markerIconColor", "markerIconColor",
"markerIconSize", "markerIconSize",
], ],
[ },
"timelineStyle", {
label: "Timeline style",
isCollapsible: true,
properties: [
"connectorColor", "connectorColor",
"connectorWidth", "connectorWidth",
"timelineLayout", "timelineLayout",
"eventsAlignment", "eventsAlignment",
], ],
},
], ],
customSettingsPropertiesOrder: [["data"]], customSettingsPropertiesOrder: ["data"],
}, },
properties: { properties: {
// Marker styling // Marker styling
markerStyle: {
type: "Title",
label: { en: "Marker Style" },
section: "style",
editorOnly: true,
},
markerShape: { markerShape: {
label: { en: "Marker Shape" }, label: { en: "Marker Shape" },
type: "TextRadioGroup", type: "TextRadioGroup",
@ -110,12 +110,6 @@ export default {
}, },
// Timeline styling // Timeline styling
timelineStyle: {
type: "Title",
label: { en: "Timeline Style" },
section: "style",
editorOnly: true,
},
connectorColor: { connectorColor: {
label: { en: "Connector Color" }, label: { en: "Connector Color" },
type: "Color", type: "Color",

View File

@ -1,7 +1,7 @@
import { omit } from 'lodash-es'; import { omit } from 'lodash-es';
import { computed, watch, provide, ref } from 'vue'; import { computed, watch, provide, ref } from 'vue';
export function useFormInputs({ updateInputValidity, removeInputValidity }) { export function useFormInputs({ updateInputValidity, removeInputValidity, validationType }) {
const inputsMap = ref({}); const inputsMap = ref({});
const formInputs = computed(() => { const formInputs = computed(() => {
@ -12,7 +12,7 @@ export function useFormInputs({ updateInputValidity, removeInputValidity }) {
.filter(([key, value]) => key !== 'null' && value !== null) .filter(([key, value]) => key !== 'null' && value !== null)
.map(([key, value]) => [ .map(([key, value]) => [
key, key,
omit(value, ['forceValidateField', 'updateValue', 'pending', 'initialValue']), omit(value, ['forceValidateField', 'updateValue', 'cancelValidation', 'pending', 'initialValue', 'initialIsValid']),
]) ])
); );
}); });
@ -27,7 +27,8 @@ export function useFormInputs({ updateInputValidity, removeInputValidity }) {
const input = inputsMap.value[id]; const input = inputsMap.value[id];
if (!input) return; if (!input) return;
updateFn(input); updateFn(input);
updateInputValidity(id, Object.values(inputsMap.value[id])?.[0]?.isValid ?? null); const newIsValid = Object.values(inputsMap.value[id])?.[0]?.isValid ?? null;
updateInputValidity(id, newIsValid);
} }
function unregisterInput(id) { function unregisterInput(id) {
@ -66,7 +67,7 @@ export function useFormInputs({ updateInputValidity, removeInputValidity }) {
name: name, name: name,
value: input.value, value: input.value,
isValid: isValid, isValid: isValid,
error: input.error || input.validationMessage || 'Validation failed' error: input.error || input.validationMessage || 'Validation failed',
}); });
} }
} }
@ -78,31 +79,29 @@ export function useFormInputs({ updateInputValidity, removeInputValidity }) {
return { return {
isValid, isValid,
invalidFields, invalidFields,
validityMap validityMap,
}; };
} }
function resetInputs(initialValues = {}) { function resetInputs(initialValues = {}) {
initialValues ||= {}; initialValues ||= {};
for (const [id, inputs] of Object.entries(inputsMap.value)) { for (const [id, inputs] of Object.entries(inputsMap.value)) {
for (const [name, input] of Object.entries(inputs)) { for (const [name, input] of Object.entries(inputs)) {
if (input && typeof input === 'object') { if (input && typeof input === 'object') {
updateInput(id, input => { updateInput(id, input => {
if (input[name]) { if (input[name]) {
// Priority order for values: // Determine reset value and whether it's a forced value
// 1. Value from passed initialValues object
// 2. Field's stored initialValue from useForm
// 3. Default empty value based on type
let newValue; let newValue;
let isForcedValue = false;
if (initialValues[name] !== undefined) { if (initialValues[name] !== undefined) {
// Use value from initialValues parameter
newValue = initialValues[name]; newValue = initialValues[name];
isForcedValue = true;
} else if (input[name].initialValue !== undefined) { } else if (input[name].initialValue !== undefined) {
// Use the field's own initialValue that was set during registration
newValue = input[name].initialValue; newValue = input[name].initialValue;
} else { } else {
// Reset to empty value based on the input type // Default empty value based on type
if (Array.isArray(input[name].value)) { if (Array.isArray(input[name].value)) {
newValue = []; newValue = [];
} else if (typeof input[name].value === 'object' && input[name].value !== null) { } else if (typeof input[name].value === 'object' && input[name].value !== null) {
@ -116,17 +115,36 @@ export function useFormInputs({ updateInputValidity, removeInputValidity }) {
} }
} }
// Update both the form input's value and the component's reactive value reference // Update value
input[name].value = newValue; input[name].value = newValue;
if (input[name].updateValue) { if (input[name].updateValue) {
input[name].updateValue(newValue); input[name].updateValue(newValue);
} }
// Reset validation state // In submit mode, never trigger validation on reset (even for forced values)
input[name].isValid = null; // In change mode, allow validation for forced values only
const isSubmitMode = validationType?.value === 'submit';
const shouldResetValidation = isSubmitMode || !isForcedValue;
if (shouldResetValidation) {
// Reset validation state to initial state AFTER value update
// This ensures we overwrite any validation triggered by the value change
input[name].isValid = input[name].initialIsValid ?? null;
input[name].pending = false; input[name].pending = false;
// Cancel any pending validation in the next tick
// This ensures we cancel validations that were queued by watchers during this tick
setTimeout(() => {
if (input[name]?.cancelValidation) {
input[name].cancelValidation();
}
}, 0);
}
// For forced values in change mode, let the validation watcher run naturally
} }
}); });
// Note: updateInput already calls updateInputValidity, so we don't need to call it again here
} }
} }
} }

View File

@ -10,10 +10,18 @@ export function useFormState() {
isSubmitted: computed(() => isSubmitted.value), isSubmitted: computed(() => isSubmitted.value),
isValid: computed(() => { isValid: computed(() => {
const inputsValidity = Object.values(inputValidityMap.value); const inputsValidity = Object.values(inputValidityMap.value);
if (inputsValidity.some(v => v === null)) { if (inputsValidity.length === 0) {
return null; return null;
} }
const hasNullValidity = inputsValidity.some(v => v === null);
// If any input has null validity, the form validity is null (not yet validated)
if (hasNullValidity) {
return null;
}
// If all inputs are validated (true/false), check if all are valid
return !inputsValidity.some(isValid => !isValid); return !inputsValidity.some(isValid => !isValid);
}), }),
}); });

View File

@ -74,14 +74,23 @@ export function useForm(
} }
} }
const initialIsValid = !required.value && !customValidation.value ? true : null;
const initialValueRef = unref(initialValue) !== undefined ? unref(initialValue) : value.value;
function cancelValidation() {
debouncedUpdateInputValidity.cancel();
}
registerFormInput(id, { registerFormInput(id, {
[_fieldName.value]: { [_fieldName.value]: {
value: value.value, value: value.value,
isValid: !required.value && !customValidation.value ? true : null, isValid: initialIsValid,
pending: false, pending: false,
forceValidateField, forceValidateField,
updateValue, updateValue,
cancelValidation, // Allow canceling pending validations during reset
initialValue: unref(initialValue), // Store the initialValue so it can be used during form reset initialValue: unref(initialValue), // Store the initialValue so it can be used during form reset
initialIsValid, // Store the initial isValid state for reset
}, },
}); });
@ -103,18 +112,22 @@ export function useForm(
// Use custom required validation if provided, otherwise use default isEmpty check // Use custom required validation if provided, otherwise use default isEmpty check
const hasValue = requiredValidation ? requiredValidation(value) : !isValueEmpty(value); const hasValue = requiredValidation ? requiredValidation(value) : !isValueEmpty(value);
let finalResult;
// If not required, field is valid unless there's custom validation // If not required, field is valid unless there's custom validation
if (!required) { if (!required) {
return validationResult; finalResult = validationResult;
} }
// If required and has custom validation, both must be true // If required and has custom validation, both must be true
if (customValidation && validation) { else if (customValidation && validation) {
return hasValue && validationResult; finalResult = hasValue && validationResult;
}
// If just required, check for value using custom or default validation
else {
finalResult = hasValue;
} }
// If just required, check for value using custom or default validation return finalResult;
return hasValue;
}; };
function updateInputValidity(isValid) { function updateInputValidity(isValid) {
@ -137,16 +150,22 @@ export function useForm(
); );
let isFirst = true; let isFirst = true;
let hasSetInitialIsValid = false;
const computedValidation = computed(() => { const computedValidation = computed(() => {
// We have to compute the validation here, otherwise the reactivity will not work const isValid = computeValidation(
const isValid = computeValidation(value.value, required.value, customValidation.value, validation.value, requiredValidation); value.value,
required.value,
customValidation.value,
validation.value,
requiredValidation
);
if (isFirst) { if (isFirst) {
isFirst = false; isFirst = false;
return null; return null;
} }
return isValid; return isValid;
}); });
watch(computedValidation, isValid => { watch(computedValidation, (isValid, oldIsValid) => {
if (form.validationType.value === 'change') { if (form.validationType.value === 'change') {
updateFormInput(id, input => { updateFormInput(id, input => {
if (!input[_fieldName.value]) { if (!input[_fieldName.value]) {
@ -156,17 +175,39 @@ export function useForm(
input[_fieldName.value].pending = true; input[_fieldName.value].pending = true;
}); });
debouncedUpdateInputValidity(isValid); debouncedUpdateInputValidity(isValid);
// Capture the initial isValid state after the first validation completes
// This ensures reset returns to the correct initial state
// Only capture for fields that have validation (required or custom) AND
// only when the value hasn't changed from initial (this is the initial mount validation)
const currentValue = value.value;
const isStillInitialValue = isEqual(currentValue, initialValueRef);
if (!hasSetInitialIsValid && oldIsValid === null && (required.value || customValidation.value) && isStillInitialValue) {
hasSetInitialIsValid = true;
setTimeout(() => {
updateFormInput(id, input => {
if (input[_fieldName.value]) {
input[_fieldName.value].initialIsValid = input[_fieldName.value].isValid;
}
});
}, form.debounceDelay.value + 10);
}
} }
}); });
watch( watch(
() => form.validationType.value, () => form.validationType.value,
validationType => { (validationType, oldValidationType) => {
if (validationType === 'change') { if (validationType === 'change') {
updateInputValidity( const computedResult = computeValidation(
computeValidation(value.value, required?.value, customValidation?.value, validation?.value, requiredValidation) value.value,
required?.value,
customValidation?.value,
validation?.value,
requiredValidation
); );
updateInputValidity(computedResult);
} else if (validationType === 'submit') { } else if (validationType === 'submit') {
updateInputValidity(true); updateInputValidity(null);
} }
} }
); );
@ -175,8 +216,18 @@ export function useForm(
}); });
function forceValidateField() { function forceValidateField() {
debouncedUpdateInputValidity.cancel(); debouncedUpdateInputValidity.cancel();
const isValid = computeValidation(value.value, required?.value, customValidation?.value, validation?.value, requiredValidation); const isValid = computeValidation(
value.value,
required?.value,
customValidation?.value,
validation?.value,
requiredValidation
);
updateInputValidity(isValid); updateInputValidity(isValid);
// Don't capture initialIsValid for submit mode - it should always stay null
// Only capture for onChange mode (which happens in the watch of computedValidation)
return isValid; return isValid;
} }

View File

@ -56,6 +56,7 @@ export default {
const { formInputs, forceValidateAllFields, resetInputs } = useFormInputs({ const { formInputs, forceValidateAllFields, resetInputs } = useFormInputs({
updateInputValidity, updateInputValidity,
removeInputValidity, removeInputValidity,
validationType,
}); });
const isValid = computed(() => formState.isValid.value); const isValid = computed(() => formState.isValid.value);

View File

@ -5,8 +5,6 @@ export default {
bubble: { icon: 'upload' }, bubble: { icon: 'upload' },
customSettingsPropertiesOrder: [ customSettingsPropertiesOrder: [
// UX properties // UX properties
['formInfobox', 'fieldName', 'customValidation', 'validation'],
[
'type', 'type',
'reorder', 'reorder',
'drop', 'drop',
@ -20,12 +18,14 @@ export default {
'customExtensions', 'customExtensions',
'exposeBase64', 'exposeBase64',
'exposeBinary', 'exposeBinary',
], ['formInfobox', 'fieldName', 'customValidation', 'validation'],
], ],
customStylePropertiesOrder: [ customStylePropertiesOrder: [
// Dropzone properties // Dropzone properties
[ {
'dropzoneTitle', label: "Dropzone",
isCollapsible: true,
properties: [
'dropzoneBorderColor', 'dropzoneBorderColor',
'dropzoneBorderStyle', 'dropzoneBorderStyle',
'dropzoneBorderWidth', 'dropzoneBorderWidth',
@ -36,9 +36,12 @@ export default {
'dropzonePadding', 'dropzonePadding',
'dropzoneMinHeight', 'dropzoneMinHeight',
], ],
},
// Icon properties // Icon properties
[ {
'iconTitle', label: "Icon",
isCollapsible: true,
properties: [
'showUploadIcon', 'showUploadIcon',
'uploadIcon', 'uploadIcon',
'uploadIconColor', 'uploadIconColor',
@ -46,9 +49,12 @@ export default {
'uploadIconMargin', 'uploadIconMargin',
'uploadIconPosition', 'uploadIconPosition',
], ],
},
// Label properties // Label properties
[ {
'labelTitle', label: "Label",
isCollapsible: true,
properties: [
'labelMessage', 'labelMessage',
'labelFontFamily', 'labelFontFamily',
'labelFontSize', 'labelFontSize',
@ -56,9 +62,12 @@ export default {
'labelColor', 'labelColor',
'labelMargin', 'labelMargin',
], ],
},
// Info messages properties // Info messages properties
[ {
'infoMessagesTitle', label: "Info messages",
isCollapsible: true,
properties: [
'extensionsMessage', 'extensionsMessage',
'extensionsMessageFontFamily', 'extensionsMessageFontFamily',
'extensionsMessageFontSize', 'extensionsMessageFontSize',
@ -72,8 +81,12 @@ export default {
'maxFileMessageColor', 'maxFileMessageColor',
'maxFileMessageMargin', 'maxFileMessageMargin',
], ],
},
// File list properties // File list properties
[ {
label: "File list",
isCollapsible: true,
properties: [
'fileListTitle', 'fileListTitle',
'fileItemBackground', 'fileItemBackground',
'fileItemBorderColor', 'fileItemBorderColor',
@ -83,28 +96,44 @@ export default {
'fileItemShadow', 'fileItemShadow',
'progressBarColor', 'progressBarColor',
'progressBarColorWarning', 'progressBarColorWarning',
'fileItemHoverTitle', ],
},
{
label: "File item hover states",
isCollapsible: true,
properties: [
'fileItemHoverBorderColor', 'fileItemHoverBorderColor',
'fileItemHoverBackground', 'fileItemHoverBackground',
'fileItemHoverShadow', 'fileItemHoverShadow',
], ],
},
// File details properties // File details properties
[ {
'fileNameTitle', label: "File name",
isCollapsible: true,
properties: [
'fileNameFontFamily', 'fileNameFontFamily',
'fileNameFontSize', 'fileNameFontSize',
'fileNameFontWeight', 'fileNameFontWeight',
'fileNameColor', 'fileNameColor',
'fileDetailsTitle', ],
},
{
label: "File details",
isCollapsible: true,
properties: [
'showFileInfo', 'showFileInfo',
'fileDetailsFontFamily', 'fileDetailsFontFamily',
'fileDetailsFontSize', 'fileDetailsFontSize',
'fileDetailsFontWeight', 'fileDetailsFontWeight',
'fileDetailsColor', 'fileDetailsColor',
], ],
},
// Action buttons properties // Action buttons properties
[ {
'actionButtonsTitle', label: "Remove buttons",
isCollapsible: true,
properties: [
'actionButtonSize', 'actionButtonSize',
'actionButtonBackground', 'actionButtonBackground',
'actionButtonHoverBackground', 'actionButtonHoverBackground',
@ -114,15 +143,19 @@ export default {
'actionButtonBorderRadius', 'actionButtonBorderRadius',
'actionButtonMargin', 'actionButtonMargin',
], ],
},
// Circle animation properties // Circle animation properties
[ {
'circleAnimationTitle', label: "Drag & drop animation",
isCollapsible: true,
properties: [
'enableCircleAnimation', 'enableCircleAnimation',
'circleSize', 'circleSize',
'circleColor', 'circleColor',
'circleOpacity', 'circleOpacity',
'animationSpeed', 'animationSpeed',
], ],
},
], ],
hint: (_, sidePanelContent) => { hint: (_, sidePanelContent) => {
if (!sidePanelContent.parentSelection) return null; if (!sidePanelContent.parentSelection) return null;
@ -293,11 +326,6 @@ export default {
}, },
// ======== DROPZONE PROPERTIES ======== // ======== DROPZONE PROPERTIES ========
dropzoneTitle: {
type: 'Title',
label: { en: 'Dropzone' },
section: 'style',
},
dropzoneBorderColor: { dropzoneBorderColor: {
label: { en: 'Border color' }, label: { en: 'Border color' },
type: 'Color', type: 'Color',
@ -413,11 +441,6 @@ export default {
}, },
// ======== ICON PROPERTIES ======== // ======== ICON PROPERTIES ========
iconTitle: {
type: 'Title',
label: { en: 'Icon' },
section: 'icon',
},
showUploadIcon: { showUploadIcon: {
label: { en: 'Show upload icon' }, label: { en: 'Show upload icon' },
type: 'OnOff', type: 'OnOff',
@ -491,11 +514,6 @@ export default {
}, },
// ======== LABEL PROPERTIES ======== // ======== LABEL PROPERTIES ========
labelTitle: {
type: 'Title',
label: { en: 'Label' },
section: 'style',
},
labelMessage: { labelMessage: {
label: { en: 'Label' }, label: { en: 'Label' },
type: 'Text', type: 'Text',
@ -571,11 +589,6 @@ export default {
}, },
// ======== INFO MESSAGES PROPERTIES ======== // ======== INFO MESSAGES PROPERTIES ========
infoMessagesTitle: {
type: 'Title',
label: { en: 'Info Messages' },
section: 'style',
},
extensionsMessage: { extensionsMessage: {
label: { en: 'Extensions message' }, label: { en: 'Extensions message' },
type: 'Text', type: 'Text',
@ -724,11 +737,6 @@ export default {
}, },
// ======== FILE LIST PROPERTIES ======== // ======== FILE LIST PROPERTIES ========
fileListTitle: {
type: 'Title',
label: { en: 'File List' },
section: 'style',
},
fileItemBackground: { fileItemBackground: {
label: { en: 'Background color' }, label: { en: 'Background color' },
type: 'Color', type: 'Color',
@ -813,11 +821,6 @@ export default {
}, },
editorOnly: true, editorOnly: true,
}, },
fileItemHoverTitle: {
type: 'Title',
label: { en: 'File Item Hover States' },
section: 'style',
},
fileItemHoverBorderColor: { fileItemHoverBorderColor: {
label: { en: 'Hover border color' }, label: { en: 'Hover border color' },
type: 'Color', type: 'Color',
@ -850,11 +853,6 @@ export default {
}, },
// ======== FILE DETAILS PROPERTIES ======== // ======== FILE DETAILS PROPERTIES ========
fileNameTitle: {
type: 'Title',
label: { en: 'File Name' },
section: 'style',
},
fileNameFontFamily: { fileNameFontFamily: {
label: { en: 'Font family' }, label: { en: 'Font family' },
type: 'FontFamily', type: 'FontFamily',
@ -912,11 +910,6 @@ export default {
responsive: true, responsive: true,
bindable: true, bindable: true,
}, },
fileDetailsTitle: {
type: 'Title',
label: { en: 'File Details' },
section: 'style',
},
fileDetailsFontFamily: { fileDetailsFontFamily: {
label: { en: 'Font family' }, label: { en: 'Font family' },
type: 'FontFamily', type: 'FontFamily',
@ -976,11 +969,6 @@ export default {
}, },
// ======== ACTION BUTTON PROPERTIES ======== // ======== ACTION BUTTON PROPERTIES ========
actionButtonsTitle: {
type: 'Title',
label: { en: 'Remove Buttons' },
section: 'style',
},
actionButtonSize: { actionButtonSize: {
label: { en: 'Size' }, label: { en: 'Size' },
type: 'Length', type: 'Length',
@ -1076,11 +1064,6 @@ export default {
}, },
// ======== CIRCLE ANIMATION PROPERTIES ======== // ======== CIRCLE ANIMATION PROPERTIES ========
circleAnimationTitle: {
type: 'Title',
label: { en: 'Drag & Drop Animation' },
section: 'style',
},
enableCircleAnimation: { enableCircleAnimation: {
label: { en: 'Enable circle animation' }, label: { en: 'Enable circle animation' },
type: 'OnOff', type: 'OnOff',

View File

@ -6,10 +6,7 @@
<div <div
v-for="env in environments" v-for="env in environments"
:key="env" :key="env"
:class="[ :class="['ww-tab-item', { 'ww-tab-active': activeEnvironment === env }]"
'ww-tab-item',
{ 'ww-tab-active': activeEnvironment === env }
]"
@click="activeEnvironment = env" @click="activeEnvironment = env"
> >
<span class="ww-tab-label"> <span class="ww-tab-label">
@ -25,7 +22,6 @@
<!-- Environment Configuration --> <!-- Environment Configuration -->
<div v-for="env in environments" :key="`config-${env}`" v-show="activeEnvironment === env"> <div v-for="env in environments" :key="`config-${env}`" v-show="activeEnvironment === env">
<!-- Connection Mode Selector --> <!-- Connection Mode Selector -->
<wwEditorFormRow label="Connection Mode" class="w-100 mb-3"> <wwEditorFormRow label="Connection Mode" class="w-100 mb-3">
<wwEditorInputRadio <wwEditorInputRadio
@ -34,17 +30,25 @@
{ label: 'Guided (recommended)', value: 'oauth', default: true }, { label: 'Guided (recommended)', value: 'oauth', default: true },
{ label: 'Custom', value: 'custom' }, { label: 'Custom', value: 'custom' },
]" ]"
@update:modelValue="(mode) => changeConnectionMode(env, mode)" @update:modelValue="mode => changeConnectionMode(env, mode)"
/> />
</wwEditorFormRow> </wwEditorFormRow>
<!-- OAuth Connection --> <!-- OAuth Connection -->
<template v-if="getConnectionMode(env) === 'oauth'"> <template v-if="getConnectionMode(env) === 'oauth'">
<div v-if="!hasOAuthToken()" class="body-sm content-brand-secondary bg-brand-secondary border-brand-secondary p-2 mb-2 rounded-02"> <div
v-if="!hasOAuthToken()"
class="body-sm content-brand-secondary bg-brand-secondary border-brand-secondary p-2 mb-2 rounded-02"
>
<span>Connect to enable the Back-end panel and AI assistance.</span> <span>Connect to enable the Back-end panel and AI assistance.</span>
</div> </div>
<div class="flex items-center justify-center mb-3"> <div class="flex items-center justify-center mb-3">
<button class="ww-editor-button -secondary" @click="connect" type="button" :disabled="!!hasOAuthToken()"> <button
class="ww-editor-button -secondary"
@click="connect"
type="button"
:disabled="!!hasOAuthToken()"
>
<wwEditorIcon name="logos/supabase" class="ww-editor-button-icon -left" /> <wwEditorIcon name="logos/supabase" class="ww-editor-button-icon -left" />
{{ hasOAuthToken() ? 'Account connected' : 'Connect Supabase' }} {{ hasOAuthToken() ? 'Account connected' : 'Connect Supabase' }}
</button> </button>
@ -86,11 +90,15 @@
placeholder="https://your-project.supabase.co" placeholder="https://your-project.supabase.co"
:model-value="getProjectSelectValue(env)" :model-value="getProjectSelectValue(env)"
:options="projectsOptions" :options="projectsOptions"
@update:modelValue="(val) => changeProjectUrl(val, env)" @update:modelValue="val => changeProjectUrl(val, env)"
class="-full" class="-full"
/> />
</wwEditorFormRow> </wwEditorFormRow>
<button type="button" class="ww-editor-button -primary -small -icon ml-2 mt-1" @click="refreshProjects"> <button
type="button"
class="ww-editor-button -primary -small -icon ml-2 mt-1"
@click="refreshProjects"
>
<wwEditorIcon name="refresh" medium /> <wwEditorIcon name="refresh" medium />
</button> </button>
</div> </div>
@ -103,11 +111,15 @@
placeholder="Default (main)" placeholder="Default (main)"
:model-value="selectedBranches?.[env] || ''" :model-value="selectedBranches?.[env] || ''"
:options="branchOptions(env)" :options="branchOptions(env)"
@update:modelValue="(val) => changeBranch(val, env)" @update:modelValue="val => changeBranch(val, env)"
class="-full" class="-full"
/> />
</wwEditorFormRow> </wwEditorFormRow>
<button type="button" class="ww-editor-button -primary -small -icon ml-2 mt-1" @click="loadBranches(env)"> <button
type="button"
class="ww-editor-button -primary -small -icon ml-2 mt-1"
@click="loadBranches(env)"
>
<wwEditorIcon name="refresh" medium /> <wwEditorIcon name="refresh" medium />
</button> </button>
<div v-if="branchErrors?.[env]" class="body-xs content-tertiary ml-2 mt-1"> <div v-if="branchErrors?.[env]" class="body-xs content-tertiary ml-2 mt-1">
@ -130,7 +142,7 @@
placeholder="https://your-project.supabase.co" placeholder="https://your-project.supabase.co"
:required="env === 'production'" :required="env === 'production'"
:model-value="getCurrentEnvConfig(env).projectUrl" :model-value="getCurrentEnvConfig(env).projectUrl"
@update:modelValue="(val) => changeProjectUrl(val, env)" @update:modelValue="val => changeProjectUrl(val, env)"
/> />
<wwEditorInputRow <wwEditorInputRow
@ -139,7 +151,7 @@
type="query" type="query"
placeholder="Enter your public API key" placeholder="Enter your public API key"
:model-value="getCurrentEnvConfig(env).apiKey" :model-value="getCurrentEnvConfig(env).apiKey"
@update:modelValue="(val) => changeApiKey(val, env)" @update:modelValue="val => changeApiKey(val, env)"
/> />
<wwEditorFormRow label="Service role key"> <wwEditorFormRow label="Service role key">
@ -151,7 +163,7 @@
class="w-full" class="w-full"
:style="{ '-webkit-text-security': 'disc' }" :style="{ '-webkit-text-security': 'disc' }"
:model-value="getCurrentEnvPrivateConfig(env).apiKey" :model-value="getCurrentEnvPrivateConfig(env).apiKey"
@update:modelValue="(val) => changePrivateApiKey(val, env)" @update:modelValue="val => changePrivateApiKey(val, env)"
/> />
<wwEditorQuestionMark <wwEditorQuestionMark
tooltip-position="top-left" tooltip-position="top-left"
@ -168,7 +180,9 @@
<template v-else-if="selectModes[env] === 'create'"> <template v-else-if="selectModes[env] === 'create'">
<div v-if="isComingUp" class="body-md flex items-center p-2"> <div v-if="isComingUp" class="body-md flex items-center p-2">
<wwLoaderSmall loading class="mr-2" /> <wwLoaderSmall loading class="mr-2" />
<div>We're now preparing your database. Please wait a few moments, it may take up to 1 minute.</div> <div>
We're now preparing your database. Please wait a few moments, it may take up to 1 minute.
</div>
</div> </div>
<template v-else> <template v-else>
<wwEditorInputRow <wwEditorInputRow
@ -220,11 +234,7 @@
</button> </button>
</div> </div>
</wwEditorFormRow> </wwEditorFormRow>
<button <button class="ww-editor-button -primary" @click="createProject(env)" type="button">
class="ww-editor-button -primary"
@click="createProject(env)"
type="button"
>
Create project for {{ capitalize(env) }} Create project for {{ capitalize(env) }}
</button> </button>
</template> </template>
@ -235,7 +245,10 @@
<!-- Custom Connection Mode --> <!-- Custom Connection Mode -->
<template v-else> <template v-else>
<div class="body-sm content-secondary bg-secondary border-secondary p-2 rounded-02 mb-2"> <div class="body-sm content-secondary bg-secondary border-secondary p-2 rounded-02 mb-2">
<span>Use this mode for self-hosted projects, local development, or if you don't want to connect your account.</span> <span
>Use this mode for self-hosted projects, local development, or if you don't want to connect your
account.</span
>
</div> </div>
<div class="body-sm content-warning-secondary bg-warning-secondary p-2 rounded-02 mb-3"> <div class="body-sm content-warning-secondary bg-warning-secondary p-2 rounded-02 mb-3">
<span>Using this mode disables the Back-end panel and AI assistance.</span> <span>Using this mode disables the Back-end panel and AI assistance.</span>
@ -247,7 +260,7 @@
placeholder="https://your-project.supabase.co" placeholder="https://your-project.supabase.co"
:required="env === 'production'" :required="env === 'production'"
:model-value="getCurrentEnvConfig(env).projectUrl" :model-value="getCurrentEnvConfig(env).projectUrl"
@update:modelValue="(val) => changeProjectUrl(val, env)" @update:modelValue="val => changeProjectUrl(val, env)"
/> />
<wwEditorInputRow <wwEditorInputRow
@ -255,7 +268,7 @@
type="query" type="query"
placeholder="https://your-custom-domain.com" placeholder="https://your-custom-domain.com"
:model-value="getCurrentEnvConfig(env).customDomain" :model-value="getCurrentEnvConfig(env).customDomain"
@update:modelValue="(val) => changeCustomDomain(val, env)" @update:modelValue="val => changeCustomDomain(val, env)"
/> />
<wwEditorInputRow <wwEditorInputRow
@ -264,7 +277,7 @@
type="query" type="query"
placeholder="Enter your public API key" placeholder="Enter your public API key"
:model-value="getCurrentEnvConfig(env).apiKey" :model-value="getCurrentEnvConfig(env).apiKey"
@update:modelValue="(val) => changeApiKey(val, env)" @update:modelValue="val => changeApiKey(val, env)"
/> />
<wwEditorFormRow label="Service role key"> <wwEditorFormRow label="Service role key">
@ -276,7 +289,7 @@
class="w-full" class="w-full"
:style="{ '-webkit-text-security': 'disc' }" :style="{ '-webkit-text-security': 'disc' }"
:model-value="getCurrentEnvPrivateConfig(env).apiKey" :model-value="getCurrentEnvPrivateConfig(env).apiKey"
@update:modelValue="(val) => changePrivateApiKey(val, env)" @update:modelValue="val => changePrivateApiKey(val, env)"
/> />
<wwEditorQuestionMark <wwEditorQuestionMark
tooltip-position="top-left" tooltip-position="top-left"
@ -321,12 +334,12 @@ export default {
selectModes: { selectModes: {
production: 'select', production: 'select',
staging: 'select', staging: 'select',
editor: 'select' editor: 'select',
}, },
showSettings: { showSettings: {
production: false, production: false,
staging: false, staging: false,
editor: false editor: false,
}, },
projects: [], projects: [],
isLoading: false, isLoading: false,
@ -351,11 +364,12 @@ export default {
region: 'us-east-1', region: 'us-east-1',
organizationId: '', organizationId: '',
dbPass: '', dbPass: '',
} },
}, },
branches: {}, branches: {},
selectedBranches: {}, selectedBranches: {},
branchErrors: {}, branchErrors: {},
branchChangeAbortController: null, // AbortController for cancelling in-flight requests
}; };
}, },
watch: { watch: {
@ -367,7 +381,9 @@ export default {
await this.fetchOrganizations(); await this.fetchOrganizations();
// Initialize new project data for this environment // Initialize new project data for this environment
this.newProjects[env] = { this.newProjects[env] = {
name: `WeWeb - ${wwLib.$store.getters['websiteData/getDesignInfo'].name} (${this.capitalize(env)})`, name: `WeWeb - ${wwLib.$store.getters['websiteData/getDesignInfo'].name} (${this.capitalize(
env
)})`,
region: 'us-east-1', region: 'us-east-1',
organizationId: this.organizations[0]?.id || '', organizationId: this.organizations[0]?.id || '',
dbPass: wwLib.wwUtils.getUid(), dbPass: wwLib.wwUtils.getUid(),
@ -375,35 +391,41 @@ export default {
} }
} }
}, },
deep: true deep: true,
} },
}, },
computed: { computed: {
isValid() {
// Prevent saving while branch change is in progress
return !this.branchChangeAbortController;
},
projectRef() { projectRef() {
const config = this.getCurrentEnvConfig(); const config = this.getCurrentEnvConfig();
return config?.projectUrl?.replace('https://', '').replace('.supabase.co', ''); return config?.projectUrl?.replace('https://', '').replace('.supabase.co', '');
}, },
projectsOptions() { projectsOptions() {
return ( return this.projects
this.projects
.map(project => ({ .map(project => ({
label: `${project.name} (${project.id}) ${project.status === 'INACTIVE' ? '#PAUSED' : ''}`, label: `${project.name} (${project.id}) ${project.status === 'INACTIVE' ? '#PAUSED' : ''}`,
value: `https://${project.id}.supabase.co`, value: `https://${project.id}.supabase.co`,
})) }))
.sort((a, b) => (a.label.includes('#PAUSED') ? 1 : 0) - (b.label.includes('#PAUSED') ? 1 : 0)) .sort((a, b) => (a.label.includes('#PAUSED') ? 1 : 0) - (b.label.includes('#PAUSED') ? 1 : 0));
);
}, },
branchOptions() { branchOptions() {
return (env) => { return env => {
const items = this.branches?.[env] || []; const items = this.branches?.[env] || [];
if (items.length === 0) return []; if (items.length === 0) return [];
return items.map(b => ({ label: `${b.name}${b.is_default ? ' (default)' : ''}`, value: b.project_ref || b.ref || b.id || b.name })); return items.map(b => ({
} label: `${b.name}${b.is_default ? ' (default)' : ''}`,
value: b.project_ref || b.ref || b.id || b.name,
}));
};
}, },
}, },
async mounted() { async mounted() {
// Check if this is a fresh install and try to sync from Supabase plugin // Check if this is a fresh install and try to sync from Supabase plugin
const isFreshInstall = !this.settings.publicData?.environments && const isFreshInstall =
!this.settings.publicData?.environments &&
!this.settings.publicData?.projectUrl && !this.settings.publicData?.projectUrl &&
!this.settings.privateData?.accessToken; !this.settings.privateData?.accessToken;
@ -444,10 +466,9 @@ export default {
this.environments.forEach(env => { this.environments.forEach(env => {
const config = envConfigs[env]; const config = envConfigs[env];
if (!config) return; if (!config) return;
if ( const hasLegacyAccess = Object.hasOwn(config, 'accessToken');
Object.prototype.hasOwnProperty.call(config, 'accessToken') || const hasLegacyRefresh = Object.hasOwn(config, 'refreshToken');
Object.prototype.hasOwnProperty.call(config, 'refreshToken') if (hasLegacyAccess || hasLegacyRefresh) {
) {
const { accessToken: _legacyAccess, refreshToken: _legacyRefresh, ...rest } = config; const { accessToken: _legacyAccess, refreshToken: _legacyRefresh, ...rest } = config;
sanitizedEnvs[env] = rest; sanitizedEnvs[env] = rest;
hasChanges = true; hasChanges = true;
@ -495,7 +516,8 @@ export default {
getProjectSelectValue(env) { getProjectSelectValue(env) {
const config = this.getCurrentEnvConfig(env); const config = this.getCurrentEnvConfig(env);
if (!config) return ''; if (!config) return '';
const baseRef = config.baseProjectRef || config.projectUrl?.replace('https://', '').replace('.supabase.co', ''); const baseRef =
config.baseProjectRef || config.projectUrl?.replace('https://', '').replace('.supabase.co', '');
return baseRef ? `https://${baseRef}.supabase.co` : config.projectUrl; return baseRef ? `https://${baseRef}.supabase.co` : config.projectUrl;
}, },
@ -514,7 +536,7 @@ export default {
return { return {
projectUrl: this.settings.publicData.projectUrl, projectUrl: this.settings.publicData.projectUrl,
apiKey: this.settings.publicData.apiKey, apiKey: this.settings.publicData.apiKey,
customDomain: this.settings.publicData.customDomain customDomain: this.settings.publicData.customDomain,
}; };
} }
return {}; return {};
@ -522,8 +544,11 @@ export default {
getCurrentEnvPrivateConfig(env = this.activeEnvironment) { getCurrentEnvPrivateConfig(env = this.activeEnvironment) {
if (this.settings.privateData?.environments?.[env]) { if (this.settings.privateData?.environments?.[env]) {
const { accessToken: _legacyAccess, refreshToken: _legacyRefresh, ...rest } = const {
this.settings.privateData.environments[env] || {}; accessToken: _legacyAccess,
refreshToken: _legacyRefresh,
...rest
} = this.settings.privateData.environments[env] || {};
return rest; return rest;
} }
// Fallback to legacy format for production // Fallback to legacy format for production
@ -532,7 +557,7 @@ export default {
connectionMode: this.settings.privateData.connectionMode || 'oauth', connectionMode: this.settings.privateData.connectionMode || 'oauth',
apiKey: this.settings.privateData.apiKey, apiKey: this.settings.privateData.apiKey,
databasePassword: this.settings.privateData.databasePassword, databasePassword: this.settings.privateData.databasePassword,
connectionString: this.settings.privateData.connectionString connectionString: this.settings.privateData.connectionString,
}; };
} }
return {}; return {};
@ -549,9 +574,9 @@ export default {
production: { production: {
projectUrl: this.settings.publicData?.projectUrl || '', projectUrl: this.settings.publicData?.projectUrl || '',
apiKey: this.settings.publicData?.apiKey || '', apiKey: this.settings.publicData?.apiKey || '',
customDomain: this.settings.publicData?.customDomain || '' customDomain: this.settings.publicData?.customDomain || '',
} },
} },
}, },
privateData: { privateData: {
...this.settings.privateData, ...this.settings.privateData,
@ -560,10 +585,10 @@ export default {
connectionMode: connectionMode, connectionMode: connectionMode,
apiKey: this.settings.privateData?.apiKey || '', apiKey: this.settings.privateData?.apiKey || '',
databasePassword: this.settings.privateData?.databasePassword || '', databasePassword: this.settings.privateData?.databasePassword || '',
connectionString: this.settings.privateData?.connectionString || '' connectionString: this.settings.privateData?.connectionString || '',
} },
} },
} },
}; };
this.$emit('update:settings', newSettings); this.$emit('update:settings', newSettings);
}, },
@ -571,8 +596,8 @@ export default {
changeConnectionMode(env, mode) { changeConnectionMode(env, mode) {
this.updateEnvironmentConfig(env, { this.updateEnvironmentConfig(env, {
privateData: { privateData: {
connectionMode: mode connectionMode: mode,
} },
}); });
}, },
@ -599,9 +624,9 @@ export default {
accessToken: '', accessToken: '',
refreshToken: '', refreshToken: '',
environments: { environments: {
...this.settings.privateData?.environments ...this.settings.privateData?.environments,
} },
} },
}; };
// Clear tokens from all environments // Clear tokens from all environments
@ -620,7 +645,7 @@ export default {
if (!projectUrl) { if (!projectUrl) {
this.updateEnvironmentConfig(env, { this.updateEnvironmentConfig(env, {
publicData: { projectUrl: '', apiKey: '' }, publicData: { projectUrl: '', apiKey: '' },
privateData: { apiKey: '', connectionString: '' } privateData: { apiKey: '', connectionString: '' },
}); });
return; return;
} }
@ -639,11 +664,6 @@ export default {
let baseProjectRef = projectUrl.replace('https://', '').replace('.supabase.co', ''); let baseProjectRef = projectUrl.replace('https://', '').replace('.supabase.co', '');
// Reset branch state while loading new project data (prevents showing stale options) // Reset branch state while loading new project data (prevents showing stale options)
if (this.$set) {
this.$set(this.branches, env, []);
this.$set(this.branchErrors, env, '');
this.$set(this.selectedBranches, env, '');
} else {
const branchCopy = { ...(this.branches || {}) }; const branchCopy = { ...(this.branches || {}) };
branchCopy[env] = []; branchCopy[env] = [];
this.branches = branchCopy; this.branches = branchCopy;
@ -653,7 +673,6 @@ export default {
const selectedCopy = { ...(this.selectedBranches || {}) }; const selectedCopy = { ...(this.selectedBranches || {}) };
selectedCopy[env] = ''; selectedCopy[env] = '';
this.selectedBranches = selectedCopy; this.selectedBranches = selectedCopy;
}
if (this.hasOAuthToken() && this.getConnectionMode(env) === 'oauth') { if (this.hasOAuthToken() && this.getConnectionMode(env) === 'oauth') {
const projectData = await this.fetchProject( const projectData = await this.fetchProject(
@ -662,7 +681,8 @@ export default {
if (projectData) { if (projectData) {
apiKey = projectData.apiKeys?.find(key => key.name === 'anon')?.api_key || apiKey; apiKey = projectData.apiKeys?.find(key => key.name === 'anon')?.api_key || apiKey;
privateApiKey = projectData.apiKeys?.find(key => key.name === 'service_role')?.api_key || privateApiKey; privateApiKey =
projectData.apiKeys?.find(key => key.name === 'service_role')?.api_key || privateApiKey;
connectionString = projectData.pgbouncer?.connection_string || connectionString; connectionString = projectData.pgbouncer?.connection_string || connectionString;
baseProjectRef = baseProjectRef =
projectData.project?.parent_project_ref || projectData.project?.parent_project_ref ||
@ -674,7 +694,7 @@ export default {
this.updateEnvironmentConfig(env, { this.updateEnvironmentConfig(env, {
publicData: { projectUrl, apiKey, baseProjectRef, branch: null, branchSlug: null }, publicData: { projectUrl, apiKey, baseProjectRef, branch: null, branchSlug: null },
privateData: { apiKey: privateApiKey, connectionString } privateData: { apiKey: privateApiKey, connectionString },
}); });
// Load branches // Load branches
@ -688,7 +708,10 @@ export default {
async loadBranches(env, overrideRef = '') { async loadBranches(env, overrideRef = '') {
try { try {
const cfg = this.getCurrentEnvConfig(env); const cfg = this.getCurrentEnvConfig(env);
const baseRef = overrideRef || cfg.baseProjectRef || cfg.projectUrl?.replace('https://', '').replace('.supabase.co', ''); const baseRef =
overrideRef ||
cfg.baseProjectRef ||
cfg.projectUrl?.replace('https://', '').replace('.supabase.co', '');
const ref = baseRef; const ref = baseRef;
const paramsBaseRef = cfg.baseProjectRef || overrideRef || ''; const paramsBaseRef = cfg.baseProjectRef || overrideRef || '';
if (!ref || !this.hasOAuthToken()) return; if (!ref || !this.hasOAuthToken()) return;
@ -698,44 +721,43 @@ export default {
params: { baseProjectRef: paramsBaseRef }, params: { baseProjectRef: paramsBaseRef },
}); });
const branches = data?.data || []; const branches = data?.data || [];
if (this.$set) {
this.$set(this.branches, env, branches);
this.$set(this.branchErrors, env, '');
} else {
this.branches = { ...(this.branches || {}), [env]: branches }; this.branches = { ...(this.branches || {}), [env]: branches };
const errors = { ...(this.branchErrors || {}) }; const errors = { ...(this.branchErrors || {}) };
delete errors[env]; delete errors[env];
this.branchErrors = errors; this.branchErrors = errors;
}
const defaultBranch = branches.find(b => b.is_default); const defaultBranch = branches.find(b => b.is_default);
const defaultValue = defaultBranch?.project_ref || defaultBranch?.ref || defaultBranch?.id || ''; const defaultValue = defaultBranch?.project_ref || defaultBranch?.ref || defaultBranch?.id || '';
const currentSelection = this.selectedBranches?.[env]; const currentSelection = this.selectedBranches?.[env];
const savedSelection = this.getCurrentEnvConfig(env)?.branch || ''; const savedSelection = this.getCurrentEnvConfig(env)?.branch || '';
const targetSelection = currentSelection || savedSelection || ''; const targetSelection = currentSelection || savedSelection || '';
const selectionExistsInList = targetSelection && branches.some(b => (b.project_ref || b.ref || b.id || b.name) === targetSelection); const selectionExistsInList =
targetSelection &&
branches.some(b => (b.project_ref || b.ref || b.id || b.name) === targetSelection);
let nextSelection = ''; let nextSelection = '';
let shouldFetchBranchData = false;
if (selectionExistsInList) { if (selectionExistsInList) {
nextSelection = targetSelection; nextSelection = targetSelection;
} else if (defaultValue) { } else if (defaultValue) {
nextSelection = defaultValue; nextSelection = defaultValue;
// Auto-selected default branch needs data fetched
shouldFetchBranchData = !savedSelection;
} }
if (nextSelection || currentSelection || savedSelection) { if (nextSelection || currentSelection || savedSelection) {
if (this.$set) this.$set(this.selectedBranches, env, nextSelection); this.selectedBranches = { ...(this.selectedBranches || {}), [env]: nextSelection };
else this.selectedBranches = { ...(this.selectedBranches || {}), [env]: nextSelection };
}
// If we auto-selected a default branch, fetch its data
if (shouldFetchBranchData && nextSelection) {
await this.changeBranch(nextSelection, env);
}
}
} catch (e) { } catch (e) {
const msg = e?.response?.data?.error || e?.message || 'Unable to load branches'; const msg = e?.response?.data?.error || e?.message || 'Unable to load branches';
if (this.$set) {
this.$set(this.branches, env, []);
this.$set(this.branchErrors, env, msg);
} else {
this.branches = { ...(this.branches || {}), [env]: [] }; this.branches = { ...(this.branches || {}), [env]: [] };
this.branchErrors = { ...(this.branchErrors || {}), [env]: msg }; this.branchErrors = { ...(this.branchErrors || {}), [env]: msg };
}
console.warn('[Supabase auth plugin] loadBranches error', { env, status: e?.response?.status, msg }); console.warn('[Supabase auth plugin] loadBranches error', { env, status: e?.response?.status, msg });
} }
}, },
@ -743,29 +765,55 @@ export default {
// removed features gating; we call /branches directly // removed features gating; we call /branches directly
async changeBranch(branchValue, env) { async changeBranch(branchValue, env) {
if (this.$set) this.$set(this.selectedBranches, env, branchValue || ''); // Cancel any in-flight branch change request
else this.selectedBranches = { ...(this.selectedBranches || {}), [env]: branchValue || '' }; if (this.branchChangeAbortController) {
const baseRef = this.getCurrentEnvConfig(env).baseProjectRef || this.getCurrentEnvConfig(env).projectUrl?.replace('https://', '').replace('.supabase.co', ''); this.branchChangeAbortController.abort();
}
// Create new AbortController for this request
this.branchChangeAbortController = new AbortController();
const signal = this.branchChangeAbortController.signal;
// Set flag to prevent saving while branch change is in progress
this.setLoadingFlag(true);
this.isLoading = true;
try {
this.selectedBranches = { ...(this.selectedBranches || {}), [env]: branchValue || '' };
const baseRef =
this.getCurrentEnvConfig(env).baseProjectRef ||
this.getCurrentEnvConfig(env).projectUrl?.replace('https://', '').replace('.supabase.co', '');
if (!baseRef) return; if (!baseRef) return;
let targetRef = baseRef; let targetRef = baseRef;
let branchSlug = ''; let branchSlug = '';
if (branchValue) { if (branchValue) {
const list = this.branches?.[env] || []; const branchList = this.branches?.[env] || [];
const b = list.find(it => (it.project_ref || it.ref || it.id || it.name) === branchValue); const matchedBranch = branchList.find(
targetRef = b?.project_ref || b?.ref || branchValue; it => (it.project_ref || it.ref || it.id || it.name) === branchValue
branchSlug = b?.name || ''; );
targetRef = matchedBranch?.project_ref || matchedBranch?.ref || branchValue;
branchSlug = matchedBranch?.name || '';
} }
const effectiveBranchSlug = branchValue ? (branchSlug || this.getCurrentEnvConfig(env)?.branchSlug || '') : ''; const effectiveBranchSlug = branchValue
? branchSlug || this.getCurrentEnvConfig(env)?.branchSlug || ''
: '';
const projectData = await this.fetchProject(baseRef, { const projectData = await this.fetchProject(baseRef, {
branchSlug: effectiveBranchSlug, branchSlug: effectiveBranchSlug,
branchRef: branchValue ? targetRef : '', branchRef: branchValue ? targetRef : '',
signal, // Pass AbortController signal
}); });
const apiKey = projectData?.apiKeys?.find(key => key.name === 'anon')?.api_key; const apiKey = projectData?.apiKeys?.find(key => key.name === 'anon')?.api_key;
const privateApiKey = projectData?.apiKeys?.find(key => key.name === 'service_role')?.api_key; const privateApiKey = projectData?.apiKeys?.find(key => key.name === 'service_role')?.api_key;
const connectionString = projectData?.pgbouncer?.connection_string; const connectionString = projectData?.pgbouncer?.connection_string;
const resolvedBranchRef = branchValue const resolvedBranchRef = branchValue
? projectData?.branchRef || projectData?.project?.project_ref || projectData?.project?.ref || targetRef ? projectData?.branchRef ||
projectData?.project?.project_ref ||
projectData?.project?.ref ||
targetRef
: ''; : '';
const runtimeRef = resolvedBranchRef || baseRef; const runtimeRef = resolvedBranchRef || baseRef;
const projectUrl = `https://${runtimeRef}.supabase.co`; const projectUrl = `https://${runtimeRef}.supabase.co`;
@ -783,27 +831,61 @@ export default {
privateData: { privateData: {
apiKey: displayServiceKey, apiKey: displayServiceKey,
connectionString: connectionString || this.getCurrentEnvPrivateConfig(env).connectionString, connectionString: connectionString || this.getCurrentEnvPrivateConfig(env).connectionString,
} },
}); });
await this.loadBranches(env, baseRef); await this.loadBranches(env, baseRef);
} catch (error) {
// Ignore abort errors silently
if (error.name === 'AbortError' || error.code === 'ERR_CANCELED') {
return;
}
throw error;
} finally {
// Clear loading flag when complete (success or abort)
this.setLoadingFlag(false);
this.isLoading = false;
// Clear abort controller if this request completed
if (this.branchChangeAbortController?.signal === signal) {
this.branchChangeAbortController = null;
}
}
},
setLoadingFlag(isLoading) {
// Emit settings update with loading flag
const newSettings = {
...this.settings,
privateData: {
...this.settings.privateData,
_isBranchChanging: isLoading || undefined, // undefined removes the key when false
},
};
this.$emit('update:settings', newSettings);
}, },
changeApiKey(apiKey, env) { changeApiKey(apiKey, env) {
this.updateEnvironmentConfig(env, { this.updateEnvironmentConfig(env, {
publicData: { apiKey } publicData: { apiKey },
});
},
changeApiKey(apiKey, env) {
this.updateEnvironmentConfig(env, {
publicData: { apiKey },
}); });
}, },
changeCustomDomain(customDomain, env) { changeCustomDomain(customDomain, env) {
this.updateEnvironmentConfig(env, { this.updateEnvironmentConfig(env, {
publicData: { customDomain } publicData: { customDomain },
}); });
}, },
changePrivateApiKey(apiKey, env) { changePrivateApiKey(apiKey, env) {
this.updateEnvironmentConfig(env, { this.updateEnvironmentConfig(env, {
privateData: { apiKey } privateData: { apiKey },
}); });
}, },
@ -814,15 +896,15 @@ export default {
const previousGlobalRefresh = this.settings.privateData?.refreshToken; const previousGlobalRefresh = this.settings.privateData?.refreshToken;
const privateUpdates = { ...(updates.privateData || {}) }; const privateUpdates = { ...(updates.privateData || {}) };
const updateAccessToken = Object.prototype.hasOwnProperty.call(privateUpdates, 'accessToken') const updateAccessToken = Object.hasOwn(privateUpdates, 'accessToken')
? privateUpdates.accessToken ? privateUpdates.accessToken
: undefined; : undefined;
const updateRefreshToken = Object.prototype.hasOwnProperty.call(privateUpdates, 'refreshToken') const updateRefreshToken = Object.hasOwn(privateUpdates, 'refreshToken')
? privateUpdates.refreshToken ? privateUpdates.refreshToken
: undefined; : undefined;
if (Object.prototype.hasOwnProperty.call(privateUpdates, 'accessToken')) delete privateUpdates.accessToken; if (Object.hasOwn(privateUpdates, 'accessToken')) delete privateUpdates.accessToken;
if (Object.prototype.hasOwnProperty.call(privateUpdates, 'refreshToken')) delete privateUpdates.refreshToken; if (Object.hasOwn(privateUpdates, 'refreshToken')) delete privateUpdates.refreshToken;
const sanitizedCurrentPrivate = { ...currentPrivateConfig }; const sanitizedCurrentPrivate = { ...currentPrivateConfig };
delete sanitizedCurrentPrivate.accessToken; delete sanitizedCurrentPrivate.accessToken;
@ -836,9 +918,9 @@ export default {
...this.settings.publicData?.environments, ...this.settings.publicData?.environments,
[env]: { [env]: {
...currentPublicConfig, ...currentPublicConfig,
...(updates.publicData || {}) ...(updates.publicData || {}),
} },
} },
}, },
privateData: { privateData: {
...this.settings.privateData, ...this.settings.privateData,
@ -847,9 +929,9 @@ export default {
[env]: { [env]: {
...sanitizedCurrentPrivate, ...sanitizedCurrentPrivate,
...privateUpdates, ...privateUpdates,
} },
} },
} },
}; };
if (env === 'production') { if (env === 'production') {
@ -888,7 +970,11 @@ export default {
return; return;
} }
if (!confirm(`Are you sure you want to clear the ${env} environment configuration? This will remove all settings for this environment.`)) { if (
!confirm(
`Are you sure you want to clear the ${env} environment configuration? This will remove all settings for this environment.`
)
) {
return; return;
} }
@ -901,9 +987,9 @@ export default {
[env]: { [env]: {
projectUrl: '', projectUrl: '',
apiKey: '', apiKey: '',
customDomain: '' customDomain: '',
} },
} },
}, },
privateData: { privateData: {
...this.settings.privateData, ...this.settings.privateData,
@ -913,10 +999,10 @@ export default {
connectionMode: 'custom', connectionMode: 'custom',
apiKey: '', apiKey: '',
databasePassword: '', databasePassword: '',
connectionString: '' connectionString: '',
} },
} },
} },
}; };
this.selectModes[env] = 'select'; this.selectModes[env] = 'select';
@ -956,7 +1042,7 @@ export default {
} }
}, },
async fetchProject(projectId, { branchSlug = '', branchRef = '' } = {}) { async fetchProject(projectId, { branchSlug = '', branchRef = '', signal = null } = {}) {
if (!projectId) { if (!projectId) {
return null; return null;
} }
@ -972,10 +1058,14 @@ export default {
...(branchRef ? { branchRef } : {}), ...(branchRef ? { branchRef } : {}),
} }
: undefined, : undefined,
signal, // Pass AbortController signal to requestAPI
}); });
return data?.data; return data?.data;
} catch (error) { } catch (error) {
// Don't log abort errors
if (error.name !== 'AbortError' && error.code !== 'ERR_CANCELED') {
console.warn(`Failed to fetch project ${projectId}:`, error); console.warn(`Failed to fetch project ${projectId}:`, error);
}
return null; return null;
} }
}, },
@ -1014,7 +1104,7 @@ export default {
apiKey: '', apiKey: '',
connectionString: '', connectionString: '',
databasePassword: '', databasePassword: '',
} },
}); });
const { data } = await wwLib.wwPlugins.supabase.requestAPI({ const { data } = await wwLib.wwPlugins.supabase.requestAPI({
@ -1041,7 +1131,9 @@ export default {
const projectData = await this.fetchProject(projectId); const projectData = await this.fetchProject(projectId);
if (projectData) { if (projectData) {
const apiKey = projectData.apiKeys?.find(key => key.name === 'anon')?.api_key; const apiKey = projectData.apiKeys?.find(key => key.name === 'anon')?.api_key;
const privateApiKey = projectData.apiKeys?.find(key => key.name === 'service_role')?.api_key; const privateApiKey = projectData.apiKeys?.find(
key => key.name === 'service_role'
)?.api_key;
const connectionString = projectData.pgbouncer?.connection_string; const connectionString = projectData.pgbouncer?.connection_string;
const databasePassword = newProject.dbPass; const databasePassword = newProject.dbPass;
@ -1051,7 +1143,7 @@ export default {
apiKey: privateApiKey, apiKey: privateApiKey,
connectionString: connectionString, connectionString: connectionString,
databasePassword, databasePassword,
} },
}); });
this.isComingUp = false; this.isComingUp = false;

View File

@ -10,6 +10,11 @@ export default {
edit: () => import('./src/components/Configuration/SettingsEditMultiEnv.vue'), edit: () => import('./src/components/Configuration/SettingsEditMultiEnv.vue'),
summary: () => import('./src/components/Configuration/SettingsSummary.vue'), summary: () => import('./src/components/Configuration/SettingsSummary.vue'),
getIsValid(settings) { getIsValid(settings) {
// Prevent saving while branch change is in progress
if (settings.privateData?._isBranchChanging) {
return false;
}
// Check if using new multi-environment format // Check if using new multi-environment format
if (settings.publicData?.environments) { if (settings.publicData?.environments) {
// Production environment is required // Production environment is required

View File

@ -6,10 +6,7 @@
<div <div
v-for="env in environments" v-for="env in environments"
:key="env" :key="env"
:class="[ :class="['ww-tab-item', { 'ww-tab-active': activeEnvironment === env }]"
'ww-tab-item',
{ 'ww-tab-active': activeEnvironment === env }
]"
@click="activeEnvironment = env" @click="activeEnvironment = env"
> >
<span class="ww-tab-label"> <span class="ww-tab-label">
@ -25,7 +22,6 @@
<!-- Environment Configuration --> <!-- Environment Configuration -->
<div v-for="env in environments" :key="`config-${env}`" v-show="activeEnvironment === env"> <div v-for="env in environments" :key="`config-${env}`" v-show="activeEnvironment === env">
<!-- Connection Mode Selector --> <!-- Connection Mode Selector -->
<wwEditorFormRow label="Connection Mode" class="w-100 mb-3"> <wwEditorFormRow label="Connection Mode" class="w-100 mb-3">
<wwEditorInputRadio <wwEditorInputRadio
@ -34,17 +30,25 @@
{ label: 'Guided (recommended)', value: 'oauth', default: true }, { label: 'Guided (recommended)', value: 'oauth', default: true },
{ label: 'Custom', value: 'custom' }, { label: 'Custom', value: 'custom' },
]" ]"
@update:modelValue="(mode) => changeConnectionMode(env, mode)" @update:modelValue="mode => changeConnectionMode(env, mode)"
/> />
</wwEditorFormRow> </wwEditorFormRow>
<!-- OAuth Connection --> <!-- OAuth Connection -->
<template v-if="getConnectionMode(env) === 'oauth'"> <template v-if="getConnectionMode(env) === 'oauth'">
<div v-if="!hasOAuthToken()" class="body-sm content-brand-secondary bg-brand-secondary border-brand-secondary p-2 mb-2 rounded-02"> <div
v-if="!hasOAuthToken()"
class="body-sm content-brand-secondary bg-brand-secondary border-brand-secondary p-2 mb-2 rounded-02"
>
<span>Connect to enable the Back-end panel and AI assistance.</span> <span>Connect to enable the Back-end panel and AI assistance.</span>
</div> </div>
<div class="flex items-center justify-center mb-3"> <div class="flex items-center justify-center mb-3">
<button class="ww-editor-button -secondary" @click="connect" type="button" :disabled="!!hasOAuthToken()"> <button
class="ww-editor-button -secondary"
@click="connect"
type="button"
:disabled="!!hasOAuthToken()"
>
<wwEditorIcon name="logos/supabase" class="ww-editor-button-icon -left" /> <wwEditorIcon name="logos/supabase" class="ww-editor-button-icon -left" />
{{ hasOAuthToken() ? 'Account connected' : 'Connect Supabase' }} {{ hasOAuthToken() ? 'Account connected' : 'Connect Supabase' }}
</button> </button>
@ -86,11 +90,15 @@
placeholder="https://your-project.supabase.co" placeholder="https://your-project.supabase.co"
:model-value="getProjectSelectValue(env)" :model-value="getProjectSelectValue(env)"
:options="projectsOptions" :options="projectsOptions"
@update:modelValue="(val) => changeProjectUrl(val, env)" @update:modelValue="val => changeProjectUrl(val, env)"
class="-full" class="-full"
/> />
</wwEditorFormRow> </wwEditorFormRow>
<button type="button" class="ww-editor-button -primary -small -icon ml-2 mt-1" @click="refreshProjects"> <button
type="button"
class="ww-editor-button -primary -small -icon ml-2 mt-1"
@click="refreshProjects"
>
<wwEditorIcon name="refresh" medium /> <wwEditorIcon name="refresh" medium />
</button> </button>
</div> </div>
@ -103,11 +111,15 @@
placeholder="Default (main)" placeholder="Default (main)"
:model-value="selectedBranches?.[env] || ''" :model-value="selectedBranches?.[env] || ''"
:options="branchOptions(env)" :options="branchOptions(env)"
@update:modelValue="(val) => changeBranch(val, env)" @update:modelValue="val => changeBranch(val, env)"
class="-full" class="-full"
/> />
</wwEditorFormRow> </wwEditorFormRow>
<button type="button" class="ww-editor-button -primary -small -icon ml-2 mt-1" @click="loadBranches(env)"> <button
type="button"
class="ww-editor-button -primary -small -icon ml-2 mt-1"
@click="loadBranches(env)"
>
<wwEditorIcon name="refresh" medium /> <wwEditorIcon name="refresh" medium />
</button> </button>
<div v-if="branchErrors?.[env]" class="body-xs content-tertiary ml-2 mt-1"> <div v-if="branchErrors?.[env]" class="body-xs content-tertiary ml-2 mt-1">
@ -130,7 +142,7 @@
placeholder="https://your-project.supabase.co" placeholder="https://your-project.supabase.co"
:required="env === 'production'" :required="env === 'production'"
:model-value="getCurrentEnvConfig(env).projectUrl" :model-value="getCurrentEnvConfig(env).projectUrl"
@update:modelValue="(val) => changeProjectUrl(val, env)" @update:modelValue="val => changeProjectUrl(val, env)"
/> />
<wwEditorInputRow <wwEditorInputRow
@ -139,7 +151,7 @@
type="query" type="query"
placeholder="Enter your public API key" placeholder="Enter your public API key"
:model-value="getCurrentEnvConfig(env).apiKey" :model-value="getCurrentEnvConfig(env).apiKey"
@update:modelValue="(val) => changeApiKey(val, env)" @update:modelValue="val => changeApiKey(val, env)"
/> />
<wwEditorFormRow label="Service role key"> <wwEditorFormRow label="Service role key">
@ -151,7 +163,7 @@
class="w-full" class="w-full"
:style="{ '-webkit-text-security': 'disc' }" :style="{ '-webkit-text-security': 'disc' }"
:model-value="getCurrentEnvPrivateConfig(env).apiKey" :model-value="getCurrentEnvPrivateConfig(env).apiKey"
@update:modelValue="(val) => changePrivateApiKey(val, env)" @update:modelValue="val => changePrivateApiKey(val, env)"
/> />
<wwEditorQuestionMark <wwEditorQuestionMark
tooltip-position="top-left" tooltip-position="top-left"
@ -168,7 +180,9 @@
<template v-else-if="selectModes[env] === 'create'"> <template v-else-if="selectModes[env] === 'create'">
<div v-if="isComingUp" class="body-md flex items-center p-2"> <div v-if="isComingUp" class="body-md flex items-center p-2">
<wwLoaderSmall loading class="mr-2" /> <wwLoaderSmall loading class="mr-2" />
<div>We're now preparing your database. Please wait a few moments, it may take up to 1 minute.</div> <div>
We're now preparing your database. Please wait a few moments, it may take up to 1 minute.
</div>
</div> </div>
<template v-else> <template v-else>
<wwEditorInputRow <wwEditorInputRow
@ -220,11 +234,7 @@
</button> </button>
</div> </div>
</wwEditorFormRow> </wwEditorFormRow>
<button <button class="ww-editor-button -primary" @click="createProject(env)" type="button">
class="ww-editor-button -primary"
@click="createProject(env)"
type="button"
>
Create project for {{ capitalize(env) }} Create project for {{ capitalize(env) }}
</button> </button>
</template> </template>
@ -235,7 +245,10 @@
<!-- Custom Connection Mode --> <!-- Custom Connection Mode -->
<template v-else> <template v-else>
<div class="body-sm content-secondary bg-secondary border-secondary p-2 rounded-02 mb-2"> <div class="body-sm content-secondary bg-secondary border-secondary p-2 rounded-02 mb-2">
<span>Use this mode for self-hosted projects, local development, or if you don't want to connect your account.</span> <span
>Use this mode for self-hosted projects, local development, or if you don't want to connect your
account.</span
>
</div> </div>
<div class="body-sm content-warning-secondary bg-warning-secondary p-2 rounded-02 mb-3"> <div class="body-sm content-warning-secondary bg-warning-secondary p-2 rounded-02 mb-3">
<span>Using this mode disables the Back-end panel and AI assistance.</span> <span>Using this mode disables the Back-end panel and AI assistance.</span>
@ -247,7 +260,7 @@
placeholder="https://your-project.supabase.co" placeholder="https://your-project.supabase.co"
:required="env === 'production'" :required="env === 'production'"
:model-value="getCurrentEnvConfig(env).projectUrl" :model-value="getCurrentEnvConfig(env).projectUrl"
@update:modelValue="(val) => changeProjectUrl(val, env)" @update:modelValue="val => changeProjectUrl(val, env)"
/> />
<wwEditorInputRow <wwEditorInputRow
@ -255,7 +268,7 @@
type="query" type="query"
placeholder="https://your-custom-domain.com" placeholder="https://your-custom-domain.com"
:model-value="getCurrentEnvConfig(env).customDomain" :model-value="getCurrentEnvConfig(env).customDomain"
@update:modelValue="(val) => changeCustomDomain(val, env)" @update:modelValue="val => changeCustomDomain(val, env)"
/> />
<wwEditorInputRow <wwEditorInputRow
@ -264,7 +277,7 @@
type="query" type="query"
placeholder="Enter your public API key" placeholder="Enter your public API key"
:model-value="getCurrentEnvConfig(env).apiKey" :model-value="getCurrentEnvConfig(env).apiKey"
@update:modelValue="(val) => changeApiKey(val, env)" @update:modelValue="val => changeApiKey(val, env)"
/> />
<wwEditorFormRow label="Service role key"> <wwEditorFormRow label="Service role key">
@ -276,7 +289,7 @@
class="w-full" class="w-full"
:style="{ '-webkit-text-security': 'disc' }" :style="{ '-webkit-text-security': 'disc' }"
:model-value="getCurrentEnvPrivateConfig(env).apiKey" :model-value="getCurrentEnvPrivateConfig(env).apiKey"
@update:modelValue="(val) => changePrivateApiKey(val, env)" @update:modelValue="val => changePrivateApiKey(val, env)"
/> />
<wwEditorQuestionMark <wwEditorQuestionMark
tooltip-position="top-left" tooltip-position="top-left"
@ -321,12 +334,12 @@ export default {
selectModes: { selectModes: {
production: 'select', production: 'select',
staging: 'select', staging: 'select',
editor: 'select' editor: 'select',
}, },
showSettings: { showSettings: {
production: false, production: false,
staging: false, staging: false,
editor: false editor: false,
}, },
projects: [], projects: [],
isLoading: false, isLoading: false,
@ -351,11 +364,12 @@ export default {
region: 'us-east-1', region: 'us-east-1',
organizationId: '', organizationId: '',
dbPass: '', dbPass: '',
} },
}, },
branches: {}, branches: {},
selectedBranches: {}, selectedBranches: {},
branchErrors: {}, branchErrors: {},
branchChangeAbortController: null, // AbortController for cancelling in-flight requests
}; };
}, },
watch: { watch: {
@ -367,7 +381,9 @@ export default {
await this.fetchOrganizations(); await this.fetchOrganizations();
// Initialize new project data for this environment // Initialize new project data for this environment
this.newProjects[env] = { this.newProjects[env] = {
name: `WeWeb - ${wwLib.$store.getters['websiteData/getDesignInfo'].name} (${this.capitalize(env)})`, name: `WeWeb - ${wwLib.$store.getters['websiteData/getDesignInfo'].name} (${this.capitalize(
env
)})`,
region: 'us-east-1', region: 'us-east-1',
organizationId: this.organizations[0]?.id || '', organizationId: this.organizations[0]?.id || '',
dbPass: wwLib.wwUtils.getUid(), dbPass: wwLib.wwUtils.getUid(),
@ -375,35 +391,41 @@ export default {
} }
} }
}, },
deep: true deep: true,
} },
}, },
computed: { computed: {
isValid() {
// Prevent saving while branch change is in progress
return !this.branchChangeAbortController;
},
projectRef() { projectRef() {
const config = this.getCurrentEnvConfig(); const config = this.getCurrentEnvConfig();
return config?.projectUrl?.replace('https://', '').replace('.supabase.co', ''); return config?.projectUrl?.replace('https://', '').replace('.supabase.co', '');
}, },
projectsOptions() { projectsOptions() {
return ( return this.projects
this.projects
.map(project => ({ .map(project => ({
label: `${project.name} (${project.id}) ${project.status === 'INACTIVE' ? '#PAUSED' : ''}`, label: `${project.name} (${project.id}) ${project.status === 'INACTIVE' ? '#PAUSED' : ''}`,
value: `https://${project.id}.supabase.co`, value: `https://${project.id}.supabase.co`,
})) }))
.sort((a, b) => (a.label.includes('#PAUSED') ? 1 : 0) - (b.label.includes('#PAUSED') ? 1 : 0)) .sort((a, b) => (a.label.includes('#PAUSED') ? 1 : 0) - (b.label.includes('#PAUSED') ? 1 : 0));
);
}, },
branchOptions() { branchOptions() {
return (env) => { return env => {
const items = this.branches?.[env] || []; const items = this.branches?.[env] || [];
if (items.length === 0) return []; if (items.length === 0) return [];
return items.map(b => ({ label: `${b.name}${b.is_default ? ' (default)' : ''}`, value: b.project_ref || b.ref || b.id || b.name })); return items.map(b => ({
} label: `${b.name}${b.is_default ? ' (default)' : ''}`,
value: b.project_ref || b.ref || b.id || b.name,
}));
};
}, },
}, },
async mounted() { async mounted() {
// Check if this is a fresh install and try to sync from Supabase Auth plugin // Check if this is a fresh install and try to sync from Supabase Auth plugin
const isFreshInstall = !this.settings.publicData?.environments && const isFreshInstall =
!this.settings.publicData?.environments &&
!this.settings.publicData?.projectUrl && !this.settings.publicData?.projectUrl &&
!this.settings.privateData?.accessToken; !this.settings.privateData?.accessToken;
@ -445,10 +467,9 @@ export default {
this.environments.forEach(env => { this.environments.forEach(env => {
const config = envConfigs[env]; const config = envConfigs[env];
if (!config) return; if (!config) return;
if ( const hasLegacyAccess = Object.hasOwn(config, 'accessToken');
Object.prototype.hasOwnProperty.call(config, 'accessToken') || const hasLegacyRefresh = Object.hasOwn(config, 'refreshToken');
Object.prototype.hasOwnProperty.call(config, 'refreshToken') if (hasLegacyAccess || hasLegacyRefresh) {
) {
const { accessToken: _legacyAccess, refreshToken: _legacyRefresh, ...rest } = config; const { accessToken: _legacyAccess, refreshToken: _legacyRefresh, ...rest } = config;
sanitizedEnvs[env] = rest; sanitizedEnvs[env] = rest;
hasChanges = true; hasChanges = true;
@ -506,7 +527,8 @@ export default {
getProjectSelectValue(env) { getProjectSelectValue(env) {
const config = this.getCurrentEnvConfig(env); const config = this.getCurrentEnvConfig(env);
if (!config) return ''; if (!config) return '';
const baseRef = config.baseProjectRef || config.projectUrl?.replace('https://', '').replace('.supabase.co', ''); const baseRef =
config.baseProjectRef || config.projectUrl?.replace('https://', '').replace('.supabase.co', '');
return baseRef ? `https://${baseRef}.supabase.co` : config.projectUrl; return baseRef ? `https://${baseRef}.supabase.co` : config.projectUrl;
}, },
@ -525,7 +547,7 @@ export default {
return { return {
projectUrl: this.settings.publicData.projectUrl, projectUrl: this.settings.publicData.projectUrl,
apiKey: this.settings.publicData.apiKey, apiKey: this.settings.publicData.apiKey,
customDomain: this.settings.publicData.customDomain customDomain: this.settings.publicData.customDomain,
}; };
} }
return {}; return {};
@ -533,8 +555,11 @@ export default {
getCurrentEnvPrivateConfig(env = this.activeEnvironment) { getCurrentEnvPrivateConfig(env = this.activeEnvironment) {
if (this.settings.privateData?.environments?.[env]) { if (this.settings.privateData?.environments?.[env]) {
const { accessToken: _legacyAccess, refreshToken: _legacyRefresh, ...rest } = const {
this.settings.privateData.environments[env] || {}; accessToken: _legacyAccess,
refreshToken: _legacyRefresh,
...rest
} = this.settings.privateData.environments[env] || {};
return rest; return rest;
} }
// Fallback to legacy format for production // Fallback to legacy format for production
@ -543,7 +568,7 @@ export default {
connectionMode: this.settings.privateData.connectionMode || 'oauth', connectionMode: this.settings.privateData.connectionMode || 'oauth',
apiKey: this.settings.privateData.apiKey, apiKey: this.settings.privateData.apiKey,
databasePassword: this.settings.privateData.databasePassword, databasePassword: this.settings.privateData.databasePassword,
connectionString: this.settings.privateData.connectionString connectionString: this.settings.privateData.connectionString,
}; };
} }
return {}; return {};
@ -561,9 +586,9 @@ export default {
production: { production: {
projectUrl: this.settings.publicData?.projectUrl || '', projectUrl: this.settings.publicData?.projectUrl || '',
apiKey: this.settings.publicData?.apiKey || '', apiKey: this.settings.publicData?.apiKey || '',
customDomain: this.settings.publicData?.customDomain || '' customDomain: this.settings.publicData?.customDomain || '',
} },
} },
}, },
privateData: { privateData: {
...this.settings.privateData, ...this.settings.privateData,
@ -572,10 +597,10 @@ export default {
connectionMode: connectionMode, connectionMode: connectionMode,
apiKey: this.settings.privateData?.apiKey || '', apiKey: this.settings.privateData?.apiKey || '',
databasePassword: this.settings.privateData?.databasePassword || '', databasePassword: this.settings.privateData?.databasePassword || '',
connectionString: this.settings.privateData?.connectionString || '' connectionString: this.settings.privateData?.connectionString || '',
} },
} },
} },
}; };
this.$emit('update:settings', newSettings); this.$emit('update:settings', newSettings);
@ -584,8 +609,8 @@ export default {
changeConnectionMode(env, mode) { changeConnectionMode(env, mode) {
this.updateEnvironmentConfig(env, { this.updateEnvironmentConfig(env, {
privateData: { privateData: {
connectionMode: mode connectionMode: mode,
} },
}); });
}, },
@ -612,9 +637,9 @@ export default {
accessToken: '', accessToken: '',
refreshToken: '', refreshToken: '',
environments: { environments: {
...this.settings.privateData?.environments ...this.settings.privateData?.environments,
} },
} },
}; };
// Clear tokens from all environments // Clear tokens from all environments
@ -633,7 +658,7 @@ export default {
if (!projectUrl) { if (!projectUrl) {
this.updateEnvironmentConfig(env, { this.updateEnvironmentConfig(env, {
publicData: { projectUrl: '', apiKey: '' }, publicData: { projectUrl: '', apiKey: '' },
privateData: { apiKey: '', connectionString: '' } privateData: { apiKey: '', connectionString: '' },
}); });
return; return;
} }
@ -671,7 +696,8 @@ export default {
if (projectData) { if (projectData) {
apiKey = projectData.apiKeys?.find(key => key.name === 'anon')?.api_key || apiKey; apiKey = projectData.apiKeys?.find(key => key.name === 'anon')?.api_key || apiKey;
privateApiKey = projectData.apiKeys?.find(key => key.name === 'service_role')?.api_key || privateApiKey; privateApiKey =
projectData.apiKeys?.find(key => key.name === 'service_role')?.api_key || privateApiKey;
connectionString = projectData.pgbouncer?.connection_string || connectionString; connectionString = projectData.pgbouncer?.connection_string || connectionString;
baseProjectRef = baseProjectRef =
projectData.project?.parent_project_ref || projectData.project?.parent_project_ref ||
@ -683,7 +709,7 @@ export default {
this.updateEnvironmentConfig(env, { this.updateEnvironmentConfig(env, {
publicData: { projectUrl, apiKey, baseProjectRef, branch: null, branchSlug: null }, publicData: { projectUrl, apiKey, baseProjectRef, branch: null, branchSlug: null },
privateData: { apiKey: privateApiKey, connectionString } privateData: { apiKey: privateApiKey, connectionString },
}); });
// Load branches // Load branches
@ -697,7 +723,10 @@ export default {
async loadBranches(env, overrideRef = '') { async loadBranches(env, overrideRef = '') {
try { try {
const cfg = this.getCurrentEnvConfig(env); const cfg = this.getCurrentEnvConfig(env);
const baseRef = overrideRef || cfg.baseProjectRef || cfg.projectUrl?.replace('https://', '').replace('.supabase.co', ''); const baseRef =
overrideRef ||
cfg.baseProjectRef ||
cfg.projectUrl?.replace('https://', '').replace('.supabase.co', '');
const ref = baseRef; const ref = baseRef;
const paramsBaseRef = cfg.baseProjectRef || overrideRef || ''; const paramsBaseRef = cfg.baseProjectRef || overrideRef || '';
if (!ref || !this.hasOAuthToken()) return; if (!ref || !this.hasOAuthToken()) return;
@ -707,44 +736,43 @@ export default {
params: { baseProjectRef: paramsBaseRef }, params: { baseProjectRef: paramsBaseRef },
}); });
const branches = data?.data || []; const branches = data?.data || [];
if (this.$set) {
this.$set(this.branches, env, branches);
this.$set(this.branchErrors, env, '');
} else {
this.branches = { ...(this.branches || {}), [env]: branches }; this.branches = { ...(this.branches || {}), [env]: branches };
const errors = { ...(this.branchErrors || {}) }; const errors = { ...(this.branchErrors || {}) };
delete errors[env]; delete errors[env];
this.branchErrors = errors; this.branchErrors = errors;
}
const defaultBranch = branches.find(b => b.is_default); const defaultBranch = branches.find(b => b.is_default);
const defaultValue = defaultBranch?.project_ref || defaultBranch?.ref || defaultBranch?.id || ''; const defaultValue = defaultBranch?.project_ref || defaultBranch?.ref || defaultBranch?.id || '';
const currentSelection = this.selectedBranches?.[env]; const currentSelection = this.selectedBranches?.[env];
const savedSelection = this.getCurrentEnvConfig(env)?.branch || ''; const savedSelection = this.getCurrentEnvConfig(env)?.branch || '';
const targetSelection = currentSelection || savedSelection || ''; const targetSelection = currentSelection || savedSelection || '';
const selectionExistsInList = targetSelection && branches.some(b => (b.project_ref || b.ref || b.id || b.name) === targetSelection); const selectionExistsInList =
targetSelection &&
branches.some(b => (b.project_ref || b.ref || b.id || b.name) === targetSelection);
let nextSelection = ''; let nextSelection = '';
let shouldFetchBranchData = false;
if (selectionExistsInList) { if (selectionExistsInList) {
nextSelection = targetSelection; nextSelection = targetSelection;
} else if (defaultValue) { } else if (defaultValue) {
nextSelection = defaultValue; nextSelection = defaultValue;
// Auto-selected default branch needs data fetched
shouldFetchBranchData = !savedSelection;
} }
if (nextSelection || currentSelection || savedSelection) { if (nextSelection || currentSelection || savedSelection) {
if (this.$set) this.$set(this.selectedBranches, env, nextSelection); this.selectedBranches = { ...(this.selectedBranches || {}), [env]: nextSelection };
else this.selectedBranches = { ...(this.selectedBranches || {}), [env]: nextSelection };
}
// If we auto-selected a default branch, fetch its data
if (shouldFetchBranchData && nextSelection) {
await this.changeBranch(nextSelection, env);
}
}
} catch (e) { } catch (e) {
const msg = e?.response?.data?.error || e?.message || 'Unable to load branches'; const msg = e?.response?.data?.error || e?.message || 'Unable to load branches';
if (this.$set) {
this.$set(this.branches, env, []);
this.$set(this.branchErrors, env, msg);
} else {
this.branches = { ...(this.branches || {}), [env]: [] }; this.branches = { ...(this.branches || {}), [env]: [] };
this.branchErrors = { ...(this.branchErrors || {}), [env]: msg }; this.branchErrors = { ...(this.branchErrors || {}), [env]: msg };
}
console.warn('[Supabase plugin] loadBranches error', { env, status: e?.response?.status, msg }); console.warn('[Supabase plugin] loadBranches error', { env, status: e?.response?.status, msg });
} }
}, },
@ -752,10 +780,26 @@ export default {
// removed features gating; we call /branches directly // removed features gating; we call /branches directly
async changeBranch(branchValue, env) { async changeBranch(branchValue, env) {
// Cancel any in-flight branch change request
if (this.branchChangeAbortController) {
this.branchChangeAbortController.abort();
}
// Create new AbortController for this request
this.branchChangeAbortController = new AbortController();
const signal = this.branchChangeAbortController.signal;
// Set flag to prevent saving while branch change is in progress
this.setLoadingFlag(true);
this.isLoading = true;
try {
if (this.$set) this.$set(this.selectedBranches, env, branchValue || ''); if (this.$set) this.$set(this.selectedBranches, env, branchValue || '');
else this.selectedBranches = { ...(this.selectedBranches || {}), [env]: branchValue || '' }; else this.selectedBranches = { ...(this.selectedBranches || {}), [env]: branchValue || '' };
const baseRef = this.getCurrentEnvConfig(env).baseProjectRef || this.getCurrentEnvConfig(env).projectUrl?.replace('https://', '').replace('.supabase.co', ''); const baseRef =
this.getCurrentEnvConfig(env).baseProjectRef ||
this.getCurrentEnvConfig(env).projectUrl?.replace('https://', '').replace('.supabase.co', '');
if (!baseRef) return; if (!baseRef) return;
let targetRef = baseRef; let targetRef = baseRef;
@ -767,16 +811,23 @@ export default {
branchSlug = branch?.name || ''; branchSlug = branch?.name || '';
} }
const effectiveBranchSlug = branchValue ? (branchSlug || this.getCurrentEnvConfig(env)?.branchSlug || '') : ''; const effectiveBranchSlug = branchValue
? branchSlug || this.getCurrentEnvConfig(env)?.branchSlug || ''
: '';
const projectData = await this.fetchProject(baseRef, { const projectData = await this.fetchProject(baseRef, {
branchSlug: effectiveBranchSlug, branchSlug: effectiveBranchSlug,
branchRef: branchValue ? targetRef : '', branchRef: branchValue ? targetRef : '',
signal, // Pass AbortController signal
}); });
const apiKey = projectData?.apiKeys?.find(key => key.name === 'anon')?.api_key; const apiKey = projectData?.apiKeys?.find(key => key.name === 'anon')?.api_key;
const privateApiKey = projectData?.apiKeys?.find(key => key.name === 'service_role')?.api_key; const privateApiKey = projectData?.apiKeys?.find(key => key.name === 'service_role')?.api_key;
const connectionString = projectData?.pgbouncer?.connection_string; const connectionString = projectData?.pgbouncer?.connection_string;
const resolvedBranchRef = branchValue const resolvedBranchRef = branchValue
? projectData?.branchRef || projectData?.project?.project_ref || projectData?.project?.ref || targetRef ? projectData?.branchRef ||
projectData?.project?.project_ref ||
projectData?.project?.ref ||
targetRef
: ''; : '';
const runtimeRef = resolvedBranchRef || baseRef; const runtimeRef = resolvedBranchRef || baseRef;
const projectUrl = `https://${runtimeRef}.supabase.co`; const projectUrl = `https://${runtimeRef}.supabase.co`;
@ -794,27 +845,55 @@ export default {
privateData: { privateData: {
apiKey: displayServiceKey, apiKey: displayServiceKey,
connectionString: connectionString || this.getCurrentEnvPrivateConfig(env).connectionString, connectionString: connectionString || this.getCurrentEnvPrivateConfig(env).connectionString,
} },
}); });
await this.loadBranches(env, baseRef); await this.loadBranches(env, baseRef);
} catch (error) {
// Ignore abort errors silently
if (error.name === 'AbortError' || error.code === 'ERR_CANCELED') {
return;
}
throw error;
} finally {
// Clear loading flag when complete (success or abort)
this.setLoadingFlag(false);
this.isLoading = false;
// Clear abort controller if this request completed
if (this.branchChangeAbortController?.signal === signal) {
this.branchChangeAbortController = null;
}
}
},
setLoadingFlag(isLoading) {
// Emit settings update with loading flag
const newSettings = {
...this.settings,
privateData: {
...this.settings.privateData,
_isBranchChanging: isLoading || undefined, // undefined removes the key when false
},
};
this.$emit('update:settings', newSettings);
}, },
changeApiKey(apiKey, env) { changeApiKey(apiKey, env) {
this.updateEnvironmentConfig(env, { this.updateEnvironmentConfig(env, {
publicData: { apiKey } publicData: { apiKey },
}); });
}, },
changeCustomDomain(customDomain, env) { changeCustomDomain(customDomain, env) {
this.updateEnvironmentConfig(env, { this.updateEnvironmentConfig(env, {
publicData: { customDomain } publicData: { customDomain },
}); });
}, },
changePrivateApiKey(apiKey, env) { changePrivateApiKey(apiKey, env) {
this.updateEnvironmentConfig(env, { this.updateEnvironmentConfig(env, {
privateData: { apiKey } privateData: { apiKey },
}); });
}, },
@ -825,15 +904,15 @@ export default {
const previousGlobalRefresh = this.settings.privateData?.refreshToken; const previousGlobalRefresh = this.settings.privateData?.refreshToken;
const privateUpdates = { ...(updates.privateData || {}) }; const privateUpdates = { ...(updates.privateData || {}) };
const updateAccessToken = Object.prototype.hasOwnProperty.call(privateUpdates, 'accessToken') const updateAccessToken = Object.hasOwn(privateUpdates, 'accessToken')
? privateUpdates.accessToken ? privateUpdates.accessToken
: undefined; : undefined;
const updateRefreshToken = Object.prototype.hasOwnProperty.call(privateUpdates, 'refreshToken') const updateRefreshToken = Object.hasOwn(privateUpdates, 'refreshToken')
? privateUpdates.refreshToken ? privateUpdates.refreshToken
: undefined; : undefined;
if (Object.prototype.hasOwnProperty.call(privateUpdates, 'accessToken')) delete privateUpdates.accessToken; if (Object.hasOwn(privateUpdates, 'accessToken')) delete privateUpdates.accessToken;
if (Object.prototype.hasOwnProperty.call(privateUpdates, 'refreshToken')) delete privateUpdates.refreshToken; if (Object.hasOwn(privateUpdates, 'refreshToken')) delete privateUpdates.refreshToken;
const sanitizedCurrentPrivate = { ...currentPrivateConfig }; const sanitizedCurrentPrivate = { ...currentPrivateConfig };
delete sanitizedCurrentPrivate.accessToken; delete sanitizedCurrentPrivate.accessToken;
@ -847,9 +926,9 @@ export default {
...this.settings.publicData?.environments, ...this.settings.publicData?.environments,
[env]: { [env]: {
...currentPublicConfig, ...currentPublicConfig,
...(updates.publicData || {}) ...(updates.publicData || {}),
} },
} },
}, },
privateData: { privateData: {
...this.settings.privateData, ...this.settings.privateData,
@ -858,9 +937,9 @@ export default {
[env]: { [env]: {
...sanitizedCurrentPrivate, ...sanitizedCurrentPrivate,
...privateUpdates, ...privateUpdates,
} },
} },
} },
}; };
// Keep legacy fields in sync for production (backward compatibility) // Keep legacy fields in sync for production (backward compatibility)
@ -900,7 +979,11 @@ export default {
return; return;
} }
if (!confirm(`Are you sure you want to clear the ${env} environment configuration? This will remove all settings for this environment.`)) { if (
!confirm(
`Are you sure you want to clear the ${env} environment configuration? This will remove all settings for this environment.`
)
) {
return; return;
} }
@ -913,9 +996,9 @@ export default {
[env]: { [env]: {
projectUrl: '', projectUrl: '',
apiKey: '', apiKey: '',
customDomain: '' customDomain: '',
} },
} },
}, },
privateData: { privateData: {
...this.settings.privateData, ...this.settings.privateData,
@ -925,10 +1008,10 @@ export default {
connectionMode: 'custom', connectionMode: 'custom',
apiKey: '', apiKey: '',
databasePassword: '', databasePassword: '',
connectionString: '' connectionString: '',
} },
} },
} },
}; };
this.selectModes[env] = 'select'; this.selectModes[env] = 'select';
@ -968,7 +1051,7 @@ export default {
} }
}, },
async fetchProject(projectId, { branchSlug = '', branchRef = '' } = {}) { async fetchProject(projectId, { branchSlug = '', branchRef = '', signal = null } = {}) {
if (!projectId) { if (!projectId) {
return null; return null;
} }
@ -984,13 +1067,17 @@ export default {
...(branchRef ? { branchRef } : {}), ...(branchRef ? { branchRef } : {}),
} }
: undefined, : undefined,
signal, // Pass AbortController signal to requestAPI
}); });
const project = data?.data?.project || {}; const project = data?.data?.project || {};
const apiKeys = data?.data?.apiKeys || []; const apiKeys = data?.data?.apiKeys || [];
const pgbouncer = data?.data?.pgbouncer; const pgbouncer = data?.data?.pgbouncer;
return data?.data; return data?.data;
} catch (error) { } catch (error) {
// Don't log abort errors
if (error.name !== 'AbortError' && error.code !== 'ERR_CANCELED') {
console.warn(`Failed to fetch project ${projectId}:`, error); console.warn(`Failed to fetch project ${projectId}:`, error);
}
return null; return null;
} }
}, },
@ -1029,7 +1116,7 @@ export default {
apiKey: '', apiKey: '',
connectionString: '', connectionString: '',
databasePassword: '', databasePassword: '',
} },
}); });
const { data } = await wwLib.wwPlugins.supabase.requestAPI({ const { data } = await wwLib.wwPlugins.supabase.requestAPI({
@ -1056,7 +1143,9 @@ export default {
const projectData = await this.fetchProject(projectId); const projectData = await this.fetchProject(projectId);
if (projectData) { if (projectData) {
const apiKey = projectData.apiKeys?.find(key => key.name === 'anon')?.api_key; const apiKey = projectData.apiKeys?.find(key => key.name === 'anon')?.api_key;
const privateApiKey = projectData.apiKeys?.find(key => key.name === 'service_role')?.api_key; const privateApiKey = projectData.apiKeys?.find(
key => key.name === 'service_role'
)?.api_key;
const connectionString = projectData.pgbouncer?.connection_string; const connectionString = projectData.pgbouncer?.connection_string;
const databasePassword = newProject.dbPass; const databasePassword = newProject.dbPass;
@ -1066,7 +1155,7 @@ export default {
apiKey: privateApiKey, apiKey: privateApiKey,
connectionString: connectionString, connectionString: connectionString,
databasePassword, databasePassword,
} },
}); });
this.isComingUp = false; this.isComingUp = false;

View File

@ -83,17 +83,30 @@ export default {
this.subscribeTables(settings.publicData.realtimeTables || {}); this.subscribeTables(settings.publicData.realtimeTables || {});
} }
}, },
async requestAPI({ method, path, data, params }, retry = true) { async requestAPI({ method, path, data, params, signal }, retry = true) {
try { try {
// Get current environment and send it explicitly
const config = getCurrentSupabaseSettings('supabase');
const currentEnv = config.environment; // 'editor', 'staging', or 'production'
return await wwAxios({ return await wwAxios({
method, method,
url: `${wwLib.wwApiRequests._getPluginsUrl()}/designs/${ url: `${wwLib.wwApiRequests._getPluginsUrl()}/designs/${
wwLib.$store.getters['websiteData/getDesignInfo'].id wwLib.$store.getters['websiteData/getDesignInfo'].id
}/supabase${path}`, }/supabase${path}`,
data, data,
params, params: {
...params,
wwEnv: currentEnv, // Send environment explicitly
},
signal, // Support AbortController signal
}); });
} catch (error) { } catch (error) {
// Don't retry if request was aborted
if (error.name === 'AbortError' || error.code === 'ERR_CANCELED') {
throw error;
}
const isOauthToken = wwLib.wwPlugins.supabase.settings.privateData.accessToken?.startsWith('sbp_oauth'); const isOauthToken = wwLib.wwPlugins.supabase.settings.privateData.accessToken?.startsWith('sbp_oauth');
if (retry && [401, 403].includes(error.response?.status) && isOauthToken) { if (retry && [401, 403].includes(error.response?.status) && isOauthToken) {
const { data } = await wwAxios.post( const { data } = await wwAxios.post(
@ -101,7 +114,7 @@ export default {
wwLib.$store.getters['websiteData/getDesignInfo'].id wwLib.$store.getters['websiteData/getDesignInfo'].id
}/supabase/refresh` }/supabase/refresh`
); );
return await this.requestAPI({ method, path, data, params }, false); return await this.requestAPI({ method, path, data, params, signal }, false);
} }
wwLib.wwNotification.open({ text: 'Error while requesting the supabase project.', color: 'red' }); wwLib.wwNotification.open({ text: 'Error while requesting the supabase project.', color: 'red' });
throw error; throw error;

View File

@ -10,6 +10,11 @@ export default {
edit: () => import('./src/components/Configuration/SettingsEditMultiEnv.vue'), edit: () => import('./src/components/Configuration/SettingsEditMultiEnv.vue'),
summary: () => import('./src/components/Configuration/SettingsSummary.vue'), summary: () => import('./src/components/Configuration/SettingsSummary.vue'),
getIsValid(settings) { getIsValid(settings) {
// Prevent saving while branch change is in progress
if (settings.privateData?._isBranchChanging) {
return false;
}
// Check if using new multi-environment format // Check if using new multi-environment format
if (settings.publicData?.environments) { if (settings.publicData?.environments) {
// Production environment is required // Production environment is required

View File

@ -22,7 +22,6 @@ import wwobject3a7d637912d3438798ffb332bb492a63 from '@/components/elements/elem
import wwobjectb783dc65d5284f748c14e27c934c39b1 from '@/components/elements/element-b783dc65-d528-4f74-8c14-e27c934c39b1/ww-config.js'; import wwobjectb783dc65d5284f748c14e27c934c39b1 from '@/components/elements/element-b783dc65-d528-4f74-8c14-e27c934c39b1/ww-config.js';
import wwobject14723a2101784d92a7e9d1dfeaec29a7 from '@/components/elements/element-14723a21-0178-4d92-a7e9-d1dfeaec29a7/ww-config.js'; import wwobject14723a2101784d92a7e9d1dfeaec29a7 from '@/components/elements/element-14723a21-0178-4d92-a7e9-d1dfeaec29a7/ww-config.js';
import wwobject53401515b6944c79a88dabeecb1de562 from '@/components/elements/element-53401515-b694-4c79-a88d-abeecb1de562/ww-config.js'; import wwobject53401515b6944c79a88dabeecb1de562 from '@/components/elements/element-53401515-b694-4c79-a88d-abeecb1de562/ww-config.js';
import wwobject9896d6950c964a0fb587c25e879ece77 from '@/components/elements/element-9896d695-0c96-4a0f-b587-c25e879ece77/ww-config.js';
import wwobject547a655e37cd49ff9c4fc6b917a0b680 from '@/components/elements/element-547a655e-37cd-49ff-9c4f-c6b917a0b680/ww-config.js'; import wwobject547a655e37cd49ff9c4fc6b917a0b680 from '@/components/elements/element-547a655e-37cd-49ff-9c4f-c6b917a0b680/ww-config.js';
import wwobject6d692ca26cdc4805aa0c211102f335d0 from '@/components/elements/element-6d692ca2-6cdc-4805-aa0c-211102f335d0/ww-config.js'; import wwobject6d692ca26cdc4805aa0c211102f335d0 from '@/components/elements/element-6d692ca2-6cdc-4805-aa0c-211102f335d0/ww-config.js';
import wwobject1b1e21739b7842cca8eea6167caea340 from '@/components/elements/element-1b1e2173-9b78-42cc-a8ee-a6167caea340/ww-config.js'; import wwobject1b1e21739b7842cca8eea6167caea340 from '@/components/elements/element-1b1e2173-9b78-42cc-a8ee-a6167caea340/ww-config.js';
@ -31,11 +30,12 @@ import wwobject57831abf83ad49adba973bd30b035710 from '@/components/elements/elem
import wwobject59dca300db7842e4a7a60cbf22d3cc82 from '@/components/elements/element-59dca300-db78-42e4-a7a6-0cbf22d3cc82/ww-config.js'; import wwobject59dca300db7842e4a7a60cbf22d3cc82 from '@/components/elements/element-59dca300-db78-42e4-a7a6-0cbf22d3cc82/ww-config.js';
import wwobject1be951afde7143e6ad1ee9b36de15529 from '@/components/elements/element-1be951af-de71-43e6-ad1e-e9b36de15529/ww-config.js'; import wwobject1be951afde7143e6ad1ee9b36de15529 from '@/components/elements/element-1be951af-de71-43e6-ad1e-e9b36de15529/ww-config.js';
import wwobjectdeb10a015eef4aa190171b51c2ad6fd0 from '@/components/elements/element-deb10a01-5eef-4aa1-9017-1b51c2ad6fd0/ww-config.js'; import wwobjectdeb10a015eef4aa190171b51c2ad6fd0 from '@/components/elements/element-deb10a01-5eef-4aa1-9017-1b51c2ad6fd0/ww-config.js';
import wwobject985570fcb3c04566800482ab3b30a11d from '@/components/elements/element-985570fc-b3c0-4566-8004-82ab3b30a11d/ww-config.js';
import wwobjecta823467cbdc74ceca38c71875c4c214a from '@/components/elements/element-a823467c-bdc7-4cec-a38c-71875c4c214a/ww-config.js';
import wwobject9ae1fce82e314bfda4d20450235bdfd5 from '@/components/elements/element-9ae1fce8-2e31-4bfd-a4d2-0450235bdfd5/ww-config.js'; import wwobject9ae1fce82e314bfda4d20450235bdfd5 from '@/components/elements/element-9ae1fce8-2e31-4bfd-a4d2-0450235bdfd5/ww-config.js';
import wwobjectc6c0c00e49fd4cb9bd785bc09945721e from '@/components/elements/element-c6c0c00e-49fd-4cb9-bd78-5bc09945721e/ww-config.js'; import wwobjectc6c0c00e49fd4cb9bd785bc09945721e from '@/components/elements/element-c6c0c00e-49fd-4cb9-bd78-5bc09945721e/ww-config.js';
import wwobjecteb4b02a3cbe647e98db5322da8047160 from '@/components/elements/element-eb4b02a3-cbe6-47e9-8db5-322da8047160/ww-config.js'; import wwobjecteb4b02a3cbe647e98db5322da8047160 from '@/components/elements/element-eb4b02a3-cbe6-47e9-8db5-322da8047160/ww-config.js';
import wwobject9896d6950c964a0fb587c25e879ece77 from '@/components/elements/element-9896d695-0c96-4a0f-b587-c25e879ece77/ww-config.js';
import wwobjecta823467cbdc74ceca38c71875c4c214a from '@/components/elements/element-a823467c-bdc7-4cec-a38c-71875c4c214a/ww-config.js';
import wwobject985570fcb3c04566800482ab3b30a11d from '@/components/elements/element-985570fc-b3c0-4566-8004-82ab3b30a11d/ww-config.js';
import wwobject9ecb2cfccef74be8b7363e17a3b7e9ff from '@/components/elements/element-9ecb2cfc-cef7-4be8-b736-3e17a3b7e9ff/ww-config.js'; import wwobject9ecb2cfccef74be8b7363e17a3b7e9ff from '@/components/elements/element-9ecb2cfc-cef7-4be8-b736-3e17a3b7e9ff/ww-config.js';
/* wwFront:end */ /* wwFront:end */
@ -63,7 +63,6 @@ export const useComponentBasesStore = defineStore('componentBases', () => {
'wwobject-b783dc65-d528-4f74-8c14-e27c934c39b1': getInheritedConfiguration({ ...wwobjectb783dc65d5284f748c14e27c934c39b1, name: 'wwobject-b783dc65-d528-4f74-8c14-e27c934c39b1' }), 'wwobject-b783dc65-d528-4f74-8c14-e27c934c39b1': getInheritedConfiguration({ ...wwobjectb783dc65d5284f748c14e27c934c39b1, name: 'wwobject-b783dc65-d528-4f74-8c14-e27c934c39b1' }),
'wwobject-14723a21-0178-4d92-a7e9-d1dfeaec29a7': getInheritedConfiguration({ ...wwobject14723a2101784d92a7e9d1dfeaec29a7, name: 'wwobject-14723a21-0178-4d92-a7e9-d1dfeaec29a7' }), 'wwobject-14723a21-0178-4d92-a7e9-d1dfeaec29a7': getInheritedConfiguration({ ...wwobject14723a2101784d92a7e9d1dfeaec29a7, name: 'wwobject-14723a21-0178-4d92-a7e9-d1dfeaec29a7' }),
'wwobject-53401515-b694-4c79-a88d-abeecb1de562': getInheritedConfiguration({ ...wwobject53401515b6944c79a88dabeecb1de562, name: 'wwobject-53401515-b694-4c79-a88d-abeecb1de562' }), 'wwobject-53401515-b694-4c79-a88d-abeecb1de562': getInheritedConfiguration({ ...wwobject53401515b6944c79a88dabeecb1de562, name: 'wwobject-53401515-b694-4c79-a88d-abeecb1de562' }),
'wwobject-9896d695-0c96-4a0f-b587-c25e879ece77': getInheritedConfiguration({ ...wwobject9896d6950c964a0fb587c25e879ece77, name: 'wwobject-9896d695-0c96-4a0f-b587-c25e879ece77' }),
'wwobject-547a655e-37cd-49ff-9c4f-c6b917a0b680': getInheritedConfiguration({ ...wwobject547a655e37cd49ff9c4fc6b917a0b680, name: 'wwobject-547a655e-37cd-49ff-9c4f-c6b917a0b680' }), 'wwobject-547a655e-37cd-49ff-9c4f-c6b917a0b680': getInheritedConfiguration({ ...wwobject547a655e37cd49ff9c4fc6b917a0b680, name: 'wwobject-547a655e-37cd-49ff-9c4f-c6b917a0b680' }),
'wwobject-6d692ca2-6cdc-4805-aa0c-211102f335d0': getInheritedConfiguration({ ...wwobject6d692ca26cdc4805aa0c211102f335d0, name: 'wwobject-6d692ca2-6cdc-4805-aa0c-211102f335d0' }), 'wwobject-6d692ca2-6cdc-4805-aa0c-211102f335d0': getInheritedConfiguration({ ...wwobject6d692ca26cdc4805aa0c211102f335d0, name: 'wwobject-6d692ca2-6cdc-4805-aa0c-211102f335d0' }),
'wwobject-1b1e2173-9b78-42cc-a8ee-a6167caea340': getInheritedConfiguration({ ...wwobject1b1e21739b7842cca8eea6167caea340, name: 'wwobject-1b1e2173-9b78-42cc-a8ee-a6167caea340' }), 'wwobject-1b1e2173-9b78-42cc-a8ee-a6167caea340': getInheritedConfiguration({ ...wwobject1b1e21739b7842cca8eea6167caea340, name: 'wwobject-1b1e2173-9b78-42cc-a8ee-a6167caea340' }),
@ -72,11 +71,12 @@ export const useComponentBasesStore = defineStore('componentBases', () => {
'wwobject-59dca300-db78-42e4-a7a6-0cbf22d3cc82': getInheritedConfiguration({ ...wwobject59dca300db7842e4a7a60cbf22d3cc82, name: 'wwobject-59dca300-db78-42e4-a7a6-0cbf22d3cc82' }), 'wwobject-59dca300-db78-42e4-a7a6-0cbf22d3cc82': getInheritedConfiguration({ ...wwobject59dca300db7842e4a7a60cbf22d3cc82, name: 'wwobject-59dca300-db78-42e4-a7a6-0cbf22d3cc82' }),
'wwobject-1be951af-de71-43e6-ad1e-e9b36de15529': getInheritedConfiguration({ ...wwobject1be951afde7143e6ad1ee9b36de15529, name: 'wwobject-1be951af-de71-43e6-ad1e-e9b36de15529' }), 'wwobject-1be951af-de71-43e6-ad1e-e9b36de15529': getInheritedConfiguration({ ...wwobject1be951afde7143e6ad1ee9b36de15529, name: 'wwobject-1be951af-de71-43e6-ad1e-e9b36de15529' }),
'wwobject-deb10a01-5eef-4aa1-9017-1b51c2ad6fd0': getInheritedConfiguration({ ...wwobjectdeb10a015eef4aa190171b51c2ad6fd0, name: 'wwobject-deb10a01-5eef-4aa1-9017-1b51c2ad6fd0' }), 'wwobject-deb10a01-5eef-4aa1-9017-1b51c2ad6fd0': getInheritedConfiguration({ ...wwobjectdeb10a015eef4aa190171b51c2ad6fd0, name: 'wwobject-deb10a01-5eef-4aa1-9017-1b51c2ad6fd0' }),
'wwobject-985570fc-b3c0-4566-8004-82ab3b30a11d': getInheritedConfiguration({ ...wwobject985570fcb3c04566800482ab3b30a11d, name: 'wwobject-985570fc-b3c0-4566-8004-82ab3b30a11d' }),
'wwobject-a823467c-bdc7-4cec-a38c-71875c4c214a': getInheritedConfiguration({ ...wwobjecta823467cbdc74ceca38c71875c4c214a, name: 'wwobject-a823467c-bdc7-4cec-a38c-71875c4c214a' }),
'wwobject-9ae1fce8-2e31-4bfd-a4d2-0450235bdfd5': getInheritedConfiguration({ ...wwobject9ae1fce82e314bfda4d20450235bdfd5, name: 'wwobject-9ae1fce8-2e31-4bfd-a4d2-0450235bdfd5' }), 'wwobject-9ae1fce8-2e31-4bfd-a4d2-0450235bdfd5': getInheritedConfiguration({ ...wwobject9ae1fce82e314bfda4d20450235bdfd5, name: 'wwobject-9ae1fce8-2e31-4bfd-a4d2-0450235bdfd5' }),
'wwobject-c6c0c00e-49fd-4cb9-bd78-5bc09945721e': getInheritedConfiguration({ ...wwobjectc6c0c00e49fd4cb9bd785bc09945721e, name: 'wwobject-c6c0c00e-49fd-4cb9-bd78-5bc09945721e' }), 'wwobject-c6c0c00e-49fd-4cb9-bd78-5bc09945721e': getInheritedConfiguration({ ...wwobjectc6c0c00e49fd4cb9bd785bc09945721e, name: 'wwobject-c6c0c00e-49fd-4cb9-bd78-5bc09945721e' }),
'wwobject-eb4b02a3-cbe6-47e9-8db5-322da8047160': getInheritedConfiguration({ ...wwobjecteb4b02a3cbe647e98db5322da8047160, name: 'wwobject-eb4b02a3-cbe6-47e9-8db5-322da8047160' }), 'wwobject-eb4b02a3-cbe6-47e9-8db5-322da8047160': getInheritedConfiguration({ ...wwobjecteb4b02a3cbe647e98db5322da8047160, name: 'wwobject-eb4b02a3-cbe6-47e9-8db5-322da8047160' }),
'wwobject-9896d695-0c96-4a0f-b587-c25e879ece77': getInheritedConfiguration({ ...wwobject9896d6950c964a0fb587c25e879ece77, name: 'wwobject-9896d695-0c96-4a0f-b587-c25e879ece77' }),
'wwobject-a823467c-bdc7-4cec-a38c-71875c4c214a': getInheritedConfiguration({ ...wwobjecta823467cbdc74ceca38c71875c4c214a, name: 'wwobject-a823467c-bdc7-4cec-a38c-71875c4c214a' }),
'wwobject-985570fc-b3c0-4566-8004-82ab3b30a11d': getInheritedConfiguration({ ...wwobject985570fcb3c04566800482ab3b30a11d, name: 'wwobject-985570fc-b3c0-4566-8004-82ab3b30a11d' }),
'wwobject-9ecb2cfc-cef7-4be8-b736-3e17a3b7e9ff': getInheritedConfiguration({ ...wwobject9ecb2cfccef74be8b7363e17a3b7e9ff, name: 'wwobject-9ecb2cfc-cef7-4be8-b736-3e17a3b7e9ff' })}; 'wwobject-9ecb2cfc-cef7-4be8-b736-3e17a3b7e9ff': getInheritedConfiguration({ ...wwobject9ecb2cfccef74be8b7363e17a3b7e9ff, name: 'wwobject-9ecb2cfc-cef7-4be8-b736-3e17a3b7e9ff' })};
/* wwFront:end */ /* wwFront:end */

View File

@ -12,20 +12,20 @@
<link rel="icon" type="image/x-icon" href="favicon.ico?_wwcv={{cacheVersion}}" /> <link rel="icon" type="image/x-icon" href="favicon.ico?_wwcv={{cacheVersion}}" />
<link rel="manifest" href="manifest.json?_wwcv=205" /> <link rel="manifest" href="manifest.json?_wwcv=206" />
<meta name="theme-color" content="" /> <meta name="theme-color" content="" />
<link rel="apple-touch-icon" sizes="48x48" href="images/48-favicon.png?_wwcv=205"> <link rel="apple-touch-icon" sizes="48x48" href="images/48-favicon.png?_wwcv=206">
<link rel="apple-touch-icon" sizes="72x72" href="images/72-favicon.png?_wwcv=205"> <link rel="apple-touch-icon" sizes="72x72" href="images/72-favicon.png?_wwcv=206">
<link rel="apple-touch-icon" sizes="96x96" href="images/96-favicon.png?_wwcv=205"> <link rel="apple-touch-icon" sizes="96x96" href="images/96-favicon.png?_wwcv=206">
<link rel="apple-touch-icon" sizes="128x128" href="images/128-favicon.png?_wwcv=205"> <link rel="apple-touch-icon" sizes="128x128" href="images/128-favicon.png?_wwcv=206">
<link rel="apple-touch-icon" sizes="144x144" href="images/144-favicon.png?_wwcv=205"> <link rel="apple-touch-icon" sizes="144x144" href="images/144-favicon.png?_wwcv=206">
<link rel="apple-touch-icon" sizes="152x152" href="images/152-favicon.png?_wwcv=205"> <link rel="apple-touch-icon" sizes="152x152" href="images/152-favicon.png?_wwcv=206">
<link rel="apple-touch-icon" sizes="192x192" href="images/192-favicon.png?_wwcv=205"> <link rel="apple-touch-icon" sizes="192x192" href="images/192-favicon.png?_wwcv=206">
<link rel="apple-touch-icon" sizes="256x256" href="images/256-favicon.png?_wwcv=205"> <link rel="apple-touch-icon" sizes="256x256" href="images/256-favicon.png?_wwcv=206">
<link rel="apple-touch-icon" sizes="384x384" href="images/384-favicon.png?_wwcv=205"> <link rel="apple-touch-icon" sizes="384x384" href="images/384-favicon.png?_wwcv=206">
<link rel="apple-touch-icon" sizes="512x512" href="images/512-favicon.png?_wwcv=205"> <link rel="apple-touch-icon" sizes="512x512" href="images/512-favicon.png?_wwcv=206">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@100;200;300;400;500;600;700;800;900&display=swap" rel="preload" as="style" onload="this.onload=null;this.rel='stylesheet'"> <link href="https://fonts.googleapis.com/css2?family=Inter:wght@100;200;300;400;500;600;700;800;900&display=swap" rel="preload" as="style" onload="this.onload=null;this.rel='stylesheet'">
<link href="/fonts/Phosphor/font.css?_wwcv=205" rel="preload" as="style" onload="this.onload=null;this.rel='stylesheet'"> <link href="/fonts/Phosphor/font.css?_wwcv=206" rel="preload" as="style" onload="this.onload=null;this.rel='stylesheet'">
<link href="https://fonts.googleapis.com/css2?family=Raleway:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap" rel="preload" as="style" onload="this.onload=null;this.rel='stylesheet'"> <link href="https://fonts.googleapis.com/css2?family=Raleway:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap" rel="preload" as="style" onload="this.onload=null;this.rel='stylesheet'">
<style>:root{ --ww-default-font-family: 'Raleway', sans-serif }</style> <style>:root{ --ww-default-font-family: 'Raleway', sans-serif }</style>
<style> <style>

File diff suppressed because one or more lines are too long