PluginProbe
Extendify / 1.17.1
Extendify v1.17.1
3.2.0 3.1.6 3.1.5 3.1.4 3.1.3 3.1.2 3.1.1 3.1.0 3.0.6 3.0.5 3.0.4 trunk 0.1.0 0.10.0 0.10.1 0.10.2 0.11.0 0.11.1 0.2.0 0.3.0 0.3.1 0.4.0 0.5.0 0.6.0 0.7.0 All 126 releases
extendify / src / HelpCenter / state / tours.js

tours.js in Extendify 1.17.1, at src/HelpCenter/state/tours.js

227 lines 6.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 import apiFetch from '@wordpress/api-fetch';
2 import { safeParseJson } from '@shared/lib/parsing';
3 import { create } from 'zustand';
4 import { devtools, persist, createJSONStorage } from 'zustand/middleware';
5
6 const key = 'extendify-help-center-tour-progress';
7 const startingState = {
8 currentTour: null,
9 currentStep: undefined,
10 preparingStep: undefined,
11 progress: [],
12 // initialize the state with default values
13 ...(safeParseJson(window.extHelpCenterData.userData.tourData)?.state ?? {}),
14 };
15
16 const state = (set, get) => ({
17 ...startingState,
18 startTour: async (tourData) => {
19 const { trackTourProgress, updateProgress, getStepData, onTourPage } =
20 get();
21
22 if (onTourPage(tourData?.settings?.startFrom)) {
23 await tourData?.onStart?.(tourData);
24 tourData.steps =
25 tourData.steps?.filter(
26 // Filter out steps that define a condition
27 (s) => s?.showOnlyIf?.() || s?.showOnlyIf?.() === undefined,
28 ) || [];
29 await getStepData(0, tourData)?.events?.beforeAttach?.(tourData);
30 }
31
32 set({ currentTour: tourData, currentStep: 0, preparingStep: undefined });
33 // Increment the opened count
34 const tour = trackTourProgress(tourData.id);
35 updateProgress(tour.id, {
36 openedCount: Number(tour.openedCount) + 1,
37 lastAction: 'started',
38 });
39 },
40 onTourPage: (startFrom = null) => {
41 const url = window.location.href;
42 if (startFrom?.includes(url)) return true;
43 const { currentTour } = get();
44 return currentTour?.settings?.startFrom?.includes(url);
45 },
46 completeCurrentTour: async () => {
47 const { currentTour, wasCompleted, findTourProgress, updateProgress } =
48 get();
49 const tour = findTourProgress(currentTour?.id);
50 if (!tour?.id) return;
51 // if already completed, don't update the completedAt
52 if (!wasCompleted(tour.id)) {
53 updateProgress(tour.id, {
54 completedAt: new Date().toISOString(),
55 lastAction: 'completed',
56 });
57 }
58 // Track how many times it was completed
59 updateProgress(tour.id, {
60 completedCount: Number(tour.completedCount) + 1,
61 lastAction: 'completed',
62 });
63 await currentTour?.onDetach?.();
64 await currentTour?.onFinish?.();
65 set({ currentTour: null, currentStep: undefined });
66
67 // fire an event to update the site assistant tour status in assistant code base.
68 if (tour?.id === 'site-assistant-tour') {
69 window.dispatchEvent(
70 new CustomEvent('extendify-assist:is-tour-finished', {
71 detail: { isFinished: true },
72 }),
73 );
74 }
75 },
76 closeCurrentTour: async (lastAction) => {
77 const { currentTour, findTourProgress, updateProgress } = get();
78 const tour = findTourProgress(currentTour?.id);
79 if (!tour?.id) return;
80 const additional = {};
81 if (['redirected'].includes(lastAction)) {
82 return updateProgress(tour?.id, { lastAction });
83 }
84 if (['closed-by-caught-error'].includes(lastAction)) {
85 return updateProgress(tour?.id, { lastAction, errored: true });
86 }
87 if (lastAction === 'closed-manually') {
88 additional.closedManuallyCount = Number(tour.closedManuallyCount) + 1;
89 }
90
91 await currentTour?.onDetach?.();
92 await currentTour?.onFinish?.();
93 updateProgress(tour?.id, { lastAction, ...additional });
94 set({
95 currentTour: null,
96 currentStep: undefined,
97 preparingStep: undefined,
98 });
99 },
100 findTourProgress: (tourId) =>
101 get().progress.find((tour) => tour.id === tourId),
102 wasCompleted: (tourId) => get().findTourProgress(tourId)?.completedAt,
103 wasOpened: (tourId) =>
104 Number(get().findTourProgress(tourId)?.openedCount ?? 0) > 0,
105 isSeen: (tourId) => get().findTourProgress(tourId)?.firstSeenAt,
106 trackTourProgress: (tourId) => {
107 const { findTourProgress } = get();
108 // If we are already tracking it, return that
109 if (findTourProgress(tourId)) {
110 return findTourProgress(tourId);
111 }
112 set((state) => ({
113 progress: [
114 ...state.progress,
115 {
116 id: tourId,
117 firstSeenAt: new Date().toISOString(),
118 updatedAt: new Date().toISOString(),
119 completedAt: null,
120 lastAction: 'init',
121 currentStep: 0,
122 openedCount: 0,
123 closedManuallyCount: 0,
124 completedCount: 0,
125 errored: false,
126 },
127 ],
128 }));
129 return findTourProgress(tourId);
130 },
131 updateProgress: (tourId, update) => {
132 const lastAction = update?.lastAction ?? 'unknown';
133 set((state) => {
134 const progress = state.progress.map((tour) => {
135 if (tour.id === tourId) {
136 return {
137 ...tour,
138 ...update,
139 lastAction,
140 updatedAt: new Date().toISOString(),
141 };
142 }
143 return tour;
144 });
145 return { progress };
146 });
147 },
148 getStepData: (step, tour = get().currentTour) => tour?.steps?.[step] ?? {},
149 hasNextStep: () => {
150 if (!get().currentTour) return false;
151 return Number(get().currentStep) < get().currentTour.steps.length - 1;
152 },
153 nextStep: async () => {
154 const { currentTour, goToStep, updateProgress, currentStep } = get();
155 const step = Number(currentStep) + 1;
156 await goToStep(step);
157 updateProgress(currentTour.id, {
158 currentStep: step,
159 lastAction: 'next',
160 });
161 },
162 hasPreviousStep: () => {
163 if (!get().currentTour) return false;
164 return Number(get().currentStep) > 0;
165 },
166 prevStep: async () => {
167 const { currentTour, goToStep, updateProgress, currentStep } = get();
168 const step = currentStep - 1;
169 await goToStep(step);
170 updateProgress(currentTour.id, {
171 currentStep: step,
172 lastAction: 'prev',
173 });
174 },
175 goToStep: async (step) => {
176 const { currentTour, updateProgress, closeCurrentTour, getStepData } =
177 get();
178 const tour = currentTour;
179
180 // Check that the step is valid
181 if (step < 0 || step > tour.steps.length - 1) {
182 closeCurrentTour('closed-by-caught-error');
183 return;
184 }
185
186 updateProgress(tour.id, {
187 currentStep: step,
188 lastAction: `go-to-step-${step}`,
189 });
190
191 const events = getStepData(step)?.events;
192
193 if (events?.beforeAttach) {
194 set(() => ({ preparingStep: step }));
195 // Make sure the preparing animation runs at least 300ms
196 await Promise.allSettled([
197 events.beforeAttach?.(tour),
198 new Promise((resolve) => setTimeout(resolve, 300)),
199 ]);
200 set(() => ({ preparingStep: undefined }));
201 }
202
203 set(() => ({ currentStep: step }));
204 },
205 });
206
207 const path = '/extendify/v1/help-center/tour-data';
208 const storage = {
209 getItem: async () => await apiFetch({ path }),
210 setItem: async (_name, state) =>
211 await apiFetch({ path, method: 'POST', data: { state } }),
212 };
213
214 export const useTourStore = create(
215 persist(devtools(state, { name: 'Extendify Tour Progress' }), {
216 name: key,
217 storage: createJSONStorage(() => storage),
218 skipHydration: true,
219 partialize: (state) => {
220 // return without currentTour or currentStep
221 // eslint-disable-next-line no-unused-vars
222 const { currentTour, currentStep, preparingStep, ...newState } = state;
223 return newState;
224 },
225 }),
226 );
227