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/6] 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 0000000000..c15b2dcd32 --- /dev/null +++ b/src/components/ProjectRepresentativeDialog.vue @@ -0,0 +1,128 @@ + + + + + diff --git a/src/views/NewEditProjectView.vue b/src/views/NewEditProjectView.vue index d732045f0a..2936232427 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 = ""; + } } 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/6] 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 c15b2dcd32..51afcb5f9f 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 2936232427..1d006d4894 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 */ From 9ff24f825887ca6874d9f4ed919ffcae6d9b9876 Mon Sep 17 00:00:00 2001 From: Trent Larson Date: Fri, 7 Nov 2025 18:11:11 -0700 Subject: [PATCH 3/6] 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 1d006d4894..8f93aa4b3a 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 ); From b1fa6ac458ee3ca38d67a5fb116fd21c9799c9cf Mon Sep 17 00:00:00 2001 From: Trent Larson Date: Fri, 7 Nov 2025 18:27:05 -0700 Subject: [PATCH 4/6] 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 6c84eb7b47..42d93eeda1 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; } From 2530bc0ec2430872762857f91d7c2a1fe630b223 Mon Sep 17 00:00:00 2001 From: Jose Olarte III Date: Tue, 11 Nov 2025 15:06:07 +0800 Subject: [PATCH 5/6] fix: ensure consistent "Recently Added" contacts in ProjectRepresentativeDialog EntityGrid's recentContacts assumes contacts are sorted by date added (newest first), but ProjectRepresentativeDialog was receiving contacts sorted alphabetically from NewEditProjectView, causing it to show different "Recently Added" contacts than GiftedDialog. - Changed NewEditProjectView to use $contactsByDateAdded() instead of $getAllContacts() - Added documentation comments to EntityGrid.vue to prevent this issue in future reuses --- src/components/EntityGrid.vue | 16 +++++++++++++++- src/views/NewEditProjectView.vue | 4 ++-- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/src/components/EntityGrid.vue b/src/components/EntityGrid.vue index 6c84eb7b47..311aac6ac2 100644 --- a/src/components/EntityGrid.vue +++ b/src/components/EntityGrid.vue @@ -208,7 +208,18 @@ export default class EntityGrid extends Vue { infiniteScrollReset?: () => void; scrollContainer?: HTMLElement; - /** Array of entities to display */ + /** + * Array of entities to display + * + * IMPORTANT: When passing Contact[] arrays, they must be sorted by date added + * (newest first) for the "Recently Added" section to display correctly. + * Use $contactsByDateAdded() instead of $getAllContacts() or $contacts(). + * + * The recentContacts computed property assumes contacts are already sorted + * by date added and simply takes the first 3. If contacts are sorted + * alphabetically or in another order, the wrong contacts will appear in + * "Recently Added". + */ @Prop({ required: true }) entities!: Contact[] | PlanData[]; @@ -308,6 +319,9 @@ export default class EntityGrid extends Vue { /** * Get the 3 most recently added contacts (when showing contacts and not searching) + * + * NOTE: This assumes entities are already sorted by date added (newest first). + * See the entities prop documentation for details on using $contactsByDateAdded(). */ get recentContacts(): Contact[] { if (this.entityType !== "people" || this.searchTerm.trim()) { diff --git a/src/views/NewEditProjectView.vue b/src/views/NewEditProjectView.vue index 1d006d4894..5343aff435 100644 --- a/src/views/NewEditProjectView.vue +++ b/src/views/NewEditProjectView.vue @@ -461,9 +461,9 @@ export default class NewEditProjectView extends Vue { // eslint-disable-next-line @typescript-eslint/no-explicit-any this.allMyDids = await (this as any).$getAllAccountDids(); - // Load contacts + // Load contacts sorted by date added (newest first) for consistent "Recently Added" display // eslint-disable-next-line @typescript-eslint/no-explicit-any - this.allContacts = await (this as any).$getAllContacts(); + this.allContacts = await (this as any).$contactsByDateAdded(); this.apiServer = settings.apiServer || ""; this.showGeneralAdvanced = !!settings.showGeneralAdvanced; From 4e3e293495d4295620ce212c1710cda8c8e8f8b7 Mon Sep 17 00:00:00 2001 From: Jose Olarte III Date: Tue, 11 Nov 2025 15:32:11 +0800 Subject: [PATCH 6/6] refactor(EntityGrid): simplify alphabetical section label Change "Everyone Else" to "Everyone" for clearer, more concise labeling --- src/components/EntityGrid.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/EntityGrid.vue b/src/components/EntityGrid.vue index 9660e1c891..b85e6d09a5 100644 --- a/src/components/EntityGrid.vue +++ b/src/components/EntityGrid.vue @@ -108,7 +108,7 @@ projects, and special entities with selection. * * @author Matthew Raymer */
  • - Everyone Else + Everyone