mirror of
https://github.com/sabjorn7/meetguru.git
synced 2025-12-16 22:57:33 +03:00
v206 - bagi
This commit is contained in:
parent
01f1703975
commit
9b6a03977f
@ -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
@ -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
@ -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"}
|
||||
@ -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
File diff suppressed because it is too large
Load Diff
@ -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;
|
||||
},
|
||||
|
||||
@ -11,100 +11,129 @@ export default {
|
||||
"lang",
|
||||
"format",
|
||||
"customFormat",
|
||||
[
|
||||
"displayTitle",
|
||||
"orientation",
|
||||
"menuPosition",
|
||||
"enableCalendarOnly",
|
||||
"stickedDatePicker",
|
||||
"calendarOnlyFit",
|
||||
"enableLeftSidebar",
|
||||
"enableRightSidebar",
|
||||
"enableMultiCalendars",
|
||||
"multiCalendars",
|
||||
"multiCalendarsSolo",
|
||||
],
|
||||
[
|
||||
"stylesTitle",
|
||||
"themeFontFamily",
|
||||
"themeFontSize",
|
||||
"themeTimeFontSize",
|
||||
{
|
||||
label: "Display",
|
||||
isCollapsible: true,
|
||||
properties: [
|
||||
"orientation",
|
||||
"menuPosition",
|
||||
"enableCalendarOnly",
|
||||
"stickedDatePicker",
|
||||
"calendarOnlyFit",
|
||||
"enableLeftSidebar",
|
||||
"enableRightSidebar",
|
||||
"enableMultiCalendars",
|
||||
"multiCalendars",
|
||||
"multiCalendarsSolo",
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "Style",
|
||||
isCollapsible: true,
|
||||
properties: [
|
||||
"themeFontFamily",
|
||||
"themeFontSize",
|
||||
"themeTimeFontSize",
|
||||
|
||||
"advancedStyles",
|
||||
"themePreviewFontSize",
|
||||
"themeBorderRadius",
|
||||
"themeCellBorderRadius",
|
||||
"themeCellSize",
|
||||
"themeCellPadding",
|
||||
"themeMenuMinWidth",
|
||||
],
|
||||
[
|
||||
"colorsTitle",
|
||||
"themePrimaryColor",
|
||||
"themeSecondaryColor",
|
||||
"themeBackgroundColor",
|
||||
"themeTextColor",
|
||||
"themePrimaryTextColor",
|
||||
"themeHoverColor",
|
||||
"themeDisabledColor",
|
||||
"advancedStyles",
|
||||
"themePreviewFontSize",
|
||||
"themeBorderRadius",
|
||||
"themeCellBorderRadius",
|
||||
"themeCellSize",
|
||||
"themeCellPadding",
|
||||
"themeMenuMinWidth",
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "Colors",
|
||||
isCollapsible: true,
|
||||
properties: [
|
||||
"themePrimaryColor",
|
||||
"themeSecondaryColor",
|
||||
"themeBackgroundColor",
|
||||
"themeTextColor",
|
||||
"themePrimaryTextColor",
|
||||
"themeHoverColor",
|
||||
"themeDisabledColor",
|
||||
|
||||
"advancedColors",
|
||||
"themeHoverTextColor",
|
||||
"themeHoverIconColor",
|
||||
"themeBorderColor",
|
||||
"themeMenuBorderColor",
|
||||
"themeBorderHoverColor",
|
||||
"themeScrollBarBackgroundColor",
|
||||
"themeScrollBarColor",
|
||||
"themeSuccessColor",
|
||||
"themeSuccessDisabledColor",
|
||||
"themeIconColor",
|
||||
"themeDangerColor",
|
||||
"themeHighlightColor",
|
||||
],
|
||||
"advancedColors",
|
||||
"themeHoverTextColor",
|
||||
"themeHoverIconColor",
|
||||
"themeBorderColor",
|
||||
"themeMenuBorderColor",
|
||||
"themeBorderHoverColor",
|
||||
"themeScrollBarBackgroundColor",
|
||||
"themeScrollBarColor",
|
||||
"themeSuccessColor",
|
||||
"themeSuccessDisabledColor",
|
||||
"themeIconColor",
|
||||
"themeDangerColor",
|
||||
"themeHighlightColor",
|
||||
],
|
||||
},
|
||||
],
|
||||
customSettingsPropertiesOrder: [
|
||||
"readonly",
|
||||
"required",
|
||||
[
|
||||
"selectionTitle",
|
||||
"selectionMode",
|
||||
"initValueSingle",
|
||||
"initValueRangeStart",
|
||||
"initValueRangeEnd",
|
||||
"initValueMulti",
|
||||
"multiDatesLimit",
|
||||
"rangeMode",
|
||||
"autoRange",
|
||||
"enablePartialRange",
|
||||
"minRange",
|
||||
"maxRange",
|
||||
"noDisabledRange",
|
||||
],
|
||||
["behaviorTitle", "autoApply", "closeOnAutoApply"],
|
||||
["timeTitle", "dateMode", "timezone", "use24", "enableSeconds"],
|
||||
[
|
||||
"datesTitle",
|
||||
"startDate",
|
||||
"minDate",
|
||||
"maxDate",
|
||||
{
|
||||
label: "Selection",
|
||||
isCollapsible: true,
|
||||
properties: [
|
||||
"selectionMode",
|
||||
"initValueSingle",
|
||||
"initValueRangeStart",
|
||||
"initValueRangeEnd",
|
||||
"initValueMulti",
|
||||
"multiDatesLimit",
|
||||
"rangeMode",
|
||||
"autoRange",
|
||||
"enablePartialRange",
|
||||
"minRange",
|
||||
"maxRange",
|
||||
"noDisabledRange",
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "Behavior",
|
||||
isCollapsible: true,
|
||||
properties: ["autoApply", "closeOnAutoApply"],
|
||||
},
|
||||
{
|
||||
label: "Time",
|
||||
isCollapsible: true,
|
||||
properties: ["dateMode", "timezone", "use24", "enableSeconds"],
|
||||
},
|
||||
{
|
||||
label: "Dates",
|
||||
isCollapsible: true,
|
||||
properties: [
|
||||
"startDate",
|
||||
"minDate",
|
||||
"maxDate",
|
||||
|
||||
"advancedDates",
|
||||
"preventMinMaxNavigation",
|
||||
"ignoreTimeValidation",
|
||||
"allowedDates",
|
||||
"disabledDates",
|
||||
"disabledWeekDays",
|
||||
],
|
||||
[
|
||||
"weeksAndMonthsTitle",
|
||||
"weekStart",
|
||||
"weekNumbers",
|
||||
"hideOffsetDates",
|
||||
"disableMonthYearSelect",
|
||||
],
|
||||
|
||||
["flowTitle", "enableFlow", "flowHint", "flowSteps"],
|
||||
"advancedDates",
|
||||
"preventMinMaxNavigation",
|
||||
"ignoreTimeValidation",
|
||||
"allowedDates",
|
||||
"disabledDates",
|
||||
"disabledWeekDays",
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "Weeks / Months",
|
||||
isCollapsible: true,
|
||||
properties: [
|
||||
"weekStart",
|
||||
"weekNumbers",
|
||||
"hideOffsetDates",
|
||||
"disableMonthYearSelect",
|
||||
],
|
||||
},
|
||||
{
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -8,34 +8,34 @@ export default {
|
||||
},
|
||||
icon: "history",
|
||||
customStylePropertiesOrder: [
|
||||
[
|
||||
"markerStyle",
|
||||
"markerShape",
|
||||
"markerSize",
|
||||
"markerBackgroundColor",
|
||||
"markerIconOnOff",
|
||||
"markerIcon",
|
||||
"markerIconColor",
|
||||
"markerIconSize",
|
||||
],
|
||||
[
|
||||
"timelineStyle",
|
||||
"connectorColor",
|
||||
"connectorWidth",
|
||||
"timelineLayout",
|
||||
"eventsAlignment",
|
||||
],
|
||||
{
|
||||
label: "Marker style",
|
||||
isCollapsible: true,
|
||||
properties: [
|
||||
"markerShape",
|
||||
"markerSize",
|
||||
"markerBackgroundColor",
|
||||
"markerIconOnOff",
|
||||
"markerIcon",
|
||||
"markerIconColor",
|
||||
"markerIconSize",
|
||||
],
|
||||
},
|
||||
{
|
||||
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",
|
||||
|
||||
@ -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;
|
||||
input[name].pending = false;
|
||||
// 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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}),
|
||||
});
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
@ -56,6 +56,7 @@ export default {
|
||||
const { formInputs, forceValidateAllFields, resetInputs } = useFormInputs({
|
||||
updateInputValidity,
|
||||
removeInputValidity,
|
||||
validationType,
|
||||
});
|
||||
|
||||
const isValid = computed(() => formState.isValid.value);
|
||||
|
||||
@ -5,124 +5,157 @@ export default {
|
||||
bubble: { icon: 'upload' },
|
||||
customSettingsPropertiesOrder: [
|
||||
// UX properties
|
||||
'type',
|
||||
'reorder',
|
||||
'drop',
|
||||
'maxFileSize',
|
||||
'minFileSize',
|
||||
'maxTotalFileSize',
|
||||
'maxFiles',
|
||||
'required',
|
||||
'readonly',
|
||||
'extensions',
|
||||
'customExtensions',
|
||||
'exposeBase64',
|
||||
'exposeBinary',
|
||||
['formInfobox', 'fieldName', 'customValidation', 'validation'],
|
||||
[
|
||||
'type',
|
||||
'reorder',
|
||||
'drop',
|
||||
'maxFileSize',
|
||||
'minFileSize',
|
||||
'maxTotalFileSize',
|
||||
'maxFiles',
|
||||
'required',
|
||||
'readonly',
|
||||
'extensions',
|
||||
'customExtensions',
|
||||
'exposeBase64',
|
||||
'exposeBinary',
|
||||
],
|
||||
],
|
||||
customStylePropertiesOrder: [
|
||||
// Dropzone properties
|
||||
[
|
||||
'dropzoneTitle',
|
||||
'dropzoneBorderColor',
|
||||
'dropzoneBorderStyle',
|
||||
'dropzoneBorderWidth',
|
||||
'dropzoneBorderRadius',
|
||||
'dropzoneBackground',
|
||||
'dropzoneBackgroundHover',
|
||||
'dropzoneBackgroundDragging',
|
||||
'dropzonePadding',
|
||||
'dropzoneMinHeight',
|
||||
],
|
||||
{
|
||||
label: "Dropzone",
|
||||
isCollapsible: true,
|
||||
properties: [
|
||||
'dropzoneBorderColor',
|
||||
'dropzoneBorderStyle',
|
||||
'dropzoneBorderWidth',
|
||||
'dropzoneBorderRadius',
|
||||
'dropzoneBackground',
|
||||
'dropzoneBackgroundHover',
|
||||
'dropzoneBackgroundDragging',
|
||||
'dropzonePadding',
|
||||
'dropzoneMinHeight',
|
||||
],
|
||||
},
|
||||
// Icon properties
|
||||
[
|
||||
'iconTitle',
|
||||
'showUploadIcon',
|
||||
'uploadIcon',
|
||||
'uploadIconColor',
|
||||
'uploadIconSize',
|
||||
'uploadIconMargin',
|
||||
'uploadIconPosition',
|
||||
],
|
||||
{
|
||||
label: "Icon",
|
||||
isCollapsible: true,
|
||||
properties: [
|
||||
'showUploadIcon',
|
||||
'uploadIcon',
|
||||
'uploadIconColor',
|
||||
'uploadIconSize',
|
||||
'uploadIconMargin',
|
||||
'uploadIconPosition',
|
||||
],
|
||||
},
|
||||
// Label properties
|
||||
[
|
||||
'labelTitle',
|
||||
'labelMessage',
|
||||
'labelFontFamily',
|
||||
'labelFontSize',
|
||||
'labelFontWeight',
|
||||
'labelColor',
|
||||
'labelMargin',
|
||||
],
|
||||
{
|
||||
label: "Label",
|
||||
isCollapsible: true,
|
||||
properties: [
|
||||
'labelMessage',
|
||||
'labelFontFamily',
|
||||
'labelFontSize',
|
||||
'labelFontWeight',
|
||||
'labelColor',
|
||||
'labelMargin',
|
||||
],
|
||||
},
|
||||
// Info messages properties
|
||||
[
|
||||
'infoMessagesTitle',
|
||||
'extensionsMessage',
|
||||
'extensionsMessageFontFamily',
|
||||
'extensionsMessageFontSize',
|
||||
'extensionsMessageFontWeight',
|
||||
'extensionsMessageColor',
|
||||
'extensionsMessageMargin',
|
||||
'maxFileMessage',
|
||||
'maxFileMessageFontFamily',
|
||||
'maxFileMessageFontSize',
|
||||
'maxFileMessageFontWeight',
|
||||
'maxFileMessageColor',
|
||||
'maxFileMessageMargin',
|
||||
],
|
||||
{
|
||||
label: "Info messages",
|
||||
isCollapsible: true,
|
||||
properties: [
|
||||
'extensionsMessage',
|
||||
'extensionsMessageFontFamily',
|
||||
'extensionsMessageFontSize',
|
||||
'extensionsMessageFontWeight',
|
||||
'extensionsMessageColor',
|
||||
'extensionsMessageMargin',
|
||||
'maxFileMessage',
|
||||
'maxFileMessageFontFamily',
|
||||
'maxFileMessageFontSize',
|
||||
'maxFileMessageFontWeight',
|
||||
'maxFileMessageColor',
|
||||
'maxFileMessageMargin',
|
||||
],
|
||||
},
|
||||
// File list properties
|
||||
[
|
||||
'fileListTitle',
|
||||
'fileItemBackground',
|
||||
'fileItemBorderColor',
|
||||
'fileItemBorderRadius',
|
||||
'fileItemPadding',
|
||||
'fileItemMargin',
|
||||
'fileItemShadow',
|
||||
'progressBarColor',
|
||||
'progressBarColorWarning',
|
||||
'fileItemHoverTitle',
|
||||
'fileItemHoverBorderColor',
|
||||
'fileItemHoverBackground',
|
||||
'fileItemHoverShadow',
|
||||
],
|
||||
{
|
||||
label: "File list",
|
||||
isCollapsible: true,
|
||||
properties: [
|
||||
'fileListTitle',
|
||||
'fileItemBackground',
|
||||
'fileItemBorderColor',
|
||||
'fileItemBorderRadius',
|
||||
'fileItemPadding',
|
||||
'fileItemMargin',
|
||||
'fileItemShadow',
|
||||
'progressBarColor',
|
||||
'progressBarColorWarning',
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "File item hover states",
|
||||
isCollapsible: true,
|
||||
properties: [
|
||||
'fileItemHoverBorderColor',
|
||||
'fileItemHoverBackground',
|
||||
'fileItemHoverShadow',
|
||||
],
|
||||
},
|
||||
// File details properties
|
||||
[
|
||||
'fileNameTitle',
|
||||
'fileNameFontFamily',
|
||||
'fileNameFontSize',
|
||||
'fileNameFontWeight',
|
||||
'fileNameColor',
|
||||
'fileDetailsTitle',
|
||||
'showFileInfo',
|
||||
'fileDetailsFontFamily',
|
||||
'fileDetailsFontSize',
|
||||
'fileDetailsFontWeight',
|
||||
'fileDetailsColor',
|
||||
],
|
||||
{
|
||||
label: "File name",
|
||||
isCollapsible: true,
|
||||
properties: [
|
||||
'fileNameFontFamily',
|
||||
'fileNameFontSize',
|
||||
'fileNameFontWeight',
|
||||
'fileNameColor',
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "File details",
|
||||
isCollapsible: true,
|
||||
properties: [
|
||||
'showFileInfo',
|
||||
'fileDetailsFontFamily',
|
||||
'fileDetailsFontSize',
|
||||
'fileDetailsFontWeight',
|
||||
'fileDetailsColor',
|
||||
],
|
||||
},
|
||||
// Action buttons properties
|
||||
[
|
||||
'actionButtonsTitle',
|
||||
'actionButtonSize',
|
||||
'actionButtonBackground',
|
||||
'actionButtonHoverBackground',
|
||||
'actionButtonColor',
|
||||
'actionButtonBorderColor',
|
||||
'actionButtonHoverBorderColor',
|
||||
'actionButtonBorderRadius',
|
||||
'actionButtonMargin',
|
||||
],
|
||||
{
|
||||
label: "Remove buttons",
|
||||
isCollapsible: true,
|
||||
properties: [
|
||||
'actionButtonSize',
|
||||
'actionButtonBackground',
|
||||
'actionButtonHoverBackground',
|
||||
'actionButtonColor',
|
||||
'actionButtonBorderColor',
|
||||
'actionButtonHoverBorderColor',
|
||||
'actionButtonBorderRadius',
|
||||
'actionButtonMargin',
|
||||
],
|
||||
},
|
||||
// Circle animation properties
|
||||
[
|
||||
'circleAnimationTitle',
|
||||
'enableCircleAnimation',
|
||||
'circleSize',
|
||||
'circleColor',
|
||||
'circleOpacity',
|
||||
'animationSpeed',
|
||||
],
|
||||
{
|
||||
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',
|
||||
|
||||
@ -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,37 +391,43 @@ 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
|
||||
.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))
|
||||
);
|
||||
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));
|
||||
},
|
||||
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 &&
|
||||
!this.settings.publicData?.projectUrl &&
|
||||
!this.settings.privateData?.accessToken;
|
||||
const isFreshInstall =
|
||||
!this.settings.publicData?.environments &&
|
||||
!this.settings.publicData?.projectUrl &&
|
||||
!this.settings.privateData?.accessToken;
|
||||
|
||||
if (isFreshInstall) {
|
||||
await this.syncFromOtherPlugin('supabase');
|
||||
@ -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,21 +664,15 @@ 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;
|
||||
const errorCopy = { ...(this.branchErrors || {}) };
|
||||
delete errorCopy[env];
|
||||
this.branchErrors = errorCopy;
|
||||
const selectedCopy = { ...(this.selectedBranches || {}) };
|
||||
selectedCopy[env] = '';
|
||||
this.selectedBranches = selectedCopy;
|
||||
}
|
||||
const branchCopy = { ...(this.branches || {}) };
|
||||
branchCopy[env] = [];
|
||||
this.branches = branchCopy;
|
||||
const errorCopy = { ...(this.branchErrors || {}) };
|
||||
delete errorCopy[env];
|
||||
this.branchErrors = errorCopy;
|
||||
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;
|
||||
}
|
||||
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 };
|
||||
}
|
||||
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,67 +765,127 @@ 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', '');
|
||||
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 || '';
|
||||
// Cancel any in-flight branch change request
|
||||
if (this.branchChangeAbortController) {
|
||||
this.branchChangeAbortController.abort();
|
||||
}
|
||||
|
||||
const effectiveBranchSlug = branchValue ? (branchSlug || this.getCurrentEnvConfig(env)?.branchSlug || '') : '';
|
||||
const projectData = await this.fetchProject(baseRef, {
|
||||
branchSlug: effectiveBranchSlug,
|
||||
branchRef: branchValue ? targetRef : '',
|
||||
});
|
||||
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
|
||||
: '';
|
||||
const runtimeRef = resolvedBranchRef || baseRef;
|
||||
const projectUrl = `https://${runtimeRef}.supabase.co`;
|
||||
const displayAnonKey = apiKey || this.getCurrentEnvConfig(env).apiKey;
|
||||
const displayServiceKey = privateApiKey || this.getCurrentEnvPrivateConfig(env).apiKey;
|
||||
// Create new AbortController for this request
|
||||
this.branchChangeAbortController = new AbortController();
|
||||
const signal = this.branchChangeAbortController.signal;
|
||||
|
||||
this.updateEnvironmentConfig(env, {
|
||||
publicData: {
|
||||
projectUrl,
|
||||
apiKey: displayAnonKey,
|
||||
branch: resolvedBranchRef || null,
|
||||
branchSlug: effectiveBranchSlug || null,
|
||||
baseProjectRef: baseRef,
|
||||
},
|
||||
privateData: {
|
||||
apiKey: displayServiceKey,
|
||||
connectionString: connectionString || this.getCurrentEnvPrivateConfig(env).connectionString,
|
||||
// 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 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 || '';
|
||||
}
|
||||
});
|
||||
|
||||
await this.loadBranches(env, baseRef);
|
||||
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
|
||||
: '';
|
||||
const runtimeRef = resolvedBranchRef || baseRef;
|
||||
const projectUrl = `https://${runtimeRef}.supabase.co`;
|
||||
const displayAnonKey = apiKey || this.getCurrentEnvConfig(env).apiKey;
|
||||
const displayServiceKey = privateApiKey || this.getCurrentEnvPrivateConfig(env).apiKey;
|
||||
|
||||
this.updateEnvironmentConfig(env, {
|
||||
publicData: {
|
||||
projectUrl,
|
||||
apiKey: displayAnonKey,
|
||||
branch: resolvedBranchRef || null,
|
||||
branchSlug: effectiveBranchSlug || null,
|
||||
baseProjectRef: baseRef,
|
||||
},
|
||||
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) {
|
||||
console.warn(`Failed to fetch project ${projectId}:`, 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;
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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,37 +391,43 @@ 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
|
||||
.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))
|
||||
);
|
||||
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));
|
||||
},
|
||||
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 &&
|
||||
!this.settings.publicData?.projectUrl &&
|
||||
!this.settings.privateData?.accessToken;
|
||||
const isFreshInstall =
|
||||
!this.settings.publicData?.environments &&
|
||||
!this.settings.publicData?.projectUrl &&
|
||||
!this.settings.privateData?.accessToken;
|
||||
|
||||
if (isFreshInstall) {
|
||||
await this.syncFromOtherPlugin('supabaseAuth');
|
||||
@ -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
|
||||
@ -696,10 +722,13 @@ 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 ref = baseRef;
|
||||
const paramsBaseRef = cfg.baseProjectRef || overrideRef || '';
|
||||
const cfg = this.getCurrentEnvConfig(env);
|
||||
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;
|
||||
const { data } = await wwLib.wwPlugins.supabase.requestAPI({
|
||||
method: 'GET',
|
||||
@ -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;
|
||||
}
|
||||
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 };
|
||||
}
|
||||
this.branches = { ...(this.branches || {}), [env]: [] };
|
||||
this.branchErrors = { ...(this.branchErrors || {}), [env]: msg };
|
||||
console.warn('[Supabase plugin] loadBranches error', { env, status: e?.response?.status, msg });
|
||||
}
|
||||
},
|
||||
@ -752,69 +780,120 @@ 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', '');
|
||||
if (!baseRef) return;
|
||||
|
||||
let targetRef = baseRef;
|
||||
let branchSlug = '';
|
||||
if (branchValue) {
|
||||
const list = this.branches?.[env] || [];
|
||||
const branch = list.find(it => (it.project_ref || it.ref || it.id || it.name) === branchValue);
|
||||
targetRef = branch?.project_ref || branch?.ref || branchValue;
|
||||
branchSlug = branch?.name || '';
|
||||
// Cancel any in-flight branch change request
|
||||
if (this.branchChangeAbortController) {
|
||||
this.branchChangeAbortController.abort();
|
||||
}
|
||||
|
||||
const effectiveBranchSlug = branchValue ? (branchSlug || this.getCurrentEnvConfig(env)?.branchSlug || '') : '';
|
||||
const projectData = await this.fetchProject(baseRef, {
|
||||
branchSlug: effectiveBranchSlug,
|
||||
branchRef: branchValue ? targetRef : '',
|
||||
});
|
||||
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
|
||||
: '';
|
||||
const runtimeRef = resolvedBranchRef || baseRef;
|
||||
const projectUrl = `https://${runtimeRef}.supabase.co`;
|
||||
const displayAnonKey = apiKey || this.getCurrentEnvConfig(env).apiKey;
|
||||
const displayServiceKey = privateApiKey || this.getCurrentEnvPrivateConfig(env).apiKey;
|
||||
// Create new AbortController for this request
|
||||
this.branchChangeAbortController = new AbortController();
|
||||
const signal = this.branchChangeAbortController.signal;
|
||||
|
||||
this.updateEnvironmentConfig(env, {
|
||||
publicData: {
|
||||
projectUrl,
|
||||
apiKey: displayAnonKey,
|
||||
branch: resolvedBranchRef || null,
|
||||
branchSlug: effectiveBranchSlug || null,
|
||||
baseProjectRef: baseRef,
|
||||
},
|
||||
privateData: {
|
||||
apiKey: displayServiceKey,
|
||||
connectionString: connectionString || this.getCurrentEnvPrivateConfig(env).connectionString,
|
||||
// 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', '');
|
||||
if (!baseRef) return;
|
||||
|
||||
let targetRef = baseRef;
|
||||
let branchSlug = '';
|
||||
if (branchValue) {
|
||||
const list = this.branches?.[env] || [];
|
||||
const branch = list.find(it => (it.project_ref || it.ref || it.id || it.name) === branchValue);
|
||||
targetRef = branch?.project_ref || branch?.ref || branchValue;
|
||||
branchSlug = branch?.name || '';
|
||||
}
|
||||
});
|
||||
|
||||
await this.loadBranches(env, baseRef);
|
||||
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
|
||||
: '';
|
||||
const runtimeRef = resolvedBranchRef || baseRef;
|
||||
const projectUrl = `https://${runtimeRef}.supabase.co`;
|
||||
const displayAnonKey = apiKey || this.getCurrentEnvConfig(env).apiKey;
|
||||
const displayServiceKey = privateApiKey || this.getCurrentEnvPrivateConfig(env).apiKey;
|
||||
|
||||
this.updateEnvironmentConfig(env, {
|
||||
publicData: {
|
||||
projectUrl,
|
||||
apiKey: displayAnonKey,
|
||||
branch: resolvedBranchRef || null,
|
||||
branchSlug: effectiveBranchSlug || null,
|
||||
baseProjectRef: baseRef,
|
||||
},
|
||||
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) {
|
||||
console.warn(`Failed to fetch project ${projectId}:`, 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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 */
|
||||
|
||||
|
||||
@ -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
Loading…
x
Reference in New Issue
Block a user