diff --git a/src/components/EntityGrid.vue b/src/components/EntityGrid.vue index b85e6d09..1a964d8c 100644 --- a/src/components/EntityGrid.vue +++ b/src/components/EntityGrid.vue @@ -76,7 +76,7 @@ projects, and special entities with selection. * * @author Matthew Raymer */ -
  • +
  • {{ emptyStateMessage }}
  • @@ -164,6 +164,10 @@ import { Contact } from "../db/tables/contacts"; import { PlanData } from "../interfaces/records"; import { NotificationIface } from "../constants/app"; import { UNNAMED_ENTITY_NAME } from "@/constants/entities"; +import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin"; +import { getHeaders } from "../libs/endorserServer"; +import { logger } from "../utils/logger"; +import { TIMEOUTS } from "@/utils/notify"; /** * Constants for infinite scroll configuration @@ -191,6 +195,7 @@ const RECENT_CONTACTS_COUNT = 3; ProjectCard, SpecialEntityCard, }, + mixins: [PlatformServiceMixin], }) export default class EntityGrid extends Vue { /** Type of entities to display */ @@ -202,6 +207,16 @@ export default class EntityGrid extends Vue { isSearching = false; searchTimeout: NodeJS.Timeout | null = null; filteredEntities: Contact[] | PlanData[] = []; + searchBeforeId: string | undefined = undefined; + isLoadingSearchMore = false; + + // API server for project searches + apiServer = ""; + + // Internal project state (when entities prop not provided for projects) + allProjects: PlanData[] = []; + loadBeforeId: string | undefined = undefined; + isLoadingProjects = false; // Infinite scroll state displayedCount = INITIAL_BATCH_SIZE; @@ -211,17 +226,17 @@ export default class EntityGrid extends Vue { /** * Array of entities to display * + * For contacts (entityType === 'people'): REQUIRED - Must be a COMPLETE list from local database. + * Use $contactsByDateAdded() to ensure all contacts are included. + * Client-side filtering assumes the complete list is available. * 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". + * For projects (entityType === 'projects'): OPTIONAL - If not provided, EntityGrid loads + * projects internally from the API server. If provided, uses the provided list. */ - @Prop({ required: true }) - entities!: Contact[] | PlanData[]; + @Prop({ required: false }) + entities?: Contact[] | PlanData[]; /** Active user's DID */ @Prop({ required: true }) @@ -293,6 +308,33 @@ export default class EntityGrid extends Vue { return "text-xs text-slate-500 italic col-span-full"; } + /** + * Check if there are no entities to display + */ + get hasNoEntities(): boolean { + if (this.entityType === "projects") { + // For projects: check internal state if no entities prop, otherwise check prop + const projectsToCheck = this.entities || this.allProjects; + return projectsToCheck.length === 0; + } else { + // For people: entities prop is required + return !this.entities || this.entities.length === 0; + } + } + + /** + * Get the entities array to use (prop or internal state) + */ + get entitiesToUse(): Contact[] | PlanData[] { + if (this.entityType === "projects") { + // For projects: use prop if provided, otherwise use internal state + return this.entities || this.allProjects; + } else { + // For people: entities prop is required + return this.entities || []; + } + } + /** * Computed entities to display - uses function prop if provided, otherwise uses infinite scroll * When searching, returns filtered results with infinite scroll applied @@ -305,12 +347,12 @@ export default class EntityGrid extends Vue { // If custom function provided, use it (disables infinite scroll) if (this.displayEntitiesFunction) { - return this.displayEntitiesFunction(this.entities, this.entityType); + return this.displayEntitiesFunction(this.entitiesToUse, this.entityType); } // Default: projects use infinite scroll if (this.entityType === "projects") { - return (this.entities as PlanData[]).slice(0, this.displayedCount); + return (this.entitiesToUse as PlanData[]).slice(0, this.displayedCount); } // People: handled by recentContacts + alphabeticalContacts (both use displayedCount) @@ -324,7 +366,11 @@ export default class EntityGrid extends Vue { * See the entities prop documentation for details on using $contactsByDateAdded(). */ get recentContacts(): Contact[] { - if (this.entityType !== "people" || this.searchTerm.trim()) { + if ( + this.entityType !== "people" || + this.searchTerm.trim() || + !this.entities + ) { return []; } // Entities are already sorted by date added (newest first) @@ -336,7 +382,11 @@ export default class EntityGrid extends Vue { * Uses infinite scroll to control how many are displayed */ get alphabeticalContacts(): Contact[] { - if (this.entityType !== "people" || this.searchTerm.trim()) { + if ( + this.entityType !== "people" || + this.searchTerm.trim() || + !this.entities + ) { return []; } // Skip the first few (recent contacts) and sort the rest alphabetically @@ -457,47 +507,28 @@ export default class EntityGrid extends Vue { /** * Perform the actual search + * Routes to server-side search for projects or client-side filtering for contacts */ async performSearch(): Promise { if (!this.searchTerm.trim()) { this.filteredEntities = []; this.displayedCount = INITIAL_BATCH_SIZE; + this.searchBeforeId = undefined; this.infiniteScrollReset?.(); return; } this.isSearching = true; + this.searchBeforeId = undefined; // Reset pagination for new search try { - // Simulate async search (in case we need to add API calls later) - await new Promise((resolve) => setTimeout(resolve, 100)); - - const searchLower = this.searchTerm.toLowerCase().trim(); - - if (this.entityType === "people") { - this.filteredEntities = (this.entities as Contact[]) - .filter((contact: Contact) => { - const name = contact.name?.toLowerCase() || ""; - const did = contact.did.toLowerCase(); - return name.includes(searchLower) || did.includes(searchLower); - }) - .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); - }); + if (this.entityType === "projects") { + // Server-side search for projects (initial load, no beforeId) + const searchLower = this.searchTerm.toLowerCase().trim(); + await this.fetchProjects(undefined, searchLower); } else { - this.filteredEntities = (this.entities as PlanData[]) - .filter((project: PlanData) => { - const name = project.name?.toLowerCase() || ""; - const handleId = project.handleId.toLowerCase(); - return name.includes(searchLower) || handleId.includes(searchLower); - }) - .sort((a: PlanData, b: PlanData) => { - // Sort alphabetically by name - return a.name.toLowerCase().localeCompare(b.name.toLowerCase()); - }); + // Client-side filtering for contacts (complete list) + await this.performContactSearch(); } // Reset displayed count when search completes @@ -508,6 +539,194 @@ export default class EntityGrid extends Vue { } } + /** + * Fetch projects from API server + * Unified method for both loading all projects and searching projects. + * If claimContents is provided, performs search and updates filteredEntities. + * If claimContents is not provided, loads all projects and updates allProjects. + * + * @param beforeId - Optional rowId for pagination (loads projects before this ID) + * @param claimContents - Optional search term (if provided, performs search; if not, loads all) + */ + async fetchProjects( + beforeId?: string, + claimContents?: string, + ): Promise { + if (!this.apiServer) { + if (claimContents) { + this.filteredEntities = []; + } else { + this.allProjects = []; + } + if (this.notify) { + this.notify( + { + group: "alert", + type: "danger", + title: "Error", + text: "API server not configured", + }, + TIMEOUTS.SHORT, + ); + } + return; + } + + const isSearch = !!claimContents; + let url = `${this.apiServer}/api/v2/report/plans`; + + // Build query parameters + const params: string[] = []; + if (claimContents) { + params.push( + `claimContents=${encodeURIComponent(claimContents.toLowerCase().trim())}`, + ); + } + if (beforeId) { + params.push(`beforeId=${encodeURIComponent(beforeId)}`); + } + if (params.length > 0) { + url += `?${params.join("&")}`; + } + + try { + const response = await fetch(url, { + method: "GET", + headers: await getHeaders(this.activeDid), + }); + + if (response.status !== 200) { + throw new Error( + isSearch ? "Failed to search projects" : "Failed to load projects", + ); + } + + const results = await response.json(); + if (results.data) { + const newProjects = results.data.map( + (plan: PlanData & { rowId?: string }) => ({ + ...plan, + rowId: plan.rowId, + }), + ); + + if (isSearch) { + // Search mode: update filteredEntities + if (beforeId) { + // Pagination: append new projects to existing search results + this.filteredEntities.push(...newProjects); + } else { + // Initial search: replace array + this.filteredEntities = newProjects; + } + + // Update searchBeforeId for next pagination + if (newProjects.length > 0) { + const lastProject = newProjects[newProjects.length - 1]; + this.searchBeforeId = lastProject.rowId || undefined; + } else { + this.searchBeforeId = undefined; // No more results + } + } else { + // Load mode: update allProjects + if (beforeId) { + // Pagination: append new projects + this.allProjects.push(...newProjects); + } else { + // Initial load: replace array + this.allProjects = newProjects; + } + + // Update loadBeforeId for next pagination + if (newProjects.length > 0) { + const lastProject = newProjects[newProjects.length - 1]; + this.loadBeforeId = lastProject.rowId || undefined; + } else { + this.loadBeforeId = undefined; // No more results + } + } + } else { + // No data in response + if (isSearch) { + if (!beforeId) { + // Only clear on initial search, not pagination + this.filteredEntities = []; + } + this.searchBeforeId = undefined; + } else { + if (!beforeId) { + // Only clear on initial load, not pagination + this.allProjects = []; + } + this.loadBeforeId = undefined; + } + } + } catch (error) { + logger.error( + `Error ${isSearch ? "searching" : "loading"} projects:`, + error, + ); + if (isSearch) { + if (!beforeId) { + // Only clear on initial search error, not pagination error + this.filteredEntities = []; + } + this.searchBeforeId = undefined; + } else { + if (!beforeId) { + // Only clear on initial load error, not pagination error + this.allProjects = []; + } + this.loadBeforeId = undefined; + } + if (this.notify) { + this.notify( + { + group: "alert", + type: "danger", + title: "Error", + text: isSearch + ? "Failed to search projects. Please try again." + : "Failed to load projects. Please try again.", + }, + TIMEOUTS.STANDARD, + ); + } + } + } + + /** + * Client-side contact search + * Assumes entities prop contains complete contact list from local database + */ + async performContactSearch(): Promise { + if (!this.entities) { + this.filteredEntities = []; + return; + } + + // Simulate async (for consistency with project search) + await new Promise((resolve) => setTimeout(resolve, 100)); + + const searchLower = this.searchTerm.toLowerCase().trim(); + + this.filteredEntities = (this.entities as Contact[]) + .filter((contact: Contact) => { + const name = contact.name?.toLowerCase() || ""; + const did = contact.did.toLowerCase(); + return name.includes(searchLower) || did.includes(searchLower); + }) + .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); + }); + + // Contacts don't need pagination (complete list) + this.searchBeforeId = undefined; + } + /** * Clear the search */ @@ -516,6 +735,7 @@ export default class EntityGrid extends Vue { this.filteredEntities = []; this.isSearching = false; this.displayedCount = INITIAL_BATCH_SIZE; + this.searchBeforeId = undefined; this.infiniteScrollReset?.(); // Clear any pending timeout @@ -535,17 +755,48 @@ export default class EntityGrid extends Vue { } if (this.searchTerm.trim()) { - // Search mode: check filtered entities - return this.displayedCount < this.filteredEntities.length; + // Search mode: check if more results available + if (this.entityType === "projects") { + // Projects: can load more if: + // 1. We have more already-loaded results to show, OR + // 2. We've shown all loaded results AND there's a searchBeforeId to load more + const hasMoreLoaded = + this.displayedCount < this.filteredEntities.length; + const canLoadMoreFromServer = + this.displayedCount >= this.filteredEntities.length && + !!this.searchBeforeId && + !this.isLoadingSearchMore; + return hasMoreLoaded || canLoadMoreFromServer; + } else { + // Contacts: client-side filtering returns all results at once + return this.displayedCount < this.filteredEntities.length; + } } + // Non-search mode if (this.entityType === "projects") { - // Projects: check if more available - return this.displayedCount < this.entities.length; + // Projects: check internal state or prop + const projectsToCheck = this.entities || this.allProjects; + const beforeId = this.entities ? undefined : this.loadBeforeId; + + // Can load more if: + // 1. We have more already-loaded results to show, OR + // 2. We've shown all loaded results AND there's a beforeId to load more (and not using entities prop) + const hasMoreLoaded = this.displayedCount < projectsToCheck.length; + const canLoadMoreFromServer = + !this.entities && + this.displayedCount >= projectsToCheck.length && + !!beforeId && + !this.isLoadingProjects; + + return hasMoreLoaded || canLoadMoreFromServer; } // People: check if more alphabetical contacts available // Total available = recent + all alphabetical + if (!this.entities) { + return false; + } const totalAvailable = RECENT_CONTACTS_COUNT + this.entities.length; return this.displayedCount < totalAvailable; } @@ -553,16 +804,112 @@ export default class EntityGrid extends Vue { /** * Initialize infinite scroll on mount */ - mounted(): void { + async mounted(): Promise { + // Load apiServer for project searches/loads + if (this.entityType === "projects") { + const settings = await this.$accountSettings(); + this.apiServer = settings.apiServer || ""; + + // Load projects on mount if entities prop not provided + if (!this.entities && this.apiServer) { + this.isLoadingProjects = true; + try { + await this.fetchProjects(); + } catch (error) { + logger.error("Error loading projects on mount:", error); + } finally { + this.isLoadingProjects = false; + } + } + } + + // Validate entities prop for people + if (this.entityType === "people" && !this.entities) { + logger.error( + "EntityGrid: entities prop is required when entityType is 'people'", + ); + if (this.notify) { + this.notify( + { + group: "alert", + type: "danger", + title: "Error", + text: "Contacts data is required but not provided.", + }, + TIMEOUTS.SHORT, + ); + } + } + this.$nextTick(() => { const container = this.$refs.scrollContainer as HTMLElement; if (container) { const { reset } = useInfiniteScroll( container, - () => { - // Load more: increment displayedCount - this.displayedCount += INCREMENT_SIZE; + async () => { + // Search mode: handle search pagination + if (this.searchTerm.trim()) { + if (this.entityType === "projects") { + // Projects: load more search results if available + if ( + this.displayedCount >= this.filteredEntities.length && + this.searchBeforeId && + !this.isLoadingSearchMore + ) { + this.isLoadingSearchMore = true; + try { + const searchLower = this.searchTerm.toLowerCase().trim(); + await this.fetchProjects(this.searchBeforeId, searchLower); + // After loading more, reset scroll state to allow further loading + this.infiniteScrollReset?.(); + } catch (error) { + logger.error("Error loading more search results:", error); + // Error already handled in fetchProjects + } finally { + this.isLoadingSearchMore = false; + } + } else { + // Show more from already-loaded search results + this.displayedCount += INCREMENT_SIZE; + } + } else { + // Contacts: show more from already-filtered results + this.displayedCount += INCREMENT_SIZE; + } + } else { + // Non-search mode + if (this.entityType === "projects") { + const projectsToCheck = this.entities || this.allProjects; + const beforeId = this.entities ? undefined : this.loadBeforeId; + + // If using internal state and need to load more from server + if ( + !this.entities && + this.displayedCount >= projectsToCheck.length && + beforeId && + !this.isLoadingProjects + ) { + this.isLoadingProjects = true; + try { + await this.fetchProjects(beforeId); + // After loading more, reset scroll state to allow further loading + this.infiniteScrollReset?.(); + } catch (error) { + logger.error("Error loading more projects:", error); + // Error already handled in fetchProjects + } finally { + this.isLoadingProjects = false; + } + } else { + // Normal case: increment displayedCount to show more from memory + this.displayedCount += INCREMENT_SIZE; + } + } else { + // People: increment displayedCount to show more from memory + this.displayedCount += INCREMENT_SIZE; + } + } }, { distance: 50, // pixels from bottom @@ -588,21 +935,35 @@ export default class EntityGrid extends Vue { } /** - * Watch for changes in search term to reset displayed count + * Watch for changes in search term to reset displayed count and pagination */ @Watch("searchTerm") onSearchTermChange(): void { + // Reset displayed count and pagination when search term changes this.displayedCount = INITIAL_BATCH_SIZE; + this.searchBeforeId = undefined; this.infiniteScrollReset?.(); } /** - * Watch for changes in entities prop to reset displayed count + * Watch for changes in entities prop to clear search and reset displayed count */ @Watch("entities") onEntitiesChange(): void { + // Clear search when entities change (fresh dialog open) + if (this.searchTerm) { + this.searchTerm = ""; + this.filteredEntities = []; + this.searchBeforeId = undefined; + } this.displayedCount = INITIAL_BATCH_SIZE; this.infiniteScrollReset?.(); + + // For projects: if entities prop is provided, clear internal state + if (this.entityType === "projects" && this.entities) { + this.allProjects = []; + this.loadBeforeId = undefined; + } } /** diff --git a/src/components/EntitySelectionStep.vue b/src/components/EntitySelectionStep.vue index fd62ccdd..3fba2141 100644 --- a/src/components/EntitySelectionStep.vue +++ b/src/components/EntitySelectionStep.vue @@ -14,7 +14,7 @@ properties * * @author Matthew Raymer */ +
    +
    + +

    Select Project

    + + + + + +
    + +
    +
    +
    + + + + + diff --git a/src/components/ProjectCard.vue b/src/components/ProjectCard.vue index 77d2aa34..4b995a21 100644 --- a/src/components/ProjectCard.vue +++ b/src/components/ProjectCard.vue @@ -8,7 +8,7 @@ issuer information. * * @author Matthew Raymer */ > diff --git a/src/interfaces/records.ts b/src/interfaces/records.ts index 03627904..ff2a0dec 100644 --- a/src/interfaces/records.ts +++ b/src/interfaces/records.ts @@ -57,7 +57,12 @@ export interface OfferToPlanSummaryRecord extends OfferSummaryRecord { planName: string; } -// a summary record; the VC is not currently part of this record +/** + * A summary record + * The VC is not currently part of this record. + * + * If you change this, you may want to update NewActivityView.vue to handle differences correctly. + */ export interface PlanSummaryRecord { agentDid?: string; description: string; @@ -76,7 +81,9 @@ export interface PlanSummaryRecord { export interface PlanSummaryAndPreviousClaim { plan: PlanSummaryRecord; - wrappedClaimBefore: GenericCredWrapper; + // This can be undefined, eg. if a project is starred after the stored last-seen-change-jwt ID. + // The endorser-ch test code shows some cases. + wrappedClaimBefore?: GenericCredWrapper; } /** diff --git a/src/views/HomeView.vue b/src/views/HomeView.vue index 718a731f..3d343e6d 100644 --- a/src/views/HomeView.vue +++ b/src/views/HomeView.vue @@ -898,7 +898,13 @@ export default class HomeView extends Vue { this.starredPlanHandleIds, this.lastAckedStarredPlanChangesJwtId, ); - this.numNewStarredProjectChanges = starredProjectChanges.data.length; + // filter out any data elements where there is no wrappedClaimBefore + const filteredNewStarredProjectChanges = + starredProjectChanges.data.filter( + (change) => change.wrappedClaimBefore !== undefined, + ); + this.numNewStarredProjectChanges = + filteredNewStarredProjectChanges.length; this.newStarredProjectChangesHitLimit = starredProjectChanges.hitLimit; } catch (error) { // Don't show errors for starred project changes as it's a secondary feature diff --git a/src/views/NewActivityView.vue b/src/views/NewActivityView.vue index 6dff8909..cbaa5cfd 100644 --- a/src/views/NewActivityView.vue +++ b/src/views/NewActivityView.vue @@ -284,7 +284,10 @@ -
    The changes did not affect essential project data.
    +
    + The changes are not important, like it was saved by accident or + you've seen it all before. +
    + +
    { + const projectLink = + this.currentMeeting?.projectLink || + this.newOrUpdatedMeetingInputs?.projectLink; + + if (!projectLink) { + this.selectedProjectData = null; + return; + } + + await this.fetchProjectByHandleId(projectLink); + } + + /** + * Fetch a single project by handleId + * @param handleId - The project handleId to fetch + */ + async fetchProjectByHandleId(handleId: string): Promise { + try { + const headers = await getHeaders(this.activeDid); + const url = `${this.apiServer}/api/v2/report/plans?handleId=${encodeURIComponent(handleId)}`; + const resp = await this.axios.get(url, { headers }); + + if (resp.status === 200 && resp.data.data && resp.data.data.length > 0) { + const project = resp.data.data[0]; + this.selectedProjectData = { + name: project.name, + description: project.description, + image: project.image, + handleId: project.handleId, + issuerDid: project.issuerDid, + rowId: project.rowId, + }; + } else { + this.selectedProjectData = null; + } + } catch (error) { + this.$logAndConsole( + "Error fetching project by handleId: " + errorStringForLog(error), + true, + ); + this.selectedProjectData = null; + } + } + async createMeeting() { this.isLoading = true; @@ -576,7 +699,7 @@ export default class OnboardMeetingView extends Vue { } } - startEditing() { + async startEditing() { // Populate form with existing meeting data if (this.currentMeeting) { const localExpiresAt = new Date(this.currentMeeting.expiresAt); @@ -587,6 +710,10 @@ export default class OnboardMeetingView extends Vue { password: this.currentMeeting.password || "", projectLink: this.currentMeeting.projectLink || "", }; + // Ensure selected project is loaded if projectLink exists + if (this.currentMeeting.projectLink) { + await this.ensureSelectedProjectLoaded(); + } } else { this.$logError( "There is no current meeting to edit. We should never get here.", @@ -594,9 +721,15 @@ export default class OnboardMeetingView extends Vue { } } - cancelEditing() { + async cancelEditing() { // Reset form data this.newOrUpdatedMeetingInputs = null; + // Restore selected project from currentMeeting if it exists + if (this.currentMeeting?.projectLink) { + await this.ensureSelectedProjectLoaded(); + } else { + this.selectedProjectData = null; + } } async updateMeeting() { @@ -710,5 +843,78 @@ export default class OnboardMeetingView extends Vue { this.notify.error("Failed to copy meeting link to clipboard."); } } + + /** + * Computed property for selected project + * Returns the separately stored selected project data + */ + get selectedProject(): PlanData | null { + return this.selectedProjectData; + } + + /** + * Computed property for selected project issuer display name + * Uses didInfo to format the issuer name similar to ProjectCard + */ + get selectedProjectIssuerName(): string { + if (!this.selectedProject) { + return ""; + } + return didInfo( + this.selectedProject.issuerDid, + this.activeDid, + this.allMyDids, + this.allContacts, + ); + } + + /** + * Open the project link selection dialog + */ + openProjectLinkDialog(): void { + (this.$refs.meetingProjectDialog as MeetingProjectDialog).open(); + } + + /** + * Handle project assignment from dialog + */ + handleProjectLinkAssigned(project: PlanData): void { + // Store the selected project directly + this.selectedProjectData = project; + + if (this.newOrUpdatedMeetingInputs) { + this.newOrUpdatedMeetingInputs.projectLink = project.handleId; + } + } + + /** + * Unset the project link and revert to initial state + */ + unsetProjectLink(): void { + this.selectedProjectData = null; + if (this.newOrUpdatedMeetingInputs) { + this.newOrUpdatedMeetingInputs.projectLink = ""; + } + } + + /** + * Handle dialog open event - stop auto-refresh in MembersList + */ + handleDialogOpen(): void { + const membersList = this.$refs.membersList as MembersList; + if (membersList) { + membersList.stopAutoRefresh(); + } + } + + /** + * Handle dialog close event - start auto-refresh in MembersList + */ + handleDialogClose(): void { + const membersList = this.$refs.membersList as MembersList; + if (membersList) { + membersList.startAutoRefresh(); + } + } } diff --git a/src/views/TestView.vue b/src/views/TestView.vue index 7d0b6315..a56b6258 100644 --- a/src/views/TestView.vue +++ b/src/views/TestView.vue @@ -57,6 +57,9 @@ + @@ -525,6 +528,11 @@ export default class Help extends Vue { this.executeSql(); } + setActiveIdentityQuery() { + this.sqlQuery = "SELECT * FROM active_identity;"; + this.executeSql(); + } + setContactsQuery() { this.sqlQuery = "SELECT * FROM contacts;"; this.executeSql();