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; } diff --git a/src/components/ProjectRepresentativeDialog.vue b/src/components/ProjectRepresentativeDialog.vue new file mode 100644 index 00000000..51afcb5f --- /dev/null +++ b/src/components/ProjectRepresentativeDialog.vue @@ -0,0 +1,117 @@ + + + + + diff --git a/src/views/NewEditProjectView.vue b/src/views/NewEditProjectView.vue index d732045f..8f93aa4b 100644 --- a/src/views/NewEditProjectView.vue +++ b/src/views/NewEditProjectView.vue @@ -60,12 +60,60 @@ - +
+
+
+ + +
+
+
+ {{ + selectedRepresentative + ? selectedRepresentative.name || AppString.NO_CONTACT_NAME + : "Assign Authorized Representative…" + }} +
+
+ {{ agentDid }} +
+
+
+ +
+ + +

Beware! @@ -232,9 +280,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 +319,7 @@ import { retrieveAccountCount, retrieveFullyDecryptedAccount, } from "../libs/util"; +import { Contact } from "../db/tables/contacts"; import { EventTemplate, @@ -323,7 +375,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 +394,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 +409,8 @@ export default class NewEditProjectView extends Vue { // Component state properties activeDid = ""; agentDid = ""; + allContacts: Array = []; + allMyDids: string[] = []; apiServer = ""; endDateInput?: string; endTimeInput?: string; @@ -392,16 +457,24 @@ 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; 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); } } } @@ -411,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 { @@ -432,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( @@ -536,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) { @@ -870,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 @@ -885,6 +966,7 @@ export default class NewEditProjectView extends Vue { */ get shouldShowOwnershipWarning(): boolean { return ( + this.isSavedProject() && this.activeDid !== this.projectIssuerDid && this.agentDid !== this.projectIssuerDid ); @@ -961,5 +1043,37 @@ 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; + } + + /** + * Unset the representative and revert to initial state + */ + unsetRepresentative(): void { + this.agentDid = ""; + } }