diff --git a/src/addons/messageoutput/airnotifier/airnotifier.module.ts b/src/addons/messageoutput/airnotifier/airnotifier.module.ts
new file mode 100644
index 000000000..2d8b19f1b
--- /dev/null
+++ b/src/addons/messageoutput/airnotifier/airnotifier.module.ts
@@ -0,0 +1,49 @@
+// (C) Copyright 2015 Moodle Pty Ltd.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import { APP_INITIALIZER, NgModule } from '@angular/core';
+import { Routes } from '@angular/router';
+
+import { CoreMainMenuTabRoutingModule } from '@features/mainmenu/mainmenu-tab-routing.module';
+import { AddonMessageOutputDelegate } from '@addons/messageoutput/services/messageoutput-delegate';
+import {
+ AddonMessageOutputAirnotifierHandler,
+ AddonMessageOutputAirnotifierHandlerService,
+} from './services/handlers/messageoutput';
+
+const routes: Routes = [
+ {
+ path: AddonMessageOutputAirnotifierHandlerService.PAGE_NAME,
+ loadChildren: () => import('./pages/devices/devices.module').then( m => m.AddonMessageOutputAirnotifierDevicesPageModule),
+ },
+];
+
+@NgModule({
+ declarations: [
+ ],
+ imports: [
+ CoreMainMenuTabRoutingModule.forChild(routes),
+ ],
+ providers: [
+ {
+ provide: APP_INITIALIZER,
+ multi: true,
+ deps: [],
+ useFactory: () => () => {
+ AddonMessageOutputDelegate.instance.registerHandler(AddonMessageOutputAirnotifierHandler.instance);
+ },
+ },
+ ],
+})
+export class AddonMessageOutputAirnotifierModule {}
diff --git a/src/addons/messageoutput/airnotifier/lang.json b/src/addons/messageoutput/airnotifier/lang.json
new file mode 100644
index 000000000..a6f460bbb
--- /dev/null
+++ b/src/addons/messageoutput/airnotifier/lang.json
@@ -0,0 +1,3 @@
+{
+ "processorsettingsdesc": "Configure devices"
+}
\ No newline at end of file
diff --git a/src/addons/messageoutput/airnotifier/pages/devices/devices.html b/src/addons/messageoutput/airnotifier/pages/devices/devices.html
new file mode 100644
index 000000000..134300b25
--- /dev/null
+++ b/src/addons/messageoutput/airnotifier/pages/devices/devices.html
@@ -0,0 +1,27 @@
+
+
+
+
+
+ {{ 'addon.messageoutput_airnotifier.processorsettingsdesc' | translate }}
+
+
+
+
+
+
+
+
+
+
+ {{ device.name }} {{ device.model }} {{ device.platform }} {{ device.version }}
+ ({{ 'core.currentdevice' | translate }})
+
+
+
+
+
+
+
+
diff --git a/src/addons/messageoutput/airnotifier/pages/devices/devices.module.ts b/src/addons/messageoutput/airnotifier/pages/devices/devices.module.ts
new file mode 100644
index 000000000..fa7cb1aac
--- /dev/null
+++ b/src/addons/messageoutput/airnotifier/pages/devices/devices.module.ts
@@ -0,0 +1,48 @@
+// (C) Copyright 2015 Moodle Pty Ltd.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import { NgModule } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { RouterModule, Routes } from '@angular/router';
+import { FormsModule } from '@angular/forms';
+import { IonicModule } from '@ionic/angular';
+import { TranslateModule } from '@ngx-translate/core';
+
+import { CoreComponentsModule } from '@components/components.module';
+import { CoreDirectivesModule } from '@directives/directives.module';
+import { AddonMessageOutputAirnotifierDevicesPage } from './devices';
+
+const routes: Routes = [
+ {
+ path: '',
+ component: AddonMessageOutputAirnotifierDevicesPage,
+ },
+];
+
+@NgModule({
+ imports: [
+ RouterModule.forChild(routes),
+ CommonModule,
+ IonicModule,
+ FormsModule,
+ TranslateModule.forChild(),
+ CoreComponentsModule,
+ CoreDirectivesModule,
+ ],
+ declarations: [
+ AddonMessageOutputAirnotifierDevicesPage,
+ ],
+ exports: [RouterModule],
+})
+export class AddonMessageOutputAirnotifierDevicesPageModule {}
diff --git a/src/addons/messageoutput/airnotifier/pages/devices/devices.ts b/src/addons/messageoutput/airnotifier/pages/devices/devices.ts
new file mode 100644
index 000000000..ebd03ea82
--- /dev/null
+++ b/src/addons/messageoutput/airnotifier/pages/devices/devices.ts
@@ -0,0 +1,161 @@
+// (C) Copyright 2015 Moodle Pty Ltd.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import { Component, OnDestroy, OnInit } from '@angular/core';
+import { IonRefresher } from '@ionic/angular';
+
+import { CoreDomUtils } from '@services/utils/dom';
+import { CorePushNotifications } from '@features/pushnotifications/services/pushnotifications';
+import { AddonMessageOutputAirnotifier, AddonMessageOutputAirnotifierDevice } from '../../services/airnotifier';
+import { CoreUtils } from '@services/utils/utils';
+
+/**
+ * Page that displays the list of devices.
+ */
+@Component({
+ selector: 'page-addon-message-output-airnotifier-devices',
+ templateUrl: 'devices.html',
+})
+export class AddonMessageOutputAirnotifierDevicesPage implements OnInit, OnDestroy {
+
+ devices?: AddonMessageOutputAirnotifierDeviceFormatted[] = [];
+ devicesLoaded = false;
+
+ protected updateTimeout?: number;
+
+ /**
+ * Component being initialized.
+ */
+ ngOnInit(): void {
+ this.fetchDevices();
+ }
+
+ /**
+ * Fetches the list of devices.
+ *
+ * @return Promise resolved when done.
+ */
+ protected async fetchDevices(): Promise {
+ try {
+ const devices = await AddonMessageOutputAirnotifier.instance.getUserDevices();
+
+ this.devices = this.formatDevices(devices);
+ } catch (error) {
+ CoreDomUtils.instance.showErrorModal(error);
+ } finally {
+ this.devicesLoaded = true;
+ }
+ }
+
+ /**
+ * Add some calculated data for devices.
+ *
+ * @param devices Devices to format.
+ * @return Formatted devices.
+ */
+ protected formatDevices(devices: AddonMessageOutputAirnotifierDevice[]): AddonMessageOutputAirnotifierDeviceFormatted[] {
+ const formattedDevices: AddonMessageOutputAirnotifierDeviceFormatted[] = devices;
+ const pushId = CorePushNotifications.instance.getPushId();
+
+ // Convert enabled to boolean and search current device.
+ formattedDevices.forEach((device) => {
+ device.enable = !!device.enable;
+ device.current = !!(pushId && pushId == device.pushid);
+ });
+
+ return formattedDevices;
+ }
+
+ /**
+ * Update list of devices after a certain time. The purpose is to store the updated data, it won't be reflected in the view.
+ */
+ protected updateDevicesAfterDelay(): void {
+ // Cancel pending updates.
+ if (this.updateTimeout) {
+ clearTimeout(this.updateTimeout);
+ }
+
+ this.updateTimeout = window.setTimeout(() => {
+ this.updateTimeout = undefined;
+ this.updateDevices();
+ }, 5000);
+ }
+
+ /**
+ * Fetch devices. The purpose is to store the updated data, it won't be reflected in the view.
+ */
+ protected async updateDevices(): Promise {
+ await CoreUtils.instance.ignoreErrors(AddonMessageOutputAirnotifier.instance.invalidateUserDevices());
+
+ await AddonMessageOutputAirnotifier.instance.getUserDevices();
+ }
+
+ /**
+ * Refresh the list of devices.
+ *
+ * @param refresher Refresher.
+ */
+ async refreshDevices(refresher: CustomEvent): Promise {
+ try {
+ await CoreUtils.instance.ignoreErrors(AddonMessageOutputAirnotifier.instance.invalidateUserDevices());
+
+ await this.fetchDevices();
+ } finally {
+ refresher?.detail.complete();
+ }
+ }
+
+ /**
+ * Enable or disable a certain device.
+ *
+ * @param device The device object.
+ * @param enable True to enable the device, false to disable it.
+ */
+ async enableDevice(device: AddonMessageOutputAirnotifierDeviceFormatted, enable: boolean): Promise {
+ device.updating = true;
+
+ try {
+ await AddonMessageOutputAirnotifier.instance.enableDevice(device.id, enable);
+
+ // Update the list of devices since it was modified.
+ this.updateDevicesAfterDelay();
+ } catch (error) {
+ // Show error and revert change.
+ CoreDomUtils.instance.showErrorModal(error);
+ device.enable = !device.enable;
+ } finally {
+ device.updating = false;
+ }
+ }
+
+ /**
+ * Page destroyed.
+ */
+ ngOnDestroy(): void {
+ // If there is a pending action to update devices, execute it right now.
+ if (this.updateTimeout) {
+ clearTimeout(this.updateTimeout);
+ this.updateDevices();
+ }
+ }
+
+}
+
+/**
+ * User device with some calculated data.
+ */
+type AddonMessageOutputAirnotifierDeviceFormatted = AddonMessageOutputAirnotifierDevice & {
+ current?: boolean; // Calculated in the app. Whether it's the current device.
+ updating?: boolean; // Calculated in the app. Whether the device enable is being updated right now.
+};
diff --git a/src/addons/messageoutput/airnotifier/services/airnotifier.ts b/src/addons/messageoutput/airnotifier/services/airnotifier.ts
new file mode 100644
index 000000000..954e29bcf
--- /dev/null
+++ b/src/addons/messageoutput/airnotifier/services/airnotifier.ts
@@ -0,0 +1,190 @@
+// (C) Copyright 2015 Moodle Pty Ltd.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import { Injectable } from '@angular/core';
+
+import { CoreSites } from '@services/sites';
+import { CoreWSExternalWarning } from '@services/ws';
+import { CoreConstants } from '@/core/constants';
+import { CoreSite, CoreSiteWSPreSets } from '@classes/site';
+import { CoreError } from '@classes/errors/error';
+import { CoreWSError } from '@classes/errors/wserror';
+import { makeSingleton } from '@singletons';
+import { CoreEvents, CoreEventSiteData } from '@singletons/events';
+
+const ROOT_CACHE_KEY = 'mmaMessageOutputAirnotifier:';
+
+/**
+ * Service to handle Airnotifier message output.
+ */
+@Injectable({ providedIn: 'root' })
+export class AddonMessageOutputAirnotifierProvider {
+
+ constructor() {
+ CoreEvents.on(CoreEvents.DEVICE_REGISTERED_IN_MOODLE, async (data: CoreEventSiteData) => {
+ // Get user devices to make Moodle send the devices data to Airnotifier.
+ this.getUserDevices(true, data.siteId);
+ });
+ }
+
+ /**
+ * Enables or disables a device.
+ *
+ * @param deviceId Device ID.
+ * @param enable True to enable, false to disable.
+ * @param siteId Site ID. If not defined, current site.
+ * @return Promise resolved if success.
+ */
+ async enableDevice(deviceId: number, enable: boolean, siteId?: string): Promise {
+ const site = await CoreSites.instance.getSite(siteId);
+
+ const data: AddonMessageOutputAirnotifierEnableDeviceWSParams = {
+ deviceid: deviceId,
+ enable: !!enable,
+ };
+
+ const result = await site.write(
+ 'message_airnotifier_enable_device',
+ data,
+ );
+
+ if (result.success) {
+ return;
+ }
+
+ // Fail. Reject with warning message if any.
+ if (result.warnings?.length) {
+ throw new CoreWSError(result.warnings[0]);
+ }
+
+ throw new CoreError('Error enabling device');
+ }
+
+ /**
+ * Get the cache key for the get user devices call.
+ *
+ * @return Cache key.
+ */
+ protected getUserDevicesCacheKey(): string {
+ return ROOT_CACHE_KEY + 'userDevices';
+ }
+
+ /**
+ * Get user devices.
+ *
+ * @param ignoreCache Whether to ignore cache.
+ * @param siteId Site ID. If not defined, use current site.
+ * @return Promise resolved with the devices.
+ */
+ async getUserDevices(ignoreCache?: boolean, siteId?: string): Promise {
+
+ const site = await CoreSites.instance.getSite(siteId);
+
+ const data: AddonMessageOutputAirnotifierGetUserDevicesWSParams = {
+ appid: CoreConstants.CONFIG.app_id,
+ };
+ const preSets: CoreSiteWSPreSets = {
+ cacheKey: this.getUserDevicesCacheKey(),
+ updateFrequency: CoreSite.FREQUENCY_RARELY,
+ };
+
+ if (ignoreCache) {
+ preSets.getFromCache = false;
+ preSets.emergencyCache = false;
+ }
+
+ const result = await site.read(
+ 'message_airnotifier_get_user_devices',
+ data,
+ preSets,
+ );
+
+ return result.devices;
+ }
+
+ /**
+ * Invalidate get user devices.
+ *
+ * @param siteId Site ID. If not defined, current site.
+ * @return Promise resolved when data is invalidated.
+ */
+ async invalidateUserDevices(siteId?: string): Promise {
+ const site = await CoreSites.instance.getSite(siteId);
+
+ return site.invalidateWsCacheForKey(this.getUserDevicesCacheKey());
+ }
+
+ /**
+ * Returns whether or not the plugin is enabled for the current site.
+ *
+ * @return True if enabled, false otherwise.
+ * @since 3.2
+ */
+ isEnabled(): boolean {
+ return CoreSites.instance.wsAvailableInCurrentSite('message_airnotifier_enable_device') &&
+ CoreSites.instance.wsAvailableInCurrentSite('message_airnotifier_get_user_devices');
+ }
+
+}
+
+export class AddonMessageOutputAirnotifier extends makeSingleton(AddonMessageOutputAirnotifierProvider) {}
+
+/**
+ * Device data returned by WS message_airnotifier_get_user_devices.
+ */
+export type AddonMessageOutputAirnotifierDevice = {
+ id: number; // Device id (in the message_airnotifier table).
+ appid: string; // The app id, something like com.moodle.moodlemobile.
+ name: string; // The device name, 'occam' or 'iPhone' etc.
+ model: string; // The device model 'Nexus4' or 'iPad1,1' etc.
+ platform: string; // The device platform 'iOS' or 'Android' etc.
+ version: string; // The device version '6.1.2' or '4.2.2' etc.
+ pushid: string; // The device PUSH token/key/identifier/registration id.
+ uuid: string; // The device UUID.
+ enable: number | boolean; // Whether the device is enabled or not.
+ timecreated: number; // Time created.
+ timemodified: number; // Time modified.
+};
+
+/**
+ * Params of message_airnotifier_enable_device WS.
+ */
+export type AddonMessageOutputAirnotifierEnableDeviceWSParams = {
+ deviceid: number; // The device id.
+ enable: boolean; // True for enable the device, false otherwise.
+};
+
+/**
+ * Result of WS message_airnotifier_enable_device.
+ */
+export type AddonMessageOutputAirnotifierEnableDeviceWSResponse = {
+ success: boolean; // True if success.
+ warnings?: CoreWSExternalWarning[];
+};
+
+/**
+ * Params of message_airnotifier_get_user_devices WS.
+ */
+export type AddonMessageOutputAirnotifierGetUserDevicesWSParams = {
+ appid: string; // App unique id (usually a reversed domain).
+ userid?: number; // User id, 0 for current user.
+};
+
+/**
+ * Result of WS message_airnotifier_get_user_devices.
+ */
+export type AddonMessageOutputAirnotifierGetUserDevicesWSResponse = {
+ devices: AddonMessageOutputAirnotifierDevice[]; // List of devices.
+ warnings?: CoreWSExternalWarning[];
+};
diff --git a/src/addons/messageoutput/airnotifier/services/handlers/messageoutput.ts b/src/addons/messageoutput/airnotifier/services/handlers/messageoutput.ts
new file mode 100644
index 000000000..c2668eb39
--- /dev/null
+++ b/src/addons/messageoutput/airnotifier/services/handlers/messageoutput.ts
@@ -0,0 +1,61 @@
+// (C) Copyright 2015 Moodle Pty Ltd.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import { Injectable } from '@angular/core';
+
+import { AddonMessageOutputHandler, AddonMessageOutputHandlerData } from '@addons/messageoutput/services/messageoutput-delegate';
+import { AddonMessageOutputAirnotifierProvider } from '../airnotifier';
+import { makeSingleton } from '@singletons';
+
+/**
+ * Airnotifier message output handler.
+ */
+@Injectable({ providedIn: 'root' })
+export class AddonMessageOutputAirnotifierHandlerService implements AddonMessageOutputHandler {
+
+ static readonly PAGE_NAME = 'messageoutput-airnotifier';
+
+ name = 'AddonMessageOutputAirnotifier';
+ processorName = 'airnotifier';
+
+ constructor(private airnotifierProvider: AddonMessageOutputAirnotifierProvider) {}
+
+ /**
+ * Whether or not the module is enabled for the site.
+ *
+ * @return True if enabled, false otherwise.
+ */
+ async isEnabled(): Promise {
+ return this.airnotifierProvider.isEnabled();
+ }
+
+ /**
+ * Returns the data needed to render the handler.
+ *
+ * @param processor The processor object.
+ * @return Data.
+ */
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ getDisplayData(processor: Record): AddonMessageOutputHandlerData {
+ return {
+ priority: 600,
+ label: 'addon.messageoutput_airnotifier.processorsettingsdesc',
+ icon: 'fas-cog',
+ page: AddonMessageOutputAirnotifierHandlerService.PAGE_NAME,
+ };
+ }
+
+}
+
+export class AddonMessageOutputAirnotifierHandler extends makeSingleton(AddonMessageOutputAirnotifierHandlerService) {}
diff --git a/src/addons/messageoutput/messageoutput.module.ts b/src/addons/messageoutput/messageoutput.module.ts
new file mode 100644
index 000000000..c12500d44
--- /dev/null
+++ b/src/addons/messageoutput/messageoutput.module.ts
@@ -0,0 +1,28 @@
+// (C) Copyright 2015 Moodle Pty Ltd.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import { NgModule } from '@angular/core';
+
+import { AddonMessageOutputAirnotifierModule } from './airnotifier/airnotifier.module';
+
+@NgModule({
+ declarations: [
+ ],
+ imports: [
+ AddonMessageOutputAirnotifierModule,
+ ],
+ providers: [
+ ],
+})
+export class AddonMessageOutputModule {}
diff --git a/src/addons/messageoutput/services/messageoutput-delegate.ts b/src/addons/messageoutput/services/messageoutput-delegate.ts
new file mode 100644
index 000000000..6a82ce5a6
--- /dev/null
+++ b/src/addons/messageoutput/services/messageoutput-delegate.ts
@@ -0,0 +1,93 @@
+// (C) Copyright 2015 Moodle Pty Ltd.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import { Injectable } from '@angular/core';
+import { Params } from '@angular/router';
+
+import { CoreDelegate, CoreDelegateHandler } from '@classes/delegate';
+import { makeSingleton } from '@singletons';
+
+/**
+ * Interface that all message output handlers must implement.
+ */
+export interface AddonMessageOutputHandler extends CoreDelegateHandler {
+ /**
+ * The name of the processor. E.g. 'airnotifier'.
+ */
+ processorName: string;
+
+ /**
+ * Returns the data needed to render the handler.
+ *
+ * @param processor The processor object.
+ * @return Data.
+ */
+ getDisplayData(processor: Record): AddonMessageOutputHandlerData;
+}
+
+/**
+ * Data needed to render a message output handler. It's returned by the handler.
+ */
+export interface AddonMessageOutputHandlerData {
+ /**
+ * Handler's priority.
+ */
+ priority: number;
+
+ /**
+ * Name of the page to load for the handler.
+ */
+ page: string;
+
+ /**
+ * Label to display for the handler.
+ */
+ label: string;
+
+ /**
+ * Name of the icon to display for the handler.
+ */
+ icon: string;
+
+ /**
+ * Params to pass to the page.
+ */
+ pageParams?: Params;
+}
+
+/**
+ * Delegate to register processors (message/output) to be used in places like notification preferences.
+ */
+@Injectable({ providedIn: 'root' })
+export class AddonMessageOutputDelegateService extends CoreDelegate {
+
+ protected handlerNameProperty = 'processorName';
+
+ constructor() {
+ super('AddonMessageOutputDelegate', true);
+ }
+
+ /**
+ * Get the display data of the handler.
+ *
+ * @param processor The processor object.
+ * @return Data.
+ */
+ getDisplayData(processor: Record): AddonMessageOutputHandlerData | undefined {
+ return this.executeFunctionOnEnabled( processor.name, 'getDisplayData', [processor]);
+ }
+
+}
+
+export class AddonMessageOutputDelegate extends makeSingleton(AddonMessageOutputDelegateService) {}
diff --git a/tsconfig.app.json b/tsconfig.app.json
index 69cd4cf31..feedfbd74 100644
--- a/tsconfig.app.json
+++ b/tsconfig.app.json
@@ -11,6 +11,7 @@
"webpack-env"
],
"paths": {
+ "@addons/*": ["addons/*"],
"@classes/*": ["core/classes/*"],
"@components/*": ["core/components/*"],
"@directives/*": ["core/directives/*"],
diff --git a/tsconfig.json b/tsconfig.json
index f94ebb411..0e32c7acc 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -30,6 +30,7 @@
"webpack-env"
],
"paths": {
+ "@addons/*": ["addons/*"],
"@classes/*": ["core/classes/*"],
"@components/*": ["core/components/*"],
"@directives/*": ["core/directives/*"],
diff --git a/tsconfig.test.json b/tsconfig.test.json
index 7c1e71c67..34d2a3698 100644
--- a/tsconfig.test.json
+++ b/tsconfig.test.json
@@ -15,6 +15,7 @@
"node"
],
"paths": {
+ "@addons/*": ["addons/*"],
"@classes/*": ["core/classes/*"],
"@components/*": ["core/components/*"],
"@directives/*": ["core/directives/*"],