fix more types and make more code consistent (mobile test seems to hang)

This commit is contained in:
2026-09-20 16:10:36 -06:00
parent e350feb132
commit 45503f568a
29 changed files with 140 additions and 218 deletions
+1 -2
View File
@@ -9,7 +9,6 @@
"scripts": {
"lint": "eslint --ext .js,.ts,.vue --ignore-path .gitignore src",
"lint-fix": "eslint --ext .js,.ts,.vue --ignore-path .gitignore --fix src",
"type-safety-check": "./scripts/type-safety-check.sh",
"type-check": "tsc --noEmit",
"type-check:vue": "vue-tsc --noEmit",
"prebuild": "eslint --ext .js,.ts,.vue --ignore-path .gitignore src && node sw_combine.js && node scripts/copy-wasm.js",
@@ -17,7 +16,7 @@
"test:unit": "jest",
"check:dependencies": "./scripts/check-dependencies.sh",
"deps:update-daily-notification-plugin": "npm install @timesafari/daily-notification-plugin@git+https://gitea.anomalistdesign.com/trent_larson/daily-notification-plugin.git#master",
"test:all": "npm run lint && tsc && npm run test:unit && npm run test:web && npm run test:mobile && ./scripts/test-safety-check.sh && echo '\n\n\nGotta add the performance tests'",
"test:all": "npm run lint && npm run type-check && npm run type-check:vue && npm run test:unit && npm run test:web && npm run test:mobile && echo '\n\n\nGotta add the performance tests'",
"test:web": "npx playwright test -c playwright.config-local.ts --trace on",
"test:mobile": "./scripts/test-mobile.sh",
"test:android": "node scripts/test-android.js",
-103
View File
@@ -1,103 +0,0 @@
#!/bin/bash
# Type Safety Pre-commit Check Script
# This script ensures type safety before commits by running linting and type checking
set -e
echo "🔍 Running Type Safety Pre-commit Checks..."
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
# Function to print colored output
print_status() {
echo -e "${GREEN}✅ $1${NC}"
}
print_warning() {
echo -e "${YELLOW}⚠️ $1${NC}"
}
print_error() {
echo -e "${RED}❌ $1${NC}"
}
# Check if we're in the right directory
if [ ! -f "package.json" ]; then
print_error "Must run from project root directory"
exit 1
fi
# Step 1: Run ESLint with TypeScript rules
print_status "Running ESLint TypeScript checks..."
if npm run lint > /dev/null 2>&1; then
print_status "ESLint passed - no type safety issues found"
else
print_error "ESLint failed - type safety issues detected"
echo ""
echo "Running lint with details..."
npm run lint
echo ""
print_error "Please fix the above type safety issues before committing"
exit 1
fi
# Step 2: Run TypeScript type checking
print_status "Running TypeScript type checking..."
if npm run type-check > /dev/null 2>&1; then
print_status "TypeScript compilation passed"
else
print_error "TypeScript compilation failed"
echo ""
echo "Running type check with details..."
npm run type-check
echo ""
print_error "Please fix the above TypeScript errors before committing"
exit 1
fi
# Step 3: Check for any remaining 'any' types
print_status "Scanning for any remaining 'any' types..."
ANY_COUNT=$(grep -r "any" src/ --include="*.ts" --include="*.vue" | grep -v "// eslint-disable" | grep -v "eslint-disable-next-line" | wc -l)
if [ "$ANY_COUNT" -eq 0 ]; then
print_status "No 'any' types found in source code"
else
print_warning "Found $ANY_COUNT instances of 'any' type usage"
echo ""
echo "Instances found:"
grep -r "any" src/ --include="*.ts" --include="*.vue" | grep -v "// eslint-disable" | grep -v "eslint-disable-next-line" || true
echo ""
print_error "Please replace 'any' types with proper TypeScript types before committing"
exit 1
fi
# Step 4: Verify database migration status
print_status "Checking database migration status..."
if grep -r "databaseUtil" src/ --include="*.ts" --include="*.vue" > /dev/null 2>&1; then
print_warning "Found databaseUtil imports - ensure migration is complete"
echo ""
echo "Files with databaseUtil imports:"
grep -r "databaseUtil" src/ --include="*.ts" --include="*.vue" | head -5 || true
echo ""
print_warning "Consider completing database migration to PlatformServiceMixin"
else
print_status "No databaseUtil imports found - migration appears complete"
fi
# All checks passed
echo ""
print_status "All type safety checks passed! 🎉"
print_status "Your code is ready for commit"
echo ""
echo "📚 Remember to follow the Type Safety Guidelines:"
echo " - doc/typescript-type-safety-guidelines.md"
echo " - Use proper error handling patterns"
echo " - Leverage existing type definitions"
echo " - Run 'npm run lint-fix' for automatic fixes"
exit 0
+2 -7
View File
@@ -130,6 +130,7 @@
<script lang="ts">
import { Vue, Component, Prop } from "vue-facing-decorator";
import { NotificationIface } from "@/constants/app";
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
import { SOMEONE_UNNAMED } from "@/constants/entities";
@@ -149,10 +150,7 @@ export default class BulkMembersDialog extends Vue {
@Prop({ required: true }) isOrganizer!: boolean;
// Vue notification system
$notify!: (
notification: { group: string; type: string; title: string; text: string },
timeout?: number,
) => void;
$notify!: (notification: NotificationIface, timeout?: number) => void;
// Notification system
notify!: ReturnType<typeof createNotifyHelpers>;
@@ -381,9 +379,6 @@ export default class BulkMembersDialog extends Vue {
contact,
);
if (result.success) {
if (result.embeddedRecordError) {
throw new Error(result.embeddedRecordError);
}
await this.$updateContact(member.did, { registered: true });
} else {
throw result;
+1
View File
@@ -106,6 +106,7 @@ import { Router } from "vue-router";
import * as R from "ramda";
import { NotificationIface } from "../constants/app";
import { Contact } from "../db/tables/contacts";
import { logger } from "../utils/logger";
import { createNotifyHelpers, TIMEOUTS } from "@/utils/notify";
+1 -1
View File
@@ -135,7 +135,7 @@ export default class EntitySummaryButton extends Vue {
}
// If the entity does not have a set name, but is not the special "Unnamed", use their DID
return this.entity?.did;
return this.entity?.did ?? "";
}
/**
-3
View File
@@ -43,9 +43,6 @@ export default class InfiniteScroll extends Vue {
/** Intersection Observer instance for detecting scroll position */
private observer!: IntersectionObserver;
/** Flag to track initial render state */
private isInitialRender = true;
/** Flag to prevent multiple simultaneous loading states */
private isLoading = false;
+1 -1
View File
@@ -251,7 +251,7 @@ export default class PhotoDialog extends Vue {
* Provides cached access to platform capabilities
*/
get platformCapabilities() {
return this.$platformService.getCapabilities();
return this.platformService.getCapabilities();
}
// =================================================
+19
View File
@@ -0,0 +1,19 @@
import type { Camera, Scene } from "three";
/**
* Type declarations for World.js, which is plain JavaScript.
*
* Only the members used outside the module are declared; its internals stay
* untyped.
*/
export declare class World {
camera: Camera;
scene: Scene;
constructor(container: Element | null, vue: unknown);
update(time: number): void;
render(): void;
start(): void;
stop(): void;
setExposedWorldProperties(key: string, value: unknown): void;
}
+17
View File
@@ -15,6 +15,23 @@ export interface GenericCredWrapper<T extends GenericVerifiableCredential> {
publicUrls?: Record<string, string>;
}
/**
* The fields the certificate canvases read off a claim.
*
* GenericVerifiableCredential carries an index signature of `unknown`, which is
* correct for a claim of any type but leaves each field unusable without a
* cast. This names the handful the drawing code actually touches. An `agent` is
* either a bare DID string or an object carrying one, depending on claim type.
*/
export interface CertifiableClaim extends GenericVerifiableCredential {
agent?: string | { identifier?: string };
description?: string;
includesObject?: { amountOfThisGood?: number; unitCode?: string };
itemOffered?: { description?: string };
name?: string;
object?: { amountOfThisGood?: number; unitCode?: string };
}
export interface ErrorResponse {
error?: {
message?: string;
+22 -6
View File
@@ -381,6 +381,19 @@ export function didInfoOrNobody(
/**
* return text description without any references to "you" as user
*/
/**
* A claim's agent is either a bare DID string or an object carrying one,
* depending on the claim type.
*
* @param agent - the `agent` field of a claim, in either shape
* @returns the DID, or undefined when the agent carries none
*/
export function didOfAgent(
agent: string | { identifier?: string } | undefined,
): string | undefined {
return typeof agent === "string" ? agent : agent?.identifier;
}
export function didInfoForCertificate(
did: string | undefined,
contacts: Contact[],
@@ -676,15 +689,18 @@ export async function setPlanInCache(
/**
* Extracts user-friendly message from server error
* @param {AxiosErrorResponse} error - Error thrown from Endorser server call
*
* Takes `unknown` because callers pass it straight from a `catch`, and every
* access below is optional-chained, so a value of any shape is safe.
*
* @param error - Error thrown from an Endorser server call
* @returns {string|undefined} User-friendly message or undefined if none found
*/
export function serverMessageForUser(
error: AxiosErrorResponse,
): string | undefined {
export function serverMessageForUser(error: unknown): string | undefined {
const err = error as AxiosErrorResponse;
return (
error?.response?.data?.error?.userMessage ||
error?.response?.data?.error?.message
err?.response?.data?.error?.userMessage ||
err?.response?.data?.error?.message
);
}
+10
View File
@@ -2392,6 +2392,16 @@ declare module "@vue/runtime-core" {
currentActiveDid: string | null;
$updateActiveDid(newDid: string | null): Promise<void>;
// Reads a column that may hold JSON text or an already-parsed value
_parseJsonField<T>(value: unknown, defaultValue: T): T;
// Account selection, implemented in the mixin above
$countAccounts(): Promise<number>;
$getAccountDidById(id: number): Promise<string>;
$getActiveDid(): Promise<string | null>;
$pickNextAccountDid(all: string[], current?: string): string;
$setActiveDid(did: string | null): Promise<void>;
// Ultra-concise database methods (shortest possible names)
$db(sql: string, params?: unknown[]): Promise<QueryExecResult | undefined>;
$exec(sql: string, params?: unknown[]): Promise<DatabaseExecResult>;
-39
View File
@@ -972,8 +972,6 @@ import "leaflet/dist/leaflet.css";
import { Buffer } from "buffer/";
import "dexie-export-import";
// @ts-expect-error - they aren't exporting it but it's there
import { ImportProgress } from "dexie-export-import";
import { LeafletMouseEvent } from "leaflet";
import * as L from "leaflet";
import { IIdentifier } from "@veramo/core";
@@ -2389,19 +2387,6 @@ export default class AccountViewView extends Vue {
);
}
private progressCallback(progress: ImportProgress): boolean {
logger.log(
`Import progress: ${progress.completedRows} of ${progress.totalRows} rows completed.`,
);
if (progress.done) {
this.notify.success(
ACCOUNT_VIEW_CONSTANTS.SUCCESS.IMPORT_COMPLETE,
TIMEOUTS.LONG,
);
}
return true;
}
async checkLimits(): Promise<void> {
this.loadingLimits = true;
const did = this.activeDid;
@@ -3111,30 +3096,6 @@ export default class AccountViewView extends Vue {
};
}
/**
* Clear profile location
*/
private clearProfileLocation(profileData: ProfileData): ProfileData {
return {
...profileData,
latitude: 0,
longitude: 0,
includeLocation: false,
};
}
/**
* Get default profile data
*/
private getDefaultProfile(): ProfileData {
return {
description: "",
latitude: 0,
longitude: 0,
includeLocation: false,
};
}
/**
* Delete user profile from the partner API
*/
+12 -11
View File
@@ -16,7 +16,7 @@ import { nextTick } from "vue";
import QRCode from "qrcode";
import { APP_SERVER, NotificationIface } from "../constants/app";
import * as serverUtil from "../libs/endorserServer";
import { GenericCredWrapper, GenericVerifiableCredential } from "../interfaces";
import { CertifiableClaim, GenericCredWrapper } from "../interfaces";
import { logger } from "../utils/logger";
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
import { createNotifyHelpers, TIMEOUTS } from "@/utils/notify";
@@ -39,6 +39,13 @@ export default class ClaimCertificateView extends Vue {
async created() {
this.notify = createNotifyHelpers(this.$notify);
// Read the id from the URL before the first await, so nothing can render
// or act against an empty id while the awaits below resolve.
const pathParams = window.location.pathname.substring(
"/claim-cert/".length,
);
this.claimId = pathParams;
const settings = await this.$accountSettings();
// Get activeDid from active_identity table (single source of truth)
@@ -47,10 +54,6 @@ export default class ClaimCertificateView extends Vue {
this.activeDid = activeIdentity.activeDid || "";
this.apiServer = settings.apiServer || "";
const pathParams = window.location.pathname.substring(
"/claim-cert/".length,
);
this.claimId = pathParams;
await this.fetchClaim();
}
@@ -91,7 +94,7 @@ export default class ClaimCertificateView extends Vue {
}
async drawCanvas(
claimData: GenericCredWrapper<GenericVerifiableCredential>,
claimData: GenericCredWrapper<CertifiableClaim>,
confirmerIds: Array<string>,
) {
const allContacts = await this.$getAllContacts();
@@ -142,8 +145,7 @@ export default class ClaimCertificateView extends Vue {
(CANVAS_WIDTH - presentedWidth) / 2, // Center horizontally
CANVAS_HEIGHT * 0.37,
);
const agentDid =
claimData.claim.agent.identifier || claimData.claim.agent;
const agentDid = serverUtil.didOfAgent(claimData.claim.agent);
const agentText = serverUtil.didInfoForCertificate(
agentDid,
allContacts,
@@ -168,8 +170,7 @@ export default class ClaimCertificateView extends Vue {
CANVAS_HEIGHT * 0.37,
);
// fulfills
const agentDid =
claimData.claim.agent.identifier || claimData.claim.agent;
const agentDid = serverUtil.didOfAgent(claimData.claim.agent);
const agentText = serverUtil.didInfoForCertificate(
agentDid,
allContacts,
@@ -223,7 +224,7 @@ export default class ClaimCertificateView extends Vue {
serverUtil.isHiddenDid(claimData.issuer) ||
// don't show if issuer claimed for themselves
// (The confirmations are the good stuff anyway, and self-issued certs shouldn't detract from that.)
claimData.issuer !== claimData.claim.agent?.identifier
claimData.issuer !== serverUtil.didOfAgent(claimData.claim.agent)
) {
ctx.font = "14px Arial";
let fullIssuer = serverUtil.didInfoForCertificate(
+9 -9
View File
@@ -13,10 +13,7 @@ import QRCode from "qrcode";
import { APP_SERVER, NotificationIface } from "../constants/app";
import * as endorserServer from "../libs/endorserServer";
import {
GenericCredWrapper,
GenericVerifiableCredential,
} from "../interfaces/common";
import { CertifiableClaim, GenericCredWrapper } from "../interfaces/common";
import { logger } from "../utils/logger";
import { Contact } from "@/db/tables/contacts";
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
@@ -53,6 +50,13 @@ export default class ClaimReportCertificateView extends Vue {
// Initialize notification helper
this.notify = createNotifyHelpers(this.$notify);
// Read the id from the URL before the first await, so nothing can render
// or act against an empty id while the awaits below resolve.
const pathParams = window.location.pathname.substring(
"/claim-cert/".length,
);
this.claimId = pathParams;
const settings = await this.$accountSettings();
// Get activeDid from active_identity table (single source of truth)
@@ -61,10 +65,6 @@ export default class ClaimReportCertificateView extends Vue {
this.activeDid = activeIdentity.activeDid || "";
this.apiServer = settings.apiServer || "";
const pathParams = window.location.pathname.substring(
"/claim-cert/".length,
);
this.claimId = pathParams;
await this.fetchClaim();
}
@@ -88,7 +88,7 @@ export default class ClaimReportCertificateView extends Vue {
}
}
async drawCanvas(claimData: GenericCredWrapper<GenericVerifiableCredential>) {
async drawCanvas(claimData: GenericCredWrapper<CertifiableClaim>) {
if (!claimData || !claimData.claim) {
return;
}
+8 -6
View File
@@ -502,6 +502,7 @@ export default class ConfirmGiftView extends Vue {
apiServer = "";
canShare = false;
claimId = "";
confirmerIdList: string[] = []; // list of DIDs that have confirmed this claim excluding the issuer
confsVisibleErrorMessage = "";
confsVisibleToIdList: string[] = []; // list of DIDs that can see any confirmer
@@ -549,6 +550,11 @@ export default class ConfirmGiftView extends Vue {
*/
async mounted() {
this.isLoading = true;
// Read the claim id from the URL before the first await, so nothing can
// render or act against an empty id while the awaits below resolve.
this.claimId = decodeURIComponent(
window.location.pathname.substring("/confirm-gift/".length),
);
try {
await this.initializeSettings();
await this.loadClaimFromUrl();
@@ -586,15 +592,11 @@ export default class ConfirmGiftView extends Vue {
* Loads and processes claim from URL parameters
*/
private async loadClaimFromUrl() {
const pathParam = window.location.pathname.substring(
"/confirm-gift/".length,
);
if (!pathParam) {
const claimId = this.claimId;
if (!claimId) {
throw new Error("No claim ID was provided.");
}
const claimId = decodeURIComponent(pathParam);
// Use production URL for sharing to avoid localhost issues in development
this.windowLocation = `${APP_SERVER}/deep-link/confirm-gift/${claimId}`;
+12 -2
View File
@@ -178,11 +178,11 @@
{{ capitalizeAndInsertSpacesBeforeCaps(contactField) }}
</div>
<div v-if="contactField === 'labels'" class="border p-1">
{{ value.old.join(", ") }}
{{ joinIfList(value.old) }}
</div>
<div v-else class="border p-1">{{ value.old }}</div>
<div v-if="contactField === 'labels'" class="border p-1">
{{ value.new.join(", ") }}
{{ joinIfList(value.new) }}
</div>
<div v-else class="border p-1">{{ value.new }}</div>
</div>
@@ -398,6 +398,16 @@ export default class ContactImportView extends Vue {
// Constants
AppString = AppString;
capitalizeAndInsertSpacesBeforeCaps = capitalizeAndInsertSpacesBeforeCaps;
/**
* Renders one side of a contact difference.
*
* The `labels` field holds a list while every other field holds a scalar, and
* the template cannot narrow the union, so the join happens here.
*/
joinIfList(value: unknown): string {
return Array.isArray(value) ? value.join(", ") : String(value ?? "");
}
libsUtil = libsUtil;
R = R;
+1 -9
View File
@@ -728,16 +728,8 @@ export default class ContactQRScanShow extends Vue {
// Add new contact
// @ts-expect-error because we're just using the value to store to the DB
// eslint-disable-next-line @typescript-eslint/no-explicit-any
contact.contactMethods = JSON.stringify(
(
this as {
_parseJsonField: (
value: unknown,
defaultValue: unknown[],
) => unknown[];
}
)._parseJsonField(contact.contactMethods, []),
this._parseJsonField(contact.contactMethods, []),
);
await this.$insertContact(contact);
+6 -2
View File
@@ -533,6 +533,7 @@ export default class DIDView extends Vue {
userProfileData: UserProfile | null = null;
userProfileError: string | null = null;
userProfileLoading = false;
didFromUrl = "";
viewingDid?: string;
capitalizeAndInsertSpacesBeforeCaps = capitalizeAndInsertSpacesBeforeCaps;
@@ -558,6 +559,10 @@ export default class DIDView extends Vue {
* 5. Determines if viewing own DID
*/
async mounted() {
// Read the DID from the URL before the first await, so nothing can render
// or act against an empty value while the awaits below resolve.
this.didFromUrl = window.location.pathname.substring("/did/".length);
await this.initializeSettings();
await this.determineDIDToDisplay();
if (this.viewingDid) {
@@ -592,8 +597,7 @@ export default class DIDView extends Vue {
* Validates DID format and shows error for invalid DIDs
*/
private async determineDIDToDisplay() {
const pathParam = window.location.pathname.substring("/did/".length);
let showDid = pathParam;
let showDid = this.didFromUrl;
if (!showDid) {
// No DID provided in URL, use active DID
+1 -1
View File
@@ -108,7 +108,7 @@ export default class DeepLinkRedirectView extends Vue {
webUrl: string | null = null; // web link, eg "https://timesafari.app/..."
isDevelopment: boolean = false;
userAgent: string = "";
private platformService = PlatformServiceFactory.getInstance();
platformService = PlatformServiceFactory.getInstance();
mounted() {
// Get the path from the route parameter (catch-all parameter)
+1 -1
View File
@@ -547,7 +547,7 @@ export default class GiftedDetails extends Vue {
confirmDeleteImage() {
this.notify.confirm(
NOTIFY_GIFTED_DETAILS_DELETE_IMAGE_CONFIRM.message,
() => {
async () => {
// Stage the image for deletion on submit rather than deleting immediately,
// so that canceling the edit doesn't destroy the referenced image.
this.imageUrlToDelete = this.imageUrl;
+4 -1
View File
@@ -115,9 +115,12 @@
</template>
<script lang="ts">
import { Component, Vue } from "vue-facing-decorator";
import { Router } from "vue-router";
import QuickNav from "../components/QuickNav.vue";
@Component({ components: { QuickNav } })
export default class HelpNotificationTypesView extends Vue {}
export default class HelpNotificationTypesView extends Vue {
$router!: Router;
}
</script>
+2
View File
@@ -133,12 +133,14 @@
<script lang="ts">
import { Component, Vue } from "vue-facing-decorator";
import { Router } from "vue-router";
import QuickNav from "../components/QuickNav.vue";
import { SUPPORT_EMAIL } from "../constants/app";
@Component({ components: { QuickNav } })
export default class HelpTermsView extends Vue {
$router!: Router;
SUPPORT_EMAIL = SUPPORT_EMAIL;
}
</script>
-8
View File
@@ -1855,14 +1855,6 @@ export default class HomeView extends Vue {
this.isImageViewerOpen = true;
}
private handleQRCodeClick() {
if (Capacitor.isNativePlatform()) {
this.$router.push({ name: "contact-qr-scan-full" });
} else {
this.$router.push({ name: "contact-qr" });
}
}
openPersonDialog(giver?: GiverReceiverInputInfo, prompt?: string) {
this.openDialog(giver, prompt);
}
+4 -2
View File
@@ -236,8 +236,10 @@ export default class IdentitySwitcherView extends Vue {
}
async switchAccount(did?: string) {
// Update the active DID in the active_identity table
await this.$updateActiveDid(did);
// Update the active DID in the active_identity table.
// Coerce to null: the value reaches a SQL parameter, where undefined is not
// a valid binding.
await this.$updateActiveDid(did ?? null);
// Check if we need to load user-specific settings for the new DID
if (did) {
+1 -1
View File
@@ -130,7 +130,7 @@ export default class ImportAccountView extends Vue {
}),
);
const filteredDecryptedAccounts: Account[] = decryptedAccounts.filter(
(account) => account !== undefined,
(account): account is Account => account !== undefined,
);
// group by account.mnemonic
+1 -1
View File
@@ -1232,7 +1232,7 @@ export default class ProjectViewView extends Vue {
undefined,
);
// Immediately select "Unnamed" and move to Step 2
(this.$refs.giveDialogToThis as GiftedDialog).selectGiver();
(this.$refs.giveDialogToThis as GiftedDialog).selectGiverPerson();
} else {
// Open straight to Step 2 with current user as giver and current project as recipient
(this.$refs.giveDialogToThis as GiftedDialog).open(
+1 -1
View File
@@ -79,7 +79,7 @@ export default class StatisticsView extends Vue {
$notify!: (notification: NotificationIface, timeout?: number) => void;
$router!: Router;
world: World;
world!: World;
worldProperties: Dictionary<number> = {};
mounted() {
+1 -1
View File
@@ -745,7 +745,7 @@ export default class Help extends Vue {
* @returns True if not on production server (enables test utilities)
*/
public isNotProdServer() {
return isNotProdServerUtil(this.apiServer);
return isNotProdServerUtil(this.apiServer ?? "");
}
async registerMe() {
+2
View File
@@ -31,6 +31,7 @@
<script lang="ts">
import { Component, Vue } from "vue-facing-decorator";
import { Router } from "vue-router";
import NotificationDebugPanel from "@/components/dev/NotificationDebugPanel.vue";
import { includeDevToolkitRoutes } from "@/utils/includeDevToolkitRoutes";
@@ -38,6 +39,7 @@ import { includeDevToolkitRoutes } from "@/utils/includeDevToolkitRoutes";
components: { NotificationDebugPanel },
})
export default class NotificationDebugView extends Vue {
$router!: Router;
readonly isDev: boolean = includeDevToolkitRoutes;
}
</script>