diff --git a/src/components/EntityGrid.vue b/src/components/EntityGrid.vue index 314ce298..a2552cd5 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 3fba2141..85900b66 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 f31ed095..7acbc42a 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 33a80446..1d085c47 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 9671e801..b7f4d056 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 eebd8049..e9e46cd3 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 3d343e6d..ba053022 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 1cd0424d..42025744 100644 --- a/src/views/ProjectViewView.vue +++ b/src/views/ProjectViewView.vue @@ -238,10 +238,9 @@ @@ -521,10 +520,9 @@