forked from trent_larson/crowd-funder-for-time-pwa
feat(meetings): add project selection dialog for meeting setup
Replace Project Link text input with interactive selection dialog using new MeetingProjectDialog component. Dialog displays user's projects with icons and issuer information, following the same pattern as ProjectRepresentativeDialog. - Create MeetingProjectDialog with EntityGrid integration - Add clickable project field with icon, name, and issuer display - Load projects from /api/v2/report/plansByIssuer endpoint - Show issuer name instead of handleId for better UX - Refactor loadProjects to remove unused rowId field
This commit is contained in:
123
src/components/MeetingProjectDialog.vue
Normal file
123
src/components/MeetingProjectDialog.vue
Normal file
@@ -0,0 +1,123 @@
|
||||
<template>
|
||||
<div v-if="visible" class="dialog-overlay">
|
||||
<div class="dialog">
|
||||
<!-- Header -->
|
||||
<h2 class="text-lg font-semibold leading-[1.25] mb-4">Select Project</h2>
|
||||
|
||||
<!-- EntityGrid for projects -->
|
||||
<EntityGrid
|
||||
:entity-type="'projects'"
|
||||
:entities="allProjects"
|
||||
:active-did="activeDid"
|
||||
:all-my-dids="allMyDids"
|
||||
:all-contacts="allContacts"
|
||||
:conflict-checker="() => false"
|
||||
:show-you-entity="false"
|
||||
:show-unnamed-entity="false"
|
||||
:notify="notify"
|
||||
:conflict-context="'project'"
|
||||
@entity-selected="handleEntitySelected"
|
||||
/>
|
||||
|
||||
<!-- Cancel Button -->
|
||||
<div class="flex gap-2 mt-4">
|
||||
<button
|
||||
class="block w-full text-center text-md uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2 py-2 rounded-md"
|
||||
@click="handleCancel"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue, Emit } from "vue-facing-decorator";
|
||||
import EntityGrid from "./EntityGrid.vue";
|
||||
import { Contact } from "../db/tables/contacts";
|
||||
import { PlanData } from "../interfaces/records";
|
||||
import { NotificationIface } from "../constants/app";
|
||||
|
||||
/**
|
||||
* MeetingProjectDialog - Dialog for selecting a project link for a meeting
|
||||
*
|
||||
* Features:
|
||||
* - EntityGrid integration for project selection
|
||||
* - No special entities (You, Unnamed)
|
||||
* - Immediate assignment on project selection
|
||||
* - Cancel button to close without selection
|
||||
*/
|
||||
@Component({
|
||||
components: {
|
||||
EntityGrid,
|
||||
},
|
||||
})
|
||||
export default class MeetingProjectDialog extends Vue {
|
||||
/** Whether the dialog is visible */
|
||||
visible = false;
|
||||
|
||||
/** Array of available projects */
|
||||
@Prop({ required: true })
|
||||
allProjects!: PlanData[];
|
||||
|
||||
/** Active user's DID */
|
||||
@Prop({ required: true })
|
||||
activeDid!: string;
|
||||
|
||||
/** All user's DIDs */
|
||||
@Prop({ required: true })
|
||||
allMyDids!: string[];
|
||||
|
||||
/** All contacts */
|
||||
@Prop({ required: true })
|
||||
allContacts!: Contact[];
|
||||
|
||||
/** Notification function from parent component */
|
||||
@Prop()
|
||||
notify?: (notification: NotificationIface, timeout?: number) => void;
|
||||
|
||||
/**
|
||||
* Handle entity selection from EntityGrid
|
||||
* Immediately assigns the selected project and closes the dialog
|
||||
*/
|
||||
handleEntitySelected(event: {
|
||||
type: "person" | "project";
|
||||
data: Contact | PlanData;
|
||||
}) {
|
||||
const project = event.data as PlanData;
|
||||
this.emitAssign(project);
|
||||
this.close();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle cancel button click
|
||||
*/
|
||||
handleCancel(): void {
|
||||
this.close();
|
||||
}
|
||||
|
||||
/**
|
||||
* Open the dialog
|
||||
*/
|
||||
open(): void {
|
||||
this.visible = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Close the dialog
|
||||
*/
|
||||
close(): void {
|
||||
this.visible = false;
|
||||
}
|
||||
|
||||
// Emit methods using @Emit decorator
|
||||
|
||||
@Emit("assign")
|
||||
emitAssign(project: PlanData): PlanData {
|
||||
return project;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
@@ -8,7 +8,7 @@ issuer information. * * @author Matthew Raymer */
|
||||
>
|
||||
<ProjectIcon
|
||||
:entity-id="project.handleId"
|
||||
:icon-size="48"
|
||||
:icon-size="30"
|
||||
:image-url="project.image"
|
||||
class="!size-[2rem] shrink-0 border border-slate-300 bg-white overflow-hidden rounded-full"
|
||||
/>
|
||||
|
||||
@@ -186,16 +186,59 @@
|
||||
<div>
|
||||
<label
|
||||
for="projectLink"
|
||||
class="block text-sm font-medium text-gray-700"
|
||||
class="block text-sm font-medium text-gray-700 mb-1"
|
||||
>Project Link</label
|
||||
>
|
||||
<input
|
||||
id="projectLink"
|
||||
v-model="newOrUpdatedMeetingInputs.projectLink"
|
||||
type="text"
|
||||
class="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-blue-500 focus:outline-none"
|
||||
placeholder="Project ID"
|
||||
/>
|
||||
<div class="w-full flex items-stretch">
|
||||
<div
|
||||
class="flex items-center gap-2 grow border border-slate-400 border-r-0 last:border-r px-3 py-2 rounded-l last:rounded overflow-hidden cursor-pointer hover:bg-slate-100"
|
||||
@click="openProjectLinkDialog"
|
||||
>
|
||||
<div>
|
||||
<ProjectIcon
|
||||
v-if="selectedProject"
|
||||
:entity-id="selectedProject.handleId"
|
||||
:icon-size="30"
|
||||
:image-url="selectedProject.image"
|
||||
class="!size-[2rem] shrink-0 border border-slate-300 bg-white overflow-hidden rounded-full"
|
||||
/>
|
||||
<font-awesome
|
||||
v-else
|
||||
icon="folder-open"
|
||||
class="text-slate-400"
|
||||
/>
|
||||
</div>
|
||||
<div class="overflow-hidden">
|
||||
<div
|
||||
:class="{
|
||||
'text-sm font-semibold': selectedProject,
|
||||
'text-slate-400': !selectedProject,
|
||||
}"
|
||||
class="truncate"
|
||||
>
|
||||
{{
|
||||
selectedProject
|
||||
? selectedProject.name || "Unnamed Project"
|
||||
: "Select Project…"
|
||||
}}
|
||||
</div>
|
||||
<div
|
||||
v-if="selectedProject"
|
||||
class="text-xs text-slate-500 truncate"
|
||||
>
|
||||
<font-awesome icon="user" class="text-slate-400" />
|
||||
{{ selectedProjectIssuerName }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
v-if="selectedProject"
|
||||
class="text-rose-600 px-3 py-2 border border-slate-400 border-l-0 rounded-r hover:bg-rose-600 hover:text-white hover:border-rose-600"
|
||||
@click="unsetProjectLink"
|
||||
>
|
||||
<font-awesome icon="trash-can" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
@@ -224,6 +267,16 @@
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<MeetingProjectDialog
|
||||
ref="meetingProjectDialog"
|
||||
:all-projects="allProjects"
|
||||
:active-did="activeDid"
|
||||
:all-my-dids="allMyDids"
|
||||
:all-contacts="allContacts"
|
||||
:notify="$notify"
|
||||
@assign="handleProjectLinkAssigned"
|
||||
/>
|
||||
|
||||
<!-- Members Section -->
|
||||
<div
|
||||
v-if="!isLoading && currentMeeting != null && !!currentMeeting.password"
|
||||
@@ -292,10 +345,13 @@ import { RouteLocationNormalizedLoaded, Router } from "vue-router";
|
||||
import QuickNav from "../components/QuickNav.vue";
|
||||
import TopMessage from "../components/TopMessage.vue";
|
||||
import MembersList from "../components/MembersList.vue";
|
||||
import MeetingProjectDialog from "../components/MeetingProjectDialog.vue";
|
||||
import ProjectIcon from "../components/ProjectIcon.vue";
|
||||
import {
|
||||
errorStringForLog,
|
||||
getHeaders,
|
||||
serverMessageForUser,
|
||||
didInfo,
|
||||
} from "../libs/endorserServer";
|
||||
import { encryptMessage } from "../libs/crypto";
|
||||
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
|
||||
@@ -309,6 +365,8 @@ import {
|
||||
NOTIFY_MEETING_DELETED,
|
||||
NOTIFY_MEETING_LINK_COPIED,
|
||||
} from "@/constants/notifications";
|
||||
import { PlanData } from "../interfaces/records";
|
||||
import { Contact } from "../db/tables/contacts";
|
||||
interface ServerMeeting {
|
||||
groupId: number; // from the server
|
||||
name: string; // to & from the server
|
||||
@@ -331,6 +389,8 @@ interface MeetingSetupInputs {
|
||||
QuickNav,
|
||||
TopMessage,
|
||||
MembersList,
|
||||
MeetingProjectDialog,
|
||||
ProjectIcon,
|
||||
},
|
||||
mixins: [PlatformServiceMixin],
|
||||
})
|
||||
@@ -354,6 +414,9 @@ export default class OnboardMeetingView extends Vue {
|
||||
isRegistered = false;
|
||||
showDeleteConfirm = false;
|
||||
fullName = "";
|
||||
allProjects: PlanData[] = [];
|
||||
allContacts: Contact[] = [];
|
||||
allMyDids: string[] = [];
|
||||
get minDateTime() {
|
||||
const now = new Date();
|
||||
now.setMinutes(now.getMinutes() + 5); // Set minimum 5 minutes in the future
|
||||
@@ -370,6 +433,15 @@ export default class OnboardMeetingView extends Vue {
|
||||
this.fullName = settings?.firstName || "";
|
||||
this.isRegistered = !!settings?.isRegistered;
|
||||
|
||||
// Load contacts and DIDs for dialog
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
this.allContacts = await (this as any).$contactsByDateAdded();
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
this.allMyDids = await (this as any).$getAllAccountDids();
|
||||
|
||||
// Load projects
|
||||
await this.loadProjects();
|
||||
|
||||
await this.fetchCurrentMeeting();
|
||||
this.isLoading = false;
|
||||
}
|
||||
@@ -710,5 +782,97 @@ export default class OnboardMeetingView extends Vue {
|
||||
this.notify.error("Failed to copy meeting link to clipboard.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load projects from the API
|
||||
*/
|
||||
async loadProjects() {
|
||||
try {
|
||||
const headers = await getHeaders(this.activeDid);
|
||||
const url = `${this.apiServer}/api/v2/report/plansByIssuer`;
|
||||
const resp = await this.axios.get(url, { headers });
|
||||
|
||||
if (resp.status === 200 && resp.data.data) {
|
||||
this.allProjects = resp.data.data.map(
|
||||
(plan: {
|
||||
name: string;
|
||||
description: string;
|
||||
image?: string;
|
||||
handleId: string;
|
||||
issuerDid: string;
|
||||
}) => ({
|
||||
name: plan.name,
|
||||
description: plan.description,
|
||||
image: plan.image,
|
||||
handleId: plan.handleId,
|
||||
issuerDid: plan.issuerDid,
|
||||
}),
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
this.$logAndConsole(
|
||||
"Error loading projects: " + errorStringForLog(error),
|
||||
true,
|
||||
);
|
||||
// Don't show error to user - just leave projects empty
|
||||
this.allProjects = [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Computed property for selected project
|
||||
* Derives the project from projectLink by finding it in allProjects
|
||||
*/
|
||||
get selectedProject(): PlanData | null {
|
||||
if (!this.newOrUpdatedMeetingInputs?.projectLink) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
this.allProjects.find(
|
||||
(p) => p.handleId === this.newOrUpdatedMeetingInputs?.projectLink,
|
||||
) || null
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
if (this.newOrUpdatedMeetingInputs) {
|
||||
this.newOrUpdatedMeetingInputs.projectLink = project.handleId;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unset the project link and revert to initial state
|
||||
*/
|
||||
unsetProjectLink(): void {
|
||||
if (this.newOrUpdatedMeetingInputs) {
|
||||
this.newOrUpdatedMeetingInputs.projectLink = "";
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user