Browse Source

Convert Vue components to use @Emit decorator instead of manual emits declarations

Replace manual emits declarations with proper @Emit decorator usage across components:
- ActivityListItem: Add @Emit methods for viewImage, loadClaim, confirmClaim
- ContactInputForm: Convert handleQRScan to use @Emit("qr-scan")
- ContactBulkActions: Add @Emit methods for toggle-all-selection, copy-selected
- ContactListHeader: Add @Emit methods for all 5 emitted events
- MembersList: Add @Emit("error") method for error handling
- LargeIdenticonModal: Add @Emit("close") method
- ContactListItem: Add @Emit methods for all 4 emitted events

Update all templates to call emit methods instead of direct $emit calls.
Fix TypeScript type issues with optional parameters.
Resolves Vue warning about undeclared emitted events.

Follows vue-facing-decorator best practices and improves code consistency.
pull/151/head
Matthew Raymer 3 days ago
parent
commit
07c5c6fd31
  1. 28
      src/components/ActivityListItem.vue
  2. 17
      src/components/ContactBulkActions.vue
  3. 4
      src/components/ContactInputForm.vue
  4. 38
      src/components/ContactListHeader.vue
  5. 33
      src/components/ContactListItem.vue
  6. 5
      src/components/IdentitySection.vue
  7. 2
      src/components/ImageViewer.vue
  8. 10
      src/components/LargeIdenticonModal.vue
  9. 16
      src/components/MembersList.vue
  10. 44
      src/libs/util.ts
  11. 24
      src/test/PlatformServiceMixinTest.vue
  12. 59
      src/utils/PlatformServiceMixin.ts
  13. 5
      src/views/AccountViewView.vue

28
src/components/ActivityListItem.vue

@ -52,7 +52,7 @@
<a
class="cursor-pointer"
data-testid="circle-info-link"
@click="$emit('loadClaim', record.jwtId)"
@click="emitLoadClaim(record.jwtId)"
>
<font-awesome icon="circle-info" class="fa-fw text-slate-500" />
</a>
@ -67,7 +67,7 @@
>
<a
class="block bg-slate-100/50 backdrop-blur-md px-6 py-4 cursor-pointer"
@click="$emit('viewImage', record.image)"
@click="emitViewImage(record.image)"
>
<img
class="w-full h-auto max-w-lg max-h-96 object-contain mx-auto drop-shadow-md"
@ -80,7 +80,7 @@
<!-- Description -->
<p class="font-medium">
<a class="cursor-pointer" @click="$emit('loadClaim', record.jwtId)">
<a class="cursor-pointer" @click="emitLoadClaim(record.jwtId)">
{{ description }}
</a>
</p>
@ -248,7 +248,7 @@
</template>
<script lang="ts">
import { Component, Prop, Vue } from "vue-facing-decorator";
import { Component, Prop, Vue, Emit } from "vue-facing-decorator";
import { GiveRecordWithContactInfo } from "@/interfaces/give";
import EntityIcon from "./EntityIcon.vue";
import { isGiveClaimType, notifyWhyCannotConfirm } from "../libs/util";
@ -340,7 +340,19 @@ export default class ActivityListItem extends Vue {
return true;
}
handleConfirmClick() {
// Emit methods using @Emit decorator
@Emit("viewImage")
emitViewImage(imageUrl: string) {
return imageUrl;
}
@Emit("loadClaim")
emitLoadClaim(jwtId: string) {
return jwtId;
}
@Emit("confirmClaim")
emitConfirmClaim() {
if (!this.canConfirm) {
notifyWhyCannotConfirm(
(msg, timeout) => this.notify.info(msg.text ?? "", timeout),
@ -352,7 +364,11 @@ export default class ActivityListItem extends Vue {
);
return;
}
this.$emit("confirmClaim", this.record);
return this.record;
}
handleConfirmClick() {
this.emitConfirmClaim();
}
get friendlyDate(): string {

17
src/components/ContactBulkActions.vue

@ -6,13 +6,13 @@
:checked="allContactsSelected"
class="align-middle ml-2 h-6 w-6"
data-testId="contactCheckAllBottom"
@click="$emit('toggle-all-selection')"
@click="emitToggleAllSelection"
/>
<button
v-if="!showGiveNumbers"
:class="copyButtonClass"
:disabled="copyButtonDisabled"
@click="$emit('copy-selected')"
@click="emitCopySelected"
>
Copy
</button>
@ -20,7 +20,7 @@
</template>
<script lang="ts">
import { Component, Vue, Prop } from "vue-facing-decorator";
import { Component, Vue, Prop, Emit } from "vue-facing-decorator";
/**
* ContactBulkActions - Contact bulk actions component
@ -38,5 +38,16 @@ export default class ContactBulkActions extends Vue {
@Prop({ required: true }) allContactsSelected!: boolean;
@Prop({ required: true }) copyButtonClass!: string;
@Prop({ required: true }) copyButtonDisabled!: boolean;
// Emit methods using @Emit decorator
@Emit("toggle-all-selection")
emitToggleAllSelection() {
// No parameters needed
}
@Emit("copy-selected")
emitCopySelected() {
// No parameters needed
}
}
</script>

4
src/components/ContactInputForm.vue

@ -64,7 +64,7 @@
</template>
<script lang="ts">
import { Component, Vue, Prop } from "vue-facing-decorator";
import { Component, Vue, Prop, Emit } from "vue-facing-decorator";
/**
* ContactInputForm - Contact input form component
@ -165,9 +165,9 @@ export default class ContactInputForm extends Vue {
* Handle QR scan button click
* Emits qr-scan event for parent handling
*/
@Emit("qr-scan")
private handleQRScan(): void {
console.log("[ContactInputForm] QR scan button clicked");
this.$emit("qr-scan");
}
}
</script>

38
src/components/ContactListHeader.vue

@ -8,21 +8,21 @@
:checked="allContactsSelected"
class="align-middle ml-2 h-6 w-6"
data-testId="contactCheckAllTop"
@click="$emit('toggle-all-selection')"
@click="emitToggleAllSelection"
/>
<button
v-if="!showGiveNumbers"
:class="copyButtonClass"
:disabled="copyButtonDisabled"
data-testId="copySelectedContactsButtonTop"
@click="$emit('copy-selected')"
@click="emitCopySelected"
>
Copy
</button>
<font-awesome
icon="circle-info"
class="text-2xl text-blue-500 ml-2"
@click="$emit('show-copy-info')"
@click="emitShowCopyInfo"
/>
</div>
</div>
@ -33,7 +33,7 @@
v-if="showGiveNumbers"
class="text-md bg-gradient-to-b shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-3 py-1.5 rounded-md"
:class="giveAmountsButtonClass"
@click="$emit('toggle-give-totals')"
@click="emitToggleGiveTotals"
>
{{ giveAmountsButtonText }}
<font-awesome icon="left-right" class="fa-fw" />
@ -41,7 +41,7 @@
<button
class="text-md bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-3 py-1.5 rounded-md"
@click="$emit('toggle-show-actions')"
@click="emitToggleShowActions"
>
{{ showActionsButtonText }}
</button>
@ -50,7 +50,7 @@
</template>
<script lang="ts">
import { Component, Vue, Prop } from "vue-facing-decorator";
import { Component, Vue, Prop, Emit } from "vue-facing-decorator";
/**
* ContactListHeader - Contact list header component
@ -71,5 +71,31 @@ export default class ContactListHeader extends Vue {
@Prop({ required: true }) giveAmountsButtonText!: string;
@Prop({ required: true }) showActionsButtonText!: string;
@Prop({ required: true }) giveAmountsButtonClass!: Record<string, boolean>;
// Emit methods using @Emit decorator
@Emit("toggle-all-selection")
emitToggleAllSelection() {
// No parameters needed
}
@Emit("copy-selected")
emitCopySelected() {
// No parameters needed
}
@Emit("show-copy-info")
emitShowCopyInfo() {
// No parameters needed
}
@Emit("toggle-give-totals")
emitToggleGiveTotals() {
// No parameters needed
}
@Emit("toggle-show-actions")
emitToggleShowActions() {
// No parameters needed
}
}
</script>

33
src/components/ContactListItem.vue

@ -9,14 +9,14 @@
:checked="isSelected"
class="ml-2 h-6 w-6 flex-shrink-0"
data-testId="contactCheckOne"
@click="$emit('toggle-selection', contact.did)"
@click="emitToggleSelection(contact.did)"
/>
<EntityIcon
:contact="contact"
:icon-size="48"
class="shrink-0 align-text-bottom border border-slate-300 rounded cursor-pointer overflow-hidden"
@click="$emit('show-identicon', contact)"
@click="emitShowIdenticon(contact)"
/>
<div class="overflow-hidden">
@ -63,7 +63,7 @@
<button
class="text-sm bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2.5 py-1.5 rounded-l-md"
:title="getGiveDescriptionForContact(contact.did, true)"
@click="$emit('show-gifted-dialog', contact.did, activeDid)"
@click="emitShowGiftedDialog(contact.did, activeDid)"
>
{{ getGiveAmountForContact(contact.did, true) }}
</button>
@ -71,7 +71,7 @@
<button
class="text-sm bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2.5 py-1.5 rounded-r-md border-l"
:title="getGiveDescriptionForContact(contact.did, false)"
@click="$emit('show-gifted-dialog', activeDid, contact.did)"
@click="emitShowGiftedDialog(activeDid, contact.did)"
>
{{ getGiveAmountForContact(contact.did, false) }}
</button>
@ -81,7 +81,7 @@
<button
class="text-sm bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2 py-1.5 rounded-md"
data-testId="offerButton"
@click="$emit('open-offer-dialog', contact.did, contact.name)"
@click="emitOpenOfferDialog(contact.did, contact.name)"
>
Offer
</button>
@ -102,7 +102,7 @@
</template>
<script lang="ts">
import { Component, Vue, Prop } from "vue-facing-decorator";
import { Component, Vue, Prop, Emit } from "vue-facing-decorator";
import EntityIcon from "./EntityIcon.vue";
import { Contact } from "../db/tables/contacts";
import { AppString } from "../constants/app";
@ -140,6 +140,27 @@ export default class ContactListItem extends Vue {
// Constants
AppString = AppString;
// Emit methods using @Emit decorator
@Emit("toggle-selection")
emitToggleSelection(did: string) {
return did;
}
@Emit("show-identicon")
emitShowIdenticon(contact: Contact) {
return contact;
}
@Emit("show-gifted-dialog")
emitShowGiftedDialog(fromDid: string, toDid: string) {
return { fromDid, toDid };
}
@Emit("open-offer-dialog")
emitOpenOfferDialog(did: string, name: string | undefined) {
return { did, name };
}
/**
* Format contact name with non-breaking spaces
*/

5
src/components/IdentitySection.vue

@ -55,10 +55,7 @@
aria-label="Delete profile image"
@click="deleteImage"
>
<font-awesome
icon="trash-can"
aria-hidden="true"
/>
<font-awesome icon="trash-can" aria-hidden="true" />
</button>
</span>
<div v-else class="text-center">

2
src/components/ImageViewer.vue

@ -25,7 +25,7 @@
<div class="flex-1 flex items-center justify-center p-2">
<div class="w-full h-full flex items-center justify-center">
<img
:src="transformedImageUrl"
:src="imageUrl"
class="max-h-[calc(100vh-5rem)] w-full h-full object-contain"
alt="expanded shared content"
@click="close"

10
src/components/LargeIdenticonModal.vue

@ -7,14 +7,14 @@
:contact="contact"
:icon-size="512"
class="flex w-11/12 max-w-sm mx-auto mb-3 overflow-hidden bg-white rounded-lg shadow-lg"
@click="$emit('close')"
@click="emitClose"
/>
</div>
</div>
</template>
<script lang="ts">
import { Component, Vue, Prop } from "vue-facing-decorator";
import { Component, Vue, Prop, Emit } from "vue-facing-decorator";
import EntityIcon from "./EntityIcon.vue";
import { Contact } from "../db/tables/contacts";
@ -34,5 +34,11 @@ import { Contact } from "../db/tables/contacts";
})
export default class LargeIdenticonModal extends Vue {
@Prop({ required: true }) contact!: Contact | undefined;
// Emit methods using @Emit decorator
@Emit("close")
emitClose() {
// No parameters needed
}
}
</script>

16
src/components/MembersList.vue

@ -178,7 +178,7 @@
// Validation: Passes lint checks and TypeScript compilation
// Navigation: Contacts Chair Icon Start/Join Meeting Members List
import { Component, Vue, Prop } from "vue-facing-decorator";
import { Component, Vue, Prop, Emit } from "vue-facing-decorator";
import {
errorStringForLog,
@ -222,6 +222,12 @@ export default class MembersList extends Vue {
@Prop({ required: true }) password!: string;
@Prop({ default: false }) showOrganizerTools!: boolean;
// Emit methods using @Emit decorator
@Emit("error")
emitError(message: string) {
return message;
}
decryptedMembers: DecryptedMember[] = [];
firstName = "";
isLoading = true;
@ -262,10 +268,7 @@ export default class MembersList extends Vue {
"Error fetching members: " + errorStringForLog(error),
true,
);
this.$emit(
"error",
serverMessageForUser(error) || "Failed to fetch members.",
);
this.emitError(serverMessageForUser(error) || "Failed to fetch members.");
} finally {
this.isLoading = false;
}
@ -478,8 +481,7 @@ export default class MembersList extends Vue {
"Error toggling admission: " + errorStringForLog(error),
true,
);
this.$emit(
"error",
this.emitError(
serverMessageForUser(error) ||
"Failed to update member admission status.",
);

44
src/libs/util.ts

@ -974,28 +974,28 @@ export async function importFromMnemonic(
if (isTestUser0) {
// Set up Test User #0 specific settings with enhanced error handling
const platformService = await getPlatformService();
try {
// First, ensure the DID-specific settings record exists
await platformService.insertDidSpecificSettings(newId.did);
// Then update with Test User #0 specific settings
await platformService.updateDidSpecificSettings(newId.did, {
firstName: "User Zero",
isRegistered: true,
});
// Verify the settings were saved correctly
const verificationResult = await platformService.dbQuery(
"SELECT firstName, isRegistered FROM settings WHERE accountDid = ?",
[newId.did],
);
if (verificationResult?.values?.length) {
const settings = verificationResult.values[0];
const firstName = settings[0];
const isRegistered = settings[1];
logger.info("[importFromMnemonic] Test User #0 settings verification", {
did: newId.did,
firstName,
@ -1003,40 +1003,50 @@ export async function importFromMnemonic(
expectedFirstName: "User Zero",
expectedIsRegistered: true,
});
// If settings weren't saved correctly, try individual updates
if (firstName !== "User Zero" || isRegistered !== 1) {
logger.warn("[importFromMnemonic] Test User #0 settings not saved correctly, retrying with individual updates");
logger.warn(
"[importFromMnemonic] Test User #0 settings not saved correctly, retrying with individual updates",
);
await platformService.dbExec(
"UPDATE settings SET firstName = ? WHERE accountDid = ?",
["User Zero", newId.did],
);
await platformService.dbExec(
"UPDATE settings SET isRegistered = ? WHERE accountDid = ?",
[1, newId.did],
);
// Verify again
const retryResult = await platformService.dbQuery(
"SELECT firstName, isRegistered FROM settings WHERE accountDid = ?",
[newId.did],
);
if (retryResult?.values?.length) {
const retrySettings = retryResult.values[0];
logger.info("[importFromMnemonic] Test User #0 settings after retry", {
firstName: retrySettings[0],
isRegistered: retrySettings[1],
});
logger.info(
"[importFromMnemonic] Test User #0 settings after retry",
{
firstName: retrySettings[0],
isRegistered: retrySettings[1],
},
);
}
}
} else {
logger.error("[importFromMnemonic] Failed to verify Test User #0 settings - no record found");
logger.error(
"[importFromMnemonic] Failed to verify Test User #0 settings - no record found",
);
}
} catch (error) {
logger.error("[importFromMnemonic] Error setting up Test User #0 settings:", error);
logger.error(
"[importFromMnemonic] Error setting up Test User #0 settings:",
error,
);
// Don't throw - allow the import to continue even if settings fail
}
}

24
src/test/PlatformServiceMixinTest.vue

@ -3,16 +3,18 @@
<h2>PlatformServiceMixin Test</h2>
<button @click="testInsert">Test Insert</button>
<button @click="testUpdate">Test Update</button>
<button
:class="primaryButtonClasses"
@click="testUserZeroSettings"
>
<button :class="primaryButtonClasses" @click="testUserZeroSettings">
Test User #0 Settings
</button>
<div v-if="userZeroTestResult" class="mt-4 p-4 border border-gray-300 rounded-md bg-gray-50">
<div
v-if="userZeroTestResult"
class="mt-4 p-4 border border-gray-300 rounded-md bg-gray-50"
>
<h4 class="font-semibold mb-2">User #0 Settings Test Result:</h4>
<pre class="text-sm">{{ JSON.stringify(userZeroTestResult, null, 2) }}</pre>
<pre class="text-sm">{{
JSON.stringify(userZeroTestResult, null, 2)
}}</pre>
</div>
<pre>{{ result }}</pre>
</div>
@ -55,16 +57,16 @@ export default class PlatformServiceMixinTest extends Vue {
try {
// User #0's DID
const userZeroDid = "did:ethr:0x0000694B58C2cC69658993A90D3840C560f2F51F";
this.result = "Testing User #0 settings...";
// Test the debug methods
await this.$debugMergedSettings(userZeroDid);
// Get the actual settings
const didSettings = await this.$debugDidSettings(userZeroDid);
const accountSettings = await this.$accountSettings(userZeroDid);
this.userZeroTestResult = {
didSettings,
accountSettings,
@ -72,7 +74,7 @@ export default class PlatformServiceMixinTest extends Vue {
firstName: accountSettings.firstName,
timestamp: new Date().toISOString(),
};
this.result = `User #0 settings test completed. isRegistered: ${accountSettings.isRegistered}`;
} catch (error) {
this.result = `Error testing User #0 settings: ${error}`;

59
src/utils/PlatformServiceMixin.ts

@ -87,7 +87,7 @@ interface VueComponentWithMixin {
// VueComponentWithMixin,
// Map<string, CacheEntry<unknown>>
// >();
//
//
// /**
// * Cache configuration constants
// */
@ -220,13 +220,20 @@ export const PlatformServiceMixin = {
const obj: Record<string, unknown> = {};
columns.forEach((column, index) => {
let value = row[index];
// Convert SQLite integer booleans to JavaScript booleans
if (column === 'isRegistered' || column === 'finishedOnboarding' ||
column === 'filterFeedByVisible' || column === 'filterFeedByNearby' ||
column === 'hideRegisterPromptOnNewContact' || column === 'showContactGivesInline' ||
column === 'showGeneralAdvanced' || column === 'showShortcutBvc' ||
column === 'warnIfProdServer' || column === 'warnIfTestServer') {
if (
column === "isRegistered" ||
column === "finishedOnboarding" ||
column === "filterFeedByVisible" ||
column === "filterFeedByNearby" ||
column === "hideRegisterPromptOnNewContact" ||
column === "showContactGivesInline" ||
column === "showGeneralAdvanced" ||
column === "showShortcutBvc" ||
column === "warnIfProdServer" ||
column === "warnIfTestServer"
) {
if (value === 1) {
value = true;
} else if (value === 0) {
@ -234,7 +241,7 @@ export const PlatformServiceMixin = {
}
// Keep null values as null
}
obj[column] = value;
});
return obj;
@ -244,7 +251,7 @@ export const PlatformServiceMixin = {
/**
* Self-contained implementation of parseJsonField
* Safely parses JSON strings with fallback to default value
*
*
* Consolidate this with src/libs/util.ts parseJsonField
*/
_parseJsonField<T>(value: unknown, defaultValue: T): T {
@ -1403,7 +1410,9 @@ export const PlatformServiceMixin = {
);
if (!result?.values?.length) {
logger.warn(`[PlatformServiceMixin] No settings found for DID: ${did}`);
logger.warn(
`[PlatformServiceMixin] No settings found for DID: ${did}`,
);
return null;
}
@ -1413,7 +1422,9 @@ export const PlatformServiceMixin = {
);
if (!mappedResults.length) {
logger.warn(`[PlatformServiceMixin] Failed to map settings for DID: ${did}`);
logger.warn(
`[PlatformServiceMixin] Failed to map settings for DID: ${did}`,
);
return null;
}
@ -1428,7 +1439,10 @@ export const PlatformServiceMixin = {
return settings;
} catch (error) {
logger.error(`[PlatformServiceMixin] Error debugging settings for DID ${did}:`, error);
logger.error(
`[PlatformServiceMixin] Error debugging settings for DID ${did}:`,
error,
);
return null;
}
},
@ -1442,14 +1456,24 @@ export const PlatformServiceMixin = {
async $debugMergedSettings(did: string): Promise<void> {
try {
// Get default settings
const defaultSettings = await this.$getSettings(MASTER_SETTINGS_KEY, {});
logger.info(`[PlatformServiceMixin] Default settings:`, defaultSettings);
const defaultSettings = await this.$getSettings(
MASTER_SETTINGS_KEY,
{},
);
logger.info(
`[PlatformServiceMixin] Default settings:`,
defaultSettings,
);
// Get DID-specific settings
const didSettings = await this.$debugDidSettings(did);
// Get merged settings
const mergedSettings = await this.$getMergedSettings(MASTER_SETTINGS_KEY, did, defaultSettings || {});
const mergedSettings = await this.$getMergedSettings(
MASTER_SETTINGS_KEY,
did,
defaultSettings || {},
);
logger.info(`[PlatformServiceMixin] Merged settings for ${did}:`, {
defaultSettings,
@ -1458,7 +1482,10 @@ export const PlatformServiceMixin = {
isRegistered: mergedSettings.isRegistered,
});
} catch (error) {
logger.error(`[PlatformServiceMixin] Error debugging merged settings for DID ${did}:`, error);
logger.error(
`[PlatformServiceMixin] Error debugging merged settings for DID ${did}:`,
error,
);
}
},
},

5
src/views/AccountViewView.vue

@ -1415,7 +1415,8 @@ export default class AccountViewView extends Vue {
return;
}
} catch (error) {
this.limitsMessage = ACCOUNT_VIEW_CONSTANTS.LIMITS.ERROR_RETRIEVING_LIMITS;
this.limitsMessage =
ACCOUNT_VIEW_CONSTANTS.LIMITS.ERROR_RETRIEVING_LIMITS;
logger.error("Error retrieving limits: ", error);
// this.notify.error(this.limitsMessage, TIMEOUTS.STANDARD);
} finally {
@ -1475,7 +1476,7 @@ export default class AccountViewView extends Vue {
async deleteImage(): Promise<void> {
try {
// Extract the image ID from the full URL
const imageId = this.profileImageUrl?.split('/').pop();
const imageId = this.profileImageUrl?.split("/").pop();
if (!imageId) {
this.notify.error("Invalid image URL");
return;

Loading…
Cancel
Save