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 => {
// eslint-disable-next-line no-console
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() {
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";
return null;
},
@ -310,7 +310,7 @@ export default {
else if (this.content.selectionMode === "range") {
if (!value.start && !value.end) return null;
return [value.start || null, value.end || null].filter(
(item) => item !== null && item !== ""
(value) => value !== null && value !== ""
);
} else if (this.content.selectionMode === "multi") return value;
},

View File

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

View File

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

View File

@ -1,7 +1,7 @@
import { omit } from 'lodash-es';
import { computed, watch, provide, ref } from 'vue';
export function useFormInputs({ updateInputValidity, removeInputValidity }) {
export function useFormInputs({ updateInputValidity, removeInputValidity, validationType }) {
const inputsMap = ref({});
const formInputs = computed(() => {
@ -12,7 +12,7 @@ export function useFormInputs({ updateInputValidity, removeInputValidity }) {
.filter(([key, value]) => key !== 'null' && value !== null)
.map(([key, value]) => [
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];
if (!input) return;
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) {
@ -66,7 +67,7 @@ export function useFormInputs({ updateInputValidity, removeInputValidity }) {
name: name,
value: input.value,
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 {
isValid,
invalidFields,
validityMap
validityMap,
};
}
function resetInputs(initialValues = {}) {
initialValues ||= {};
for (const [id, inputs] of Object.entries(inputsMap.value)) {
for (const [name, input] of Object.entries(inputs)) {
if (input && typeof input === 'object') {
updateInput(id, input => {
if (input[name]) {
// Priority order for values:
// 1. Value from passed initialValues object
// 2. Field's stored initialValue from useForm
// 3. Default empty value based on type
// Determine reset value and whether it's a forced value
let newValue;
let isForcedValue = false;
if (initialValues[name] !== undefined) {
// Use value from initialValues parameter
newValue = initialValues[name];
isForcedValue = true;
} else if (input[name].initialValue !== undefined) {
// Use the field's own initialValue that was set during registration
newValue = input[name].initialValue;
} else {
// Reset to empty value based on the input type
// Default empty value based on type
if (Array.isArray(input[name].value)) {
newValue = [];
} 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;
if (input[name].updateValue) {
input[name].updateValue(newValue);
}
// Reset validation state
input[name].isValid = null;
// In submit mode, never trigger validation on reset (even for forced values)
// 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;
// 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),
isValid: computed(() => {
const inputsValidity = Object.values(inputValidityMap.value);
if (inputsValidity.some(v => v === null)) {
if (inputsValidity.length === 0) {
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);
}),
});

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, {
[_fieldName.value]: {
value: value.value,
isValid: !required.value && !customValidation.value ? true : null,
isValid: initialIsValid,
pending: false,
forceValidateField,
updateValue,
cancelValidation, // Allow canceling pending validations during 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
const hasValue = requiredValidation ? requiredValidation(value) : !isValueEmpty(value);
let finalResult;
// If not required, field is valid unless there's custom validation
if (!required) {
return validationResult;
finalResult = validationResult;
}
// If required and has custom validation, both must be true
if (customValidation && validation) {
return hasValue && validationResult;
else if (customValidation && validation) {
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 hasValue;
return finalResult;
};
function updateInputValidity(isValid) {
@ -137,16 +150,22 @@ export function useForm(
);
let isFirst = true;
let hasSetInitialIsValid = false;
const computedValidation = computed(() => {
// We have to compute the validation here, otherwise the reactivity will not work
const isValid = computeValidation(value.value, required.value, customValidation.value, validation.value, requiredValidation);
const isValid = computeValidation(
value.value,
required.value,
customValidation.value,
validation.value,
requiredValidation
);
if (isFirst) {
isFirst = false;
return null;
}
return isValid;
});
watch(computedValidation, isValid => {
watch(computedValidation, (isValid, oldIsValid) => {
if (form.validationType.value === 'change') {
updateFormInput(id, input => {
if (!input[_fieldName.value]) {
@ -156,17 +175,39 @@ export function useForm(
input[_fieldName.value].pending = true;
});
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(
() => form.validationType.value,
validationType => {
(validationType, oldValidationType) => {
if (validationType === 'change') {
updateInputValidity(
computeValidation(value.value, required?.value, customValidation?.value, validation?.value, requiredValidation)
const computedResult = computeValidation(
value.value,
required?.value,
customValidation?.value,
validation?.value,
requiredValidation
);
updateInputValidity(computedResult);
} else if (validationType === 'submit') {
updateInputValidity(true);
updateInputValidity(null);
}
}
);
@ -175,8 +216,18 @@ export function useForm(
});
function forceValidateField() {
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);
// 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;
}

View File

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

View File

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

View File

@ -6,10 +6,7 @@
<div
v-for="env in environments"
:key="env"
:class="[
'ww-tab-item',
{ 'ww-tab-active': activeEnvironment === env }
]"
:class="['ww-tab-item', { 'ww-tab-active': activeEnvironment === env }]"
@click="activeEnvironment = env"
>
<span class="ww-tab-label">
@ -25,7 +22,6 @@
<!-- Environment Configuration -->
<div v-for="env in environments" :key="`config-${env}`" v-show="activeEnvironment === env">
<!-- Connection Mode Selector -->
<wwEditorFormRow label="Connection Mode" class="w-100 mb-3">
<wwEditorInputRadio
@ -34,17 +30,25 @@
{ label: 'Guided (recommended)', value: 'oauth', default: true },
{ label: 'Custom', value: 'custom' },
]"
@update:modelValue="(mode) => changeConnectionMode(env, mode)"
@update:modelValue="mode => changeConnectionMode(env, mode)"
/>
</wwEditorFormRow>
<!-- OAuth Connection -->
<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>
</div>
<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" />
{{ hasOAuthToken() ? 'Account connected' : 'Connect Supabase' }}
</button>
@ -86,11 +90,15 @@
placeholder="https://your-project.supabase.co"
:model-value="getProjectSelectValue(env)"
:options="projectsOptions"
@update:modelValue="(val) => changeProjectUrl(val, env)"
@update:modelValue="val => changeProjectUrl(val, env)"
class="-full"
/>
</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 />
</button>
</div>
@ -103,11 +111,15 @@
placeholder="Default (main)"
:model-value="selectedBranches?.[env] || ''"
:options="branchOptions(env)"
@update:modelValue="(val) => changeBranch(val, env)"
@update:modelValue="val => changeBranch(val, env)"
class="-full"
/>
</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 />
</button>
<div v-if="branchErrors?.[env]" class="body-xs content-tertiary ml-2 mt-1">
@ -130,7 +142,7 @@
placeholder="https://your-project.supabase.co"
:required="env === 'production'"
:model-value="getCurrentEnvConfig(env).projectUrl"
@update:modelValue="(val) => changeProjectUrl(val, env)"
@update:modelValue="val => changeProjectUrl(val, env)"
/>
<wwEditorInputRow
@ -139,7 +151,7 @@
type="query"
placeholder="Enter your public API key"
:model-value="getCurrentEnvConfig(env).apiKey"
@update:modelValue="(val) => changeApiKey(val, env)"
@update:modelValue="val => changeApiKey(val, env)"
/>
<wwEditorFormRow label="Service role key">
@ -151,7 +163,7 @@
class="w-full"
:style="{ '-webkit-text-security': 'disc' }"
:model-value="getCurrentEnvPrivateConfig(env).apiKey"
@update:modelValue="(val) => changePrivateApiKey(val, env)"
@update:modelValue="val => changePrivateApiKey(val, env)"
/>
<wwEditorQuestionMark
tooltip-position="top-left"
@ -168,7 +180,9 @@
<template v-else-if="selectModes[env] === 'create'">
<div v-if="isComingUp" class="body-md flex items-center p-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>
<template v-else>
<wwEditorInputRow
@ -220,11 +234,7 @@
</button>
</div>
</wwEditorFormRow>
<button
class="ww-editor-button -primary"
@click="createProject(env)"
type="button"
>
<button class="ww-editor-button -primary" @click="createProject(env)" type="button">
Create project for {{ capitalize(env) }}
</button>
</template>
@ -235,7 +245,10 @@
<!-- Custom Connection Mode -->
<template v-else>
<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 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>
@ -247,7 +260,7 @@
placeholder="https://your-project.supabase.co"
:required="env === 'production'"
:model-value="getCurrentEnvConfig(env).projectUrl"
@update:modelValue="(val) => changeProjectUrl(val, env)"
@update:modelValue="val => changeProjectUrl(val, env)"
/>
<wwEditorInputRow
@ -255,7 +268,7 @@
type="query"
placeholder="https://your-custom-domain.com"
:model-value="getCurrentEnvConfig(env).customDomain"
@update:modelValue="(val) => changeCustomDomain(val, env)"
@update:modelValue="val => changeCustomDomain(val, env)"
/>
<wwEditorInputRow
@ -264,7 +277,7 @@
type="query"
placeholder="Enter your public API key"
:model-value="getCurrentEnvConfig(env).apiKey"
@update:modelValue="(val) => changeApiKey(val, env)"
@update:modelValue="val => changeApiKey(val, env)"
/>
<wwEditorFormRow label="Service role key">
@ -276,7 +289,7 @@
class="w-full"
:style="{ '-webkit-text-security': 'disc' }"
:model-value="getCurrentEnvPrivateConfig(env).apiKey"
@update:modelValue="(val) => changePrivateApiKey(val, env)"
@update:modelValue="val => changePrivateApiKey(val, env)"
/>
<wwEditorQuestionMark
tooltip-position="top-left"
@ -321,12 +334,12 @@ export default {
selectModes: {
production: 'select',
staging: 'select',
editor: 'select'
editor: 'select',
},
showSettings: {
production: false,
staging: false,
editor: false
editor: false,
},
projects: [],
isLoading: false,
@ -351,11 +364,12 @@ export default {
region: 'us-east-1',
organizationId: '',
dbPass: '',
}
},
},
branches: {},
selectedBranches: {},
branchErrors: {},
branchChangeAbortController: null, // AbortController for cancelling in-flight requests
};
},
watch: {
@ -367,7 +381,9 @@ export default {
await this.fetchOrganizations();
// Initialize new project data for this environment
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',
organizationId: this.organizations[0]?.id || '',
dbPass: wwLib.wwUtils.getUid(),
@ -375,35 +391,41 @@ export default {
}
}
},
deep: true
}
deep: true,
},
},
computed: {
isValid() {
// Prevent saving while branch change is in progress
return !this.branchChangeAbortController;
},
projectRef() {
const config = this.getCurrentEnvConfig();
return config?.projectUrl?.replace('https://', '').replace('.supabase.co', '');
},
projectsOptions() {
return (
this.projects
return this.projects
.map(project => ({
label: `${project.name} (${project.id}) ${project.status === 'INACTIVE' ? '#PAUSED' : ''}`,
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() {
return (env) => {
return env => {
const items = this.branches?.[env] || [];
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() {
// 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.privateData?.accessToken;
@ -444,10 +466,9 @@ export default {
this.environments.forEach(env => {
const config = envConfigs[env];
if (!config) return;
if (
Object.prototype.hasOwnProperty.call(config, 'accessToken') ||
Object.prototype.hasOwnProperty.call(config, 'refreshToken')
) {
const hasLegacyAccess = Object.hasOwn(config, 'accessToken');
const hasLegacyRefresh = Object.hasOwn(config, 'refreshToken');
if (hasLegacyAccess || hasLegacyRefresh) {
const { accessToken: _legacyAccess, refreshToken: _legacyRefresh, ...rest } = config;
sanitizedEnvs[env] = rest;
hasChanges = true;
@ -495,7 +516,8 @@ export default {
getProjectSelectValue(env) {
const config = this.getCurrentEnvConfig(env);
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;
},
@ -514,7 +536,7 @@ export default {
return {
projectUrl: this.settings.publicData.projectUrl,
apiKey: this.settings.publicData.apiKey,
customDomain: this.settings.publicData.customDomain
customDomain: this.settings.publicData.customDomain,
};
}
return {};
@ -522,8 +544,11 @@ export default {
getCurrentEnvPrivateConfig(env = this.activeEnvironment) {
if (this.settings.privateData?.environments?.[env]) {
const { accessToken: _legacyAccess, refreshToken: _legacyRefresh, ...rest } =
this.settings.privateData.environments[env] || {};
const {
accessToken: _legacyAccess,
refreshToken: _legacyRefresh,
...rest
} = this.settings.privateData.environments[env] || {};
return rest;
}
// Fallback to legacy format for production
@ -532,7 +557,7 @@ export default {
connectionMode: this.settings.privateData.connectionMode || 'oauth',
apiKey: this.settings.privateData.apiKey,
databasePassword: this.settings.privateData.databasePassword,
connectionString: this.settings.privateData.connectionString
connectionString: this.settings.privateData.connectionString,
};
}
return {};
@ -549,9 +574,9 @@ export default {
production: {
projectUrl: this.settings.publicData?.projectUrl || '',
apiKey: this.settings.publicData?.apiKey || '',
customDomain: this.settings.publicData?.customDomain || ''
}
}
customDomain: this.settings.publicData?.customDomain || '',
},
},
},
privateData: {
...this.settings.privateData,
@ -560,10 +585,10 @@ export default {
connectionMode: connectionMode,
apiKey: this.settings.privateData?.apiKey || '',
databasePassword: this.settings.privateData?.databasePassword || '',
connectionString: this.settings.privateData?.connectionString || ''
}
}
}
connectionString: this.settings.privateData?.connectionString || '',
},
},
},
};
this.$emit('update:settings', newSettings);
},
@ -571,8 +596,8 @@ export default {
changeConnectionMode(env, mode) {
this.updateEnvironmentConfig(env, {
privateData: {
connectionMode: mode
}
connectionMode: mode,
},
});
},
@ -599,9 +624,9 @@ export default {
accessToken: '',
refreshToken: '',
environments: {
...this.settings.privateData?.environments
}
}
...this.settings.privateData?.environments,
},
},
};
// Clear tokens from all environments
@ -620,7 +645,7 @@ export default {
if (!projectUrl) {
this.updateEnvironmentConfig(env, {
publicData: { projectUrl: '', apiKey: '' },
privateData: { apiKey: '', connectionString: '' }
privateData: { apiKey: '', connectionString: '' },
});
return;
}
@ -639,11 +664,6 @@ export default {
let baseProjectRef = projectUrl.replace('https://', '').replace('.supabase.co', '');
// 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 || {}) };
branchCopy[env] = [];
this.branches = branchCopy;
@ -653,7 +673,6 @@ export default {
const selectedCopy = { ...(this.selectedBranches || {}) };
selectedCopy[env] = '';
this.selectedBranches = selectedCopy;
}
if (this.hasOAuthToken() && this.getConnectionMode(env) === 'oauth') {
const projectData = await this.fetchProject(
@ -662,7 +681,8 @@ export default {
if (projectData) {
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;
baseProjectRef =
projectData.project?.parent_project_ref ||
@ -674,7 +694,7 @@ export default {
this.updateEnvironmentConfig(env, {
publicData: { projectUrl, apiKey, baseProjectRef, branch: null, branchSlug: null },
privateData: { apiKey: privateApiKey, connectionString }
privateData: { apiKey: privateApiKey, connectionString },
});
// Load branches
@ -688,7 +708,10 @@ export default {
async loadBranches(env, overrideRef = '') {
try {
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 paramsBaseRef = cfg.baseProjectRef || overrideRef || '';
if (!ref || !this.hasOAuthToken()) return;
@ -698,44 +721,43 @@ export default {
params: { baseProjectRef: paramsBaseRef },
});
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 };
const errors = { ...(this.branchErrors || {}) };
delete errors[env];
this.branchErrors = errors;
}
const defaultBranch = branches.find(b => b.is_default);
const defaultValue = defaultBranch?.project_ref || defaultBranch?.ref || defaultBranch?.id || '';
const currentSelection = this.selectedBranches?.[env];
const savedSelection = this.getCurrentEnvConfig(env)?.branch || '';
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 shouldFetchBranchData = false;
if (selectionExistsInList) {
nextSelection = targetSelection;
} else if (defaultValue) {
nextSelection = defaultValue;
// Auto-selected default branch needs data fetched
shouldFetchBranchData = !savedSelection;
}
if (nextSelection || currentSelection || savedSelection) {
if (this.$set) this.$set(this.selectedBranches, env, nextSelection);
else this.selectedBranches = { ...(this.selectedBranches || {}), [env]: nextSelection };
}
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) {
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.branchErrors = { ...(this.branchErrors || {}), [env]: 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
async changeBranch(branchValue, env) {
if (this.$set) this.$set(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', '');
// 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 {
this.selectedBranches = { ...(this.selectedBranches || {}), [env]: branchValue || '' };
const baseRef =
this.getCurrentEnvConfig(env).baseProjectRef ||
this.getCurrentEnvConfig(env).projectUrl?.replace('https://', '').replace('.supabase.co', '');
if (!baseRef) return;
let targetRef = baseRef;
let branchSlug = '';
if (branchValue) {
const list = this.branches?.[env] || [];
const b = list.find(it => (it.project_ref || it.ref || it.id || it.name) === branchValue);
targetRef = b?.project_ref || b?.ref || branchValue;
branchSlug = b?.name || '';
const branchList = this.branches?.[env] || [];
const matchedBranch = branchList.find(
it => (it.project_ref || it.ref || it.id || it.name) === branchValue
);
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, {
branchSlug: effectiveBranchSlug,
branchRef: branchValue ? targetRef : '',
signal, // Pass AbortController signal
});
const apiKey = projectData?.apiKeys?.find(key => key.name === 'anon')?.api_key;
const privateApiKey = projectData?.apiKeys?.find(key => key.name === 'service_role')?.api_key;
const connectionString = projectData?.pgbouncer?.connection_string;
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 projectUrl = `https://${runtimeRef}.supabase.co`;
@ -783,27 +831,61 @@ export default {
privateData: {
apiKey: displayServiceKey,
connectionString: connectionString || this.getCurrentEnvPrivateConfig(env).connectionString,
}
},
});
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) {
this.updateEnvironmentConfig(env, {
publicData: { apiKey }
publicData: { apiKey },
});
},
changeApiKey(apiKey, env) {
this.updateEnvironmentConfig(env, {
publicData: { apiKey },
});
},
changeCustomDomain(customDomain, env) {
this.updateEnvironmentConfig(env, {
publicData: { customDomain }
publicData: { customDomain },
});
},
changePrivateApiKey(apiKey, env) {
this.updateEnvironmentConfig(env, {
privateData: { apiKey }
privateData: { apiKey },
});
},
@ -814,15 +896,15 @@ export default {
const previousGlobalRefresh = this.settings.privateData?.refreshToken;
const privateUpdates = { ...(updates.privateData || {}) };
const updateAccessToken = Object.prototype.hasOwnProperty.call(privateUpdates, 'accessToken')
const updateAccessToken = Object.hasOwn(privateUpdates, 'accessToken')
? privateUpdates.accessToken
: undefined;
const updateRefreshToken = Object.prototype.hasOwnProperty.call(privateUpdates, 'refreshToken')
const updateRefreshToken = Object.hasOwn(privateUpdates, 'refreshToken')
? privateUpdates.refreshToken
: undefined;
if (Object.prototype.hasOwnProperty.call(privateUpdates, 'accessToken')) delete privateUpdates.accessToken;
if (Object.prototype.hasOwnProperty.call(privateUpdates, 'refreshToken')) delete privateUpdates.refreshToken;
if (Object.hasOwn(privateUpdates, 'accessToken')) delete privateUpdates.accessToken;
if (Object.hasOwn(privateUpdates, 'refreshToken')) delete privateUpdates.refreshToken;
const sanitizedCurrentPrivate = { ...currentPrivateConfig };
delete sanitizedCurrentPrivate.accessToken;
@ -836,9 +918,9 @@ export default {
...this.settings.publicData?.environments,
[env]: {
...currentPublicConfig,
...(updates.publicData || {})
}
}
...(updates.publicData || {}),
},
},
},
privateData: {
...this.settings.privateData,
@ -847,9 +929,9 @@ export default {
[env]: {
...sanitizedCurrentPrivate,
...privateUpdates,
}
}
}
},
},
},
};
if (env === 'production') {
@ -888,7 +970,11 @@ export default {
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;
}
@ -901,9 +987,9 @@ export default {
[env]: {
projectUrl: '',
apiKey: '',
customDomain: ''
}
}
customDomain: '',
},
},
},
privateData: {
...this.settings.privateData,
@ -913,10 +999,10 @@ export default {
connectionMode: 'custom',
apiKey: '',
databasePassword: '',
connectionString: ''
}
}
}
connectionString: '',
},
},
},
};
this.selectModes[env] = 'select';
@ -956,7 +1042,7 @@ export default {
}
},
async fetchProject(projectId, { branchSlug = '', branchRef = '' } = {}) {
async fetchProject(projectId, { branchSlug = '', branchRef = '', signal = null } = {}) {
if (!projectId) {
return null;
}
@ -972,10 +1058,14 @@ export default {
...(branchRef ? { branchRef } : {}),
}
: undefined,
signal, // Pass AbortController signal to requestAPI
});
return data?.data;
} catch (error) {
// Don't log abort errors
if (error.name !== 'AbortError' && error.code !== 'ERR_CANCELED') {
console.warn(`Failed to fetch project ${projectId}:`, error);
}
return null;
}
},
@ -1014,7 +1104,7 @@ export default {
apiKey: '',
connectionString: '',
databasePassword: '',
}
},
});
const { data } = await wwLib.wwPlugins.supabase.requestAPI({
@ -1041,7 +1131,9 @@ export default {
const projectData = await this.fetchProject(projectId);
if (projectData) {
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 databasePassword = newProject.dbPass;
@ -1051,7 +1143,7 @@ export default {
apiKey: privateApiKey,
connectionString: connectionString,
databasePassword,
}
},
});
this.isComingUp = false;

View File

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

View File

@ -6,10 +6,7 @@
<div
v-for="env in environments"
:key="env"
:class="[
'ww-tab-item',
{ 'ww-tab-active': activeEnvironment === env }
]"
:class="['ww-tab-item', { 'ww-tab-active': activeEnvironment === env }]"
@click="activeEnvironment = env"
>
<span class="ww-tab-label">
@ -25,7 +22,6 @@
<!-- Environment Configuration -->
<div v-for="env in environments" :key="`config-${env}`" v-show="activeEnvironment === env">
<!-- Connection Mode Selector -->
<wwEditorFormRow label="Connection Mode" class="w-100 mb-3">
<wwEditorInputRadio
@ -34,17 +30,25 @@
{ label: 'Guided (recommended)', value: 'oauth', default: true },
{ label: 'Custom', value: 'custom' },
]"
@update:modelValue="(mode) => changeConnectionMode(env, mode)"
@update:modelValue="mode => changeConnectionMode(env, mode)"
/>
</wwEditorFormRow>
<!-- OAuth Connection -->
<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>
</div>
<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" />
{{ hasOAuthToken() ? 'Account connected' : 'Connect Supabase' }}
</button>
@ -86,11 +90,15 @@
placeholder="https://your-project.supabase.co"
:model-value="getProjectSelectValue(env)"
:options="projectsOptions"
@update:modelValue="(val) => changeProjectUrl(val, env)"
@update:modelValue="val => changeProjectUrl(val, env)"
class="-full"
/>
</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 />
</button>
</div>
@ -103,11 +111,15 @@
placeholder="Default (main)"
:model-value="selectedBranches?.[env] || ''"
:options="branchOptions(env)"
@update:modelValue="(val) => changeBranch(val, env)"
@update:modelValue="val => changeBranch(val, env)"
class="-full"
/>
</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 />
</button>
<div v-if="branchErrors?.[env]" class="body-xs content-tertiary ml-2 mt-1">
@ -130,7 +142,7 @@
placeholder="https://your-project.supabase.co"
:required="env === 'production'"
:model-value="getCurrentEnvConfig(env).projectUrl"
@update:modelValue="(val) => changeProjectUrl(val, env)"
@update:modelValue="val => changeProjectUrl(val, env)"
/>
<wwEditorInputRow
@ -139,7 +151,7 @@
type="query"
placeholder="Enter your public API key"
:model-value="getCurrentEnvConfig(env).apiKey"
@update:modelValue="(val) => changeApiKey(val, env)"
@update:modelValue="val => changeApiKey(val, env)"
/>
<wwEditorFormRow label="Service role key">
@ -151,7 +163,7 @@
class="w-full"
:style="{ '-webkit-text-security': 'disc' }"
:model-value="getCurrentEnvPrivateConfig(env).apiKey"
@update:modelValue="(val) => changePrivateApiKey(val, env)"
@update:modelValue="val => changePrivateApiKey(val, env)"
/>
<wwEditorQuestionMark
tooltip-position="top-left"
@ -168,7 +180,9 @@
<template v-else-if="selectModes[env] === 'create'">
<div v-if="isComingUp" class="body-md flex items-center p-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>
<template v-else>
<wwEditorInputRow
@ -220,11 +234,7 @@
</button>
</div>
</wwEditorFormRow>
<button
class="ww-editor-button -primary"
@click="createProject(env)"
type="button"
>
<button class="ww-editor-button -primary" @click="createProject(env)" type="button">
Create project for {{ capitalize(env) }}
</button>
</template>
@ -235,7 +245,10 @@
<!-- Custom Connection Mode -->
<template v-else>
<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 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>
@ -247,7 +260,7 @@
placeholder="https://your-project.supabase.co"
:required="env === 'production'"
:model-value="getCurrentEnvConfig(env).projectUrl"
@update:modelValue="(val) => changeProjectUrl(val, env)"
@update:modelValue="val => changeProjectUrl(val, env)"
/>
<wwEditorInputRow
@ -255,7 +268,7 @@
type="query"
placeholder="https://your-custom-domain.com"
:model-value="getCurrentEnvConfig(env).customDomain"
@update:modelValue="(val) => changeCustomDomain(val, env)"
@update:modelValue="val => changeCustomDomain(val, env)"
/>
<wwEditorInputRow
@ -264,7 +277,7 @@
type="query"
placeholder="Enter your public API key"
:model-value="getCurrentEnvConfig(env).apiKey"
@update:modelValue="(val) => changeApiKey(val, env)"
@update:modelValue="val => changeApiKey(val, env)"
/>
<wwEditorFormRow label="Service role key">
@ -276,7 +289,7 @@
class="w-full"
:style="{ '-webkit-text-security': 'disc' }"
:model-value="getCurrentEnvPrivateConfig(env).apiKey"
@update:modelValue="(val) => changePrivateApiKey(val, env)"
@update:modelValue="val => changePrivateApiKey(val, env)"
/>
<wwEditorQuestionMark
tooltip-position="top-left"
@ -321,12 +334,12 @@ export default {
selectModes: {
production: 'select',
staging: 'select',
editor: 'select'
editor: 'select',
},
showSettings: {
production: false,
staging: false,
editor: false
editor: false,
},
projects: [],
isLoading: false,
@ -351,11 +364,12 @@ export default {
region: 'us-east-1',
organizationId: '',
dbPass: '',
}
},
},
branches: {},
selectedBranches: {},
branchErrors: {},
branchChangeAbortController: null, // AbortController for cancelling in-flight requests
};
},
watch: {
@ -367,7 +381,9 @@ export default {
await this.fetchOrganizations();
// Initialize new project data for this environment
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',
organizationId: this.organizations[0]?.id || '',
dbPass: wwLib.wwUtils.getUid(),
@ -375,35 +391,41 @@ export default {
}
}
},
deep: true
}
deep: true,
},
},
computed: {
isValid() {
// Prevent saving while branch change is in progress
return !this.branchChangeAbortController;
},
projectRef() {
const config = this.getCurrentEnvConfig();
return config?.projectUrl?.replace('https://', '').replace('.supabase.co', '');
},
projectsOptions() {
return (
this.projects
return this.projects
.map(project => ({
label: `${project.name} (${project.id}) ${project.status === 'INACTIVE' ? '#PAUSED' : ''}`,
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() {
return (env) => {
return env => {
const items = this.branches?.[env] || [];
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() {
// 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.privateData?.accessToken;
@ -445,10 +467,9 @@ export default {
this.environments.forEach(env => {
const config = envConfigs[env];
if (!config) return;
if (
Object.prototype.hasOwnProperty.call(config, 'accessToken') ||
Object.prototype.hasOwnProperty.call(config, 'refreshToken')
) {
const hasLegacyAccess = Object.hasOwn(config, 'accessToken');
const hasLegacyRefresh = Object.hasOwn(config, 'refreshToken');
if (hasLegacyAccess || hasLegacyRefresh) {
const { accessToken: _legacyAccess, refreshToken: _legacyRefresh, ...rest } = config;
sanitizedEnvs[env] = rest;
hasChanges = true;
@ -506,7 +527,8 @@ export default {
getProjectSelectValue(env) {
const config = this.getCurrentEnvConfig(env);
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;
},
@ -525,7 +547,7 @@ export default {
return {
projectUrl: this.settings.publicData.projectUrl,
apiKey: this.settings.publicData.apiKey,
customDomain: this.settings.publicData.customDomain
customDomain: this.settings.publicData.customDomain,
};
}
return {};
@ -533,8 +555,11 @@ export default {
getCurrentEnvPrivateConfig(env = this.activeEnvironment) {
if (this.settings.privateData?.environments?.[env]) {
const { accessToken: _legacyAccess, refreshToken: _legacyRefresh, ...rest } =
this.settings.privateData.environments[env] || {};
const {
accessToken: _legacyAccess,
refreshToken: _legacyRefresh,
...rest
} = this.settings.privateData.environments[env] || {};
return rest;
}
// Fallback to legacy format for production
@ -543,7 +568,7 @@ export default {
connectionMode: this.settings.privateData.connectionMode || 'oauth',
apiKey: this.settings.privateData.apiKey,
databasePassword: this.settings.privateData.databasePassword,
connectionString: this.settings.privateData.connectionString
connectionString: this.settings.privateData.connectionString,
};
}
return {};
@ -561,9 +586,9 @@ export default {
production: {
projectUrl: this.settings.publicData?.projectUrl || '',
apiKey: this.settings.publicData?.apiKey || '',
customDomain: this.settings.publicData?.customDomain || ''
}
}
customDomain: this.settings.publicData?.customDomain || '',
},
},
},
privateData: {
...this.settings.privateData,
@ -572,10 +597,10 @@ export default {
connectionMode: connectionMode,
apiKey: this.settings.privateData?.apiKey || '',
databasePassword: this.settings.privateData?.databasePassword || '',
connectionString: this.settings.privateData?.connectionString || ''
}
}
}
connectionString: this.settings.privateData?.connectionString || '',
},
},
},
};
this.$emit('update:settings', newSettings);
@ -584,8 +609,8 @@ export default {
changeConnectionMode(env, mode) {
this.updateEnvironmentConfig(env, {
privateData: {
connectionMode: mode
}
connectionMode: mode,
},
});
},
@ -612,9 +637,9 @@ export default {
accessToken: '',
refreshToken: '',
environments: {
...this.settings.privateData?.environments
}
}
...this.settings.privateData?.environments,
},
},
};
// Clear tokens from all environments
@ -633,7 +658,7 @@ export default {
if (!projectUrl) {
this.updateEnvironmentConfig(env, {
publicData: { projectUrl: '', apiKey: '' },
privateData: { apiKey: '', connectionString: '' }
privateData: { apiKey: '', connectionString: '' },
});
return;
}
@ -671,7 +696,8 @@ export default {
if (projectData) {
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;
baseProjectRef =
projectData.project?.parent_project_ref ||
@ -683,7 +709,7 @@ export default {
this.updateEnvironmentConfig(env, {
publicData: { projectUrl, apiKey, baseProjectRef, branch: null, branchSlug: null },
privateData: { apiKey: privateApiKey, connectionString }
privateData: { apiKey: privateApiKey, connectionString },
});
// Load branches
@ -697,7 +723,10 @@ export default {
async loadBranches(env, overrideRef = '') {
try {
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 paramsBaseRef = cfg.baseProjectRef || overrideRef || '';
if (!ref || !this.hasOAuthToken()) return;
@ -707,44 +736,43 @@ export default {
params: { baseProjectRef: paramsBaseRef },
});
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 };
const errors = { ...(this.branchErrors || {}) };
delete errors[env];
this.branchErrors = errors;
}
const defaultBranch = branches.find(b => b.is_default);
const defaultValue = defaultBranch?.project_ref || defaultBranch?.ref || defaultBranch?.id || '';
const currentSelection = this.selectedBranches?.[env];
const savedSelection = this.getCurrentEnvConfig(env)?.branch || '';
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 shouldFetchBranchData = false;
if (selectionExistsInList) {
nextSelection = targetSelection;
} else if (defaultValue) {
nextSelection = defaultValue;
// Auto-selected default branch needs data fetched
shouldFetchBranchData = !savedSelection;
}
if (nextSelection || currentSelection || savedSelection) {
if (this.$set) this.$set(this.selectedBranches, env, nextSelection);
else this.selectedBranches = { ...(this.selectedBranches || {}), [env]: nextSelection };
}
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) {
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.branchErrors = { ...(this.branchErrors || {}), [env]: 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
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 || '');
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;
let targetRef = baseRef;
@ -767,16 +811,23 @@ export default {
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, {
branchSlug: effectiveBranchSlug,
branchRef: branchValue ? targetRef : '',
signal, // Pass AbortController signal
});
const apiKey = projectData?.apiKeys?.find(key => key.name === 'anon')?.api_key;
const privateApiKey = projectData?.apiKeys?.find(key => key.name === 'service_role')?.api_key;
const connectionString = projectData?.pgbouncer?.connection_string;
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 projectUrl = `https://${runtimeRef}.supabase.co`;
@ -794,27 +845,55 @@ export default {
privateData: {
apiKey: displayServiceKey,
connectionString: connectionString || this.getCurrentEnvPrivateConfig(env).connectionString,
}
},
});
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) {
this.updateEnvironmentConfig(env, {
publicData: { apiKey }
publicData: { apiKey },
});
},
changeCustomDomain(customDomain, env) {
this.updateEnvironmentConfig(env, {
publicData: { customDomain }
publicData: { customDomain },
});
},
changePrivateApiKey(apiKey, env) {
this.updateEnvironmentConfig(env, {
privateData: { apiKey }
privateData: { apiKey },
});
},
@ -825,15 +904,15 @@ export default {
const previousGlobalRefresh = this.settings.privateData?.refreshToken;
const privateUpdates = { ...(updates.privateData || {}) };
const updateAccessToken = Object.prototype.hasOwnProperty.call(privateUpdates, 'accessToken')
const updateAccessToken = Object.hasOwn(privateUpdates, 'accessToken')
? privateUpdates.accessToken
: undefined;
const updateRefreshToken = Object.prototype.hasOwnProperty.call(privateUpdates, 'refreshToken')
const updateRefreshToken = Object.hasOwn(privateUpdates, 'refreshToken')
? privateUpdates.refreshToken
: undefined;
if (Object.prototype.hasOwnProperty.call(privateUpdates, 'accessToken')) delete privateUpdates.accessToken;
if (Object.prototype.hasOwnProperty.call(privateUpdates, 'refreshToken')) delete privateUpdates.refreshToken;
if (Object.hasOwn(privateUpdates, 'accessToken')) delete privateUpdates.accessToken;
if (Object.hasOwn(privateUpdates, 'refreshToken')) delete privateUpdates.refreshToken;
const sanitizedCurrentPrivate = { ...currentPrivateConfig };
delete sanitizedCurrentPrivate.accessToken;
@ -847,9 +926,9 @@ export default {
...this.settings.publicData?.environments,
[env]: {
...currentPublicConfig,
...(updates.publicData || {})
}
}
...(updates.publicData || {}),
},
},
},
privateData: {
...this.settings.privateData,
@ -858,9 +937,9 @@ export default {
[env]: {
...sanitizedCurrentPrivate,
...privateUpdates,
}
}
}
},
},
},
};
// Keep legacy fields in sync for production (backward compatibility)
@ -900,7 +979,11 @@ export default {
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;
}
@ -913,9 +996,9 @@ export default {
[env]: {
projectUrl: '',
apiKey: '',
customDomain: ''
}
}
customDomain: '',
},
},
},
privateData: {
...this.settings.privateData,
@ -925,10 +1008,10 @@ export default {
connectionMode: 'custom',
apiKey: '',
databasePassword: '',
connectionString: ''
}
}
}
connectionString: '',
},
},
},
};
this.selectModes[env] = 'select';
@ -968,7 +1051,7 @@ export default {
}
},
async fetchProject(projectId, { branchSlug = '', branchRef = '' } = {}) {
async fetchProject(projectId, { branchSlug = '', branchRef = '', signal = null } = {}) {
if (!projectId) {
return null;
}
@ -984,13 +1067,17 @@ export default {
...(branchRef ? { branchRef } : {}),
}
: undefined,
signal, // Pass AbortController signal to requestAPI
});
const project = data?.data?.project || {};
const apiKeys = data?.data?.apiKeys || [];
const pgbouncer = data?.data?.pgbouncer;
return data?.data;
} catch (error) {
// Don't log abort errors
if (error.name !== 'AbortError' && error.code !== 'ERR_CANCELED') {
console.warn(`Failed to fetch project ${projectId}:`, error);
}
return null;
}
},
@ -1029,7 +1116,7 @@ export default {
apiKey: '',
connectionString: '',
databasePassword: '',
}
},
});
const { data } = await wwLib.wwPlugins.supabase.requestAPI({
@ -1056,7 +1143,9 @@ export default {
const projectData = await this.fetchProject(projectId);
if (projectData) {
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 databasePassword = newProject.dbPass;
@ -1066,7 +1155,7 @@ export default {
apiKey: privateApiKey,
connectionString: connectionString,
databasePassword,
}
},
});
this.isComingUp = false;

View File

@ -83,17 +83,30 @@ export default {
this.subscribeTables(settings.publicData.realtimeTables || {});
}
},
async requestAPI({ method, path, data, params }, retry = true) {
async requestAPI({ method, path, data, params, signal }, retry = true) {
try {
// Get current environment and send it explicitly
const config = getCurrentSupabaseSettings('supabase');
const currentEnv = config.environment; // 'editor', 'staging', or 'production'
return await wwAxios({
method,
url: `${wwLib.wwApiRequests._getPluginsUrl()}/designs/${
wwLib.$store.getters['websiteData/getDesignInfo'].id
}/supabase${path}`,
data,
params,
params: {
...params,
wwEnv: currentEnv, // Send environment explicitly
},
signal, // Support AbortController signal
});
} 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');
if (retry && [401, 403].includes(error.response?.status) && isOauthToken) {
const { data } = await wwAxios.post(
@ -101,7 +114,7 @@ export default {
wwLib.$store.getters['websiteData/getDesignInfo'].id
}/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' });
throw error;

View File

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

View File

@ -12,20 +12,20 @@
<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="" />
<link rel="apple-touch-icon" sizes="48x48" href="images/48-favicon.png?_wwcv=205">
<link rel="apple-touch-icon" sizes="72x72" href="images/72-favicon.png?_wwcv=205">
<link rel="apple-touch-icon" sizes="96x96" href="images/96-favicon.png?_wwcv=205">
<link rel="apple-touch-icon" sizes="128x128" href="images/128-favicon.png?_wwcv=205">
<link rel="apple-touch-icon" sizes="144x144" href="images/144-favicon.png?_wwcv=205">
<link rel="apple-touch-icon" sizes="152x152" href="images/152-favicon.png?_wwcv=205">
<link rel="apple-touch-icon" sizes="192x192" href="images/192-favicon.png?_wwcv=205">
<link rel="apple-touch-icon" sizes="256x256" href="images/256-favicon.png?_wwcv=205">
<link rel="apple-touch-icon" sizes="384x384" href="images/384-favicon.png?_wwcv=205">
<link rel="apple-touch-icon" sizes="512x512" href="images/512-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=206">
<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=206">
<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=206">
<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=206">
<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=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="/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'">
<style>:root{ --ww-default-font-family: 'Raleway', sans-serif }</style>
<style>

File diff suppressed because one or more lines are too long