From a142737771ade4112dd38bfd6d4dd43ead5ee8a8 Mon Sep 17 00:00:00 2001 From: Jose Olarte III Date: Wed, 5 Nov 2025 20:20:43 +0800 Subject: [PATCH 1/4] feat: replace authorized representative input with contact selection dialog Replace the plain text input for authorized representative with an interactive contact selection interface that provides better UX and maintains data consistency. Changes: - Add ProjectRepresentativeDialog component using EntityGrid for contact selection (excludes "You" and "Unnamed" special entities) - Replace text input with clickable field showing contact icon, name, and DID - Implement conditional UI states: initial "Assign..." placeholder vs assigned representative display with unset button - Refactor selectedRepresentative to computed property derived from agentDid (single source of truth, prevents sync issues) - Inline representativeDisplayName for simplicity - Support changing representative by clicking on assigned field - Support unsetting representative via trash button The new implementation ensures agentDid remains the authoritative state while selectedRepresentative is automatically computed, preventing the previously possible desync when agentDid was set directly (e.g., via the "make original owner an authorized representative" button). --- .../ProjectRepresentativeDialog.vue | 128 ++++++++++++++++++ src/views/NewEditProjectView.vue | 126 ++++++++++++++++- 2 files changed, 248 insertions(+), 6 deletions(-) create mode 100644 src/components/ProjectRepresentativeDialog.vue diff --git a/src/components/ProjectRepresentativeDialog.vue b/src/components/ProjectRepresentativeDialog.vue new file mode 100644 index 00000000..c15b2dcd --- /dev/null +++ b/src/components/ProjectRepresentativeDialog.vue @@ -0,0 +1,128 @@ + + + + + diff --git a/src/views/NewEditProjectView.vue b/src/views/NewEditProjectView.vue index d732045f..29362324 100644 --- a/src/views/NewEditProjectView.vue +++ b/src/views/NewEditProjectView.vue @@ -60,12 +60,62 @@ - +
+
+
+ + +
+
+
+ {{ + selectedRepresentative + ? selectedRepresentative.name || AppString.NO_CONTACT_NAME + : "Assign Authorized Representative…" + }} +
+
+ {{ agentDid }} +
+
+
+ +
+ + +

Beware! @@ -232,9 +282,12 @@ import { LMap, LMarker, LTileLayer } from "@vue-leaflet/vue-leaflet"; import { RouteLocationNormalizedLoaded, Router } from "vue-router"; import { LeafletMouseEvent } from "leaflet"; +import EntityIcon from "../components/EntityIcon.vue"; import ImageMethodDialog from "../components/ImageMethodDialog.vue"; +import ProjectRepresentativeDialog from "../components/ProjectRepresentativeDialog.vue"; import QuickNav from "../components/QuickNav.vue"; import { + AppString, DEFAULT_IMAGE_API_SERVER, DEFAULT_PARTNER_API_SERVER, NotificationIface, @@ -268,6 +321,7 @@ import { retrieveAccountCount, retrieveFullyDecryptedAccount, } from "../libs/util"; +import { Contact } from "../db/tables/contacts"; import { EventTemplate, @@ -323,7 +377,15 @@ import { logger } from "../utils/logger"; */ @Component({ - components: { ImageMethodDialog, LMap, LMarker, LTileLayer, QuickNav }, + components: { + EntityIcon, + ImageMethodDialog, + ProjectRepresentativeDialog, + LMap, + LMarker, + LTileLayer, + QuickNav, + }, mixins: [PlatformServiceMixin], }) export default class NewEditProjectView extends Vue { @@ -334,6 +396,9 @@ export default class NewEditProjectView extends Vue { // Notification helpers private notify!: ReturnType; + // Constants + AppString = AppString; + /** * Display error notification to user * Provides consistent error messaging with 5-second timeout @@ -346,6 +411,8 @@ export default class NewEditProjectView extends Vue { // Component state properties activeDid = ""; agentDid = ""; + allContacts: Array = []; + allMyDids: string[] = []; apiServer = ""; endDateInput?: string; endTimeInput?: string; @@ -392,6 +459,14 @@ export default class NewEditProjectView extends Vue { const activeIdentity = await (this as any).$getActiveIdentity(); this.activeDid = activeIdentity.activeDid || ""; + // Get all user's DIDs + // eslint-disable-next-line @typescript-eslint/no-explicit-any + this.allMyDids = await (this as any).$getAllAccountDids(); + + // Load contacts + // eslint-disable-next-line @typescript-eslint/no-explicit-any + this.allContacts = await (this as any).$getAllContacts(); + this.apiServer = settings.apiServer || ""; this.showGeneralAdvanced = !!settings.showGeneralAdvanced; @@ -961,5 +1036,44 @@ export default class NewEditProjectView extends Vue { get shouldShowSpinner(): boolean { return !this.isHiddenSpinner; } + + /** + * Computed property for selected representative contact + * Derives the contact from agentDid by finding it in allContacts + */ + get selectedRepresentative(): Contact | null { + if (!this.agentDid) { + return null; + } + return this.allContacts.find((c) => c.did === this.agentDid) || null; + } + + /** + * Open the representative selection dialog + */ + openRepresentativeDialog(): void { + (this.$refs.representativeDialog as ProjectRepresentativeDialog).open(); + } + + /** + * Handle representative assignment from dialog + */ + handleRepresentativeAssigned(contact: Contact): void { + this.agentDid = contact.did; + } + + /** + * Handle representative dialog cancel + */ + handleRepresentativeCancel(): void { + // Dialog closes itself, nothing to do here + } + + /** + * Unset the representative and revert to initial state + */ + unsetRepresentative(): void { + this.agentDid = ""; + } } -- 2.30.2 From 9a3409c29f9b6e0390070134cb30b77ff220ba73 Mon Sep 17 00:00:00 2001 From: Jose Olarte III Date: Fri, 7 Nov 2025 17:43:44 +0800 Subject: [PATCH 2/4] refactor: remove unused code from ProjectRepresentativeDialog - Remove conflictChecker prop (always passed as no-op function) - Remove unused emitCancel method and cancel event handling - Simplify handleEntitySelected by removing unnecessary type check - Update NewEditProjectView to remove conflict-checker binding and empty cancel handler The conflictChecker prop was not needed since representative selection doesn't require conflict detection. The cancel event was never emitted and the parent handler was empty, so both were removed. --- .../ProjectRepresentativeDialog.vue | 19 ++++--------------- src/views/NewEditProjectView.vue | 9 --------- 2 files changed, 4 insertions(+), 24 deletions(-) diff --git a/src/components/ProjectRepresentativeDialog.vue b/src/components/ProjectRepresentativeDialog.vue index c15b2dcd..51afcb5f 100644 --- a/src/components/ProjectRepresentativeDialog.vue +++ b/src/components/ProjectRepresentativeDialog.vue @@ -13,7 +13,7 @@ :active-did="activeDid" :all-my-dids="allMyDids" :all-contacts="allContacts" - :conflict-checker="conflictChecker" + :conflict-checker="() => false" :show-you-entity="false" :show-unnamed-entity="false" :notify="notify" @@ -70,10 +70,6 @@ export default class ProjectRepresentativeDialog extends Vue { @Prop({ required: true }) allMyDids!: string[]; - /** Function to check if a person DID would create a conflict */ - @Prop({ required: true }) - conflictChecker!: (did: string) => boolean; - /** Notification function from parent component */ @Prop() notify?: (notification: NotificationIface, timeout?: number) => void; @@ -83,11 +79,9 @@ export default class ProjectRepresentativeDialog extends Vue { * Immediately assigns the selected contact and closes the dialog */ handleEntitySelected(event: { type: "person" | "project"; data: Contact }) { - if (event.type === "person") { - const contact = event.data as Contact; - this.emitAssign(contact); - this.close(); - } + const contact = event.data as Contact; + this.emitAssign(contact); + this.close(); } /** @@ -117,11 +111,6 @@ export default class ProjectRepresentativeDialog extends Vue { emitAssign(contact: Contact): Contact { return contact; } - - @Emit("cancel") - emitCancel(): void { - // No return value needed - } } diff --git a/src/views/NewEditProjectView.vue b/src/views/NewEditProjectView.vue index 29362324..1d006d48 100644 --- a/src/views/NewEditProjectView.vue +++ b/src/views/NewEditProjectView.vue @@ -110,10 +110,8 @@ :all-contacts="allContacts" :active-did="activeDid" :all-my-dids="allMyDids" - :conflict-checker="() => false" :notify="$notify" @assign="handleRepresentativeAssigned" - @cancel="handleRepresentativeCancel" />

@@ -1062,13 +1060,6 @@ export default class NewEditProjectView extends Vue { this.agentDid = contact.did; } - /** - * Handle representative dialog cancel - */ - handleRepresentativeCancel(): void { - // Dialog closes itself, nothing to do here - } - /** * Unset the representative and revert to initial state */ -- 2.30.2 From 9ff24f825887ca6874d9f4ed919ffcae6d9b9876 Mon Sep 17 00:00:00 2001 From: Trent Larson Date: Fri, 7 Nov 2025 18:11:11 -0700 Subject: [PATCH 3/4] fix: in project-edit view, don't show agent warning on new one, and automatically switch if they're changing --- src/views/NewEditProjectView.vue | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/src/views/NewEditProjectView.vue b/src/views/NewEditProjectView.vue index 1d006d48..8f93aa4b 100644 --- a/src/views/NewEditProjectView.vue +++ b/src/views/NewEditProjectView.vue @@ -470,11 +470,11 @@ export default class NewEditProjectView extends Vue { this.projectId = (this.$route.query["projectId"] as string) || ""; - if (this.projectId) { + if (this.isSavedProject()) { if (this.numAccounts === 0) { this.notify.error(NOTIFY_PROJECT_ACCOUNT_LOADING_ERROR.message); } else { - this.loadProject(this.activeDid); + this.loadProject(this.activeDid, this.projectId); } } } @@ -484,11 +484,9 @@ export default class NewEditProjectView extends Vue { * Retrieves project information from the API and populates form fields * @param userDid - User's decentralized identifier */ - async loadProject(userDid: string) { + async loadProject(userDid: string, projectId: string) { const url = - this.apiServer + - "/api/claim/byHandle/" + - encodeURIComponent(this.projectId); + this.apiServer + "/api/claim/byHandle/" + encodeURIComponent(projectId); const headers = await getHeaders(userDid); try { @@ -505,6 +503,12 @@ export default class NewEditProjectView extends Vue { } if (this.fullClaim?.agent?.identifier) { this.agentDid = this.fullClaim.agent.identifier; + if (this.activeDid !== this.projectIssuerDid) { + this.agentDid = this.projectIssuerDid; + this.notify.warning( + "You were previously the agent, so the agent has been set to the previous owner. You can change it.", + ); + } } if (this.fullClaim.startTime) { const localDateTime = DateTime.fromISO( @@ -609,7 +613,7 @@ export default class NewEditProjectView extends Vue { private async saveProject() { // Make a claim const vcClaim: PlanActionClaim = this.fullClaim; - if (this.projectId) { + if (this.isSavedProject()) { vcClaim.lastClaimId = this.lastClaimJwtId; } if (this.agentDid) { @@ -943,6 +947,10 @@ export default class NewEditProjectView extends Vue { this.longitude = event.latlng.lng; } + private isSavedProject(): boolean { + return !!this.projectId; + } + /** * Computed property for character count display * Shows current description length and maximum character limit @@ -958,6 +966,7 @@ export default class NewEditProjectView extends Vue { */ get shouldShowOwnershipWarning(): boolean { return ( + this.isSavedProject() && this.activeDid !== this.projectIssuerDid && this.agentDid !== this.projectIssuerDid ); -- 2.30.2 From b1fa6ac458ee3ca38d67a5fb116fd21c9799c9cf Mon Sep 17 00:00:00 2001 From: Trent Larson Date: Fri, 7 Nov 2025 18:27:05 -0700 Subject: [PATCH 4/4] feat: show the recent contacts in the alphabetical section of choosers --- src/components/EntityGrid.vue | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/src/components/EntityGrid.vue b/src/components/EntityGrid.vue index 6c84eb7b..42d93eed 100644 --- a/src/components/EntityGrid.vue +++ b/src/components/EntityGrid.vue @@ -307,14 +307,14 @@ export default class EntityGrid extends Vue { } /** - * Get the 3 most recently added contacts (when showing contacts and not searching) + * Get the most recently added contacts (when showing contacts and not searching) */ get recentContacts(): Contact[] { if (this.entityType !== "people" || this.searchTerm.trim()) { return []; } // Entities are already sorted by date added (newest first) - return (this.entities as Contact[]).slice(0, 3); + return (this.entities as Contact[]).slice(0, RECENT_CONTACTS_COUNT); } /** @@ -325,16 +325,16 @@ export default class EntityGrid extends Vue { if (this.entityType !== "people" || this.searchTerm.trim()) { return []; } - // Skip the first 3 (recent contacts) and sort the rest alphabetically + // Skip the first few (recent contacts) and sort the rest alphabetically // Create a copy to avoid mutating the original array - const remaining = (this.entities as Contact[]).slice(RECENT_CONTACTS_COUNT); + const remaining = this.entities as Contact[]; const sorted = [...remaining].sort((a: Contact, b: Contact) => { // Sort alphabetically by name, falling back to DID if name is missing const nameA = (a.name || a.did).toLowerCase(); const nameB = (b.name || b.did).toLowerCase(); return nameA.localeCompare(nameB); }); - // Apply infinite scroll: show based on displayedCount (minus the 3 recent) + // Apply infinite scroll: show based on displayedCount (minus the recent contacts) const toShow = Math.max(0, this.displayedCount - RECENT_CONTACTS_COUNT); return sorted.slice(0, toShow); } @@ -531,9 +531,8 @@ export default class EntityGrid extends Vue { } // People: check if more alphabetical contacts available - // Total available = 3 recent + all alphabetical - const remaining = (this.entities as Contact[]).slice(RECENT_CONTACTS_COUNT); - const totalAvailable = RECENT_CONTACTS_COUNT + remaining.length; + // Total available = recent + all alphabetical + const totalAvailable = RECENT_CONTACTS_COUNT + this.entities.length; return this.displayedCount < totalAvailable; } -- 2.30.2