Passed
Push — trunk ( 762d62...45f6d1 )
by Christian
15:06 queued 12s
created

src/Administration/Resources/app/administration/src/app/adapter/view/vue.adapter.ts   C

Complexity

Total Complexity 56
Complexity/F 2

Size

Lines of Code 640
Function Count 28

Duplication

Duplicated Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 424
dl 0
loc 640
rs 5.5199
c 0
b 0
f 0
wmc 56
mnd 28
bc 28
fnc 28
bpm 1
cpm 2
noi 0

23 Functions

Rating   Name   Duplication   Size   Complexity  
A VueAdapter.initModuleLocales 0 13 1
A VueAdapter.initTitle 0 38 4
A VueAdapter.initFilters 0 14 1
A VueAdapter.getComponents 0 7 1
A VueAdapter.setLocaleFromUser 0 8 2
B VueAdapter.initPlugins 0 31 6
A VueAdapter.resolveMixins 0 21 5
A VueAdapter.buildAndCreateComponent 0 17 2
A VueAdapter.initDirectives 0 14 1
B VueAdapter.initDependencies 0 72 1
A VueAdapter.getComponentForRoute 0 7 1
A VueAdapter.initLocales 0 39 4
A VueAdapter.deleteReactive 0 6 1
A VueAdapter.setReactive 0 6 1
A VueAdapter.init 0 12 4
B VueAdapter.initVue2 0 57 5
A VueAdapter.getComponent 0 10 2
A VueAdapter.componentResolver 0 13 2
B VueAdapter.initVue3 0 67 3
A VueAdapter.getWrapper 0 6 1
B VueAdapter.createComponent 0 69 6
A VueAdapter.initComponents 0 16 1
A VueAdapter.getName 0 6 1

How to fix   Complexity   

Complexity

Complex classes like src/Administration/Resources/app/administration/src/app/adapter/view/vue.adapter.ts often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

1
// @ts-nocheck
2
3
/**
4
 * @package admin
5
 */
6
import ViewAdapter from 'src/core/adapter/view.adapter';
7
8
// Vue3 imports
9
import { createI18n } from 'vue-i18n_v3';
10
import type { FallbackLocale, I18n } from 'vue-i18n_v3';
11
import type Router from 'vue-router_v3';
12
13
// Vue2 imports
14
import VueRouter from 'vue-router';
15
import VueI18n from 'vue-i18n';
16
import VueMeta from 'vue-meta';
17
18
import Vue, { createApp, defineAsyncComponent, h } from 'vue';
19
import type { AsyncComponent, Component as VueComponent, PluginObject } from 'vue';
20
import VuePlugins from 'src/app/plugin';
21
import setupShopwareDevtools from 'src/app/adapter/view/sw-vue-devtools';
22
import type ApplicationBootstrapper from 'src/core/application';
23
import type { ComponentConfig } from 'src/core/factory/async-component.factory';
24
import type { Store } from 'vuex';
25
26
const { Component, State, Mixin } = Shopware;
27
28
/**
29
 * @deprecated tag:v6.6.0 - Will be private
30
 */
31
export default class VueAdapter extends ViewAdapter {
32
    private resolvedComponentConfigs: Map<string, ComponentConfig>;
33
34
    private vueComponents: {
35
        // eslint-disable-next-line @typescript-eslint/no-explicit-any
36
        [componentName: string]: VueComponent<any, any, any, any> | AsyncComponent<any, any, any, any>
37
    };
38
39
    private i18n?: I18n;
40
41
    public app;
42
43
    private vue3 = false;
44
45
    constructor(Application: ApplicationBootstrapper) {
46
        super(Application);
47
48
        this.i18n = undefined;
49
        this.resolvedComponentConfigs = new Map();
50
        this.vueComponents = {};
51
        // @ts-expect-error
52
        // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
53
        this.vue3 = !!window._features_?.vue3;
54
55
        if (this.vue3) {
56
            // eslint-disable-next-line @typescript-eslint/no-unsafe-call
57
            this.app = createApp({ template: '<sw-admin />' }) as Vue;
58
        }
59
    }
60
61
    /**
62
     * Creates the main instance for the view layer.
63
     * Is used on startup process of the main application.
64
     */
65
    // @ts-expect-error
66
    init(renderElement: string, router: Router, providers: { [key: string]: unknown }): Vue {
67
        if (this.vue3) {
68
            return this.initVue3(renderElement, router, providers);
69
        }
70
71
        return this.initVue2(renderElement, router as VueRouter, providers);
72
    }
73
74
    // @ts-expect-error
75
    initVue3(renderElement: string, router: Router, providers: { [key: string]: unknown }): Vue {
76
        this.initPlugins();
77
        this.initDirectives();
78
        this.initFilters();
79
        this.initTitle();
80
81
        const store = State._store;
82
        const i18n = this.initLocales(store) as VueI18n;
83
84
        // add router to View
85
        this.router = router as VueRouter;
86
        // add i18n to View
87
        this.i18n = i18n as I18n;
88
89
        // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
90
        this.app.config.compilerOptions.whitespace = 'preserve';
91
        // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
92
        this.app.config.performance = process.env.NODE_ENV !== 'production';
93
        // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment,@typescript-eslint/no-unsafe-member-access
94
        this.app.config.globalProperties.$t = i18n.global.t;
95
        // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment,@typescript-eslint/no-unsafe-member-access
96
        this.app.config.globalProperties.$tc = i18n.global.tc;
97
98
        /**
99
         * This is a hack for providing the services to the components.
100
         * We shouldn't use this anymore because it is not supported well
101
         * in Vue3 (because the services are lazy loaded).
102
         *
103
         * So we should convert from provide/inject to Shopware.Service
104
         */
105
        Object.keys(providers).forEach((provideKey) => {
106
            // @ts-expect-error
107
            // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
108
            Object.defineProperty(this.app._context.provides, provideKey, {
109
                get: () => providers[provideKey],
110
                enumerable: true,
111
                configurable: true,
112
                // eslint-disable-next-line @typescript-eslint/no-empty-function
113
                set() {},
114
            });
115
        });
116
117
        this.root = this.app;
118
119
        // eslint-disable-next-line @typescript-eslint/no-unsafe-call
120
        this.app.use(router);
121
        // eslint-disable-next-line @typescript-eslint/no-unsafe-call
122
        this.app.use(store);
123
        // eslint-disable-next-line @typescript-eslint/no-unsafe-call
124
        this.app.use(i18n);
125
126
        // Add global properties to root view instance
127
        // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment,@typescript-eslint/no-unsafe-member-access
128
        this.app.$tc = i18n.global.tc;
129
        // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment,@typescript-eslint/no-unsafe-member-access
130
        this.app.$t = i18n.global.t;
131
132
        // eslint-disable-next-line @typescript-eslint/no-unsafe-call
133
        this.app.mount(renderElement);
134
135
        if (process.env.NODE_ENV === 'development') {
136
            setupShopwareDevtools(this.root);
137
        }
138
139
        return this.root;
140
    }
141
142
    initVue2(renderElement: string, router: VueRouter, providers: { [key: string]: unknown }): Vue {
143
        this.initPlugins();
144
        this.initDirectives();
145
        this.initFilters();
146
        this.initTitle();
147
148
        const store = State._store;
149
        const i18n = this.initLocales(store);
150
        const components = this.getComponents();
151
152
        // add router to View
153
        this.router = router;
154
        // add i18n to View
155
        this.i18n = i18n;
156
157
        // Enable performance measurements in development mode
158
        Vue.config.performance = process.env.NODE_ENV !== 'production';
159
160
        this.root = new Vue({
161
            el: renderElement,
162
            template: '<sw-admin />',
163
            router,
164
            store,
165
            i18n,
166
            provide() {
167
                /**
168
                 * Vue 2.7 creates a new copy for each provided value. This caused problems with bottlejs.
169
                 * There should be only one instance of each provided value. Therefore we use a getter wrapper.
170
                 */
171
                return Object.keys(providers).reduce<{
172
                    [key: string]: unknown
173
                }>((acc, provideKey) => {
174
                    Object.defineProperty(acc, provideKey, {
175
                        get: () => providers[provideKey],
176
                        enumerable: true,
177
                        configurable: true,
178
                        // eslint-disable-next-line @typescript-eslint/no-empty-function
179
                        set() {},
180
                    });
181
182
                    return acc;
183
                }, {});
184
            },
185
            components,
186
            data() {
187
                return {
188
                    initError: {},
189
                };
190
            },
191
        });
192
193
        if (process.env.NODE_ENV === 'development') {
194
            setupShopwareDevtools(this.root);
195
        }
196
197
        return this.root;
198
    }
199
200
    /**
201
     * Initialize of all dependencies.
202
     */
203
    async initDependencies() {
204
        const initContainer = this.Application.getContainer('init');
205
206
        // make specific components synchronous
207
        [
208
            'sw-admin',
209
            'sw-admin-menu',
210
            'sw-button',
211
            'sw-button-process',
212
            'sw-card',
213
            'sw-card-section',
214
            'sw-card-view',
215
            'sw-container',
216
            'sw-desktop',
217
            'sw-empty-state',
218
            'sw-entity-listing',
219
            'sw-entity-multi-select',
220
            'sw-entity-multi-id-select',
221
            'sw-entity-single-select',
222
            'sw-error-boundary',
223
            'sw-extension-component-section',
224
            'sw-field',
225
            'sw-ignore-class',
226
            'sw-loader',
227
            'sw-modal',
228
            'sw-multi-select',
229
            'sw-notification-center',
230
            'sw-notifications',
231
            'sw-page',
232
            'sw-router-link',
233
            'sw-search-bar',
234
            'sw-select-result',
235
            'sw-single-select',
236
            'sw-skeleton',
237
            'sw-skeleton-bar',
238
            'sw-tabs',
239
            'sw-tabs-item',
240
            'sw-version',
241
            /**
242
             * Quickfix for modules with refs and sync behavior.
243
             * They should be removed from the list in the future
244
             * when their async problems got fixed.
245
             */
246
            'sw-sales-channel-products-assignment-single-products',
247
            'sw-sales-channel-product-assignment-categories',
248
            'sw-sales-channel-products-assignment-dynamic-product-groups',
249
            'sw-upload-listener',
250
            'sw-media-list-selection-v2',
251
            'sw-media-list-selection-item-v2',
252
            'sw-settings-document-detail',
253
            'sw-settings-product-feature-sets-detail',
254
            'sw-system-config',
255
            'sw-settings-search-searchable-content',
256
        ].forEach(componentName => {
257
            Component.markComponentAsSync(componentName);
258
        });
259
260
        // initialize all components
261
        await this.initComponents();
262
263
        // initialize all module locales
264
        this.initModuleLocales();
265
266
        // initialize all module routes
267
        const allRoutes = this.applicationFactory.module.getModuleRoutes();
268
        // eslint-disable-next-line @typescript-eslint/no-unsafe-call,@typescript-eslint/no-unsafe-member-access
269
        initContainer.router.addModuleRoutes(allRoutes);
270
271
        // create routes for core and plugins
272
        // eslint-disable-next-line @typescript-eslint/no-unsafe-call,@typescript-eslint/no-unsafe-member-access
273
        initContainer.router.createRouterInstance();
274
    }
275
276
277
    /**
278
     * Initializes all core components as Vue components.
279
     */
280
    async initComponents() {
281
        const componentRegistry = this.componentFactory.getComponentRegistry();
282
        this.componentFactory.resolveComponentTemplates();
283
284
        const initializedComponents = [...componentRegistry.keys()].map((name) => {
285
            return this.createComponent(name);
286
        });
287
288
        await Promise.all(initializedComponents);
289
290
        return this.vueComponents;
291
    }
292
293
    /**
294
     * Initializes all core components as Vue components.
295
     */
296
    initModuleLocales() {
297
        // Extend default snippets with module specific snippets
298
        const moduleSnippets = this.applicationFactory.module.getModuleSnippets();
299
300
        Object.entries(moduleSnippets).forEach(([key, moduleSnippet]) => {
301
            this.applicationFactory.locale.extend(key, moduleSnippet);
302
        });
303
304
        return this.applicationFactory.locale;
305
    }
306
307
    /**
308
     * Returns the component as a Vue component.
309
     * Includes the full rendered template with all overrides.
310
     */
311
    createComponent(componentName: string): Promise<Vue> {
312
        return new Promise((resolve) => {
313
            // load sync components directly
314
            if (Component.isSyncComponent && Component.isSyncComponent(componentName)) {
315
                const resolvedComponent = this.componentResolver(componentName);
316
317
                if (resolvedComponent === undefined) {
318
                    return;
319
                }
320
321
                // eslint-disable-next-line @typescript-eslint/no-unsafe-call
322
                void resolvedComponent.then((component) => {
323
                    let vueComponent;
324
325
                    if (this.vue3) {
326
                        // eslint-disable-next-line @typescript-eslint/no-unsafe-call
327
                        this.app.component(componentName, component);
328
                        // eslint-disable-next-line @typescript-eslint/no-unsafe-call,@typescript-eslint/no-unsafe-assignment
329
                        vueComponent = this.app.component(componentName);
330
                    } else {
331
                        // @ts-expect-error - resolved config does not match completely a standard vue component
332
                        // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
333
                        vueComponent = Vue.component(componentName, component);
334
                    }
335
336
337
                    // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
338
                    this.vueComponents[componentName] = vueComponent;
339
                    resolve(vueComponent as unknown as Vue);
340
                });
341
342
                return;
343
            }
344
345
            // load async components
346
            let vueComponent;
347
            if (this.vue3) {
348
                // eslint-disable-next-line @typescript-eslint/no-unsafe-call
349
                this.app.component(componentName, defineAsyncComponent({
350
                    // the loader function
351
                    loader: () => this.componentResolver(componentName),
352
                    // Delay before showing the loading component. Default: 200ms.
353
                    delay: 0,
354
                    loadingComponent: {
355
                        name: 'async-loading-component',
356
                        inheritAttrs: false,
357
                        render() {
358
                            return h('div', {
359
                                style: { display: 'none' },
360
                            });
361
                        },
362
                    },
363
                }));
364
365
                // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment,@typescript-eslint/no-unsafe-call
366
                vueComponent = this.app.component(componentName);
367
            } else {
368
                vueComponent = Vue.component(componentName, () => this.componentResolver(componentName));
369
            }
370
371
            // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
372
            this.vueComponents[componentName] = vueComponent;
373
374
            resolve(vueComponent as unknown as Vue);
375
        });
376
    }
377
378
    componentResolver(componentName: string) {
379
        if (!this.resolvedComponentConfigs.has(componentName)) {
380
            this.resolvedComponentConfigs.set(componentName, new Promise((resolve) => {
381
                void Component.build(componentName).then((componentConfig) => {
382
                    this.resolveMixins(componentConfig);
383
384
                    resolve(componentConfig);
385
                });
386
            }));
387
        }
388
389
        return this.resolvedComponentConfigs.get(componentName);
390
    }
391
392
    /**
393
     * Builds and creates a Vue component using the provided component configuration.
394
     */
395
    buildAndCreateComponent(componentConfig: ComponentConfig) {
396
        if (!componentConfig.name) {
397
            throw new Error('Component name is missing');
398
        }
399
400
        const componentName = componentConfig.name;
401
        this.resolveMixins(componentConfig);
402
403
        // @ts-expect-error - resolved config does not match completely a standard vue component
404
        const vueComponent = Vue.component(componentConfig.name, componentConfig);
405
        this.vueComponents[componentName] = vueComponent;
406
407
        return vueComponent;
408
    }
409
410
    /**
411
     * Returns a final Vue component by its name.
412
     */
413
    getComponent(componentName: string) {
414
        if (!this.vueComponents[componentName]) {
415
            return null;
416
        }
417
418
        return this.vueComponents[componentName] as Vue;
419
    }
420
421
    /**
422
     * Returns a final Vue component by its name without defineAsyncComponent
423
     * which cannot be used in the router.
424
     */
425
    getComponentForRoute(componentName: string) {
426
        return () => this.componentResolver(componentName);
427
    }
428
429
    /**
430
     * Returns the complete set of available Vue components.
431
     */
432
    // @ts-expect-error - resolved config for each component does not match completely a standard vue component
433
    getComponents() {
434
        return this.vueComponents;
435
    }
436
437
    /**
438
     * Returns the adapter wrapper
439
     */
440
    getWrapper() {
441
        return Vue;
442
    }
443
444
    /**
445
     * Returns the name of the adapter
446
     */
447
    getName(): string {
448
        return 'Vue.js';
449
    }
450
451
    /**
452
     * Returns the Vue.set function
453
     */
454
    setReactive(target: Vue, propertyName: string, value: unknown) {
455
        return Vue.set(target, propertyName, value);
456
    }
457
458
    /**
459
     * Returns the Vue.delete function
460
     */
461
    deleteReactive(target: Vue, propertyName: string) {
462
        return Vue.delete(target, propertyName);
463
    }
464
465
    /**
466
     * Private methods
467
     */
468
469
    /**
470
     * Initialises all plugins for VueJS
471
     *
472
     * @private
473
     */
474
    initPlugins() {
475
        if (!this.vue3) {
476
            // Add the community plugins to the plugin list
477
            VuePlugins.push(VueRouter, VueI18n, VueMeta);
478
        }
479
480
        VuePlugins.forEach((plugin) => {
481
            // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
482
            if (plugin?.install?.installed) {
483
                return;
484
            }
485
486
            if (this.vue3) {
487
                // eslint-disable-next-line @typescript-eslint/no-unsafe-call
488
                this.app.use(plugin as PluginObject<unknown>);
489
            } else {
490
                Vue.use(plugin as PluginObject<unknown>);
491
            }
492
        });
493
494
        return true;
495
    }
496
497
    /**
498
     * Initializes all custom directives.
499
     *
500
     * @private
501
     */
502
    initDirectives() {
503
        const registry = this.Application.getContainer('factory').directive.getDirectiveRegistry();
504
505
        registry.forEach((directive, name) => {
506
            Vue.directive(name, directive);
507
        });
508
509
        return true;
510
    }
511
512
    /**
513
     * Initialises helpful filters for global use
514
     *
515
     * @private
516
     */
517
    initFilters() {
518
        const registry = this.Application.getContainer('factory').filter.getRegistry();
519
520
        registry.forEach((factoryMethod, name) => {
521
            Vue.filter(name, factoryMethod);
522
        });
523
524
        return true;
525
    }
526
527
    /**
528
     * Initialises the standard locales.
529
     */
530
    initLocales(store: Store<VuexRootState>) {
531
        const registry = this.localeFactory.getLocaleRegistry();
532
        const messages = {};
533
        const fallbackLocale = Shopware.Context.app.fallbackLocale as FallbackLocale;
534
535
        registry.forEach((localeMessages, key) => {
536
            store.commit('registerAdminLocale', key);
537
            // @ts-expect-error - key is safe because we iterate through the registry
538
            messages[key] = localeMessages;
539
        });
540
541
        const lastKnownLocale = this.localeFactory.getLastKnownLocale();
542
        void store.dispatch('setAdminLocale', lastKnownLocale);
543
544
        const options = {
545
            locale: lastKnownLocale,
546
            fallbackLocale,
547
            silentFallbackWarn: true,
548
            sync: true,
549
            messages,
550
        };
551
552
        // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
553
        const i18n = window._features_?.vue3 ? createI18n(options) : new VueI18n(options);
554
555
        store.subscribe(({ type }, state) => {
556
            if (type === 'setAdminLocale') {
557
                // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
558
                i18n.locale = state.session.currentLocale!;
559
            }
560
        });
561
562
        this.setLocaleFromUser(store);
563
564
        return i18n;
565
    }
566
567
    setLocaleFromUser(store: Store<VuexRootState>) {
568
        const currentUser = store.state.session.currentUser;
569
570
        if (currentUser) {
571
            const userLocaleId = currentUser.localeId;
572
            // eslint-disable-next-line @typescript-eslint/no-unsafe-call,@typescript-eslint/no-unsafe-member-access
573
            Shopware.Service('localeHelper').setLocaleWithId(userLocaleId);
574
        }
575
    }
576
577
    /**
578
     * Extends Vue prototype to access $createTitle function
579
     *
580
     * @private
581
     */
582
    initTitle() {
583
        // eslint-disable-next-line @typescript-eslint/no-unsafe-call,@typescript-eslint/no-unsafe-member-access
584
        if (Vue.prototype.hasOwnProperty('$createTitle')) {
585
            return;
586
        }
587
588
        /**
589
         * Generates the document title out of the given VueComponent and parameters
590
         */
591
        // @ts-expect-error - additionalParams is not typed
592
        // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access,max-len
593
        Vue.prototype.$createTitle = function createTitle(this: Vue, identifier: string|null = null, ...additionalParams): string {
594
            if (!this.$root) {
595
                return '';
596
            }
597
598
            const baseTitle = this.$root.$tc('global.sw-admin-menu.textShopwareAdmin');
599
600
            if (!this.$route.meta || !this.$route.meta.$module) {
601
                return '';
602
            }
603
604
            // eslint-disable-next-line @typescript-eslint/no-unsafe-argument,@typescript-eslint/no-unsafe-member-access
605
            const pageTitle = this.$root.$tc(this.$route.meta.$module.title);
606
607
            // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
608
            const params = [baseTitle, pageTitle, identifier, ...additionalParams].filter((item) => {
609
                // eslint-disable-next-line @typescript-eslint/no-unsafe-call,@typescript-eslint/no-unsafe-member-access
610
                return item !== null && item.trim() !== '';
611
            });
612
613
            return params.reverse().join(' | ');
614
        };
615
    }
616
617
    /**
618
     * Recursively resolves mixins referenced by name
619
     *
620
     * @private
621
     */
622
    resolveMixins(componentConfig: ComponentConfig) {
623
        // If the mixin is a string, use our mixin registry
624
        if (componentConfig.mixins?.length) {
625
            componentConfig.mixins = componentConfig.mixins.map((mixin) => {
626
                if (typeof mixin === 'string') {
627
                    return Mixin.getByName(mixin);
628
                }
629
630
                return mixin;
631
            });
632
        }
633
634
        if (componentConfig.extends) {
635
            // @ts-expect-error - extends can be a string or a component config
636
            this.resolveMixins(componentConfig.extends);
637
        }
638
    }
639
}
640