• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

teableio / teable / 8479685547

29 Mar 2024 09:57AM CUT coverage: 21.612% (-0.001%) from 21.613%
8479685547

Pull #510

github

web-flow
Merge 6ef5e653f into bd248f296
Pull Request #510: feat: support import result notify

1395 of 2507 branches covered (55.64%)

0 of 12 new or added lines in 3 files covered. (0.0%)

1 existing line in 1 file now uncovered.

14588 of 67501 relevant lines covered (21.61%)

2.06 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.0
/apps/nextjs-app/src/components/Guide.tsx
1
import type { IUserMeVo } from '@teable/openapi';
×
2
import dynamic from 'next/dynamic';
×
3
import { useRouter } from 'next/router';
×
4
import { useTranslation, Trans } from 'next-i18next';
×
5
import { useEffect, useMemo, useRef, useState } from 'react';
×
6
import { ACTIONS, EVENTS, STATUS } from 'react-joyride';
×
7
import type { CallBackProps, Step, StoreHelpers } from 'react-joyride';
×
8
import colors from 'tailwindcss/colors';
×
9
import { tableConfig } from '@/features/i18n/table.config';
×
10
import { useCompletedGuideMapStore } from './store';
×
11

×
12
const JoyRideNoSSR = dynamic(() => import('react-joyride'), { ssr: false });
×
13

×
14
export const GUIDE_PREFIX = 't-guide-';
×
15

×
16
export const GUIDE_CREATE_SPACE = GUIDE_PREFIX + 'create-space';
×
17
export const GUIDE_CREATE_BASE = GUIDE_PREFIX + 'create-base';
×
18
export const GUIDE_CREATE_TABLE = GUIDE_PREFIX + 'create-table';
×
19
export const GUIDE_CREATE_VIEW = GUIDE_PREFIX + 'create-view';
×
20
export const GUIDE_VIEW_FILTERING = GUIDE_PREFIX + 'view-filtering';
×
21
export const GUIDE_VIEW_SORTING = GUIDE_PREFIX + 'view-sorting';
×
22
export const GUIDE_VIEW_GROUPING = GUIDE_PREFIX + 'view-grouping';
×
23
export const GUIDE_API_BUTTON = GUIDE_PREFIX + 'api-button';
×
24

×
25
export enum StepKey {
×
26
  CreateSpace = 'createSpace',
×
27
  CreateBase = 'createBase',
×
28
  CreateTable = 'createTable',
×
29
  CreateView = 'createView',
×
30
  ViewFiltering = 'viewFiltering',
×
31
  ViewSorting = 'viewSorting',
×
32
  ViewGrouping = 'viewGrouping',
×
33
  ApiButton = 'apiButton',
×
34
}
×
35

×
36
type EnhanceStep = { key: StepKey; step: Step };
×
37

×
38
const findStepsForPath = (
×
39
  guideMap: Record<string, EnhanceStep[]>,
×
40
  path: string
×
41
): EnhanceStep[] | null => {
×
42
  if (guideMap[path]) {
×
43
    return guideMap[path];
×
44
  }
×
45

×
46
  const includePath = Object.keys(guideMap).find((p) => path.includes(p));
×
47

×
48
  if (includePath) {
×
49
    return guideMap[includePath];
×
50
  }
×
51

×
52
  return null;
×
53
};
×
54

×
55
export const Guide = ({ user }: { user?: IUserMeVo }) => {
×
56
  const router = useRouter();
×
57
  const { t } = useTranslation(tableConfig.i18nNamespaces);
×
58
  const { completedGuideMap, setCompletedGuideMap } = useCompletedGuideMapStore();
×
59

×
60
  const helpers = useRef<StoreHelpers>();
×
61
  const [run, setRun] = useState(false);
×
62
  const [steps, setSteps] = useState<Step[]>([]);
×
63
  const [stepIndex, setStepIndex] = useState(0);
×
64

×
65
  const userId = user?.id;
×
66
  const { pathname, isReady } = router;
×
67

×
68
  const guideStepMap: Record<StepKey, Step> = useMemo(
×
69
    () => ({
×
70
      [StepKey.CreateSpace]: {
×
71
        target: `.${GUIDE_CREATE_SPACE}`,
×
72
        title: <div className="text-base">{t('guide.createSpaceTooltipTitle')}</div>,
×
73
        content: (
×
74
          <div className="text-left text-[13px]">
×
75
            <Trans
×
76
              ns="common"
×
77
              i18nKey="guide.createSpaceTooltipContent"
×
78
              components={{ br: <br /> }}
×
79
            />
×
80
          </div>
×
81
        ),
×
82
        disableBeacon: true,
×
83
      },
×
84
      [StepKey.CreateBase]: {
×
85
        target: `.${GUIDE_CREATE_BASE}`,
×
86
        title: <div className="text-base">{t('guide.createBaseTooltipTitle')}</div>,
×
87
        content: <div className="text-left text-[13px]">{t('guide.createBaseTooltipContent')}</div>,
×
88
        disableBeacon: true,
×
89
      },
×
90
      [StepKey.CreateTable]: {
×
91
        target: `.${GUIDE_CREATE_TABLE}`,
×
92
        title: <div className="text-base">{t('guide.createTableTooltipTitle')}</div>,
×
93
        content: (
×
94
          <div className="text-left text-[13px]">{t('guide.createTableTooltipContent')}</div>
×
95
        ),
×
96
        disableBeacon: true,
×
97
        placement: 'right',
×
98
      },
×
99
      [StepKey.CreateView]: {
×
100
        target: `.${GUIDE_CREATE_VIEW}`,
×
101
        title: <div className="text-base">{t('guide.createViewTooltipTitle')}</div>,
×
102
        content: (
×
103
          <div className="text-left text-[13px]">
×
104
            <Trans
×
105
              ns="common"
×
106
              i18nKey="guide.createViewTooltipContent"
×
107
              components={{ br: <br /> }}
×
108
            />
×
109
          </div>
×
110
        ),
×
111
        disableBeacon: true,
×
112
      },
×
113
      [StepKey.ViewFiltering]: {
×
114
        target: `.${GUIDE_VIEW_FILTERING}`,
×
115
        title: <div className="text-base">{t('guide.viewFilteringTooltipTitle')}</div>,
×
116
        content: (
×
117
          <div className="text-left text-[13px]">
×
118
            <Trans
×
119
              ns="common"
×
120
              i18nKey="guide.viewFilteringTooltipContent"
×
121
              components={{ br: <br /> }}
×
122
            />
×
123
          </div>
×
124
        ),
×
125
        disableBeacon: true,
×
126
      },
×
127
      [StepKey.ViewSorting]: {
×
128
        target: `.${GUIDE_VIEW_SORTING}`,
×
129
        title: <div className="text-base">{t('guide.viewSortingTooltipTitle')}</div>,
×
130
        content: (
×
131
          <div className="text-left text-[13px]">
×
132
            <Trans
×
133
              ns="common"
×
134
              i18nKey="guide.viewSortingTooltipContent"
×
135
              components={{ br: <br /> }}
×
136
            />
×
137
          </div>
×
138
        ),
×
139
        disableBeacon: true,
×
140
      },
×
141
      [StepKey.ViewGrouping]: {
×
142
        target: `.${GUIDE_VIEW_GROUPING}`,
×
143
        title: <div className="text-base">{t('guide.viewGroupingTooltipTitle')}</div>,
×
144
        content: (
×
145
          <div className="text-left text-[13px]">{t('guide.viewGroupingTooltipContent')}</div>
×
146
        ),
×
147
        disableBeacon: true,
×
148
      },
×
149
      [StepKey.ApiButton]: {
×
150
        target: `.${GUIDE_API_BUTTON}`,
×
151
        title: <div className="text-base">{t('guide.apiButtonTooltipTitle')}</div>,
×
152
        content: (
×
153
          <div className="text-left text-[13px]">
×
154
            <Trans
×
155
              ns="common"
×
156
              i18nKey="guide.apiButtonTooltipContent"
×
157
              components={{
×
158
                a: (
×
159
                  // eslint-disable-next-line jsx-a11y/anchor-has-content
×
160
                  <a
×
161
                    className="text-violet-500"
×
162
                    href="/setting/personal-access-token"
×
163
                    target="_blank"
×
164
                  />
×
165
                ),
×
166
              }}
×
167
            />
×
168
          </div>
×
169
        ),
×
170
        disableBeacon: true,
×
171
      },
×
172
    }),
×
173
    [t]
×
174
  );
×
175

×
176
  const orderedGuideMap: Record<string, EnhanceStep[]> = useMemo(
×
177
    () => ({
×
178
      '/space': [
×
179
        { key: StepKey.CreateSpace, step: guideStepMap[StepKey.CreateSpace] },
×
180
        { key: StepKey.CreateBase, step: guideStepMap[StepKey.CreateBase] },
×
181
      ],
×
182
      '/base/[baseId]': [{ key: StepKey.CreateTable, step: guideStepMap[StepKey.CreateTable] }],
×
183
      '/base/[baseId]/[tableId]/[viewId]': [
×
184
        { key: StepKey.CreateTable, step: guideStepMap[StepKey.CreateTable] },
×
185
        { key: StepKey.CreateView, step: guideStepMap[StepKey.CreateView] },
×
186
        { key: StepKey.ViewFiltering, step: guideStepMap[StepKey.ViewFiltering] },
×
187
        { key: StepKey.ViewSorting, step: guideStepMap[StepKey.ViewSorting] },
×
188
        { key: StepKey.ViewGrouping, step: guideStepMap[StepKey.ViewGrouping] },
×
189
        { key: StepKey.ApiButton, step: guideStepMap[StepKey.ApiButton] },
×
190
      ],
×
191
    }),
×
192
    [guideStepMap]
×
193
  );
×
194

×
195
  const getHelpers = (storeHelpers: StoreHelpers) => {
×
196
    helpers.current = storeHelpers;
×
197
  };
×
198

×
199
  const onCallback = (data: CallBackProps) => {
×
200
    const { action, index, status, type } = data;
×
201

×
202
    if ([ACTIONS.CLOSE, ACTIONS.SKIP].includes(action as never)) {
×
203
      setRun(false);
×
204
      if (!userId) return;
×
205
      return setCompletedGuideMap(userId, Object.keys(guideStepMap));
×
206
    }
×
207

×
208
    if ([EVENTS.STEP_AFTER, EVENTS.TARGET_NOT_FOUND].includes(type as never)) {
×
209
      setStepIndex(index + (action === ACTIONS.PREV ? -1 : 1));
×
210
    } else if (status === STATUS.FINISHED || type === EVENTS.TOUR_END) {
×
211
      setRun(false);
×
212

×
213
      if (!userId) return;
×
214
      const prevCompletedStepKeys = completedGuideMap[userId] || [];
×
215
      const enhanceSteps = findStepsForPath(orderedGuideMap, pathname);
×
216
      if (!enhanceSteps?.length) return;
×
217
      setCompletedGuideMap(userId, [
×
218
        ...new Set([...prevCompletedStepKeys, ...enhanceSteps.map(({ key }) => key)]),
×
219
      ]);
×
220
    }
×
221
  };
×
222

×
223
  useEffect(() => {
×
224
    const resetGuide = () => {
×
225
      setStepIndex(0);
×
226
      helpers.current?.reset(false);
×
227
    };
×
228

×
229
    router.events.on('routeChangeStart', resetGuide);
×
230

×
231
    return () => {
×
232
      router.events.off('routeChangeStart', resetGuide);
×
233
    };
×
234
  }, [router.events, setStepIndex]);
×
235

×
236
  useEffect(() => {
×
237
    if (!isReady) return;
×
238

×
239
    let enhanceSteps = findStepsForPath(orderedGuideMap, pathname);
×
240

×
241
    if (!enhanceSteps?.length) return;
×
242

×
243
    if (userId) {
×
244
      const prevCompletedSteps = completedGuideMap[userId] || [];
×
245

×
246
      if (prevCompletedSteps.length) {
×
247
        enhanceSteps = enhanceSteps.filter(({ key }) => !prevCompletedSteps.includes(key));
×
248
      }
×
249
    }
×
250

×
251
    if (!enhanceSteps.length) return;
×
252

×
253
    const steps = enhanceSteps.map(({ step }) => step);
×
254

×
255
    let retryCount = 0;
×
256
    let timer: number | undefined;
×
257

×
258
    timer = window.setInterval(() => {
×
259
      const step = steps[stepIndex];
×
260

×
261
      if (!step) {
×
262
        clearInterval(timer);
×
263
        timer = undefined;
×
264
        return;
×
265
      }
×
266

×
267
      const targetElement = document.querySelector(step.target as string);
×
268

×
269
      if (targetElement) {
×
270
        clearInterval(timer);
×
271
        timer = undefined;
×
272
        setSteps(steps);
×
273
        setRun(true);
×
274
        setTimeout(() => helpers.current?.reset(true), 100);
×
275
      } else {
×
276
        if (++retryCount >= 100) {
×
277
          clearInterval(timer);
×
278
          timer = undefined;
×
279
        }
×
280
      }
×
281
    }, 50);
×
282

×
283
    return () => {
×
284
      clearInterval(timer);
×
285
      timer = undefined;
×
286
    };
×
287
  }, [completedGuideMap, isReady, orderedGuideMap, pathname, stepIndex, userId]);
×
288

×
289
  return (
×
290
    <JoyRideNoSSR
×
291
      run={run}
×
292
      steps={steps}
×
293
      stepIndex={stepIndex}
×
294
      spotlightPadding={8}
×
295
      continuous
×
296
      showSkipButton
×
297
      hideBackButton
×
298
      hideCloseButton
×
299
      disableCloseOnEsc
×
300
      disableOverlayClose
×
301
      disableScrollParentFix
×
302
      styles={{
×
303
        options: {
×
304
          primaryColor: colors.black,
×
305
          width: 320,
×
306
        },
×
307
        tooltip: {
×
308
          padding: 12,
×
309
        },
×
310
        tooltipContent: {
×
311
          padding: 8,
×
312
          lineHeight: '22px',
×
313
        },
×
314
        buttonClose: {
×
315
          width: 10,
×
316
          height: 10,
×
317
          outline: 'none',
×
318
        },
×
319
        buttonNext: {
×
320
          fontSize: 13,
×
321
          padding: '8px 16px',
×
322
          outline: 'none',
×
323
        },
×
324
        buttonBack: {
×
325
          fontSize: 13,
×
326
          padding: '8px 16px',
×
327
          outline: 'none',
×
328
        },
×
329
        buttonSkip: {
×
330
          fontSize: 13,
×
331
          padding: '8px 16px',
×
332
          outline: 'none',
×
333
        },
×
334
        tooltipFooter: {
×
335
          marginTop: 8,
×
336
        },
×
337
        spotlight: {
×
338
          border: `1px solid ${colors.white}`,
×
339
          borderRadius: 8,
×
340
        },
×
341
      }}
×
342
      getHelpers={getHelpers}
×
343
      callback={onCallback}
×
344
      locale={{
×
345
        back: t('guide.prev'),
×
346
        next: t('guide.next'),
×
347
        last: t('guide.done'),
×
348
        skip: t('guide.skip'),
×
349
      }}
×
350
    />
×
351
  );
×
352
};
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc