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

ringcentral / ringcentral-js-widgets / 4878562030

pending completion
4878562030

push

github

GitHub
sync features and bugfixs from e3d1ac7 (#1714)

1724 of 6102 branches covered (28.25%)

Branch coverage included in aggregate %.

751 of 2684 new or added lines in 97 files covered. (27.98%)

457 existing lines in 34 files now uncovered.

3248 of 8911 relevant lines covered (36.45%)

18.43 hits per line

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

21.38
/packages/ringcentral-integration/modules/UserGuide/UserGuide.ts
1
import { includes } from 'ramda';
2

3
import {
4
  action,
5
  computed,
6
  RcModuleV2,
7
  state,
8
  storage,
9
  track,
10
  watch,
11
} from '@ringcentral-integration/core';
12

13
import { Module } from '../../lib/di';
14
import { proxify } from '../../lib/proxy/proxify';
15
import { trackEvents } from '../../enums/trackEvents';
16
import {
17
  CarouselOptions,
18
  CarouselState,
19
  Deps,
20
  Guides,
21
} from './UserGuide.interface';
22

23
export const SUPPORTED_LOCALES = {
1✔
24
  'en-US': 'en-US',
25
  'fr-CA': 'fr-CA',
26
};
27

28
function getFileName(fileUrl: string) {
29
  return fileUrl.split('\\').pop()?.split('/').pop();
1✔
30
}
31

32
// Since file name has included file MD5, any file name change means file change
33
function anyFileDiff(urls1: string[], urls2: string[]) {
34
  const files1 = (urls1 ?? []).map((url) => getFileName(url));
1!
35
  const files2 = (urls2 ?? []).map((url) => getFileName(url));
1✔
36
  return JSON.stringify(files1) !== JSON.stringify(files2);
1✔
37
}
38

39
@Module({
40
  name: 'UserGuide',
41
  deps: [
42
    'Auth',
43
    'Locale',
44
    'Storage',
45
    'Webphone',
46
    'AppFeatures',
47
    'Brand',
48
    { dep: 'UserGuideOptions', optional: true },
49
  ],
50
})
51
export class UserGuide extends RcModuleV2<Deps> {
52
  constructor(deps: Deps) {
53
    super({
4✔
54
      deps,
55
      enableCache: true,
56
      storageKey: 'UserGuide',
57
    });
58
  }
59

60
  @storage
61
  @state
62
  allGuides: { [key: string]: Guides } = {};
4✔
63

64
  @state
65
  carouselState: CarouselState = {
4✔
66
    curIdx: 0,
67
    entered: false,
68
    playing: false,
69
  };
70

71
  @state
72
  preLoadImageStatus = false;
4✔
73

74
  @state
75
  firstLogin = false;
4✔
76

77
  @action
78
  setPreLoadImageStatus() {
79
    this.preLoadImageStatus = true;
1✔
80
  }
81

82
  @action
83
  setGuides(guides: Guides) {
84
    const oldGuides = this.allGuides[this._deps.brand.code] ?? {};
1✔
85
    this.allGuides[this._deps.brand.code] = guides;
1✔
86
    for (const locale of Object.keys(SUPPORTED_LOCALES)) {
1✔
87
      if (anyFileDiff(guides[locale], oldGuides[locale])) {
1!
88
        this.start({ firstLogin: true });
1✔
89
        break;
1✔
90
      }
91
    }
92
  }
93

94
  @action
95
  _migrateGuides() {
96
    if (!this.allGuides[this._deps.brand.code]) {
1!
97
      this.allGuides[this._deps.brand.code] = {};
1✔
98
    }
99
    Object.keys(SUPPORTED_LOCALES).forEach((locale) => {
1✔
100
      const allGuides: Guides = this.allGuides as any;
2✔
101
      if (allGuides[locale]) {
2!
102
        this.allGuides[this._deps.brand.code][locale] = allGuides[locale];
2✔
103
        delete allGuides[locale];
2✔
104
      }
105
    });
106
  }
107

108
  @track((that: UserGuide, options: Required<CarouselOptions>) => {
109
    if (options.curIdx === 0 && options.playing) {
×
110
      return [trackEvents.whatsNew];
×
111
    }
112
  })
113
  @action
114
  setCarousel({ firstLogin, ...carouselState }: Required<CarouselOptions>) {
115
    this.carouselState = carouselState;
1✔
116
    this.firstLogin = firstLogin;
1✔
117
  }
118

119
  override async onInitSuccess() {
120
    if (this.hasPermission) {
×
121
      await this.initUserGuide();
×
122
    }
123
  }
124

125
  override _shouldInit() {
126
    return !!(
×
127
      this.pending &&
×
128
      this._deps.auth.ready &&
129
      this._deps.auth.loggedIn &&
130
      this._deps.locale.ready &&
131
      this._deps.storage.ready &&
132
      this._deps.appFeatures.ready &&
133
      this._deps.brand.ready
134
    );
135
  }
136

137
  override _shouldReset() {
138
    return !!(
×
139
      this.ready &&
×
140
      (!this._deps.auth.ready ||
141
        !this._deps.locale.ready ||
142
        !this._deps.storage.ready ||
143
        !this._deps.appFeatures.ready ||
144
        !this._deps.brand.ready)
145
    );
146
  }
147

148
  override onInitOnce() {
149
    this._migrateGuides();
×
150
    // When there is an incoming call,
151
    // the guide should be dismissed
152
    watch(
×
153
      this,
154
      () => this._deps.webphone.ringSession,
×
155
      (ringSession) => {
156
        if (this._deps.webphone.ready && ringSession) {
×
157
          this.dismiss();
×
158
        }
159
      },
160
    );
161
    watch(
×
162
      this,
163
      () => this._deps.brand.brandConfig,
×
164
      async () => {
165
        if (this.hasPermission) {
×
166
          await this.initUserGuide();
×
167
        }
168
      },
169
    );
170
  }
171

172
  @proxify
173
  async _preLoadImage(url: string) {
174
    await new Promise((resolve, reject) => {
×
175
      const img = new Image();
×
176
      img.src = url;
×
177
      img.onload = resolve;
×
178
      img.onerror = reject;
×
179
    });
180
  }
181

182
  @proxify
183
  async preLoadImage() {
184
    const url = this.guides[0];
×
185
    if (url) {
×
186
      await this._preLoadImage(url);
×
187
    }
188
    this.setPreLoadImageStatus();
×
189
  }
190

191
  /**
192
   * Using webpack `require.context` to load guides files.
193
   * Image files will be sorted by file name in ascending order.
194
   */
195
  resolveGuides(): Record<string, string[]> {
196
    let images =
197
      (this._deps.brand.brandConfig.assets?.guides as string[]) || [];
×
198

199
    if (
×
200
      images.length === 0 &&
×
201
      typeof this._deps.userGuideOptions?.context === 'function'
202
    ) {
203
      images = this._deps.userGuideOptions.context
×
204
        .keys()
205
        .sort()
206
        .map((key: string) => {
NEW
207
          const value = this._deps.userGuideOptions!.context(key);
×
208
          return typeof value === 'string' ? value : value?.default;
×
209
        }) as string[];
210
    }
211

212
    const locales = Object.keys(SUPPORTED_LOCALES);
×
213
    return images.reduce<Record<string, string[]>>((acc, curr: string) => {
×
214
      locales.forEach((locale) => {
×
215
        if (!acc[locale]) {
×
216
          acc[locale] = [];
×
217
        }
NEW
218
        if (includes(locale, curr)) {
×
219
          acc[locale].push(curr);
×
220
        }
221
      });
222
      return acc;
×
223
    }, {});
224
  }
225

226
  @proxify
227
  async dismiss() {
NEW
228
    if (
×
229
      this.carouselState.curIdx !== 0 ||
×
230
      this.carouselState.playing ||
231
      this.carouselState.entered ||
232
      this.firstLogin
233
    ) {
NEW
234
      this.updateCarousel({
×
235
        curIdx: 0,
236
        entered: false,
237
        playing: false,
238
        firstLogin: false,
239
      });
240
    }
241
  }
242

243
  @proxify
244
  async updateCarousel({
245
    curIdx,
246
    entered,
247
    playing,
248
    firstLogin = this.firstLogin,
×
249
  }: CarouselOptions) {
250
    this.setCarousel({
×
251
      curIdx,
252
      entered,
253
      playing,
254
      firstLogin,
255
    });
256
  }
257

258
  get hasPermission() {
259
    // For extensions without calling or read message permissions, most of the content in
260
    // the user guide is not applicable to them. So we should not show the user guide for
261
    // these extensions.
262
    return (
×
263
      this._deps.appFeatures.isCallingEnabled ||
×
264
      this._deps.appFeatures.hasReadMessagesPermission
265
    );
266
  }
267

268
  @proxify
269
  async initUserGuide() {
270
    const guides = this.resolveGuides();
×
271
    // Determine if it needs to be displayed when first log in,
272
    // the principles behind this is to use webpack's file hash,
273
    // i.e. if any of the guide files is changed, the file name hash
274
    // will be changed as well, in this case, it will be displayed.
275
    if (guides) {
×
276
      this.setGuides(guides);
×
277
      await this.preLoadImage();
×
278
    }
279
  }
280

281
  @proxify
282
  async start({ firstLogin = false } = {}) {
×
283
    // Start guides only when images are ready
284
    this.setCarousel({
×
285
      curIdx: 0,
286
      entered: true,
287
      playing: true,
288
      firstLogin,
289
    });
290
  }
291

292
  @computed((that: UserGuide) => [
×
293
    that._deps.locale.ready,
294
    that.allGuides[that._deps.brand.code],
295
    that._deps.locale.currentLocale,
296
  ])
297
  get guides() {
298
    if (!this._deps.locale.ready) {
×
299
      return [];
×
300
    }
301
    const brandGuides = this.allGuides[this._deps.brand.code];
×
302
    if (brandGuides) {
×
303
      const currentGuides = brandGuides[this._deps.locale.currentLocale];
×
304
      if (currentGuides && currentGuides.length > 0) {
×
305
        return currentGuides;
×
306
      }
307
      return brandGuides[SUPPORTED_LOCALES['en-US']] || [];
×
308
    }
309
    return [];
×
310
  }
311

312
  get started() {
313
    return this.carouselState.entered && this.carouselState.playing;
×
314
  }
315
}
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

© 2026 Coveralls, Inc