From 70a0ef7ef66e9d2703c4028645a0697f077efd07 Mon Sep 17 00:00:00 2001 From: Trent Larson Date: Wed, 17 Dec 2025 21:08:09 -0700 Subject: [PATCH 1/8] feat: allow changing of both giver and receiver to projects or people --- src/components/EntityGrid.vue | 44 +++++++++ src/components/EntitySelectionStep.vue | 49 ++++++--- src/components/GiftDetailsStep.vue | 17 ++-- src/components/GiftedDialog.vue | 132 ++++++++++++++++++++----- src/views/ClaimView.vue | 4 +- src/views/ContactGiftingView.vue | 8 +- src/views/ContactsView.vue | 4 +- src/views/HomeView.vue | 43 +++----- src/views/ProjectViewView.vue | 10 +- 9 files changed, 218 insertions(+), 93 deletions(-) diff --git a/src/components/EntityGrid.vue b/src/components/EntityGrid.vue index 314ce2986d..a2552cd5b3 100644 --- a/src/components/EntityGrid.vue +++ b/src/components/EntityGrid.vue @@ -1036,6 +1036,50 @@ export default class EntityGrid extends Vue { return data; } + /** + * Watch for changes in entityType to load projects when switching to projects + */ + @Watch("entityType") + async onEntityTypeChange(newType: "people" | "projects"): Promise { + // Reset displayed count and clear search when switching types + this.displayedCount = INITIAL_BATCH_SIZE; + this.searchTerm = ""; + this.filteredEntities = []; + this.searchBeforeId = undefined; + this.infiniteScrollReset?.(); + + // When switching to projects, load them if not provided via entities prop + if (newType === "projects" && !this.entities) { + // Ensure apiServer is loaded + if (!this.apiServer) { + const settings = await this.$accountSettings(); + this.apiServer = settings.apiServer || ""; + this.starredPlanHandleIds = settings.starredPlanHandleIds || []; + } + + // Load projects if we have an API server + if (this.apiServer && this.allProjects.length === 0) { + this.isLoadingProjects = true; + try { + await this.fetchProjects(); + } catch (error) { + logger.error( + "Error loading projects when switching to projects:", + error, + ); + } finally { + this.isLoadingProjects = false; + } + } + } + + // Clear project state when switching away from projects + if (newType === "people") { + this.allProjects = []; + this.loadBeforeId = undefined; + } + } + /** * Watch for changes in search term to reset displayed count and pagination */ diff --git a/src/components/EntitySelectionStep.vue b/src/components/EntitySelectionStep.vue index 3fba214129..85900b66fa 100644 --- a/src/components/EntitySelectionStep.vue +++ b/src/components/EntitySelectionStep.vue @@ -12,6 +12,17 @@ properties * * @author Matthew Raymer */ {{ stepLabel }} + +
+ +
+ diff --git a/src/components/GiftDetailsStep.vue b/src/components/GiftDetailsStep.vue index f31ed0957a..7acbc42a59 100644 --- a/src/components/GiftDetailsStep.vue +++ b/src/components/GiftDetailsStep.vue @@ -172,10 +172,6 @@ export default class GiftDetailsStep extends Vue { @Prop({ default: "" }) prompt!: string; - /** Whether this is from a project view */ - @Prop({ default: false }) - isFromProjectView!: boolean; - /** Whether there's a conflict between giver and receiver */ @Prop({ default: false }) hasConflict!: boolean; @@ -192,6 +188,14 @@ export default class GiftDetailsStep extends Vue { @Prop({ default: "" }) toProjectId!: string; + /** Whether the giver is locked and cannot be edited */ + @Prop({ default: false }) + isGiverLocked!: boolean; + + /** Whether the recipient is locked and cannot be edited */ + @Prop({ default: false }) + isRecipientLocked!: boolean; + /** * Function prop for handling description updates * Called when the description input changes, allowing parent to control validation @@ -281,14 +285,15 @@ export default class GiftDetailsStep extends Vue { * Whether the giver can be edited */ get canEditGiver(): boolean { - return !(this.isFromProjectView && this.giverEntityType === "project"); + // If giver is locked via prop, it cannot be edited + return !this.isGiverLocked; } /** * Whether the recipient can be edited */ get canEditRecipient(): boolean { - return this.recipientEntityType === "person"; + return !this.isRecipientLocked; } /** diff --git a/src/components/GiftedDialog.vue b/src/components/GiftedDialog.vue index 33a8044661..1d085c47c9 100644 --- a/src/components/GiftedDialog.vue +++ b/src/components/GiftedDialog.vue @@ -3,18 +3,18 @@
@@ -37,17 +38,18 @@ v-show="!firstStep" :giver="giver" :receiver="receiver" - :giver-entity-type="giverEntityType" - :recipient-entity-type="recipientEntityType" + :giver-entity-type="currentGiverEntityType" + :recipient-entity-type="currentRecipientEntityType" :description="description" :amount="parseFloat(amountInput) || 0" :unit-code="unitCode" :prompt="prompt" - :is-from-project-view="isFromProjectView" :has-conflict="hasPersonConflict" :offer-id="offerId" :from-project-id="fromProjectId" :to-project-id="toProjectId" + :is-giver-locked="isGiverLocked" + :is-recipient-locked="isRecipientLocked" :on-update-description="(desc: string) => (description = desc)" :on-update-amount="handleAmountUpdate" :on-update-unit-code="(code: string) => (unitCode = code)" @@ -113,11 +115,10 @@ export default class GiftedDialog extends Vue { @Prop() fromProjectId = ""; @Prop() toProjectId = ""; - @Prop() isFromProjectView = false; - @Prop({ default: "person" }) giverEntityType = "person" as + @Prop({ default: "person" }) initialGiverEntityType = "person" as | "person" | "project"; - @Prop({ default: "person" }) recipientEntityType = "person" as + @Prop({ default: "person" }) initialRecipientEntityType = "person" as | "person" | "project"; @@ -131,12 +132,16 @@ export default class GiftedDialog extends Vue { description = ""; firstStep = true; // true = Step 1 (giver/recipient selection), false = Step 2 (amount/description) giver?: libsUtil.GiverReceiverInputInfo; // undefined means no identified giver agent + currentGiverEntityType: "person" | "project" = "person"; // Mutable version (can be toggled) + currentRecipientEntityType: "person" | "project" = "person"; // Mutable version (can be toggled) offerId = ""; prompt = ""; receiver?: libsUtil.GiverReceiverInputInfo; stepType = "giver"; unitCode = "HUR"; visible = false; + isGiverLocked = false; + isRecipientLocked = false; libsUtil = libsUtil; @@ -145,8 +150,10 @@ export default class GiftedDialog extends Vue { // Computed property to help debug template logic get shouldShowProjects() { const result = - (this.stepType === "giver" && this.giverEntityType === "project") || - (this.stepType === "recipient" && this.recipientEntityType === "project"); + (this.stepType === "giver" && + this.currentGiverEntityType === "project") || + (this.stepType === "recipient" && + this.currentRecipientEntityType === "project"); return result; } @@ -154,8 +161,8 @@ export default class GiftedDialog extends Vue { get hasPersonConflict() { // Only check for conflicts when both entities are persons if ( - this.giverEntityType !== "person" || - this.recipientEntityType !== "person" + this.currentGiverEntityType !== "person" || + this.currentRecipientEntityType !== "person" ) { return false; } @@ -176,8 +183,8 @@ export default class GiftedDialog extends Vue { wouldCreateConflict(contactDid: string) { // Only check for conflicts when both entities are persons if ( - this.giverEntityType !== "person" || - this.recipientEntityType !== "person" + this.currentGiverEntityType !== "person" || + this.currentRecipientEntityType !== "person" ) { return false; } @@ -211,8 +218,9 @@ export default class GiftedDialog extends Vue { this.amountInput = amountInput || "0"; this.unitCode = unitCode || "HUR"; this.callbackOnSuccess = callbackOnSuccess; - this.firstStep = !giver; - this.stepType = "giver"; + // Initialize current entity types from initial prop values + this.currentGiverEntityType = this.initialGiverEntityType; + this.currentRecipientEntityType = this.initialRecipientEntityType; try { const settings = await this.$accountSettings(); @@ -223,6 +231,41 @@ export default class GiftedDialog extends Vue { const activeIdentity = await (this as any).$getActiveIdentity(); this.activeDid = activeIdentity.activeDid || ""; + // Determine if entities should be locked + // An entity is locked if it's provided as an input property (has did or handleId) + // For persons: locked if did is provided and not "You" (activeDid) + // For projects: locked if handleId is provided + // When entities come from ContactsView, ContactGiftingView, or ProjectViewView context, + // they should be locked if they have a valid identifier (did or handleId) + const isGiverProvided = + giver && + ((giver.did && giver.did !== this.activeDid) || + (giver.handleId && giver.handleId !== "")); + const isReceiverProvided = + receiver && + ((receiver.did && receiver.did !== this.activeDid) || + (receiver.handleId && receiver.handleId !== "")); + + // Lock entities that are provided (from context or explicitly set) + // This ensures that when entities are chosen from ContactsView, ContactGiftingView, + // or ProjectViewView, the other entity (giver or recipient) that was already set + // from context is locked + this.isGiverLocked = !!isGiverProvided; + this.isRecipientLocked = !!isReceiverProvided; + + // Determine if receiver should be locked (for step navigation logic) + // Receiver is locked only if it's provided AND it's not "You" (activeDid) + // "You" is treated as a default that can be changed + const isReceiverLocked = + receiver && receiver.did && receiver.did !== this.activeDid; + + // Only skip Step 1 if both giver and receiver are provided AND receiver is locked + // If receiver is "You" (default), still show Step 1 so user can change it + this.firstStep = !(giver && isReceiverLocked); + // If giver is provided but receiver is not locked, start with recipient selection + // Otherwise, start with giver selection + this.stepType = giver && !isReceiverLocked ? "recipient" : "giver"; + logger.debug("[GiftedDialog] Settings received:", { activeDid: this.activeDid, apiServer: this.apiServer, @@ -278,6 +321,11 @@ export default class GiftedDialog extends Vue { this.prompt = ""; this.unitCode = "HUR"; this.firstStep = true; + // Reset to initial prop values + this.currentGiverEntityType = this.initialGiverEntityType; + // Reset lock states + this.isGiverLocked = false; + this.isRecipientLocked = false; } async confirm() { @@ -356,8 +404,8 @@ export default class GiftedDialog extends Vue { let providerPlanHandleId: string | undefined; if ( - this.giverEntityType === "project" && - this.recipientEntityType === "person" + this.currentGiverEntityType === "project" && + this.currentRecipientEntityType === "person" ) { // Project-to-person gift fromDid = undefined; // No person giver @@ -365,8 +413,8 @@ export default class GiftedDialog extends Vue { fulfillsProjectHandleId = undefined; // No project recipient providerPlanHandleId = this.giver?.handleId; // Project giver } else if ( - this.giverEntityType === "person" && - this.recipientEntityType === "project" + this.currentGiverEntityType === "person" && + this.currentRecipientEntityType === "project" ) { // Person-to-project gift fromDid = giverDid as string; // Person giver @@ -526,17 +574,22 @@ export default class GiftedDialog extends Vue { return { amountInput: this.amountInput, description: this.description, - giverDid: this.giverEntityType === "person" ? this.giver?.did : undefined, + giverDid: + this.currentGiverEntityType === "person" ? this.giver?.did : undefined, giverName: this.giver?.name, offerId: this.offerId, fulfillsProjectId: - this.recipientEntityType === "project" ? this.toProjectId : undefined, + this.currentRecipientEntityType === "project" + ? this.toProjectId + : undefined, providerProjectId: - this.giverEntityType === "project" + this.currentGiverEntityType === "project" ? this.giver?.handleId : this.fromProjectId, recipientDid: - this.recipientEntityType === "person" ? this.receiver?.did : undefined, + this.currentRecipientEntityType === "person" + ? this.receiver?.did + : undefined, recipientName: this.receiver?.name, unitCode: this.unitCode, }; @@ -596,6 +649,13 @@ export default class GiftedDialog extends Vue { entityType: string; currentEntity: { did: string; name: string }; }) { + // Prevent editing if the entity is locked + if (data.entityType === "giver" && this.isGiverLocked) { + return; + } + if (data.entityType === "recipient" && this.isRecipientLocked) { + return; + } this.goBackToStep1(data.entityType); } @@ -606,6 +666,24 @@ export default class GiftedDialog extends Vue { this.confirm(); } + /** + * Handle toggle entity type request from EntitySelectionStep + */ + handleToggleEntityType() { + // Toggle the appropriate entity type based on current step + if (this.stepType === "giver") { + this.currentGiverEntityType = + this.currentGiverEntityType === "person" ? "project" : "person"; + // Clear any selected giver when toggling + this.giver = undefined; + } else if (this.stepType === "recipient") { + this.currentRecipientEntityType = + this.currentRecipientEntityType === "person" ? "project" : "person"; + // Clear any selected receiver when toggling + this.receiver = undefined; + } + } + /** * Handle amount update from GiftDetailsStep */ diff --git a/src/views/ClaimView.vue b/src/views/ClaimView.vue index 9671e80121..b7f4d0561b 100644 --- a/src/views/ClaimView.vue +++ b/src/views/ClaimView.vue @@ -236,8 +236,8 @@
@@ -165,7 +164,6 @@ export default class ContactGiftingView extends Vue { fromProjectId = ""; toProjectId = ""; showProjects = false; - isFromProjectView = false; offerId = ""; async created() { @@ -217,8 +215,6 @@ export default class ContactGiftingView extends Vue { this.toProjectId = (this.$route.query["toProjectId"] as string) || ""; this.showProjects = (this.$route.query["showProjects"] as string) === "true"; - this.isFromProjectView = - (this.$route.query["isFromProjectView"] as string) === "true"; this.offerId = (this.$route.query["offerId"] as string) || ""; // eslint-disable-next-line @typescript-eslint/no-explicit-any diff --git a/src/views/ContactsView.vue b/src/views/ContactsView.vue index eebd8049f4..e9e46cd3ce 100644 --- a/src/views/ContactsView.vue +++ b/src/views/ContactsView.vue @@ -120,8 +120,8 @@ diff --git a/src/views/HomeView.vue b/src/views/HomeView.vue index 3d343e6d03..ba053022cf 100644 --- a/src/views/HomeView.vue +++ b/src/views/HomeView.vue @@ -123,24 +123,14 @@ Raymer * @version 1.0.0 */ -
- - -
+ @@ -148,8 +138,8 @@ Raymer * @version 1.0.0 */ @@ -446,7 +436,6 @@ export default class HomeView extends Vue { userAgentInfo = new UAParser(); // see https://docs.uaparser.js.org/v2/api/ua-parser-js/get-os.html selectedImage = ""; isImageViewerOpen = false; - showProjectsDialog = false; /** * CRITICAL VUE REACTIVITY BUG WORKAROUND @@ -1811,17 +1800,19 @@ export default class HomeView extends Vue { * - this.activeDid * * @param giver Optional contact info for giver - * @param description Optional gift description + * @param prompt Optional gift prompt */ openDialog(giver?: GiverReceiverInputInfo, prompt?: string) { // Determine the giver entity based on DID logic const giverEntity = this.createGiverEntity(giver); + // In HomeView, "You" is the default recipient but it's not locked + // User can still change it in Step 1 if they want (this.$refs.giftedDialog as GiftedDialog).open( giverEntity, { did: this.activeDid, - name: "You", // In HomeView, we always use "You" as the giver + name: "You", } as GiverReceiverInputInfo, undefined, prompt, @@ -1919,15 +1910,9 @@ export default class HomeView extends Vue { } openPersonDialog(giver?: GiverReceiverInputInfo, prompt?: string) { - this.showProjectsDialog = false; this.openDialog(giver, prompt); } - openProjectDialog() { - this.showProjectsDialog = true; - (this.$refs.giftedDialog as GiftedDialog).open(); - } - /** * Computed property for registration status * diff --git a/src/views/ProjectViewView.vue b/src/views/ProjectViewView.vue index 1cd0424db6..4202574494 100644 --- a/src/views/ProjectViewView.vue +++ b/src/views/ProjectViewView.vue @@ -238,10 +238,9 @@ @@ -521,10 +520,9 @@

From 34a71190866a8deca124255a09bf41a2a06b22d6 Mon Sep 17 00:00:00 2001 From: Trent Larson Date: Wed, 17 Dec 2025 21:20:27 -0700 Subject: [PATCH 2/8] feat: disallow selection of a person or project if it's already selected on the other side (giver/receiver) --- src/components/EntityGrid.vue | 10 ++++ src/components/GiftedDialog.vue | 65 ++++++++++++++++++++---- src/components/ProjectCard.vue | 90 +++++++++++++++++++++++++++++---- 3 files changed, 144 insertions(+), 21 deletions(-) diff --git a/src/components/EntityGrid.vue b/src/components/EntityGrid.vue index a2552cd5b3..f5ab2e0188 100644 --- a/src/components/EntityGrid.vue +++ b/src/components/EntityGrid.vue @@ -155,6 +155,7 @@ projects, and special entities with selection. * * @author Matthew Raymer */ :active-did="activeDid" :all-my-dids="allMyDids" :all-contacts="allContacts" + :conflicted="isProjectConflicted(project.handleId)" :notify="notify" :conflict-context="conflictContext" @project-selected="handleProjectSelected" @@ -175,6 +176,7 @@ projects, and special entities with selection. * * @author Matthew Raymer */ :active-did="activeDid" :all-my-dids="allMyDids" :all-contacts="allContacts" + :conflicted="isProjectConflicted(project.handleId)" :notify="notify" :conflict-context="conflictContext" @project-selected="handleProjectSelected" @@ -190,6 +192,7 @@ projects, and special entities with selection. * * @author Matthew Raymer */ :active-did="activeDid" :all-my-dids="allMyDids" :all-contacts="allContacts" + :conflicted="isProjectConflicted(project.handleId)" :notify="notify" :conflict-context="conflictContext" @project-selected="handleProjectSelected" @@ -555,6 +558,13 @@ export default class EntityGrid extends Vue { return this.conflictChecker(did); } + /** + * Check if a project handleId is conflicted + */ + isProjectConflicted(handleId: string): boolean { + return this.conflictChecker(handleId); + } + /** * Handle person selection from PersonCard */ diff --git a/src/components/GiftedDialog.vue b/src/components/GiftedDialog.vue index 1d085c47c9..a383ca0956 100644 --- a/src/components/GiftedDialog.vue +++ b/src/components/GiftedDialog.vue @@ -179,22 +179,56 @@ export default class GiftedDialog extends Vue { return false; } - // Computed property to check if a contact would create a conflict when selected - wouldCreateConflict(contactDid: string) { - // Only check for conflicts when both entities are persons + // Computed property to check if current selection would create a project conflict + get hasProjectConflict() { + // Only check for conflicts when both entities are projects if ( - this.currentGiverEntityType !== "person" || - this.currentRecipientEntityType !== "person" + this.currentGiverEntityType !== "project" || + this.currentRecipientEntityType !== "project" ) { return false; } - if (this.stepType === "giver") { - // If selecting as giver, check if it conflicts with current recipient - return this.receiver?.did === contactDid; - } else if (this.stepType === "recipient") { - // If selecting as recipient, check if it conflicts with current giver - return this.giver?.did === contactDid; + // Check if giver and recipient are the same project + if ( + this.giver?.handleId && + this.receiver?.handleId && + this.giver.handleId === this.receiver.handleId + ) { + return true; + } + + return false; + } + + // Computed property to check if a contact or project would create a conflict when selected + wouldCreateConflict(identifier: string) { + // Check for person conflicts when both entities are persons + if ( + this.currentGiverEntityType === "person" && + this.currentRecipientEntityType === "person" + ) { + if (this.stepType === "giver") { + // If selecting as giver, check if it conflicts with current recipient + return this.receiver?.did === identifier; + } else if (this.stepType === "recipient") { + // If selecting as recipient, check if it conflicts with current giver + return this.giver?.did === identifier; + } + } + + // Check for project conflicts when both entities are projects + if ( + this.currentGiverEntityType === "project" && + this.currentRecipientEntityType === "project" + ) { + if (this.stepType === "giver") { + // If selecting as giver, check if it conflicts with current recipient + return this.receiver?.handleId === identifier; + } else if (this.stepType === "recipient") { + // If selecting as recipient, check if it conflicts with current giver + return this.giver?.handleId === identifier; + } } return false; @@ -363,6 +397,15 @@ export default class GiftedDialog extends Vue { return; } + // Check for project conflict + if (this.hasProjectConflict) { + this.safeNotify.error( + "You cannot select the same project as both giver and recipient.", + TIMEOUTS.STANDARD, + ); + return; + } + this.close(); this.safeNotify.toast( NOTIFY_GIFTED_DETAILS_RECORDING_GIVE.message, diff --git a/src/components/ProjectCard.vue b/src/components/ProjectCard.vue index 4b995a21d7..19824311ff 100644 --- a/src/components/ProjectCard.vue +++ b/src/components/ProjectCard.vue @@ -1,11 +1,8 @@ /** * ProjectCard.vue - Individual project display component * * Extracted from -GiftedDialog.vue to handle project entity display * with selection states and -issuer information. * * @author Matthew Raymer */ +GiftedDialog.vue to handle project entity display * with selection states, +conflict detection, and issuer information. * * @author Matthew Raymer */