forked from trent_larson/crowd-funder-for-time-pwa
Merge branch 'master' into contact-gifting-current-user
This commit is contained in:
@@ -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,11 +248,10 @@
|
||||
</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";
|
||||
import { containsHiddenDid, isHiddenDid } from "../libs/endorserServer";
|
||||
import { isHiddenDid } from "../libs/endorserServer";
|
||||
import ProjectIcon from "./ProjectIcon.vue";
|
||||
import { createNotifyHelpers } from "@/utils/notify";
|
||||
import {
|
||||
@@ -272,7 +271,6 @@ export default class ActivityListItem extends Vue {
|
||||
@Prop() lastViewedClaimId?: string;
|
||||
@Prop() isRegistered!: boolean;
|
||||
@Prop() activeDid!: string;
|
||||
@Prop() confirmerIdList?: string[];
|
||||
|
||||
/**
|
||||
* Function prop for handling image caching
|
||||
@@ -331,28 +329,15 @@ export default class ActivityListItem extends Vue {
|
||||
return unitCode === "HUR" ? (single ? "hour" : "hours") : unitCode;
|
||||
}
|
||||
|
||||
get canConfirm(): boolean {
|
||||
if (!this.isRegistered) return false;
|
||||
if (!isGiveClaimType(this.record.fullClaim?.["@type"])) return false;
|
||||
if (this.confirmerIdList?.includes(this.activeDid)) return false;
|
||||
if (this.record.issuerDid === this.activeDid) return false;
|
||||
if (containsHiddenDid(this.record.fullClaim)) return false;
|
||||
return true;
|
||||
// Emit methods using @Emit decorator
|
||||
@Emit("viewImage")
|
||||
emitViewImage(imageUrl: string) {
|
||||
return imageUrl;
|
||||
}
|
||||
|
||||
handleConfirmClick() {
|
||||
if (!this.canConfirm) {
|
||||
notifyWhyCannotConfirm(
|
||||
(msg, timeout) => this.notify.info(msg.text ?? "", timeout),
|
||||
this.isRegistered,
|
||||
this.record.fullClaim?.["@type"],
|
||||
this.record,
|
||||
this.activeDid,
|
||||
this.confirmerIdList,
|
||||
);
|
||||
return;
|
||||
}
|
||||
this.$emit("confirmClaim", this.record);
|
||||
@Emit("loadClaim")
|
||||
emitLoadClaim(jwtId: string) {
|
||||
return jwtId;
|
||||
}
|
||||
|
||||
get friendlyDate(): string {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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";
|
||||
@@ -121,6 +121,12 @@ import { AppString } from "../constants/app";
|
||||
components: {
|
||||
EntityIcon,
|
||||
},
|
||||
emits: [
|
||||
"toggle-selection",
|
||||
"show-identicon",
|
||||
"show-gifted-dialog",
|
||||
"open-offer-dialog",
|
||||
],
|
||||
})
|
||||
export default class ContactListItem extends Vue {
|
||||
@Prop({ required: true }) contact!: Contact;
|
||||
@@ -140,6 +146,25 @@ 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;
|
||||
}
|
||||
|
||||
emitShowGiftedDialog(fromDid: string, toDid: string) {
|
||||
this.$emit("show-gifted-dialog", fromDid, toDid);
|
||||
}
|
||||
|
||||
emitOpenOfferDialog(did: string, name: string | undefined) {
|
||||
this.$emit("open-offer-dialog", did, name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Format contact name with non-breaking spaces
|
||||
*/
|
||||
|
||||
@@ -54,7 +54,10 @@ messages * - Conditional UI based on platform capabilities * * @component *
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue } from "vue-facing-decorator";
|
||||
import * as R from "ramda";
|
||||
|
||||
import { AppString, NotificationIface } from "../constants/app";
|
||||
import { Contact } from "../db/tables/contacts";
|
||||
|
||||
import { logger } from "../utils/logger";
|
||||
import { contactsToExportJson } from "../libs/util";
|
||||
@@ -179,7 +182,19 @@ export default class DataExportSection extends Vue {
|
||||
const allContacts = await this.$contacts();
|
||||
|
||||
// Convert contacts to export format
|
||||
const exportData = contactsToExportJson(allContacts);
|
||||
const processedContacts: Contact[] = allContacts.map((contact) => {
|
||||
// first remove the contactMethods field, mostly to cast to a clear type (that will end up with JSON objects)
|
||||
const exContact: Contact = R.omit(["contactMethods"], contact);
|
||||
// now add contactMethods as a true array of ContactMethod objects
|
||||
exContact.contactMethods = contact.contactMethods
|
||||
? (typeof contact.contactMethods === 'string' && contact.contactMethods.trim() !== ''
|
||||
? JSON.parse(contact.contactMethods)
|
||||
: [])
|
||||
: [];
|
||||
return exContact;
|
||||
});
|
||||
|
||||
const exportData = contactsToExportJson(processedContacts);
|
||||
const jsonStr = JSON.stringify(exportData, null, 2);
|
||||
|
||||
// Use platform service to handle export (no platform-specific logic here!)
|
||||
|
||||
@@ -137,6 +137,20 @@ export default class EntitySelectionStep extends Vue {
|
||||
@Prop()
|
||||
receiver?: EntityData | null;
|
||||
|
||||
/** Form field values to preserve when navigating to "Show All" */
|
||||
@Prop({ default: "" })
|
||||
description!: string;
|
||||
|
||||
@Prop({ default: "0" })
|
||||
amountInput!: string;
|
||||
|
||||
@Prop({ default: "HUR" })
|
||||
unitCode!: string;
|
||||
|
||||
/** Offer ID for context when fulfilling an offer */
|
||||
@Prop({ default: "" })
|
||||
offerId!: string;
|
||||
|
||||
/** Notification function from parent component */
|
||||
@Prop()
|
||||
notify?: (notification: NotificationIface, timeout?: number) => void;
|
||||
@@ -225,34 +239,41 @@ export default class EntitySelectionStep extends Vue {
|
||||
* Query parameters for "Show All" navigation
|
||||
*/
|
||||
get showAllQueryParams(): Record<string, string> {
|
||||
if (this.shouldShowProjects) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return {
|
||||
const baseParams = {
|
||||
stepType: this.stepType,
|
||||
giverEntityType: this.giverEntityType,
|
||||
recipientEntityType: this.recipientEntityType,
|
||||
...(this.stepType === "giver"
|
||||
? {
|
||||
recipientProjectId: this.toProjectId || "",
|
||||
recipientProjectName: this.receiver?.name || "",
|
||||
recipientProjectImage: this.receiver?.image || "",
|
||||
recipientProjectHandleId: this.receiver?.handleId || "",
|
||||
recipientDid: this.receiver?.did || "",
|
||||
}
|
||||
: {
|
||||
giverProjectId: this.fromProjectId || "",
|
||||
giverProjectName: this.giver?.name || "",
|
||||
giverProjectImage: this.giver?.image || "",
|
||||
giverProjectHandleId: this.giver?.handleId || "",
|
||||
giverDid: this.giver?.did || "",
|
||||
}),
|
||||
// Form field values to preserve
|
||||
description: this.description,
|
||||
amountInput: this.amountInput,
|
||||
unitCode: this.unitCode,
|
||||
offerId: this.offerId,
|
||||
fromProjectId: this.fromProjectId,
|
||||
toProjectId: this.toProjectId,
|
||||
showProjects: this.showProjects.toString(),
|
||||
isFromProjectView: this.isFromProjectView.toString(),
|
||||
};
|
||||
|
||||
if (this.shouldShowProjects) {
|
||||
// For project contexts, still pass entity type information
|
||||
return baseParams;
|
||||
}
|
||||
|
||||
return {
|
||||
...baseParams,
|
||||
// Always pass both giver and recipient info for context preservation
|
||||
giverProjectId: this.fromProjectId || "",
|
||||
giverProjectName: this.giver?.name || "",
|
||||
giverProjectImage: this.giver?.image || "",
|
||||
giverProjectHandleId: this.giver?.handleId || "",
|
||||
giverDid: this.giverEntityType === "person" ? this.giver?.did || "" : "",
|
||||
recipientProjectId: this.toProjectId || "",
|
||||
recipientProjectName: this.receiver?.name || "",
|
||||
recipientProjectImage: this.receiver?.image || "",
|
||||
recipientProjectHandleId: this.receiver?.handleId || "",
|
||||
recipientDid:
|
||||
this.recipientEntityType === "person" ? this.receiver?.did || "" : "",
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -315,16 +315,15 @@ export default class GiftDetailsStep extends Vue {
|
||||
giverName: this.giver?.name,
|
||||
offerId: this.offerId,
|
||||
fulfillsProjectId:
|
||||
this.giverEntityType === "person" &&
|
||||
this.recipientEntityType === "project"
|
||||
? this.toProjectId
|
||||
: undefined,
|
||||
this.recipientEntityType === "project" ? this.toProjectId : undefined,
|
||||
providerProjectId:
|
||||
this.giverEntityType === "project" &&
|
||||
this.recipientEntityType === "person"
|
||||
this.giverEntityType === "project"
|
||||
? this.giver?.handleId
|
||||
: this.fromProjectId,
|
||||
recipientDid: this.receiver?.did,
|
||||
recipientDid:
|
||||
this.recipientEntityType === "person"
|
||||
? this.receiver?.did
|
||||
: undefined,
|
||||
recipientName: this.receiver?.name,
|
||||
unitCode: this.localUnitCode,
|
||||
},
|
||||
|
||||
@@ -1,13 +1,19 @@
|
||||
<template>
|
||||
<div v-if="visible" class="dialog-overlay">
|
||||
<div class="dialog">
|
||||
<div
|
||||
class="dialog"
|
||||
data-testid="gifted-dialog"
|
||||
:data-recipient-entity-type="recipientEntityType"
|
||||
>
|
||||
<!-- Step 1: Entity Selection -->
|
||||
<EntitySelectionStep
|
||||
v-show="firstStep"
|
||||
:step-type="stepType"
|
||||
:giver-entity-type="giverEntityType"
|
||||
:recipient-entity-type="recipientEntityType"
|
||||
:show-projects="showProjects"
|
||||
:show-projects="
|
||||
giverEntityType === 'project' || recipientEntityType === 'project'
|
||||
"
|
||||
:is-from-project-view="isFromProjectView"
|
||||
:projects="projects"
|
||||
:all-contacts="allContacts"
|
||||
@@ -18,6 +24,10 @@
|
||||
:to-project-id="toProjectId"
|
||||
:giver="giver"
|
||||
:receiver="receiver"
|
||||
:description="description"
|
||||
:amount-input="amountInput"
|
||||
:unit-code="unitCode"
|
||||
:offer-id="offerId"
|
||||
:notify="$notify"
|
||||
:hide-show-all="hideShowAll"
|
||||
@entity-selected="handleEntitySelected"
|
||||
@@ -53,7 +63,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Vue, Component, Prop, Watch } from "vue-facing-decorator";
|
||||
import { Vue, Component, Prop } from "vue-facing-decorator";
|
||||
|
||||
import {
|
||||
createAndSubmitGive,
|
||||
@@ -72,6 +82,12 @@ import GiftDetailsStep from "../components/GiftDetailsStep.vue";
|
||||
import { PlanData } from "../interfaces/records";
|
||||
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
|
||||
import { createNotifyHelpers, TIMEOUTS } from "@/utils/notify";
|
||||
import {
|
||||
NOTIFY_GIFT_ERROR_NEGATIVE_AMOUNT,
|
||||
NOTIFY_GIFT_ERROR_NO_DESCRIPTION,
|
||||
NOTIFY_GIFTED_DETAILS_NO_IDENTIFIER,
|
||||
NOTIFY_GIFTED_DETAILS_RECORDING_GIVE,
|
||||
} from "@/constants/notifications";
|
||||
|
||||
@Component({
|
||||
components: {
|
||||
@@ -98,24 +114,13 @@ export default class GiftedDialog extends Vue {
|
||||
|
||||
@Prop() fromProjectId = "";
|
||||
@Prop() toProjectId = "";
|
||||
@Prop({ default: false }) showProjects = false;
|
||||
@Prop() isFromProjectView = false;
|
||||
@Prop({ default: false }) hideShowAll = false;
|
||||
|
||||
@Watch("showProjects")
|
||||
onShowProjectsChange() {
|
||||
this.updateEntityTypes();
|
||||
}
|
||||
|
||||
@Watch("fromProjectId")
|
||||
onFromProjectIdChange() {
|
||||
this.updateEntityTypes();
|
||||
}
|
||||
|
||||
@Watch("toProjectId")
|
||||
onToProjectIdChange() {
|
||||
this.updateEntityTypes();
|
||||
}
|
||||
@Prop({ default: "person" }) giverEntityType = "person" as
|
||||
| "person"
|
||||
| "project";
|
||||
@Prop({ default: "person" }) recipientEntityType = "person" as
|
||||
| "person"
|
||||
| "project";
|
||||
|
||||
activeDid = "";
|
||||
allContacts: Array<Contact> = [];
|
||||
@@ -124,20 +129,19 @@ export default class GiftedDialog extends Vue {
|
||||
|
||||
amountInput = "0";
|
||||
callbackOnSuccess?: (amount: number) => void = () => {};
|
||||
customTitle?: string;
|
||||
description = "";
|
||||
firstStep = true; // true = Step 1 (giver/recipient selection), false = Step 2 (amount/description)
|
||||
giver?: libsUtil.GiverReceiverInputInfo; // undefined means no identified giver agent
|
||||
offerId = "";
|
||||
projects: PlanData[] = [];
|
||||
prompt = "";
|
||||
receiver?: libsUtil.GiverReceiverInputInfo;
|
||||
stepType = "giver";
|
||||
unitCode = "HUR";
|
||||
visible = false;
|
||||
|
||||
libsUtil = libsUtil;
|
||||
|
||||
projects: PlanData[] = [];
|
||||
|
||||
didInfo = didInfo;
|
||||
|
||||
// Computed property to help debug template logic
|
||||
@@ -191,56 +195,27 @@ export default class GiftedDialog extends Vue {
|
||||
return false;
|
||||
}
|
||||
|
||||
stepType = "giver";
|
||||
giverEntityType = "person" as "person" | "project";
|
||||
recipientEntityType = "person" as "person" | "project";
|
||||
|
||||
updateEntityTypes() {
|
||||
// Reset and set entity types based on current context
|
||||
this.giverEntityType = "person";
|
||||
this.recipientEntityType = "person";
|
||||
|
||||
// Determine entity types based on current context
|
||||
if (this.showProjects) {
|
||||
// HomeView "Project" button or ProjectViewView "Given by This"
|
||||
this.giverEntityType = "project";
|
||||
this.recipientEntityType = "person";
|
||||
} else if (this.fromProjectId) {
|
||||
// ProjectViewView "Given by This" button (project is giver)
|
||||
this.giverEntityType = "project";
|
||||
this.recipientEntityType = "person";
|
||||
} else if (this.toProjectId) {
|
||||
// ProjectViewView "Given to This" button (project is recipient)
|
||||
this.giverEntityType = "person";
|
||||
this.recipientEntityType = "project";
|
||||
} else {
|
||||
// HomeView "Person" button
|
||||
this.giverEntityType = "person";
|
||||
this.recipientEntityType = "person";
|
||||
}
|
||||
}
|
||||
|
||||
async open(
|
||||
giver?: libsUtil.GiverReceiverInputInfo,
|
||||
receiver?: libsUtil.GiverReceiverInputInfo,
|
||||
offerId?: string,
|
||||
customTitle?: string,
|
||||
prompt?: string,
|
||||
description?: string,
|
||||
amountInput?: string,
|
||||
unitCode?: string,
|
||||
callbackOnSuccess: (amount: number) => void = () => {},
|
||||
) {
|
||||
this.customTitle = customTitle;
|
||||
this.giver = giver;
|
||||
this.prompt = prompt || "";
|
||||
this.receiver = receiver;
|
||||
this.amountInput = "0";
|
||||
this.callbackOnSuccess = callbackOnSuccess;
|
||||
this.offerId = offerId || "";
|
||||
this.prompt = prompt || "";
|
||||
this.description = description || "";
|
||||
this.amountInput = amountInput || "0";
|
||||
this.unitCode = unitCode || "HUR";
|
||||
this.callbackOnSuccess = callbackOnSuccess;
|
||||
this.firstStep = !giver;
|
||||
this.stepType = "giver";
|
||||
|
||||
// Update entity types based on current props
|
||||
this.updateEntityTypes();
|
||||
|
||||
try {
|
||||
const settings = await this.$settings();
|
||||
this.apiServer = settings.apiServer || "";
|
||||
@@ -320,23 +295,24 @@ export default class GiftedDialog extends Vue {
|
||||
async confirm() {
|
||||
if (!this.activeDid) {
|
||||
this.safeNotify.error(
|
||||
"You must select an identifier before you can record a give.",
|
||||
TIMEOUTS.STANDARD,
|
||||
NOTIFY_GIFTED_DETAILS_NO_IDENTIFIER.message,
|
||||
TIMEOUTS.SHORT,
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (parseFloat(this.amountInput) < 0) {
|
||||
this.safeNotify.error(
|
||||
"You may not send a negative number.",
|
||||
NOTIFY_GIFT_ERROR_NEGATIVE_AMOUNT.message,
|
||||
TIMEOUTS.SHORT,
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (!this.description && !parseFloat(this.amountInput)) {
|
||||
this.safeNotify.error(
|
||||
`You must enter a description or some number of ${
|
||||
this.libsUtil.UNIT_LONG[this.unitCode]
|
||||
}.`,
|
||||
NOTIFY_GIFT_ERROR_NO_DESCRIPTION.message.replace(
|
||||
"{unit}",
|
||||
this.libsUtil.UNIT_SHORT[this.unitCode] || this.unitCode,
|
||||
),
|
||||
TIMEOUTS.SHORT,
|
||||
);
|
||||
return;
|
||||
@@ -352,7 +328,11 @@ export default class GiftedDialog extends Vue {
|
||||
}
|
||||
|
||||
this.close();
|
||||
this.safeNotify.toast("Recording the give...", undefined, TIMEOUTS.BRIEF);
|
||||
this.safeNotify.toast(
|
||||
NOTIFY_GIFTED_DETAILS_RECORDING_GIVE.message,
|
||||
undefined,
|
||||
TIMEOUTS.BRIEF,
|
||||
);
|
||||
// this is asynchronous, but we don't need to wait for it to complete
|
||||
await this.recordGive(
|
||||
(this.giver?.did as string) || null,
|
||||
@@ -479,10 +459,13 @@ export default class GiftedDialog extends Vue {
|
||||
name: contact.name || contact.did,
|
||||
};
|
||||
} else {
|
||||
this.giver = {
|
||||
did: "",
|
||||
name: "Unnamed",
|
||||
};
|
||||
// Only set to "Unnamed" if no giver is currently set
|
||||
if (!this.giver || !this.giver.did) {
|
||||
this.giver = {
|
||||
did: "",
|
||||
name: "Unnamed",
|
||||
};
|
||||
}
|
||||
}
|
||||
this.firstStep = false;
|
||||
}
|
||||
@@ -492,6 +475,10 @@ export default class GiftedDialog extends Vue {
|
||||
this.firstStep = true;
|
||||
}
|
||||
|
||||
moveToStep2() {
|
||||
this.firstStep = false;
|
||||
}
|
||||
|
||||
async loadProjects() {
|
||||
try {
|
||||
const response = await fetch(this.apiServer + "/api/v2/report/plans", {
|
||||
@@ -534,10 +521,13 @@ export default class GiftedDialog extends Vue {
|
||||
name: contact.name || contact.did,
|
||||
};
|
||||
} else {
|
||||
this.receiver = {
|
||||
did: "",
|
||||
name: "Unnamed",
|
||||
};
|
||||
// Only set to "Unnamed" if no receiver is currently set
|
||||
if (!this.receiver || !this.receiver.did) {
|
||||
this.receiver = {
|
||||
did: "",
|
||||
name: "Unnamed",
|
||||
};
|
||||
}
|
||||
}
|
||||
this.firstStep = false;
|
||||
}
|
||||
@@ -561,16 +551,13 @@ export default class GiftedDialog extends Vue {
|
||||
giverName: this.giver?.name,
|
||||
offerId: this.offerId,
|
||||
fulfillsProjectId:
|
||||
this.giverEntityType === "person" &&
|
||||
this.recipientEntityType === "project"
|
||||
? this.toProjectId
|
||||
: undefined,
|
||||
this.recipientEntityType === "project" ? this.toProjectId : undefined,
|
||||
providerProjectId:
|
||||
this.giverEntityType === "project" &&
|
||||
this.recipientEntityType === "person"
|
||||
this.giverEntityType === "project"
|
||||
? this.giver?.handleId
|
||||
: this.fromProjectId,
|
||||
recipientDid: this.receiver?.did,
|
||||
recipientDid:
|
||||
this.recipientEntityType === "person" ? this.receiver?.did : undefined,
|
||||
recipientName: this.receiver?.name,
|
||||
unitCode: this.unitCode,
|
||||
};
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -159,26 +159,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
/* TODO: Human Testing Required - PlatformServiceMixin Migration */
|
||||
// Priority: High | Migrated: 2025-07-06 | Author: Matthew Raymer
|
||||
//
|
||||
// TESTING NEEDED: Component migrated from legacy logConsoleAndDb to PlatformServiceMixin
|
||||
// but requires human validation due to meeting component accessibility limitations.
|
||||
//
|
||||
// Test Scenarios Required:
|
||||
// 1. Load members list with valid meeting password
|
||||
// 2. Test member admission toggle (organizer role)
|
||||
// 3. Test adding member as contact
|
||||
// 4. Test error scenarios: network failure, invalid password, server errors
|
||||
// 5. Verify error logging appears in console and database
|
||||
// 6. Cross-platform testing: web, mobile, desktop
|
||||
//
|
||||
// Reference: docs/migration-testing/migration-checklist-MembersList.md
|
||||
// Migration Details: Replaced 3 logConsoleAndDb() calls with this.$logAndConsole()
|
||||
// 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 +203,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 +249,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 +462,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.",
|
||||
);
|
||||
|
||||
@@ -15,26 +15,25 @@ Raymer */
|
||||
class="block w-full rounded border border-slate-400 mb-2 px-3 py-2"
|
||||
placeholder="Description of what is offered"
|
||||
/>
|
||||
<div class="flex flex-row mt-2">
|
||||
<span :class="unitCodeDisplayClasses" @click="changeUnitCode()">
|
||||
{{ libsUtil.UNIT_SHORT[amountUnitCode] }}
|
||||
</span>
|
||||
<div
|
||||
v-if="showDecrementButton"
|
||||
:class="controlButtonClasses"
|
||||
@click="decrement()"
|
||||
>
|
||||
<font-awesome icon="chevron-left" />
|
||||
</div>
|
||||
<input
|
||||
v-model="amountInput"
|
||||
<div class="flex mb-4">
|
||||
<AmountInput
|
||||
:value="parseFloat(amountInput) || 0"
|
||||
:onUpdateValue="handleAmountUpdate"
|
||||
data-testId="inputOfferAmount"
|
||||
type="number"
|
||||
:class="amountInputClasses"
|
||||
/>
|
||||
<div :class="incrementButtonClasses" @click="increment()">
|
||||
<font-awesome icon="chevron-right" />
|
||||
</div>
|
||||
|
||||
<select
|
||||
v-model="amountUnitCode"
|
||||
class="flex-1 rounded border border-slate-400 ms-2 px-3 py-2"
|
||||
>
|
||||
<option
|
||||
v-for="(displayName, code) in unitOptions"
|
||||
:key="code"
|
||||
:value="code"
|
||||
>
|
||||
{{ displayName }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="mt-4 flex justify-center">
|
||||
<span>
|
||||
@@ -73,10 +72,15 @@ import {
|
||||
NOTIFY_OFFER_CREATION_ERROR,
|
||||
NOTIFY_OFFER_SUCCESS,
|
||||
NOTIFY_OFFER_SUBMISSION_ERROR,
|
||||
NOTIFY_OFFER_ERROR_NEGATIVE_AMOUNT,
|
||||
} from "@/constants/notifications";
|
||||
import AmountInput from "./AmountInput.vue";
|
||||
|
||||
@Component({
|
||||
mixins: [PlatformServiceMixin],
|
||||
components: {
|
||||
AmountInput,
|
||||
},
|
||||
})
|
||||
export default class OfferDialog extends Vue {
|
||||
@Prop projectId?: string;
|
||||
@@ -122,35 +126,10 @@ export default class OfferDialog extends Vue {
|
||||
}
|
||||
|
||||
/**
|
||||
* CSS classes for unit code selector and increment/decrement buttons
|
||||
* Reduces template complexity for repeated border and styling patterns
|
||||
* Computed property to get unit options for the select dropdown
|
||||
*/
|
||||
get controlButtonClasses(): string {
|
||||
return "border border-r-0 border-slate-400 bg-slate-200 px-4 py-2";
|
||||
}
|
||||
|
||||
/**
|
||||
* CSS classes for unit code display span
|
||||
* Reduces template complexity for unit code button styling
|
||||
*/
|
||||
get unitCodeDisplayClasses(): string {
|
||||
return "rounded-l border border-r-0 border-slate-400 bg-slate-200 w-1/3 text-center text-blue-500 px-2 py-2";
|
||||
}
|
||||
|
||||
/**
|
||||
* CSS classes for amount input field
|
||||
* Reduces template complexity for input styling
|
||||
*/
|
||||
get amountInputClasses(): string {
|
||||
return "w-full border border-r-0 border-slate-400 px-2 py-2 text-center";
|
||||
}
|
||||
|
||||
/**
|
||||
* CSS classes for the right-most increment button
|
||||
* Reduces template complexity for border styling
|
||||
*/
|
||||
get incrementButtonClasses(): string {
|
||||
return "rounded-r border border-slate-400 bg-slate-200 px-4 py-2";
|
||||
get unitOptions() {
|
||||
return this.libsUtil.UNIT_SHORT;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -173,13 +152,7 @@ export default class OfferDialog extends Vue {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the decrement button should be visible
|
||||
* Encapsulates conditional logic from template
|
||||
*/
|
||||
get showDecrementButton(): boolean {
|
||||
return this.amountInput !== "0";
|
||||
}
|
||||
|
||||
|
||||
// =================================================
|
||||
// COMPONENT METHODS
|
||||
@@ -226,30 +199,14 @@ export default class OfferDialog extends Vue {
|
||||
this.visible = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cycle through available unit codes
|
||||
*/
|
||||
changeUnitCode() {
|
||||
const units = Object.keys(this.libsUtil.UNIT_SHORT);
|
||||
const index = units.indexOf(this.amountUnitCode);
|
||||
this.amountUnitCode = units[(index + 1) % units.length];
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Increment the amount input
|
||||
* Handle amount updates from AmountInput component
|
||||
* @param value - New amount value
|
||||
*/
|
||||
increment() {
|
||||
this.amountInput = `${(parseFloat(this.amountInput) || 0) + 1}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrement the amount input
|
||||
*/
|
||||
decrement() {
|
||||
this.amountInput = `${Math.max(
|
||||
0,
|
||||
(parseFloat(this.amountInput) || 1) - 1,
|
||||
)}`;
|
||||
handleAmountUpdate(value: number) {
|
||||
this.amountInput = value.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -273,6 +230,28 @@ export default class OfferDialog extends Vue {
|
||||
* Confirm and submit the offer
|
||||
*/
|
||||
async confirm() {
|
||||
if (!this.activeDid) {
|
||||
this.notify.error(NOTIFY_OFFER_IDENTITY_REQUIRED.message, TIMEOUTS.LONG);
|
||||
return;
|
||||
}
|
||||
|
||||
if (parseFloat(this.amountInput) < 0) {
|
||||
this.notify.error(
|
||||
NOTIFY_OFFER_ERROR_NEGATIVE_AMOUNT.message,
|
||||
TIMEOUTS.SHORT,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.description && !parseFloat(this.amountInput)) {
|
||||
const message = NOTIFY_OFFER_DESCRIPTION_REQUIRED.message.replace(
|
||||
"{unit}",
|
||||
this.libsUtil.UNIT_LONG[this.amountUnitCode],
|
||||
);
|
||||
this.notify.error(message, TIMEOUTS.SHORT);
|
||||
return;
|
||||
}
|
||||
|
||||
this.close();
|
||||
this.notify.toast(NOTIFY_OFFER_RECORDING.text, undefined, TIMEOUTS.BRIEF);
|
||||
|
||||
@@ -301,20 +280,6 @@ export default class OfferDialog extends Vue {
|
||||
unitCode: string = "HUR",
|
||||
expirationDateInput?: string,
|
||||
) {
|
||||
if (!this.activeDid) {
|
||||
this.notify.error(NOTIFY_OFFER_IDENTITY_REQUIRED.message, TIMEOUTS.LONG);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!description && !amount) {
|
||||
const message = NOTIFY_OFFER_DESCRIPTION_REQUIRED.message.replace(
|
||||
"{unit}",
|
||||
this.libsUtil.UNIT_LONG[unitCode],
|
||||
);
|
||||
this.notify.error(message, TIMEOUTS.MODAL);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await createAndSubmitOffer(
|
||||
this.axios,
|
||||
@@ -363,7 +328,7 @@ export default class OfferDialog extends Vue {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 1000;
|
||||
z-index: 50;
|
||||
}
|
||||
|
||||
.dialog {
|
||||
|
||||
@@ -180,7 +180,7 @@
|
||||
>
|
||||
Let's go!
|
||||
<br />
|
||||
See & record gratitude.
|
||||
See & record things you've received.
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
|
||||
@@ -67,7 +67,7 @@
|
||||
out of <b>{{ imageLimits?.maxImagesPerWeek ?? "?" }}</b> for this week.
|
||||
Your image counter resets at
|
||||
<b class="whitespace-nowrap">{{
|
||||
readableDate(imageLimits?.nextWeekBeginDateTime || "")
|
||||
readableDate(imageLimits?.nextWeekBeginDateTime)
|
||||
}}</b>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user