Passed
Push — trunk ( f6c20d...6c8d2d )
by Christian
12:22 queued 13s
created

VueAdapter.initDependencies   B

Complexity

Conditions 1

Size

Total Lines 72
Code Lines 57

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 57
dl 0
loc 72
rs 8.4072
c 0
b 0
f 0

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
/**
2
 * @package admin
3
 */
4
import ViewAdapter from 'src/core/adapter/view.adapter';
5
6
import Vue from 'vue';
7
import type { AsyncComponent, Component as VueComponent, PluginObject } from 'vue';
8
import VueRouter from 'vue-router';
9
import type { FallbackLocale } from 'vue-i18n';
10
import VueI18n from 'vue-i18n';
11
import VueMeta from 'vue-meta';
12
import VuePlugins from 'src/app/plugin';
13
import setupShopwareDevtools from 'src/app/adapter/view/sw-vue-devtools';
14
import type ApplicationBootstrapper from 'src/core/application';
15
import type { ComponentConfig } from 'src/core/factory/async-component.factory';
16
import type { Store } from 'vuex';
17
18
const { Component, State, Mixin } = Shopware;
19
20
/**
21
 * @deprecated tag:v6.6.0 - Will be private
22
 */
23
export default class VueAdapter extends ViewAdapter {
24
    private resolvedComponentConfigs: Map<string, ComponentConfig>;
25
26
    private vueComponents: {
27
        // eslint-disable-next-line @typescript-eslint/no-explicit-any
28
        [componentName: string]: VueComponent<any, any, any, any> | AsyncComponent<any, any, any, any>
29
    };
30
31
    private i18n?: VueI18n;
32
33
    constructor(Application: ApplicationBootstrapper) {
34
        super(Application);
35
36
        this.i18n = undefined;
37
        this.resolvedComponentConfigs = new Map();
38
39
        this.vueComponents = {};
40
    }
41
42
    /**
43
     * Creates the main instance for the view layer.
44
     * Is used on startup process of the main application.
45
     */
46
    init(renderElement: string, router: VueRouter, providers: { [key: string]: unknown }): Vue {
47
        this.initPlugins();
48
        this.initDirectives();
49
        this.initFilters();
50
        this.initTitle();
51
52
        const store = State._store;
53
        const i18n = this.initLocales(store);
54
        const components = this.getComponents();
55
56
        // add router to View
57
        this.router = router;
58
        // add i18n to View
59
        this.i18n = i18n;
60
61
        // Enable performance measurements in development mode
62
        Vue.config.performance = process.env.NODE_ENV !== 'production';
63
64
        this.root = new Vue({
65
            el: renderElement,
66
            template: '<sw-admin />',
67
            router,
68
            store,
69
            i18n,
70
            provide() {
71
                return providers;
72
            },
73
            components,
74
            data() {
75
                return {
76
                    initError: {},
77
                };
78
            },
79
        });
80
81
        if (process.env.NODE_ENV === 'development') {
82
            setupShopwareDevtools(this.root);
83
        }
84
85
        return this.root;
86
    }
87
88
    /**
89
     * Initialize of all dependencies.
90
     */
91
    async initDependencies() {
92
        const initContainer = this.Application.getContainer('init');
93
94
        // make specific components synchronous
95
        [
96
            'sw-admin',
97
            'sw-admin-menu',
98
            'sw-button',
99
            'sw-button-process',
100
            'sw-card',
101
            'sw-card-section',
102
            'sw-card-view',
103
            'sw-container',
104
            'sw-desktop',
105
            'sw-empty-state',
106
            'sw-entity-listing',
107
            'sw-entity-multi-select',
108
            'sw-entity-multi-id-select',
109
            'sw-entity-single-select',
110
            'sw-error-boundary',
111
            'sw-extension-component-section',
112
            'sw-field',
113
            'sw-ignore-class',
114
            'sw-loader',
115
            'sw-modal',
116
            'sw-multi-select',
117
            'sw-notification-center',
118
            'sw-notifications',
119
            'sw-page',
120
            'sw-router-link',
121
            'sw-search-bar',
122
            'sw-select-result',
123
            'sw-single-select',
124
            'sw-skeleton',
125
            'sw-skeleton-bar',
126
            'sw-tabs',
127
            'sw-tabs-item',
128
            'sw-version',
129
            /**
130
             * Quickfix for modules with refs and sync behavior.
131
             * They should be removed from the list in the future
132
             * when their async problems got fixed.
133
             */
134
            'sw-sales-channel-products-assignment-single-products',
135
            'sw-sales-channel-product-assignment-categories',
136
            'sw-sales-channel-products-assignment-dynamic-product-groups',
137
            'sw-upload-listener',
138
            'sw-media-list-selection-v2',
139
            'sw-media-list-selection-item-v2',
140
            'sw-settings-document-detail',
141
            'sw-settings-product-feature-sets-detail',
142
            'sw-system-config',
143
            'sw-settings-search-searchable-content',
144
        ].forEach(componentName => {
145
            Component.markComponentAsSync(componentName);
146
        });
147
148
        // initialize all components
149
        await this.initComponents();
150
151
        // initialize all module locales
152
        this.initModuleLocales();
153
154
        // initialize all module routes
155
        const allRoutes = this.applicationFactory.module.getModuleRoutes();
156
        // eslint-disable-next-line @typescript-eslint/no-unsafe-call,@typescript-eslint/no-unsafe-member-access
157
        initContainer.router.addModuleRoutes(allRoutes);
158
159
        // create routes for core and plugins
160
        // eslint-disable-next-line @typescript-eslint/no-unsafe-call,@typescript-eslint/no-unsafe-member-access
161
        initContainer.router.createRouterInstance();
162
    }
163
164
165
    /**
166
     * Initializes all core components as Vue components.
167
     */
168
    async initComponents() {
169
        const componentRegistry = this.componentFactory.getComponentRegistry();
170
        this.componentFactory.resolveComponentTemplates();
171
172
        const initializedComponents = [...componentRegistry.keys()].map((name) => {
173
            return this.createComponent(name);
174
        });
175
176
        await Promise.all(initializedComponents);
177
178
        return this.vueComponents;
179
    }
180
181
    /**
182
     * Initializes all core components as Vue components.
183
     */
184
    initModuleLocales() {
185
        // Extend default snippets with module specific snippets
186
        const moduleSnippets = this.applicationFactory.module.getModuleSnippets();
187
188
        Object.entries(moduleSnippets).forEach(([key, moduleSnippet]) => {
189
            this.applicationFactory.locale.extend(key, moduleSnippet);
190
        });
191
192
        return this.applicationFactory.locale;
193
    }
194
195
    /**
196
     * Returns the component as a Vue component.
197
     * Includes the full rendered template with all overrides.
198
     */
199
    createComponent(componentName: string): Promise<Vue> {
200
        return new Promise((resolve) => {
201
            // load sync components directly
202
            if (Component.isSyncComponent && Component.isSyncComponent(componentName)) {
203
                const resolvedComponent = this.componentResolver(componentName);
204
205
                if (resolvedComponent === undefined || resolvedComponent.component === undefined) {
206
                    return;
207
                }
208
209
                void resolvedComponent.component.then((component) => {
210
                    // @ts-expect-error - resolved config does not match completely a standard vue component
211
                    const vueComponent = Vue.component(componentName, component);
212
213
                    this.vueComponents[componentName] = vueComponent;
214
                    resolve(vueComponent as unknown as Vue);
215
                });
216
217
                return;
218
            }
219
220
            // load async components
221
            const vueComponent = Vue.component(componentName, () => this.componentResolver(componentName));
222
            this.vueComponents[componentName] = vueComponent;
223
224
            resolve(vueComponent as unknown as Vue);
225
        });
226
    }
227
228
    componentResolver(componentName: string) {
229
        if (!this.resolvedComponentConfigs.has(componentName)) {
230
            this.resolvedComponentConfigs.set(componentName, {
231
                component: new Promise((resolve) => {
232
                    void Component.build(componentName).then((componentConfig) => {
233
                        // @ts-expect-error - component config is not fully compatible with vue component
234
                        this.resolveMixins(componentConfig);
235
236
                        resolve(componentConfig);
237
                    });
238
                }),
239
                loading: {
240
                    functional: true,
241
                    render(e) {
242
                        return e('div', {
243
                            style: { display: 'none' },
244
                        });
245
                    },
246
                },
247
                delay: 0,
248
            });
249
        }
250
251
        return this.resolvedComponentConfigs.get(componentName);
252
    }
253
254
    /**
255
     * Builds and creates a Vue component using the provided component configuration.
256
     */
257
    buildAndCreateComponent(componentConfig: ComponentConfig) {
258
        if (!componentConfig.name) {
259
            throw new Error('Component name is missing');
260
        }
261
262
        const componentName = componentConfig.name;
263
        this.resolveMixins(componentConfig);
264
265
        // @ts-expect-error - resolved config does not match completely a standard vue component
266
        const vueComponent = Vue.component(componentConfig.name, componentConfig);
267
        this.vueComponents[componentName] = vueComponent;
268
269
        return vueComponent;
270
    }
271
272
    /**
273
     * Returns a final Vue component by its name.
274
     */
275
    getComponent(componentName: string) {
276
        if (!this.vueComponents[componentName]) {
277
            return null;
278
        }
279
280
        return this.vueComponents[componentName] as Vue;
281
    }
282
283
    /**
284
     * Returns the complete set of available Vue components.
285
     */
286
    // @ts-expect-error - resolved config for each component does not match completely a standard vue component
287
    getComponents() {
288
        return this.vueComponents;
289
    }
290
291
    /**
292
     * Returns the adapter wrapper
293
     */
294
    getWrapper() {
295
        return Vue;
296
    }
297
298
    /**
299
     * Returns the name of the adapter
300
     */
301
    getName(): string {
302
        return 'Vue.js';
303
    }
304
305
    /**
306
     * Returns the Vue.set function
307
     */
308
    setReactive(target: Vue, propertyName: string, value: unknown) {
309
        return Vue.set(target, propertyName, value);
310
    }
311
312
    /**
313
     * Returns the Vue.delete function
314
     */
315
    deleteReactive(target: Vue, propertyName: string) {
316
        return Vue.delete(target, propertyName);
317
    }
318
319
    /**
320
     * Private methods
321
     */
322
323
    /**
324
     * Initialises all plugins for VueJS
325
     *
326
     * @private
327
     */
328
    initPlugins() {
329
        // Add the community plugins to the plugin list
330
        VuePlugins.push(VueRouter, VueI18n, VueMeta);
331
        VuePlugins.forEach((plugin) => {
332
            // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
333
            if (plugin?.install?.installed) {
334
                return;
335
            }
336
337
            // VueI18n.install.installed
338
            Vue.use(plugin as PluginObject<unknown>);
339
        });
340
341
        return true;
342
    }
343
344
    /**
345
     * Initializes all custom directives.
346
     *
347
     * @private
348
     */
349
    initDirectives() {
350
        const registry = this.Application.getContainer('factory').directive.getDirectiveRegistry();
351
352
        registry.forEach((directive, name) => {
353
            Vue.directive(name, directive);
354
        });
355
356
        return true;
357
    }
358
359
    /**
360
     * Initialises helpful filters for global use
361
     *
362
     * @private
363
     */
364
    initFilters() {
365
        const registry = this.Application.getContainer('factory').filter.getRegistry();
366
367
        registry.forEach((factoryMethod, name) => {
368
            Vue.filter(name, factoryMethod);
369
        });
370
371
        return true;
372
    }
373
374
    /**
375
     * Initialises the standard locales.
376
     */
377
    initLocales(store: Store<VuexRootState>) {
378
        const registry = this.localeFactory.getLocaleRegistry();
379
        const messages = {};
380
        const fallbackLocale = Shopware.Context.app.fallbackLocale as FallbackLocale;
381
382
        registry.forEach((localeMessages, key) => {
383
            store.commit('registerAdminLocale', key);
384
            // @ts-expect-error - key is safe because we iterate through the registry
385
            messages[key] = localeMessages;
386
        });
387
388
        const lastKnownLocale = this.localeFactory.getLastKnownLocale();
389
        void store.dispatch('setAdminLocale', lastKnownLocale);
390
391
        const i18n = new VueI18n({
392
            locale: lastKnownLocale,
393
            fallbackLocale,
394
            silentFallbackWarn: true,
395
            sync: true,
396
            messages,
397
        });
398
399
        store.subscribe(({ type }, state) => {
400
            if (type === 'setAdminLocale') {
401
                // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
402
                i18n.locale = state.session.currentLocale!;
403
            }
404
        });
405
406
        this.setLocaleFromUser(store);
407
408
        return i18n;
409
    }
410
411
    setLocaleFromUser(store: Store<VuexRootState>) {
412
        const currentUser = store.state.session.currentUser;
413
414
        if (currentUser) {
415
            const userLocaleId = currentUser.localeId;
416
            // eslint-disable-next-line @typescript-eslint/no-unsafe-call,@typescript-eslint/no-unsafe-member-access
417
            Shopware.Service('localeHelper').setLocaleWithId(userLocaleId);
418
        }
419
    }
420
421
    /**
422
     * Extends Vue prototype to access $createTitle function
423
     *
424
     * @private
425
     */
426
    initTitle() {
427
        // eslint-disable-next-line @typescript-eslint/no-unsafe-call,@typescript-eslint/no-unsafe-member-access
428
        if (Vue.prototype.hasOwnProperty('$createTitle')) {
429
            return;
430
        }
431
432
        /**
433
         * Generates the document title out of the given VueComponent and parameters
434
         */
435
        // @ts-expect-error - additionalParams is not typed
436
        // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access,max-len
437
        Vue.prototype.$createTitle = function createTitle(this: Vue, identifier: string|null = null, ...additionalParams): string {
438
            if (!this.$root) {
439
                return '';
440
            }
441
442
            const baseTitle = this.$root.$tc('global.sw-admin-menu.textShopwareAdmin');
443
444
            if (!this.$route.meta || !this.$route.meta.$module) {
445
                return '';
446
            }
447
448
            // eslint-disable-next-line @typescript-eslint/no-unsafe-argument,@typescript-eslint/no-unsafe-member-access
449
            const pageTitle = this.$root.$tc(this.$route.meta.$module.title);
450
451
            // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
452
            const params = [baseTitle, pageTitle, identifier, ...additionalParams].filter((item) => {
453
                // eslint-disable-next-line @typescript-eslint/no-unsafe-call,@typescript-eslint/no-unsafe-member-access
454
                return item !== null && item.trim() !== '';
455
            });
456
457
            return params.reverse().join(' | ');
458
        };
459
    }
460
461
    /**
462
     * Recursively resolves mixins referenced by name
463
     *
464
     * @private
465
     */
466
    resolveMixins(componentConfig: ComponentConfig) {
467
        // If the mixin is a string, use our mixin registry
468
        if (componentConfig.mixins?.length) {
469
            componentConfig.mixins = componentConfig.mixins.map((mixin) => {
470
                if (typeof mixin === 'string') {
471
                    return Mixin.getByName(mixin);
472
                }
473
474
                return mixin;
475
            });
476
        }
477
478
        if (componentConfig.extends) {
479
            // @ts-expect-error - extends can be a string or a component config
480
            this.resolveMixins(componentConfig.extends);
481
        }
482
    }
483
}
484