Skip to content

Commit 5cb7999

Browse files
add tourStore that keeps track of a running tour
We mount `TourRunner` in `App.vue` instead of `Analysis.vue`, because this way we can have a tour run across all routes (even workflow editor, published views etc.). Note that an entire page reload resets the store still... Co-authored-by: Aysam Guerler <aysam.guerler@gmail.com>
1 parent 8c9d67d commit 5cb7999

6 files changed

Lines changed: 90 additions & 38 deletions

File tree

client/src/components/Tour/Tour.vue

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { useRouter } from "vue-router/composables";
77
import { isAdminUser, isAnonymousUser } from "@/api";
88
import type { TourRequirements, TourStep as TourStepType } from "@/api/tours";
99
import { useHistoryStore } from "@/stores/historyStore";
10+
import { useTourStore } from "@/stores/tourStore";
1011
import { useUserStore } from "@/stores/userStore";
1112
import { errorMessageAsString } from "@/utils/simple-error";
1213
@@ -32,7 +33,7 @@ type TourStepWithActions = TourStepType & {
3233
const props = defineProps<{
3334
steps: (TourStepType | TourStepWithActions)[];
3435
requirements: TourRequirements;
35-
tourId?: string;
36+
tourId: string;
3637
waitingOnElement?: string | null;
3738
onBefore?: (step: TourStepType) => Promise<void>;
3839
onNext?: (step: TourStepType) => Promise<void>;
@@ -42,16 +43,29 @@ const emit = defineEmits(["end-tour"]);
4243
4344
const router = useRouter();
4445
45-
const currentIndex = ref(-1);
4646
const errorMessage = ref("");
4747
const isPlaying = ref(false);
4848
4949
// Store variables
5050
const historyStore = useHistoryStore();
5151
const { currentHistory, historiesLoading } = storeToRefs(historyStore);
5252
const { currentUser } = storeToRefs(useUserStore());
53+
const tourStore = useTourStore();
54+
const { currentTour } = storeToRefs(tourStore);
5355
54-
// Step variables
56+
/** The current step index
57+
*
58+
* This is set and updated based on the currently active tour in the `tourStore`.
59+
* Is `-1` if no tour is currently active (or has ended).
60+
*/
61+
const currentIndex = computed({
62+
get: () => (currentTour.value?.step !== undefined ? currentTour.value?.step : -1),
63+
set: (val: number) => {
64+
tourStore.setTour(props.tourId, val);
65+
},
66+
});
67+
68+
// Local step variables
5569
const currentStep = computed(() => props.steps[currentIndex.value]);
5670
const numberOfSteps = computed(() => props.steps.length);
5771
const isFirst = computed(() => currentIndex.value === 0);
@@ -101,7 +115,7 @@ const modalContents = computed<{
101115
ok: async () => {
102116
endTour();
103117
if (router) {
104-
router.push(`/login/start${props.tourId ? `?redirect=/tours/${props.tourId}` : ""}`);
118+
router.push(`/login/start?redirect=/tours/${props.tourId}`);
105119
}
106120
},
107121
};
@@ -117,7 +131,7 @@ const modalContents = computed<{
117131
endTour();
118132
if (router) {
119133
if (isAnonymousUser(currentUser.value)) {
120-
router.push(`/login/start${props.tourId ? `?redirect=/tours/${props.tourId}` : ""}`);
134+
router.push(`/login/start?redirect=/tours/${props.tourId}`);
121135
} else {
122136
router.push("/");
123137
}
@@ -172,7 +186,6 @@ onUnmounted(() => {
172186
173187
function start() {
174188
window.addEventListener("keyup", handleKeyup);
175-
currentIndex.value = 0;
176189
}
177190
178191
function play(isCurrentlyPlaying: boolean) {
@@ -228,11 +241,10 @@ async function next() {
228241
* _In the case that_ `TourRunner` _is the parent, this will unmount the component._
229242
*/
230243
function endTour() {
231-
currentIndex.value = -1;
244+
tourStore.setTour(undefined);
232245
isPlaying.value = false;
233246
errorMessage.value = "";
234247
235-
// IMPORTANT: This is what unmounts the `TourRunner` component when that is the parent
236248
emit("end-tour");
237249
}
238250

client/src/components/Tour/TourRunner.vue

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { ref } from "vue";
33
44
import { getTourData, type TourRequirements, type TourStep } from "@/api/tours";
55
import { Toast } from "@/composables/toast";
6+
import { useTourStore } from "@/stores/tourStore";
67
import { errorMessageAsString } from "@/utils/simple-error";
78
89
import Tour from "./Tour.vue";
@@ -18,6 +19,8 @@ const props = defineProps<{
1819
1920
const emit = defineEmits(["end-tour"]);
2021
22+
const tourStore = useTourStore();
23+
2124
const steps = ref<TourStep[]>([]);
2225
const requirements = ref<TourRequirements>([]);
2326
const ready = ref(false);
@@ -37,6 +40,7 @@ async function initialize() {
3740
ready.value = true;
3841
} catch (error) {
3942
Toast.error(errorMessageAsString(error), "Failed to start tour");
43+
tourStore.setTour(undefined);
4044
}
4145
}
4246

client/src/components/Tour/runTour.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,5 +132,5 @@ export async function runTour(tourId, tourData = null) {
132132
});
133133
});
134134
const requirements = tourData.requirements || [];
135-
return mountTour({ steps, requirements });
135+
return mountTour({ steps, requirements, tourId });
136136
}

client/src/entry/analysis/App.vue

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
<UploadModal ref="uploadModal" />
4343
<BroadcastsOverlay />
4444
<DragGhost />
45+
<TourRunner v-if="currentTour?.id" :key="currentTour.id" :tour-id="currentTour.id" />
4546
</template>
4647
</div>
4748
</template>
@@ -65,11 +66,13 @@ import { useRouteQueryBool } from "@/composables/route";
6566
import { useEntryPointStore } from "@/stores/entryPointStore";
6667
import { useHistoryStore } from "@/stores/historyStore";
6768
import { useNotificationsStore } from "@/stores/notificationsStore";
69+
import { useTourStore } from "@/stores/tourStore";
6870
import { useUserStore } from "@/stores/userStore";
6971
7072
import Alert from "@/components/Alert.vue";
7173
import DragGhost from "@/components/DragGhost.vue";
7274
import BroadcastsOverlay from "@/components/Notifications/Broadcasts/BroadcastsOverlay.vue";
75+
import TourRunner from "@/components/Tour/TourRunner.vue";
7376
import Masthead from "components/Masthead/Masthead.vue";
7477
import UploadModal from "components/Upload/UploadModal.vue";
7578
@@ -82,11 +85,15 @@ export default {
8285
ConfirmDialog,
8386
UploadModal,
8487
BroadcastsOverlay,
88+
TourRunner,
8589
},
8690
directives: {
8791
short,
8892
},
8993
setup() {
94+
const tourStore = useTourStore();
95+
const { currentTour } = storeToRefs(tourStore);
96+
9097
const userStore = useUserStore();
9198
const { currentTheme } = storeToRefs(userStore);
9299
const { currentHistory } = storeToRefs(useHistoryStore());
@@ -124,7 +131,16 @@ export default {
124131
if (confirmation.value) {
125132
confirmation.value = null;
126133
}
127-
}
134+
135+
// if we are on a tour route, start a tour if it wasn't already started or change tours
136+
if ("tourId" in route.params) {
137+
const routeTourId = route.params.tourId === "null" ? null : route.params.tourId;
138+
if (routeTourId !== currentTour.value?.id) {
139+
tourStore.setTour(routeTourId);
140+
}
141+
}
142+
},
143+
{ immediate: true }
128144
);
129145
130146
return {
@@ -135,6 +151,7 @@ export default {
135151
currentTheme,
136152
currentHistory,
137153
embedded,
154+
currentTour,
138155
};
139156
},
140157
data() {
Lines changed: 2 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<script setup>
22
import { storeToRefs } from "pinia";
3-
import { onMounted, onUnmounted, ref, watch } from "vue";
4-
import { useRoute, useRouter } from "vue-router/composables";
3+
import { onMounted, onUnmounted, ref } from "vue";
4+
import { useRouter } from "vue-router/composables";
55
66
import { usePanels } from "@/composables/usePanels";
77
import { useUserStore } from "@/stores/userStore";
@@ -10,40 +10,15 @@ import CenterFrame from "./CenterFrame.vue";
1010
import ActivityBar from "@/components/ActivityBar/ActivityBar.vue";
1111
import HistoryIndex from "@/components/History/Index.vue";
1212
import FlexPanel from "@/components/Panels/FlexPanel.vue";
13-
import TourRunner from "@/components/Tour/TourRunner.vue";
1413
import DragAndDropModal from "@/components/Upload/DragAndDropModal.vue";
1514
1615
const router = useRouter();
17-
const route = useRoute();
1816
1917
const showCenter = ref(false);
2018
const { showPanels } = usePanels();
2119
2220
const { historyPanelWidth } = storeToRefs(useUserStore());
2321
24-
/** Tour state - set manually */
25-
const isTourRoute = ref(false);
26-
/** Current tour ID - set manually */
27-
const currentTourId = ref(null);
28-
29-
// Watch for route changes to detect tour routes
30-
watch(
31-
() => route,
32-
(newRoute) => {
33-
if (newRoute.path.startsWith("/tours/") && newRoute.params.tourId) {
34-
isTourRoute.value = true;
35-
currentTourId.value = newRoute.params.tourId;
36-
}
37-
},
38-
{ immediate: true, deep: true }
39-
);
40-
41-
// methods
42-
function endTour() {
43-
isTourRoute.value = false;
44-
currentTourId.value = null;
45-
}
46-
4722
function hideCenter() {
4823
showCenter.value = false;
4924
}
@@ -77,6 +52,5 @@ onUnmounted(() => {
7752
<HistoryIndex />
7853
</FlexPanel>
7954
<DragAndDropModal />
80-
<TourRunner v-if="isTourRoute" :key="currentTourId" :tour-id="currentTourId" @end-tour="endTour" />
8155
</div>
8256
</template>

client/src/stores/tourStore.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { defineStore } from "pinia";
2+
import { ref, watch } from "vue";
3+
4+
import { useUserLocalStorage } from "@/composables/userLocalStorage";
5+
6+
export const useTourStore = defineStore("tourStore", () => {
7+
/** Track what tour ID we are expecting, to prevent any unintended localStorage
8+
* reactivity problems */
9+
const localTourId = ref<string>("");
10+
11+
const currentTour = useUserLocalStorage<{ id: string; step: number } | undefined>(
12+
"currently-active-tour",
13+
undefined
14+
);
15+
16+
function setTour(tourId: string | undefined, step = 0) {
17+
localTourId.value = tourId || "";
18+
if (tourId) {
19+
currentTour.value = { id: tourId, step };
20+
} else {
21+
currentTour.value = undefined;
22+
}
23+
}
24+
25+
watch(
26+
() => currentTour.value?.id,
27+
(newVal, oldVal) => {
28+
if (newVal === oldVal) {
29+
return;
30+
}
31+
32+
// If localStorage is setting a different tour than what we expect, ignore it
33+
const expectedTour = localTourId.value || undefined;
34+
if (expectedTour && newVal !== expectedTour && oldVal === expectedTour) {
35+
setTour(expectedTour);
36+
}
37+
},
38+
{ immediate: true }
39+
);
40+
41+
return {
42+
currentTour,
43+
setTour,
44+
};
45+
});

0 commit comments

Comments
 (0)