@morscherlab/mld-sdk 0.10.0 → 0.10.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/__stories__/experiment-helpers.d.ts +25 -0
- package/dist/__tests__/composables/usePlatformContext.test.d.ts +1 -0
- package/dist/components/ExperimentPopover.vue.d.ts +4 -0
- package/dist/components/ExperimentPopover.vue.js +138 -112
- package/dist/components/ExperimentPopover.vue.js.map +1 -1
- package/dist/components/FitPanel.vue.d.ts +1 -1
- package/dist/composables/useAuth.js +6 -1
- package/dist/composables/useAuth.js.map +1 -1
- package/dist/composables/usePlatformContext.js +60 -20
- package/dist/composables/usePlatformContext.js.map +1 -1
- package/dist/styles.css +270 -121
- package/package.json +1 -1
- package/src/__stories__/experiment-helpers.ts +78 -0
- package/src/__tests__/composables/useAuth.test.ts +47 -2
- package/src/__tests__/composables/usePlatformContext.test.ts +116 -0
- package/src/components/AppLayout.story.vue +74 -0
- package/src/components/AppTopBar.story.vue +29 -0
- package/src/components/ExperimentPopover.vue +90 -58
- package/src/composables/useAuth.ts +7 -1
- package/src/composables/usePlatformContext.ts +74 -24
- package/src/styles/components/experiment-popover.css +86 -3
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { ExperimentSummary } from '../types';
|
|
2
|
+
export declare const mockExperiments: ExperimentSummary[];
|
|
3
|
+
export declare function installMockInterceptor(): void;
|
|
4
|
+
/**
|
|
5
|
+
* Wrapper component that fakes integrated mode and provides useAppExperiment context.
|
|
6
|
+
* Use in story files to demonstrate experiment selector integration.
|
|
7
|
+
*
|
|
8
|
+
* Props:
|
|
9
|
+
* preselect (boolean, default true) - pre-select the first mock experiment
|
|
10
|
+
*/
|
|
11
|
+
export declare const ExperimentProvider: import('vue').DefineComponent<import('vue').ExtractPropTypes<{
|
|
12
|
+
preselect: {
|
|
13
|
+
type: BooleanConstructor;
|
|
14
|
+
default: boolean;
|
|
15
|
+
};
|
|
16
|
+
}>, () => import('vue').VNode<import('vue').RendererNode, import('vue').RendererElement, {
|
|
17
|
+
[key: string]: any;
|
|
18
|
+
}>[] | undefined, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {}, string, import('vue').PublicProps, Readonly<import('vue').ExtractPropTypes<{
|
|
19
|
+
preselect: {
|
|
20
|
+
type: BooleanConstructor;
|
|
21
|
+
default: boolean;
|
|
22
|
+
};
|
|
23
|
+
}>> & Readonly<{}>, {
|
|
24
|
+
preselect: boolean;
|
|
25
|
+
}, {}, {}, {}, string, import('vue').ComponentProvideOptions, true, {}, any>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -7,6 +7,9 @@ interface Props {
|
|
|
7
7
|
saveLoading?: boolean;
|
|
8
8
|
saveSuccessMessage?: string;
|
|
9
9
|
saveDisabledMessage?: string;
|
|
10
|
+
confirmSave?: boolean;
|
|
11
|
+
confirmTitle?: string;
|
|
12
|
+
confirmMessage?: string;
|
|
10
13
|
}
|
|
11
14
|
declare const _default: import('vue').DefineComponent<Props, {}, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {
|
|
12
15
|
select: () => any;
|
|
@@ -21,6 +24,7 @@ declare const _default: import('vue').DefineComponent<Props, {}, {}, {}, {}, imp
|
|
|
21
24
|
showDetach: boolean;
|
|
22
25
|
saveDisabled: boolean;
|
|
23
26
|
saveLoading: boolean;
|
|
27
|
+
confirmSave: boolean;
|
|
24
28
|
}, {}, {}, {}, string, import('vue').ComponentProvideOptions, false, {
|
|
25
29
|
popoverRef: HTMLDivElement;
|
|
26
30
|
}, HTMLDivElement>;
|
|
@@ -1,51 +1,48 @@
|
|
|
1
|
-
import { defineComponent, ref, watch, onMounted, onUnmounted, openBlock, createElementBlock, createElementVNode,
|
|
1
|
+
import { defineComponent, ref, watch, onMounted, onUnmounted, openBlock, createElementBlock, createElementVNode, normalizeClass, withModifiers, toDisplayString, createCommentVNode, createTextVNode, createVNode } from "vue";
|
|
2
|
+
import _sfc_main$1 from "./ConfirmDialog.vue.js";
|
|
3
|
+
/* empty css */
|
|
2
4
|
const _hoisted_1 = { class: "mld-experiment-popover__trigger-text" };
|
|
3
|
-
const _hoisted_2 =
|
|
5
|
+
const _hoisted_2 = ["disabled", "title"];
|
|
6
|
+
const _hoisted_3 = {
|
|
4
7
|
key: 0,
|
|
5
|
-
class: "mld-experiment-
|
|
6
|
-
};
|
|
7
|
-
const _hoisted_3 = { class: "mld-experiment-popover__header" };
|
|
8
|
-
const _hoisted_4 = { class: "mld-experiment-popover__subtitle" };
|
|
9
|
-
const _hoisted_5 = {
|
|
10
|
-
key: 0,
|
|
11
|
-
class: "mld-experiment-popover__body"
|
|
8
|
+
class: "mld-experiment-popover__spinner--inline"
|
|
12
9
|
};
|
|
13
|
-
const
|
|
10
|
+
const _hoisted_4 = {
|
|
14
11
|
key: 1,
|
|
15
|
-
class: "mld-experiment-
|
|
16
|
-
};
|
|
17
|
-
const _hoisted_7 = { class: "mld-experiment-popover__card" };
|
|
18
|
-
const _hoisted_8 = { class: "mld-experiment-popover__card-info" };
|
|
19
|
-
const _hoisted_9 = { class: "mld-experiment-popover__card-name" };
|
|
20
|
-
const _hoisted_10 = {
|
|
21
|
-
key: 0,
|
|
22
|
-
class: "mld-experiment-popover__card-status"
|
|
23
|
-
};
|
|
24
|
-
const _hoisted_11 = { class: "mld-experiment-popover__card-actions" };
|
|
25
|
-
const _hoisted_12 = { class: "mld-experiment-popover__footer" };
|
|
26
|
-
const _hoisted_13 = ["disabled"];
|
|
27
|
-
const _hoisted_14 = {
|
|
28
|
-
key: 0,
|
|
29
|
-
class: "mld-experiment-popover__spinner"
|
|
30
|
-
};
|
|
31
|
-
const _hoisted_15 = {
|
|
32
|
-
key: 1,
|
|
33
|
-
class: "mld-experiment-popover__check-icon",
|
|
12
|
+
class: "mld-experiment-popover__save-trigger-icon",
|
|
34
13
|
fill: "none",
|
|
35
14
|
stroke: "currentColor",
|
|
36
15
|
viewBox: "0 0 24 24"
|
|
37
16
|
};
|
|
38
|
-
const
|
|
17
|
+
const _hoisted_5 = {
|
|
39
18
|
key: 2,
|
|
40
|
-
class: "mld-experiment-popover__save-icon",
|
|
19
|
+
class: "mld-experiment-popover__save-trigger-icon",
|
|
41
20
|
fill: "none",
|
|
42
21
|
stroke: "currentColor",
|
|
43
22
|
viewBox: "0 0 24 24"
|
|
44
23
|
};
|
|
45
|
-
const
|
|
24
|
+
const _hoisted_6 = {
|
|
46
25
|
key: 0,
|
|
47
|
-
class: "mld-experiment-
|
|
26
|
+
class: "mld-experiment-popover__panel"
|
|
48
27
|
};
|
|
28
|
+
const _hoisted_7 = { class: "mld-experiment-popover__header" };
|
|
29
|
+
const _hoisted_8 = { class: "mld-experiment-popover__subtitle" };
|
|
30
|
+
const _hoisted_9 = {
|
|
31
|
+
key: 0,
|
|
32
|
+
class: "mld-experiment-popover__body"
|
|
33
|
+
};
|
|
34
|
+
const _hoisted_10 = {
|
|
35
|
+
key: 1,
|
|
36
|
+
class: "mld-experiment-popover__body"
|
|
37
|
+
};
|
|
38
|
+
const _hoisted_11 = { class: "mld-experiment-popover__card" };
|
|
39
|
+
const _hoisted_12 = { class: "mld-experiment-popover__card-info" };
|
|
40
|
+
const _hoisted_13 = { class: "mld-experiment-popover__card-name" };
|
|
41
|
+
const _hoisted_14 = {
|
|
42
|
+
key: 0,
|
|
43
|
+
class: "mld-experiment-popover__card-status"
|
|
44
|
+
};
|
|
45
|
+
const _hoisted_15 = { class: "mld-experiment-popover__card-actions" };
|
|
49
46
|
const _sfc_main = /* @__PURE__ */ defineComponent({
|
|
50
47
|
__name: "ExperimentPopover",
|
|
51
48
|
props: {
|
|
@@ -56,7 +53,10 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
|
|
|
56
53
|
saveDisabled: { type: Boolean, default: false },
|
|
57
54
|
saveLoading: { type: Boolean, default: false },
|
|
58
55
|
saveSuccessMessage: {},
|
|
59
|
-
saveDisabledMessage: {}
|
|
56
|
+
saveDisabledMessage: {},
|
|
57
|
+
confirmSave: { type: Boolean, default: true },
|
|
58
|
+
confirmTitle: {},
|
|
59
|
+
confirmMessage: {}
|
|
60
60
|
},
|
|
61
61
|
emits: ["select", "save", "detach"],
|
|
62
62
|
setup(__props, { emit: __emit }) {
|
|
@@ -65,6 +65,7 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
|
|
|
65
65
|
const isOpen = ref(false);
|
|
66
66
|
const popoverRef = ref(null);
|
|
67
67
|
const showSuccess = ref(false);
|
|
68
|
+
const showConfirm = ref(false);
|
|
68
69
|
function toggle() {
|
|
69
70
|
isOpen.value = !isOpen.value;
|
|
70
71
|
}
|
|
@@ -77,6 +78,14 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
|
|
|
77
78
|
}
|
|
78
79
|
function handleSave() {
|
|
79
80
|
if (props.saveDisabled || props.saveLoading) return;
|
|
81
|
+
if (props.confirmSave) {
|
|
82
|
+
showConfirm.value = true;
|
|
83
|
+
} else {
|
|
84
|
+
emit("save");
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
function handleConfirmSave() {
|
|
88
|
+
showConfirm.value = false;
|
|
80
89
|
emit("save");
|
|
81
90
|
}
|
|
82
91
|
function handleDetach() {
|
|
@@ -88,11 +97,14 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
|
|
|
88
97
|
close();
|
|
89
98
|
}
|
|
90
99
|
}
|
|
100
|
+
let successTimer = null;
|
|
91
101
|
watch(() => props.saveSuccessMessage, (msg) => {
|
|
102
|
+
if (successTimer) clearTimeout(successTimer);
|
|
92
103
|
if (msg) {
|
|
93
104
|
showSuccess.value = true;
|
|
94
|
-
setTimeout(() => {
|
|
105
|
+
successTimer = setTimeout(() => {
|
|
95
106
|
showSuccess.value = false;
|
|
107
|
+
successTimer = null;
|
|
96
108
|
}, 3e3);
|
|
97
109
|
}
|
|
98
110
|
});
|
|
@@ -101,6 +113,7 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
|
|
|
101
113
|
});
|
|
102
114
|
onUnmounted(() => {
|
|
103
115
|
document.removeEventListener("click", handleClickOutside);
|
|
116
|
+
if (successTimer) clearTimeout(successTimer);
|
|
104
117
|
});
|
|
105
118
|
function formatStatus(status) {
|
|
106
119
|
return status.replace(/_/g, " ").replace(/^\w/, (c) => c.toUpperCase());
|
|
@@ -111,52 +124,88 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
|
|
|
111
124
|
ref: popoverRef,
|
|
112
125
|
class: "mld-experiment-popover"
|
|
113
126
|
}, [
|
|
114
|
-
createElementVNode("
|
|
115
|
-
type: "button",
|
|
127
|
+
createElementVNode("div", {
|
|
116
128
|
class: normalizeClass([
|
|
117
|
-
"mld-experiment-
|
|
118
|
-
{ "mld-experiment-
|
|
119
|
-
|
|
120
|
-
]),
|
|
121
|
-
onClick: withModifiers(toggle, ["stop"])
|
|
129
|
+
"mld-experiment-popover__split",
|
|
130
|
+
{ "mld-experiment-popover__split--with-save": __props.showSave && __props.experimentName }
|
|
131
|
+
])
|
|
122
132
|
}, [
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
133
|
+
createElementVNode("button", {
|
|
134
|
+
type: "button",
|
|
135
|
+
class: normalizeClass([
|
|
136
|
+
"mld-experiment-popover__trigger",
|
|
137
|
+
{ "mld-experiment-popover__trigger--active": isOpen.value },
|
|
138
|
+
{ "mld-experiment-popover__trigger--empty": !__props.experimentName }
|
|
139
|
+
]),
|
|
140
|
+
onClick: withModifiers(toggle, ["stop"])
|
|
128
141
|
}, [
|
|
129
|
-
createElementVNode("
|
|
142
|
+
_cache[1] || (_cache[1] = createElementVNode("svg", {
|
|
143
|
+
class: "mld-experiment-popover__trigger-icon",
|
|
144
|
+
fill: "none",
|
|
145
|
+
stroke: "currentColor",
|
|
146
|
+
viewBox: "0 0 24 24"
|
|
147
|
+
}, [
|
|
148
|
+
createElementVNode("path", {
|
|
149
|
+
"stroke-linecap": "round",
|
|
150
|
+
"stroke-linejoin": "round",
|
|
151
|
+
"stroke-width": "1.75",
|
|
152
|
+
d: "M19.428 15.428a2 2 0 00-1.022-.547l-2.387-.477a6 6 0 00-3.86.517l-.318.158a6 6 0 01-3.86.517L6.05 15.21a2 2 0 00-1.806.547M8 4h8l-1 1v5.172a2 2 0 00.586 1.414l5 5c1.26 1.26.367 3.414-1.415 3.414H4.828c-1.782 0-2.674-2.154-1.414-3.414l5-5A2 2 0 009 10.172V5L8 4z"
|
|
153
|
+
})
|
|
154
|
+
], -1)),
|
|
155
|
+
createElementVNode("span", _hoisted_1, toDisplayString(__props.experimentName || "No experiment"), 1),
|
|
156
|
+
_cache[2] || (_cache[2] = createElementVNode("svg", {
|
|
157
|
+
class: "mld-experiment-popover__trigger-chevron",
|
|
158
|
+
viewBox: "0 0 24 24",
|
|
159
|
+
fill: "none",
|
|
160
|
+
stroke: "currentColor",
|
|
161
|
+
"stroke-width": "2",
|
|
130
162
|
"stroke-linecap": "round",
|
|
131
|
-
"stroke-linejoin": "round"
|
|
132
|
-
|
|
133
|
-
d: "
|
|
134
|
-
|
|
135
|
-
],
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
163
|
+
"stroke-linejoin": "round"
|
|
164
|
+
}, [
|
|
165
|
+
createElementVNode("path", { d: "m6 9 6 6 6-6" })
|
|
166
|
+
], -1))
|
|
167
|
+
], 2),
|
|
168
|
+
__props.showSave && __props.experimentName ? (openBlock(), createElementBlock("button", {
|
|
169
|
+
key: 0,
|
|
170
|
+
type: "button",
|
|
171
|
+
class: normalizeClass([
|
|
172
|
+
"mld-experiment-popover__save-trigger",
|
|
173
|
+
{ "mld-experiment-popover__save-trigger--loading": __props.saveLoading },
|
|
174
|
+
{ "mld-experiment-popover__save-trigger--success": showSuccess.value },
|
|
175
|
+
{ "mld-experiment-popover__save-trigger--disabled": __props.saveDisabled && !showSuccess.value }
|
|
176
|
+
]),
|
|
177
|
+
disabled: __props.saveDisabled && !showSuccess.value,
|
|
178
|
+
title: __props.saveDisabled && __props.saveDisabledMessage ? __props.saveDisabledMessage : showSuccess.value && __props.saveSuccessMessage ? __props.saveSuccessMessage : "Save to Experiment",
|
|
179
|
+
onClick: withModifiers(handleSave, ["stop"])
|
|
145
180
|
}, [
|
|
146
|
-
|
|
147
|
-
|
|
181
|
+
__props.saveLoading ? (openBlock(), createElementBlock("span", _hoisted_3)) : showSuccess.value ? (openBlock(), createElementBlock("svg", _hoisted_4, [..._cache[3] || (_cache[3] = [
|
|
182
|
+
createElementVNode("path", {
|
|
183
|
+
"stroke-linecap": "round",
|
|
184
|
+
"stroke-linejoin": "round",
|
|
185
|
+
"stroke-width": "2.5",
|
|
186
|
+
d: "M5 13l4 4L19 7"
|
|
187
|
+
}, null, -1)
|
|
188
|
+
])])) : (openBlock(), createElementBlock("svg", _hoisted_5, [..._cache[4] || (_cache[4] = [
|
|
189
|
+
createElementVNode("path", {
|
|
190
|
+
"stroke-linecap": "round",
|
|
191
|
+
"stroke-linejoin": "round",
|
|
192
|
+
"stroke-width": "2",
|
|
193
|
+
d: "M8 7H5a2 2 0 00-2 2v9a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-3m-1 4l-3 3m0 0l-3-3m3 3V4"
|
|
194
|
+
}, null, -1)
|
|
195
|
+
])]))
|
|
196
|
+
], 10, _hoisted_2)) : createCommentVNode("", true)
|
|
148
197
|
], 2),
|
|
149
|
-
isOpen.value ? (openBlock(), createElementBlock("div",
|
|
150
|
-
createElementVNode("div",
|
|
151
|
-
_cache[
|
|
152
|
-
createElementVNode("div",
|
|
198
|
+
isOpen.value ? (openBlock(), createElementBlock("div", _hoisted_6, [
|
|
199
|
+
createElementVNode("div", _hoisted_7, [
|
|
200
|
+
_cache[5] || (_cache[5] = createElementVNode("div", { class: "mld-experiment-popover__title" }, "Experiment", -1)),
|
|
201
|
+
createElementVNode("div", _hoisted_8, toDisplayString(__props.experimentName ? "Linked experiment context" : "Link to an MLD experiment"), 1)
|
|
153
202
|
]),
|
|
154
|
-
!__props.experimentName ? (openBlock(), createElementBlock("div",
|
|
203
|
+
!__props.experimentName ? (openBlock(), createElementBlock("div", _hoisted_9, [
|
|
155
204
|
createElementVNode("button", {
|
|
156
205
|
type: "button",
|
|
157
206
|
class: "mld-experiment-popover__select-btn",
|
|
158
207
|
onClick: handleSelect
|
|
159
|
-
}, [..._cache[
|
|
208
|
+
}, [..._cache[6] || (_cache[6] = [
|
|
160
209
|
createElementVNode("svg", {
|
|
161
210
|
width: "14",
|
|
162
211
|
height: "14",
|
|
@@ -173,9 +222,9 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
|
|
|
173
222
|
], -1),
|
|
174
223
|
createTextVNode(" Select Experiment ", -1)
|
|
175
224
|
])])
|
|
176
|
-
])) : (openBlock(), createElementBlock("div",
|
|
177
|
-
createElementVNode("div",
|
|
178
|
-
_cache[
|
|
225
|
+
])) : (openBlock(), createElementBlock("div", _hoisted_10, [
|
|
226
|
+
createElementVNode("div", _hoisted_11, [
|
|
227
|
+
_cache[7] || (_cache[7] = createElementVNode("div", { class: "mld-experiment-popover__card-icon" }, [
|
|
179
228
|
createElementVNode("svg", {
|
|
180
229
|
fill: "none",
|
|
181
230
|
stroke: "currentColor",
|
|
@@ -189,11 +238,11 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
|
|
|
189
238
|
})
|
|
190
239
|
])
|
|
191
240
|
], -1)),
|
|
192
|
-
createElementVNode("div",
|
|
193
|
-
createElementVNode("div",
|
|
194
|
-
__props.experimentStatus ? (openBlock(), createElementBlock("div",
|
|
241
|
+
createElementVNode("div", _hoisted_12, [
|
|
242
|
+
createElementVNode("div", _hoisted_13, toDisplayString(__props.experimentName), 1),
|
|
243
|
+
__props.experimentStatus ? (openBlock(), createElementBlock("div", _hoisted_14, toDisplayString(formatStatus(__props.experimentStatus)), 1)) : createCommentVNode("", true)
|
|
195
244
|
]),
|
|
196
|
-
createElementVNode("div",
|
|
245
|
+
createElementVNode("div", _hoisted_15, [
|
|
197
246
|
createElementVNode("button", {
|
|
198
247
|
type: "button",
|
|
199
248
|
class: "mld-experiment-popover__change-btn",
|
|
@@ -207,41 +256,18 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
|
|
|
207
256
|
}, " Detach ")) : createCommentVNode("", true)
|
|
208
257
|
])
|
|
209
258
|
])
|
|
210
|
-
]))
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
onClick: handleSave
|
|
223
|
-
}, [
|
|
224
|
-
__props.saveLoading ? (openBlock(), createElementBlock("span", _hoisted_14)) : showSuccess.value ? (openBlock(), createElementBlock("svg", _hoisted_15, [..._cache[5] || (_cache[5] = [
|
|
225
|
-
createElementVNode("path", {
|
|
226
|
-
"stroke-linecap": "round",
|
|
227
|
-
"stroke-linejoin": "round",
|
|
228
|
-
"stroke-width": "2",
|
|
229
|
-
d: "M5 13l4 4L19 7"
|
|
230
|
-
}, null, -1)
|
|
231
|
-
])])) : (openBlock(), createElementBlock("svg", _hoisted_16, [..._cache[6] || (_cache[6] = [
|
|
232
|
-
createElementVNode("path", {
|
|
233
|
-
"stroke-linecap": "round",
|
|
234
|
-
"stroke-linejoin": "round",
|
|
235
|
-
"stroke-width": "2",
|
|
236
|
-
d: "M8 7H5a2 2 0 00-2 2v9a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-3m-1 4l-3 3m0 0l-3-3m3 3V4"
|
|
237
|
-
}, null, -1)
|
|
238
|
-
])])),
|
|
239
|
-
createElementVNode("span", null, toDisplayString(showSuccess.value && __props.saveSuccessMessage ? __props.saveSuccessMessage : "Save to Experiment"), 1)
|
|
240
|
-
], 10, _hoisted_13),
|
|
241
|
-
__props.saveDisabled && __props.saveDisabledMessage && !showSuccess.value ? (openBlock(), createElementBlock("div", _hoisted_17, toDisplayString(__props.saveDisabledMessage), 1)) : createCommentVNode("", true)
|
|
242
|
-
])
|
|
243
|
-
], 64)) : createCommentVNode("", true)
|
|
244
|
-
])) : createCommentVNode("", true)
|
|
259
|
+
]))
|
|
260
|
+
])) : createCommentVNode("", true),
|
|
261
|
+
createVNode(_sfc_main$1, {
|
|
262
|
+
modelValue: showConfirm.value,
|
|
263
|
+
"onUpdate:modelValue": _cache[0] || (_cache[0] = ($event) => showConfirm.value = $event),
|
|
264
|
+
title: __props.confirmTitle ?? "Save to Experiment",
|
|
265
|
+
message: __props.confirmMessage ?? `Save current data to ${__props.experimentName}?`,
|
|
266
|
+
variant: "info",
|
|
267
|
+
"confirm-label": "Save",
|
|
268
|
+
loading: __props.saveLoading,
|
|
269
|
+
onConfirm: handleConfirmSave
|
|
270
|
+
}, null, 8, ["modelValue", "title", "message", "loading"])
|
|
245
271
|
], 512);
|
|
246
272
|
};
|
|
247
273
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ExperimentPopover.vue.js","sources":["../../src/components/ExperimentPopover.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { ref, watch, onMounted, onUnmounted } from 'vue'\n\ninterface Props {\n experimentName?: string\n experimentStatus?: string\n showSave?: boolean\n showDetach?: boolean\n saveDisabled?: boolean\n saveLoading?: boolean\n saveSuccessMessage?: string\n saveDisabledMessage?: string\n}\n\nconst props = withDefaults(defineProps<Props>(), {\n showSave: false,\n showDetach: false,\n saveDisabled: false,\n saveLoading: false,\n})\n\nconst emit = defineEmits<{\n select: []\n save: []\n detach: []\n}>()\n\nconst isOpen = ref(false)\nconst popoverRef = ref<HTMLElement | null>(null)\nconst showSuccess = ref(false)\n\nfunction toggle() {\n isOpen.value = !isOpen.value\n}\n\nfunction close() {\n isOpen.value = false\n}\n\nfunction handleSelect() {\n emit('select')\n close()\n}\n\nfunction handleSave() {\n if (props.saveDisabled || props.saveLoading) return\n emit('save')\n}\n\nfunction handleDetach() {\n emit('detach')\n close()\n}\n\nfunction handleClickOutside(event: MouseEvent) {\n if (popoverRef.value && !popoverRef.value.contains(event.target as Node)) {\n close()\n }\n}\n\n// Show success state when saveSuccessMessage changes from empty to a value\nwatch(() => props.saveSuccessMessage, (msg) => {\n if (msg) {\n showSuccess.value = true\n setTimeout(() => {\n showSuccess.value = false\n }, 3000)\n }\n})\n\nonMounted(() => {\n document.addEventListener('click', handleClickOutside)\n})\n\nonUnmounted(() => {\n document.removeEventListener('click', handleClickOutside)\n})\n\n// Format status for display (e.g., \"ready_to_extract\" -> \"Ready to extract\")\nfunction formatStatus(status: string): string {\n return status.replace(/_/g, ' ').replace(/^\\w/, c => c.toUpperCase())\n}\n</script>\n\n<template>\n <div ref=\"popoverRef\" class=\"mld-experiment-popover\">\n <!-- Trigger button -->\n <button\n type=\"button\"\n :class=\"[\n 'mld-experiment-popover__trigger',\n { 'mld-experiment-popover__trigger--active': isOpen },\n { 'mld-experiment-popover__trigger--empty': !experimentName },\n ]\"\n @click.stop=\"toggle\"\n >\n <!-- Flask icon -->\n <svg class=\"mld-experiment-popover__trigger-icon\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n stroke-width=\"1.75\"\n d=\"M19.428 15.428a2 2 0 00-1.022-.547l-2.387-.477a6 6 0 00-3.86.517l-.318.158a6 6 0 01-3.86.517L6.05 15.21a2 2 0 00-1.806.547M8 4h8l-1 1v5.172a2 2 0 00.586 1.414l5 5c1.26 1.26.367 3.414-1.415 3.414H4.828c-1.782 0-2.674-2.154-1.414-3.414l5-5A2 2 0 009 10.172V5L8 4z\"\n />\n </svg>\n <span class=\"mld-experiment-popover__trigger-text\">\n {{ experimentName || 'No experiment' }}\n </span>\n <!-- Chevron -->\n <svg class=\"mld-experiment-popover__trigger-chevron\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <path d=\"m6 9 6 6 6-6\" />\n </svg>\n </button>\n\n <!-- Popover panel -->\n <div v-if=\"isOpen\" class=\"mld-experiment-popover__panel\">\n <!-- Header -->\n <div class=\"mld-experiment-popover__header\">\n <div class=\"mld-experiment-popover__title\">Experiment</div>\n <div class=\"mld-experiment-popover__subtitle\">\n {{ experimentName ? 'Linked experiment context' : 'Link to an MLD experiment' }}\n </div>\n </div>\n\n <!-- No experiment selected -->\n <div v-if=\"!experimentName\" class=\"mld-experiment-popover__body\">\n <button type=\"button\" class=\"mld-experiment-popover__select-btn\" @click=\"handleSelect\">\n <svg width=\"14\" height=\"14\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M12 4v16m8-8H4\" />\n </svg>\n Select Experiment\n </button>\n </div>\n\n <!-- Experiment selected -->\n <div v-else class=\"mld-experiment-popover__body\">\n <div class=\"mld-experiment-popover__card\">\n <div class=\"mld-experiment-popover__card-icon\">\n <svg fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n stroke-width=\"1.75\"\n d=\"M19.428 15.428a2 2 0 00-1.022-.547l-2.387-.477a6 6 0 00-3.86.517l-.318.158a6 6 0 01-3.86.517L6.05 15.21a2 2 0 00-1.806.547M8 4h8l-1 1v5.172a2 2 0 00.586 1.414l5 5c1.26 1.26.367 3.414-1.415 3.414H4.828c-1.782 0-2.674-2.154-1.414-3.414l5-5A2 2 0 009 10.172V5L8 4z\"\n />\n </svg>\n </div>\n <div class=\"mld-experiment-popover__card-info\">\n <div class=\"mld-experiment-popover__card-name\">{{ experimentName }}</div>\n <div v-if=\"experimentStatus\" class=\"mld-experiment-popover__card-status\">\n {{ formatStatus(experimentStatus) }}\n </div>\n </div>\n <div class=\"mld-experiment-popover__card-actions\">\n <button type=\"button\" class=\"mld-experiment-popover__change-btn\" @click=\"handleSelect\">\n Change\n </button>\n <button v-if=\"showDetach\" type=\"button\" class=\"mld-experiment-popover__detach-btn\" @click=\"handleDetach\">\n Detach\n </button>\n </div>\n </div>\n </div>\n\n <!-- Save section -->\n <template v-if=\"showSave\">\n <div class=\"mld-experiment-popover__divider\" />\n <div class=\"mld-experiment-popover__footer\">\n <button\n type=\"button\"\n :class=\"[\n 'mld-experiment-popover__save-btn',\n { 'mld-experiment-popover__save-btn--loading': saveLoading },\n { 'mld-experiment-popover__save-btn--success': showSuccess },\n ]\"\n :disabled=\"saveDisabled && !showSuccess\"\n @click=\"handleSave\"\n >\n <!-- Loading spinner -->\n <span v-if=\"saveLoading\" class=\"mld-experiment-popover__spinner\" />\n <!-- Success check -->\n <svg v-else-if=\"showSuccess\" class=\"mld-experiment-popover__check-icon\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M5 13l4 4L19 7\" />\n </svg>\n <!-- Save icon -->\n <svg v-else class=\"mld-experiment-popover__save-icon\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M8 7H5a2 2 0 00-2 2v9a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-3m-1 4l-3 3m0 0l-3-3m3 3V4\" />\n </svg>\n <!-- Label -->\n <span>{{ showSuccess && saveSuccessMessage ? saveSuccessMessage : 'Save to Experiment' }}</span>\n </button>\n <div v-if=\"saveDisabled && saveDisabledMessage && !showSuccess\" class=\"mld-experiment-popover__save-hint\">\n {{ saveDisabledMessage }}\n </div>\n </div>\n </template>\n </div>\n </div>\n</template>\n\n<style>\n@import '../styles/components/experiment-popover.css';\n</style>\n"],"names":["_createElementBlock","_createElementVNode","_normalizeClass","_toDisplayString","_openBlock","_Fragment"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"ExperimentPopover.vue.js","sources":["../../src/components/ExperimentPopover.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { ref, watch, onMounted, onUnmounted } from 'vue'\nimport ConfirmDialog from './ConfirmDialog.vue'\n\ninterface Props {\n experimentName?: string\n experimentStatus?: string\n showSave?: boolean\n showDetach?: boolean\n saveDisabled?: boolean\n saveLoading?: boolean\n saveSuccessMessage?: string\n saveDisabledMessage?: string\n confirmSave?: boolean\n confirmTitle?: string\n confirmMessage?: string\n}\n\nconst props = withDefaults(defineProps<Props>(), {\n showSave: false,\n showDetach: false,\n saveDisabled: false,\n saveLoading: false,\n confirmSave: true,\n})\n\nconst emit = defineEmits<{\n select: []\n save: []\n detach: []\n}>()\n\nconst isOpen = ref(false)\nconst popoverRef = ref<HTMLElement | null>(null)\nconst showSuccess = ref(false)\nconst showConfirm = ref(false)\n\nfunction toggle() {\n isOpen.value = !isOpen.value\n}\n\nfunction close() {\n isOpen.value = false\n}\n\nfunction handleSelect() {\n emit('select')\n close()\n}\n\nfunction handleSave() {\n if (props.saveDisabled || props.saveLoading) return\n if (props.confirmSave) {\n showConfirm.value = true\n } else {\n emit('save')\n }\n}\n\nfunction handleConfirmSave() {\n showConfirm.value = false\n emit('save')\n}\n\nfunction handleDetach() {\n emit('detach')\n close()\n}\n\nfunction handleClickOutside(event: MouseEvent) {\n if (popoverRef.value && !popoverRef.value.contains(event.target as Node)) {\n close()\n }\n}\n\nlet successTimer: ReturnType<typeof setTimeout> | null = null\n\n// Show success state when saveSuccessMessage changes from empty to a value\nwatch(() => props.saveSuccessMessage, (msg) => {\n if (successTimer) clearTimeout(successTimer)\n if (msg) {\n showSuccess.value = true\n successTimer = setTimeout(() => {\n showSuccess.value = false\n successTimer = null\n }, 3000)\n }\n})\n\nonMounted(() => {\n document.addEventListener('click', handleClickOutside)\n})\n\nonUnmounted(() => {\n document.removeEventListener('click', handleClickOutside)\n if (successTimer) clearTimeout(successTimer)\n})\n\n// Format status for display (e.g., \"ready_to_extract\" -> \"Ready to extract\")\nfunction formatStatus(status: string): string {\n return status.replace(/_/g, ' ').replace(/^\\w/, c => c.toUpperCase())\n}\n</script>\n\n<template>\n <div ref=\"popoverRef\" class=\"mld-experiment-popover\">\n <!-- Split trigger: experiment pill + inline save -->\n <div\n :class=\"[\n 'mld-experiment-popover__split',\n { 'mld-experiment-popover__split--with-save': showSave && experimentName },\n ]\"\n >\n <!-- Left: experiment trigger (opens popover) -->\n <button\n type=\"button\"\n :class=\"[\n 'mld-experiment-popover__trigger',\n { 'mld-experiment-popover__trigger--active': isOpen },\n { 'mld-experiment-popover__trigger--empty': !experimentName },\n ]\"\n @click.stop=\"toggle\"\n >\n <!-- Flask icon -->\n <svg class=\"mld-experiment-popover__trigger-icon\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n stroke-width=\"1.75\"\n d=\"M19.428 15.428a2 2 0 00-1.022-.547l-2.387-.477a6 6 0 00-3.86.517l-.318.158a6 6 0 01-3.86.517L6.05 15.21a2 2 0 00-1.806.547M8 4h8l-1 1v5.172a2 2 0 00.586 1.414l5 5c1.26 1.26.367 3.414-1.415 3.414H4.828c-1.782 0-2.674-2.154-1.414-3.414l5-5A2 2 0 009 10.172V5L8 4z\"\n />\n </svg>\n <span class=\"mld-experiment-popover__trigger-text\">\n {{ experimentName || 'No experiment' }}\n </span>\n <!-- Chevron -->\n <svg class=\"mld-experiment-popover__trigger-chevron\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <path d=\"m6 9 6 6 6-6\" />\n </svg>\n </button>\n\n <!-- Right: inline save button (direct action) -->\n <button\n v-if=\"showSave && experimentName\"\n type=\"button\"\n :class=\"[\n 'mld-experiment-popover__save-trigger',\n { 'mld-experiment-popover__save-trigger--loading': saveLoading },\n { 'mld-experiment-popover__save-trigger--success': showSuccess },\n { 'mld-experiment-popover__save-trigger--disabled': saveDisabled && !showSuccess },\n ]\"\n :disabled=\"saveDisabled && !showSuccess\"\n :title=\"saveDisabled && saveDisabledMessage ? saveDisabledMessage : showSuccess && saveSuccessMessage ? saveSuccessMessage : 'Save to Experiment'\"\n @click.stop=\"handleSave\"\n >\n <!-- Loading spinner -->\n <span v-if=\"saveLoading\" class=\"mld-experiment-popover__spinner--inline\" />\n <!-- Success check -->\n <svg v-else-if=\"showSuccess\" class=\"mld-experiment-popover__save-trigger-icon\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2.5\" d=\"M5 13l4 4L19 7\" />\n </svg>\n <!-- Save icon -->\n <svg v-else class=\"mld-experiment-popover__save-trigger-icon\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M8 7H5a2 2 0 00-2 2v9a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-3m-1 4l-3 3m0 0l-3-3m3 3V4\" />\n </svg>\n </button>\n </div>\n\n <!-- Popover panel -->\n <div v-if=\"isOpen\" class=\"mld-experiment-popover__panel\">\n <!-- Header -->\n <div class=\"mld-experiment-popover__header\">\n <div class=\"mld-experiment-popover__title\">Experiment</div>\n <div class=\"mld-experiment-popover__subtitle\">\n {{ experimentName ? 'Linked experiment context' : 'Link to an MLD experiment' }}\n </div>\n </div>\n\n <!-- No experiment selected -->\n <div v-if=\"!experimentName\" class=\"mld-experiment-popover__body\">\n <button type=\"button\" class=\"mld-experiment-popover__select-btn\" @click=\"handleSelect\">\n <svg width=\"14\" height=\"14\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M12 4v16m8-8H4\" />\n </svg>\n Select Experiment\n </button>\n </div>\n\n <!-- Experiment selected -->\n <div v-else class=\"mld-experiment-popover__body\">\n <div class=\"mld-experiment-popover__card\">\n <div class=\"mld-experiment-popover__card-icon\">\n <svg fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n stroke-width=\"1.75\"\n d=\"M19.428 15.428a2 2 0 00-1.022-.547l-2.387-.477a6 6 0 00-3.86.517l-.318.158a6 6 0 01-3.86.517L6.05 15.21a2 2 0 00-1.806.547M8 4h8l-1 1v5.172a2 2 0 00.586 1.414l5 5c1.26 1.26.367 3.414-1.415 3.414H4.828c-1.782 0-2.674-2.154-1.414-3.414l5-5A2 2 0 009 10.172V5L8 4z\"\n />\n </svg>\n </div>\n <div class=\"mld-experiment-popover__card-info\">\n <div class=\"mld-experiment-popover__card-name\">{{ experimentName }}</div>\n <div v-if=\"experimentStatus\" class=\"mld-experiment-popover__card-status\">\n {{ formatStatus(experimentStatus) }}\n </div>\n </div>\n <div class=\"mld-experiment-popover__card-actions\">\n <button type=\"button\" class=\"mld-experiment-popover__change-btn\" @click=\"handleSelect\">\n Change\n </button>\n <button v-if=\"showDetach\" type=\"button\" class=\"mld-experiment-popover__detach-btn\" @click=\"handleDetach\">\n Detach\n </button>\n </div>\n </div>\n </div>\n </div>\n\n <!-- Save confirmation dialog -->\n <ConfirmDialog\n v-model=\"showConfirm\"\n :title=\"confirmTitle ?? 'Save to Experiment'\"\n :message=\"confirmMessage ?? `Save current data to ${experimentName}?`\"\n variant=\"info\"\n confirm-label=\"Save\"\n :loading=\"saveLoading\"\n @confirm=\"handleConfirmSave\"\n />\n </div>\n</template>\n\n<style>\n@import '../styles/components/experiment-popover.css';\n</style>\n"],"names":["_createElementBlock","_createElementVNode","_normalizeClass","_toDisplayString","_openBlock","_createVNode","ConfirmDialog"],"mappings":"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"}
|
|
@@ -32,8 +32,8 @@ declare const __VLS_component: import('vue').DefineComponent<Props, {}, {}, {},
|
|
|
32
32
|
progress: number;
|
|
33
33
|
indeterminate: boolean;
|
|
34
34
|
state: FitState;
|
|
35
|
-
results: FitResultSummary[];
|
|
36
35
|
cancelLabel: string;
|
|
36
|
+
results: FitResultSummary[];
|
|
37
37
|
progressLabel: string;
|
|
38
38
|
runLabel: string;
|
|
39
39
|
}, {}, {}, {}, string, import('vue').ComponentProvideOptions, false, {}, HTMLDivElement>;
|
|
@@ -6,6 +6,7 @@ const TOKEN_REFRESH_MARGIN_MS = 5 * 60 * 1e3;
|
|
|
6
6
|
const TOKEN_REFRESH_CHECK_INTERVAL_MS = 60 * 1e3;
|
|
7
7
|
let _refreshPromise = null;
|
|
8
8
|
let _refreshTimerId = null;
|
|
9
|
+
let _mountedConsumerCount = 0;
|
|
9
10
|
function useAuth() {
|
|
10
11
|
const authStore = useAuthStore();
|
|
11
12
|
const settingsStore = useSettingsStore();
|
|
@@ -246,14 +247,18 @@ function useAuth() {
|
|
|
246
247
|
let checkInterval = null;
|
|
247
248
|
if (getCurrentInstance()) {
|
|
248
249
|
onMounted(() => {
|
|
250
|
+
_mountedConsumerCount += 1;
|
|
249
251
|
checkInterval = window.setInterval(checkAndRefreshIfNeeded, TOKEN_REFRESH_CHECK_INTERVAL_MS);
|
|
250
252
|
});
|
|
251
253
|
onUnmounted(() => {
|
|
252
|
-
stopTokenRefresh();
|
|
253
254
|
if (checkInterval !== null) {
|
|
254
255
|
window.clearInterval(checkInterval);
|
|
255
256
|
checkInterval = null;
|
|
256
257
|
}
|
|
258
|
+
_mountedConsumerCount = Math.max(0, _mountedConsumerCount - 1);
|
|
259
|
+
if (_mountedConsumerCount === 0) {
|
|
260
|
+
stopTokenRefresh();
|
|
261
|
+
}
|
|
257
262
|
});
|
|
258
263
|
watch(
|
|
259
264
|
() => authStore.tokenExpires,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useAuth.js","sources":["../../src/composables/useAuth.ts"],"sourcesContent":["import axios from 'axios'\nimport { ref, onMounted, onUnmounted, watch, getCurrentInstance, type Ref } from 'vue'\nimport { useAuthStore } from '../stores/auth'\nimport { useSettingsStore } from '../stores/settings'\nimport type { AuthConfig, UserInfo, LoginResponse, TokenVerifyResponse, UpdateProfileRequest } from '../types'\n\ninterface UserResponse {\n id: string\n username: string\n shortname: string | null\n email: string | null\n role: string\n is_active: boolean\n}\n\ninterface RefreshResponse {\n access_token: string\n expires_in: number\n token_type: string\n}\n\n// Token refresh configuration\nconst TOKEN_REFRESH_MARGIN_MS = 5 * 60 * 1000 // Refresh 5 minutes before expiry\nconst TOKEN_REFRESH_CHECK_INTERVAL_MS = 60 * 1000 // Check every minute\n\n/**\n * Authentication composable with automatic token refresh.\n *\n * Features:\n * - Automatic token refresh before expiration\n * - Login/logout/register functionality\n * - Token verification on startup\n * - User profile management\n *\n * @example\n * ```typescript\n * const { login, logout, isAuthenticated, user } = useAuth()\n *\n * // Login\n * const success = await login('username', 'password')\n *\n * // Automatic refresh is enabled by default\n * // Tokens are refreshed 5 minutes before expiration\n * ```\n */\nexport interface UseAuthReturn {\n login: (username: string, password: string) => Promise<boolean>\n logout: () => void\n register: (username: string, password: string, email?: string) => Promise<boolean>\n verifyToken: () => Promise<boolean>\n fetchAuthConfig: () => Promise<AuthConfig>\n initializeAuth: () => Promise<void>\n getCurrentUser: () => Promise<UserInfo | null>\n getAuthHeader: () => Record<string, string>\n updateProfile: (data: { email?: string; shortname?: string; currentPassword?: string; newPassword?: string }) => Promise<{ success: boolean; error?: string }>\n refreshToken: () => Promise<boolean>\n isRefreshing: Ref<boolean>\n}\n\n// Module-level singletons to prevent duplicate refresh requests and timers\n// across multiple useAuth() instances\nlet _refreshPromise: Promise<boolean> | null = null\nlet _refreshTimerId: number | null = null\n\nexport function useAuth(): UseAuthReturn {\n const authStore = useAuthStore()\n const settingsStore = useSettingsStore()\n\n const isRefreshing = ref(false)\n\n function getApiBaseUrl(): string {\n return settingsStore.getApiBaseUrl()\n }\n\n async function fetchAuthConfig(): Promise<AuthConfig> {\n try {\n const response = await axios.get<{\n auth_required: boolean\n passkey_enabled: boolean\n passkey_registered?: boolean\n registration_enabled?: boolean\n database_mode?: string\n }>(`${getApiBaseUrl()}/setup/config/public`)\n\n const config: AuthConfig = {\n authRequired: response.data.auth_required,\n passkeyEnabled: response.data.passkey_enabled,\n passkeyRegistered: response.data.passkey_registered ?? false,\n registrationEnabled: response.data.registration_enabled ?? false,\n databaseMode: response.data.database_mode ?? 'none',\n }\n\n authStore.setAuthConfig(config)\n return config\n } catch (error) {\n console.error('Failed to fetch auth config:', error)\n return {\n authRequired: false,\n passkeyEnabled: false,\n passkeyRegistered: false,\n registrationEnabled: false,\n databaseMode: 'none',\n }\n }\n }\n\n async function login(username: string, password: string): Promise<boolean> {\n authStore.setLoading(true)\n authStore.setError(null)\n\n try {\n const response = await axios.post<LoginResponse>(\n `${getApiBaseUrl()}/auth/login`,\n { username, password }\n )\n\n authStore.setToken(response.data.access_token, response.data.expires_in)\n authStore.setUsername(username)\n\n await getCurrentUser()\n\n // Start auto-refresh after successful login\n scheduleTokenRefresh()\n\n return true\n } catch (error) {\n if (axios.isAxiosError(error) && error.response) {\n authStore.setError(error.response.data.detail || 'Login failed')\n } else {\n authStore.setError('Network error. Please try again.')\n }\n return false\n } finally {\n authStore.setLoading(false)\n }\n }\n\n async function register(username: string, password: string, email?: string): Promise<boolean> {\n authStore.setLoading(true)\n authStore.setError(null)\n\n try {\n await axios.post<UserResponse>(\n `${getApiBaseUrl()}/users/register`,\n { username, password, email }\n )\n\n return await login(username, password)\n } catch (error) {\n if (axios.isAxiosError(error) && error.response) {\n authStore.setError(error.response.data.detail || 'Registration failed')\n } else {\n authStore.setError('Network error. Please try again.')\n }\n return false\n } finally {\n authStore.setLoading(false)\n }\n }\n\n async function getCurrentUser(): Promise<UserInfo | null> {\n if (!authStore.token) {\n return null\n }\n\n try {\n const response = await axios.get<UserResponse>(\n `${getApiBaseUrl()}/users/me`,\n { headers: getAuthHeader() }\n )\n\n const userInfo: UserInfo = {\n id: response.data.id,\n username: response.data.username,\n shortname: response.data.shortname,\n email: response.data.email,\n role: response.data.role,\n isActive: response.data.is_active,\n }\n\n authStore.setUserInfo(userInfo)\n return userInfo\n } catch {\n return null\n }\n }\n\n async function verifyToken(): Promise<boolean> {\n if (!authStore.token) {\n return false\n }\n\n try {\n const response = await axios.get<TokenVerifyResponse>(\n `${getApiBaseUrl()}/auth/verify`,\n {\n headers: {\n Authorization: `Bearer ${authStore.token}`,\n },\n }\n )\n\n if (response.data.valid && response.data.username) {\n authStore.setUsername(response.data.username)\n return true\n }\n\n authStore.clearToken()\n return false\n } catch {\n authStore.clearToken()\n return false\n }\n }\n\n /**\n * Refresh the authentication token.\n * Called automatically before token expiration.\n * Uses promise caching to prevent concurrent refresh requests.\n */\n async function refreshToken(): Promise<boolean> {\n if (!authStore.token) return false\n if (_refreshPromise) return _refreshPromise\n\n _refreshPromise = (async () => {\n isRefreshing.value = true\n\n try {\n const response = await axios.post<RefreshResponse>(\n `${getApiBaseUrl()}/auth/refresh`,\n {},\n { headers: getAuthHeader() }\n )\n\n authStore.setToken(response.data.access_token, response.data.expires_in)\n\n // Reschedule next refresh\n scheduleTokenRefresh()\n\n return true\n } catch (error) {\n // If refresh fails, the token may have been revoked\n // Clear auth state and let user re-login\n if (axios.isAxiosError(error) && error.response?.status === 401) {\n console.warn('[Auth] Token refresh failed - session expired')\n authStore.clearToken()\n stopTokenRefresh()\n }\n return false\n } finally {\n isRefreshing.value = false\n _refreshPromise = null\n }\n })()\n\n return _refreshPromise\n }\n\n /**\n * Schedule automatic token refresh before expiration.\n */\n function scheduleTokenRefresh(): void {\n // Clear any existing timer\n stopTokenRefresh()\n\n if (!authStore.tokenExpires) {\n return\n }\n\n const expiresAt = authStore.tokenExpires.getTime()\n const refreshAt = expiresAt - TOKEN_REFRESH_MARGIN_MS\n const now = Date.now()\n\n if (refreshAt <= now) {\n // Token is already close to expiring or expired, refresh now\n refreshToken()\n return\n }\n\n // Schedule refresh\n const delay = refreshAt - now\n _refreshTimerId = window.setTimeout(() => {\n refreshToken()\n }, delay)\n }\n\n /**\n * Stop automatic token refresh.\n */\n function stopTokenRefresh(): void {\n if (_refreshTimerId !== null) {\n window.clearTimeout(_refreshTimerId)\n _refreshTimerId = null\n }\n }\n\n /**\n * Check if token needs refresh and refresh if necessary.\n * Called periodically as a safety net.\n */\n function checkAndRefreshIfNeeded(): void {\n if (!authStore.token || !authStore.tokenExpires) {\n return\n }\n\n const expiresAt = authStore.tokenExpires.getTime()\n const refreshAt = expiresAt - TOKEN_REFRESH_MARGIN_MS\n const now = Date.now()\n\n if (now >= refreshAt) {\n refreshToken()\n }\n }\n\n async function initializeAuth(): Promise<void> {\n authStore.initialize()\n await fetchAuthConfig()\n\n if (authStore.token) {\n const valid = await verifyToken()\n if (valid) {\n await getCurrentUser()\n // Start auto-refresh for existing session\n scheduleTokenRefresh()\n }\n }\n }\n\n function logout(): void {\n stopTokenRefresh()\n authStore.logout()\n }\n\n function getAuthHeader(): Record<string, string> {\n if (authStore.token) {\n return { Authorization: `Bearer ${authStore.token}` }\n }\n return {}\n }\n\n async function updateProfile(data: {\n email?: string\n shortname?: string\n currentPassword?: string\n newPassword?: string\n }): Promise<{ success: boolean; error?: string }> {\n if (!authStore.token) {\n return { success: false, error: 'Not authenticated' }\n }\n\n try {\n const requestData: UpdateProfileRequest = {}\n if (data.email !== undefined) requestData.email = data.email\n if (data.shortname !== undefined) requestData.shortname = data.shortname\n if (data.currentPassword) requestData.current_password = data.currentPassword\n if (data.newPassword) requestData.new_password = data.newPassword\n\n const response = await axios.put<UserResponse>(\n `${getApiBaseUrl()}/users/me`,\n requestData,\n { headers: getAuthHeader() }\n )\n\n const userInfo: UserInfo = {\n id: response.data.id,\n username: response.data.username,\n shortname: response.data.shortname,\n email: response.data.email,\n role: response.data.role,\n isActive: response.data.is_active,\n }\n authStore.setUserInfo(userInfo)\n\n return { success: true }\n } catch (error) {\n if (axios.isAxiosError(error) && error.response) {\n return { success: false, error: error.response.data.detail || 'Update failed' }\n }\n return { success: false, error: 'Network error. Please try again.' }\n }\n }\n\n // Set up periodic check as safety net (only inside component setup)\n let checkInterval: number | null = null\n\n if (getCurrentInstance()) {\n onMounted(() => {\n checkInterval = window.setInterval(checkAndRefreshIfNeeded, TOKEN_REFRESH_CHECK_INTERVAL_MS)\n })\n\n onUnmounted(() => {\n stopTokenRefresh()\n if (checkInterval !== null) {\n window.clearInterval(checkInterval)\n checkInterval = null\n }\n })\n\n // Watch for token changes to reschedule refresh\n watch(\n () => authStore.tokenExpires,\n (newExpires) => {\n if (newExpires) {\n scheduleTokenRefresh()\n } else {\n stopTokenRefresh()\n }\n }\n )\n }\n\n return {\n // Core auth methods\n login,\n logout,\n register,\n verifyToken,\n fetchAuthConfig,\n initializeAuth,\n getCurrentUser,\n getAuthHeader,\n updateProfile,\n\n // Token refresh\n refreshToken,\n isRefreshing,\n }\n}\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"useAuth.js","sources":["../../src/composables/useAuth.ts"],"sourcesContent":["import axios from 'axios'\nimport { ref, onMounted, onUnmounted, watch, getCurrentInstance, type Ref } from 'vue'\nimport { useAuthStore } from '../stores/auth'\nimport { useSettingsStore } from '../stores/settings'\nimport type { AuthConfig, UserInfo, LoginResponse, TokenVerifyResponse, UpdateProfileRequest } from '../types'\n\ninterface UserResponse {\n id: string\n username: string\n shortname: string | null\n email: string | null\n role: string\n is_active: boolean\n}\n\ninterface RefreshResponse {\n access_token: string\n expires_in: number\n token_type: string\n}\n\n// Token refresh configuration\nconst TOKEN_REFRESH_MARGIN_MS = 5 * 60 * 1000 // Refresh 5 minutes before expiry\nconst TOKEN_REFRESH_CHECK_INTERVAL_MS = 60 * 1000 // Check every minute\n\n/**\n * Authentication composable with automatic token refresh.\n *\n * Features:\n * - Automatic token refresh before expiration\n * - Login/logout/register functionality\n * - Token verification on startup\n * - User profile management\n *\n * @example\n * ```typescript\n * const { login, logout, isAuthenticated, user } = useAuth()\n *\n * // Login\n * const success = await login('username', 'password')\n *\n * // Automatic refresh is enabled by default\n * // Tokens are refreshed 5 minutes before expiration\n * ```\n */\nexport interface UseAuthReturn {\n login: (username: string, password: string) => Promise<boolean>\n logout: () => void\n register: (username: string, password: string, email?: string) => Promise<boolean>\n verifyToken: () => Promise<boolean>\n fetchAuthConfig: () => Promise<AuthConfig>\n initializeAuth: () => Promise<void>\n getCurrentUser: () => Promise<UserInfo | null>\n getAuthHeader: () => Record<string, string>\n updateProfile: (data: { email?: string; shortname?: string; currentPassword?: string; newPassword?: string }) => Promise<{ success: boolean; error?: string }>\n refreshToken: () => Promise<boolean>\n isRefreshing: Ref<boolean>\n}\n\n// Module-level singletons to prevent duplicate refresh requests and timers\n// across multiple useAuth() instances\nlet _refreshPromise: Promise<boolean> | null = null\nlet _refreshTimerId: number | null = null\nlet _mountedConsumerCount = 0\n\nexport function useAuth(): UseAuthReturn {\n const authStore = useAuthStore()\n const settingsStore = useSettingsStore()\n\n const isRefreshing = ref(false)\n\n function getApiBaseUrl(): string {\n return settingsStore.getApiBaseUrl()\n }\n\n async function fetchAuthConfig(): Promise<AuthConfig> {\n try {\n const response = await axios.get<{\n auth_required: boolean\n passkey_enabled: boolean\n passkey_registered?: boolean\n registration_enabled?: boolean\n database_mode?: string\n }>(`${getApiBaseUrl()}/setup/config/public`)\n\n const config: AuthConfig = {\n authRequired: response.data.auth_required,\n passkeyEnabled: response.data.passkey_enabled,\n passkeyRegistered: response.data.passkey_registered ?? false,\n registrationEnabled: response.data.registration_enabled ?? false,\n databaseMode: response.data.database_mode ?? 'none',\n }\n\n authStore.setAuthConfig(config)\n return config\n } catch (error) {\n console.error('Failed to fetch auth config:', error)\n return {\n authRequired: false,\n passkeyEnabled: false,\n passkeyRegistered: false,\n registrationEnabled: false,\n databaseMode: 'none',\n }\n }\n }\n\n async function login(username: string, password: string): Promise<boolean> {\n authStore.setLoading(true)\n authStore.setError(null)\n\n try {\n const response = await axios.post<LoginResponse>(\n `${getApiBaseUrl()}/auth/login`,\n { username, password }\n )\n\n authStore.setToken(response.data.access_token, response.data.expires_in)\n authStore.setUsername(username)\n\n await getCurrentUser()\n\n // Start auto-refresh after successful login\n scheduleTokenRefresh()\n\n return true\n } catch (error) {\n if (axios.isAxiosError(error) && error.response) {\n authStore.setError(error.response.data.detail || 'Login failed')\n } else {\n authStore.setError('Network error. Please try again.')\n }\n return false\n } finally {\n authStore.setLoading(false)\n }\n }\n\n async function register(username: string, password: string, email?: string): Promise<boolean> {\n authStore.setLoading(true)\n authStore.setError(null)\n\n try {\n await axios.post<UserResponse>(\n `${getApiBaseUrl()}/users/register`,\n { username, password, email }\n )\n\n return await login(username, password)\n } catch (error) {\n if (axios.isAxiosError(error) && error.response) {\n authStore.setError(error.response.data.detail || 'Registration failed')\n } else {\n authStore.setError('Network error. Please try again.')\n }\n return false\n } finally {\n authStore.setLoading(false)\n }\n }\n\n async function getCurrentUser(): Promise<UserInfo | null> {\n if (!authStore.token) {\n return null\n }\n\n try {\n const response = await axios.get<UserResponse>(\n `${getApiBaseUrl()}/users/me`,\n { headers: getAuthHeader() }\n )\n\n const userInfo: UserInfo = {\n id: response.data.id,\n username: response.data.username,\n shortname: response.data.shortname,\n email: response.data.email,\n role: response.data.role,\n isActive: response.data.is_active,\n }\n\n authStore.setUserInfo(userInfo)\n return userInfo\n } catch {\n return null\n }\n }\n\n async function verifyToken(): Promise<boolean> {\n if (!authStore.token) {\n return false\n }\n\n try {\n const response = await axios.get<TokenVerifyResponse>(\n `${getApiBaseUrl()}/auth/verify`,\n {\n headers: {\n Authorization: `Bearer ${authStore.token}`,\n },\n }\n )\n\n if (response.data.valid && response.data.username) {\n authStore.setUsername(response.data.username)\n return true\n }\n\n authStore.clearToken()\n return false\n } catch {\n authStore.clearToken()\n return false\n }\n }\n\n /**\n * Refresh the authentication token.\n * Called automatically before token expiration.\n * Uses promise caching to prevent concurrent refresh requests.\n */\n async function refreshToken(): Promise<boolean> {\n if (!authStore.token) return false\n if (_refreshPromise) return _refreshPromise\n\n _refreshPromise = (async () => {\n isRefreshing.value = true\n\n try {\n const response = await axios.post<RefreshResponse>(\n `${getApiBaseUrl()}/auth/refresh`,\n {},\n { headers: getAuthHeader() }\n )\n\n authStore.setToken(response.data.access_token, response.data.expires_in)\n\n // Reschedule next refresh\n scheduleTokenRefresh()\n\n return true\n } catch (error) {\n // If refresh fails, the token may have been revoked\n // Clear auth state and let user re-login\n if (axios.isAxiosError(error) && error.response?.status === 401) {\n console.warn('[Auth] Token refresh failed - session expired')\n authStore.clearToken()\n stopTokenRefresh()\n }\n return false\n } finally {\n isRefreshing.value = false\n _refreshPromise = null\n }\n })()\n\n return _refreshPromise\n }\n\n /**\n * Schedule automatic token refresh before expiration.\n */\n function scheduleTokenRefresh(): void {\n // Clear any existing timer\n stopTokenRefresh()\n\n if (!authStore.tokenExpires) {\n return\n }\n\n const expiresAt = authStore.tokenExpires.getTime()\n const refreshAt = expiresAt - TOKEN_REFRESH_MARGIN_MS\n const now = Date.now()\n\n if (refreshAt <= now) {\n // Token is already close to expiring or expired, refresh now\n refreshToken()\n return\n }\n\n // Schedule refresh\n const delay = refreshAt - now\n _refreshTimerId = window.setTimeout(() => {\n refreshToken()\n }, delay)\n }\n\n /**\n * Stop automatic token refresh.\n */\n function stopTokenRefresh(): void {\n if (_refreshTimerId !== null) {\n window.clearTimeout(_refreshTimerId)\n _refreshTimerId = null\n }\n }\n\n /**\n * Check if token needs refresh and refresh if necessary.\n * Called periodically as a safety net.\n */\n function checkAndRefreshIfNeeded(): void {\n if (!authStore.token || !authStore.tokenExpires) {\n return\n }\n\n const expiresAt = authStore.tokenExpires.getTime()\n const refreshAt = expiresAt - TOKEN_REFRESH_MARGIN_MS\n const now = Date.now()\n\n if (now >= refreshAt) {\n refreshToken()\n }\n }\n\n async function initializeAuth(): Promise<void> {\n authStore.initialize()\n await fetchAuthConfig()\n\n if (authStore.token) {\n const valid = await verifyToken()\n if (valid) {\n await getCurrentUser()\n // Start auto-refresh for existing session\n scheduleTokenRefresh()\n }\n }\n }\n\n function logout(): void {\n stopTokenRefresh()\n authStore.logout()\n }\n\n function getAuthHeader(): Record<string, string> {\n if (authStore.token) {\n return { Authorization: `Bearer ${authStore.token}` }\n }\n return {}\n }\n\n async function updateProfile(data: {\n email?: string\n shortname?: string\n currentPassword?: string\n newPassword?: string\n }): Promise<{ success: boolean; error?: string }> {\n if (!authStore.token) {\n return { success: false, error: 'Not authenticated' }\n }\n\n try {\n const requestData: UpdateProfileRequest = {}\n if (data.email !== undefined) requestData.email = data.email\n if (data.shortname !== undefined) requestData.shortname = data.shortname\n if (data.currentPassword) requestData.current_password = data.currentPassword\n if (data.newPassword) requestData.new_password = data.newPassword\n\n const response = await axios.put<UserResponse>(\n `${getApiBaseUrl()}/users/me`,\n requestData,\n { headers: getAuthHeader() }\n )\n\n const userInfo: UserInfo = {\n id: response.data.id,\n username: response.data.username,\n shortname: response.data.shortname,\n email: response.data.email,\n role: response.data.role,\n isActive: response.data.is_active,\n }\n authStore.setUserInfo(userInfo)\n\n return { success: true }\n } catch (error) {\n if (axios.isAxiosError(error) && error.response) {\n return { success: false, error: error.response.data.detail || 'Update failed' }\n }\n return { success: false, error: 'Network error. Please try again.' }\n }\n }\n\n // Set up periodic check as safety net (only inside component setup)\n let checkInterval: number | null = null\n\n if (getCurrentInstance()) {\n onMounted(() => {\n _mountedConsumerCount += 1\n checkInterval = window.setInterval(checkAndRefreshIfNeeded, TOKEN_REFRESH_CHECK_INTERVAL_MS)\n })\n\n onUnmounted(() => {\n if (checkInterval !== null) {\n window.clearInterval(checkInterval)\n checkInterval = null\n }\n\n _mountedConsumerCount = Math.max(0, _mountedConsumerCount - 1)\n if (_mountedConsumerCount === 0) {\n stopTokenRefresh()\n }\n })\n\n // Watch for token changes to reschedule refresh\n watch(\n () => authStore.tokenExpires,\n (newExpires) => {\n if (newExpires) {\n scheduleTokenRefresh()\n } else {\n stopTokenRefresh()\n }\n }\n )\n }\n\n return {\n // Core auth methods\n login,\n logout,\n register,\n verifyToken,\n fetchAuthConfig,\n initializeAuth,\n getCurrentUser,\n getAuthHeader,\n updateProfile,\n\n // Token refresh\n refreshToken,\n isRefreshing,\n }\n}\n"],"names":[],"mappings":"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"}
|