Compare commits

...

4 Commits

Author SHA1 Message Date
Jose Olarte III
c06ffec466 refactor: combine member processing methods in BulkMembersDialog
Consolidate organizerAdmitAndAddWithVisibility() and
memberAddContactWithVisibility() into a single unified method
processSelectedMembers() that handles both organizer and member
modes based on the isOrganizer prop.

- Remove redundant handleMainAction() wrapper method
- Update template to call processSelectedMembers directly
- Reduce code duplication by ~30% (140 lines → 98 lines)
- Maintain identical functionality for both modes

This simplifies the component structure and makes the processing
logic easier to maintain.
2025-11-04 18:39:45 +08:00
Jose Olarte III
8b199ec76c refactor: remove redundant dialogType prop from BulkMembersDialog
Remove dialogType prop and consolidate to use only isOrganizer prop.

- Remove dialogType prop from BulkMembersDialog component
- Replace all dialogType checks with isOrganizer boolean checks
- Add comments clarifying isOrganizer true/false meanings
- Remove dialog-type prop binding from MembersList component

This simplifies the component API while maintaining the same functionality.
2025-11-04 17:57:38 +08:00
7e861e2fca fix: when organizer adds people, they automatically register them as well 2025-11-03 20:21:34 -07:00
73806e78bc refactor: fix the 'back' links to work consistently, so contact pages can be included in other flows 2025-11-03 19:06:01 -07:00
10 changed files with 160 additions and 144 deletions

View File

@@ -111,7 +111,7 @@
? 'bg-blue-600 text-white cursor-pointer' ? 'bg-blue-600 text-white cursor-pointer'
: 'bg-slate-400 text-slate-200 cursor-not-allowed', : 'bg-slate-400 text-slate-200 cursor-not-allowed',
]" ]"
@click="handleMainAction" @click="processSelectedMembers"
> >
{{ buttonText }} {{ buttonText }}
</button> </button>
@@ -134,8 +134,9 @@ import { Vue, Component, Prop } from "vue-facing-decorator";
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin"; import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
import { SOMEONE_UNNAMED } from "@/constants/entities"; import { SOMEONE_UNNAMED } from "@/constants/entities";
import { MemberData } from "@/interfaces"; import { MemberData } from "@/interfaces";
import { setVisibilityUtil, getHeaders } from "@/libs/endorserServer"; import { setVisibilityUtil, getHeaders, register } from "@/libs/endorserServer";
import { createNotifyHelpers } from "@/utils/notify"; import { createNotifyHelpers } from "@/utils/notify";
import { Contact } from "@/db/tables/contacts";
@Component({ @Component({
mixins: [PlatformServiceMixin], mixins: [PlatformServiceMixin],
@@ -144,7 +145,7 @@ import { createNotifyHelpers } from "@/utils/notify";
export default class BulkMembersDialog extends Vue { export default class BulkMembersDialog extends Vue {
@Prop({ default: "" }) activeDid!: string; @Prop({ default: "" }) activeDid!: string;
@Prop({ default: "" }) apiServer!: string; @Prop({ default: "" }) apiServer!: string;
@Prop({ required: true }) dialogType!: "admit" | "visibility"; // isOrganizer: true = organizer mode (admit members), false = member mode (set visibility)
@Prop({ required: true }) isOrganizer!: boolean; @Prop({ required: true }) isOrganizer!: boolean;
// Vue notification system // Vue notification system
@@ -251,35 +252,35 @@ export default class BulkMembersDialog extends Vue {
return this.selectedMembers.includes(memberDid); return this.selectedMembers.includes(memberDid);
} }
async handleMainAction() { async processSelectedMembers() {
if (this.dialogType === "admit") {
await this.admitWithVisibility();
} else {
await this.addContactWithVisibility();
}
}
async admitWithVisibility() {
try { try {
const selectedMembers = this.membersData.filter((member) => const selectedMembers: MemberData[] = this.membersData.filter((member) =>
this.selectedMembers.includes(member.did), this.selectedMembers.includes(member.did),
); );
const notSelectedMembers = this.membersData.filter( const notSelectedMembers: MemberData[] = this.membersData.filter(
(member) => !this.selectedMembers.includes(member.did), (member) => !this.selectedMembers.includes(member.did),
); );
let admittedCount = 0; let admittedCount = 0;
let contactAddedCount = 0; let contactAddedCount = 0;
let errors = 0;
for (const member of selectedMembers) { for (const member of selectedMembers) {
try { try {
// First, admit the member // Organizer mode: admit and register the member first
await this.admitMember(member); if (this.isOrganizer) {
admittedCount++; await this.admitMember(member);
await this.registerMember(member);
admittedCount++;
}
// If they're not a contact yet, add them as a contact // If they're not a contact yet, add them as a contact
if (!member.isContact) { if (!member.isContact) {
await this.addAsContact(member); // Organizer mode: set isRegistered to true, member mode: undefined
await this.addAsContact(
member,
this.isOrganizer ? true : undefined,
);
contactAddedCount++; contactAddedCount++;
} }
@@ -289,85 +290,62 @@ export default class BulkMembersDialog extends Vue {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.error(`Error processing member ${member.did}:`, error); console.error(`Error processing member ${member.did}:`, error);
// Continue with other members even if one fails // Continue with other members even if one fails
errors++;
} }
} }
// Show success notification // Show success notification
this.$notify( if (this.isOrganizer) {
{ if (admittedCount > 0) {
group: "alert", this.$notify(
type: "success", {
title: "Members Admitted Successfully", group: "alert",
text: `${admittedCount} member${admittedCount === 1 ? "" : "s"} admitted${contactAddedCount === 0 ? "" : admittedCount === contactAddedCount ? " and" : `, ${contactAddedCount}`}${contactAddedCount === 0 ? "" : ` added as contact${contactAddedCount === 1 ? "" : "s"}`}.`, type: "success",
}, title: "Members Admitted Successfully",
10000, text: `${admittedCount} member${admittedCount === 1 ? "" : "s"} admitted and registered${contactAddedCount === 0 ? "" : admittedCount === contactAddedCount ? " and" : `, ${contactAddedCount}`}${contactAddedCount === 0 ? "" : ` added as contact${contactAddedCount === 1 ? "" : "s"}`}.`,
); },
5000,
);
}
if (errors > 0) {
this.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text: "Failed to fully admit some members. Work with them individually below.",
},
5000,
);
}
} else {
// Member mode: show contacts added notification
if (contactAddedCount > 0) {
this.$notify(
{
group: "alert",
type: "success",
title: "Contacts Added Successfully",
text: `${contactAddedCount} member${contactAddedCount === 1 ? "" : "s"} added as contact${contactAddedCount === 1 ? "" : "s"}.`,
},
5000,
);
}
}
this.close(notSelectedMembers.map((member) => member.did)); this.close(notSelectedMembers.map((member) => member.did));
} catch (error) { } catch (error) {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.error("Error admitting members:", error); console.error(
`Error ${this.isOrganizer ? "admitting members" : "adding contacts"}:`,
error,
);
this.$notify( this.$notify(
{ {
group: "alert", group: "alert",
type: "danger", type: "danger",
title: "Error", title: "Error",
text: "Failed to admit some members. Please try again.", text: "Some errors occurred. Work with members individually below.",
},
5000,
);
}
}
async addContactWithVisibility() {
try {
const selectedMembers = this.membersData.filter((member) =>
this.selectedMembers.includes(member.did),
);
const notSelectedMembers = this.membersData.filter(
(member) => !this.selectedMembers.includes(member.did),
);
let contactsAddedCount = 0;
for (const member of selectedMembers) {
try {
// If they're not a contact yet, add them as a contact first
if (!member.isContact) {
await this.addAsContact(member);
contactsAddedCount++;
}
// Set their seesMe to true
await this.updateContactVisibility(member.did, true);
} catch (error) {
// eslint-disable-next-line no-console
console.error(`Error processing member ${member.did}:`, error);
// Continue with other members even if one fails
}
}
// Show success notification
this.$notify(
{
group: "alert",
type: "success",
title: "Contacts Added Successfully",
text: `${contactsAddedCount} member${contactsAddedCount === 1 ? "" : "s"} added as contact${contactsAddedCount === 1 ? "" : "s"}.`,
},
5000,
);
this.close(notSelectedMembers.map((member) => member.did));
} catch (error) {
// eslint-disable-next-line no-console
console.error("Error adding contacts:", error);
this.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text: "Failed to add some members as contacts. Please try again.",
}, },
5000, 5000,
); );
@@ -393,11 +371,39 @@ export default class BulkMembersDialog extends Vue {
} }
} }
async addAsContact(member: { did: string; name: string }) { async registerMember(member: MemberData) {
try { try {
const newContact = { const contact: Contact = { did: member.did };
const result = await register(
this.activeDid,
this.apiServer,
this.axios,
contact,
);
if (result.success) {
if (result.embeddedRecordError) {
throw new Error(result.embeddedRecordError);
}
await this.$updateContact(member.did, { registered: true });
} else {
throw result;
}
} catch (err) {
// eslint-disable-next-line no-console
console.error("Error registering member:", err);
throw err;
}
}
async addAsContact(
member: { did: string; name: string },
isRegistered?: boolean,
) {
try {
const newContact: Contact = {
did: member.did, did: member.did,
name: member.name, name: member.name,
registered: isRegistered,
}; };
await this.$insertContact(newContact); await this.$insertContact(newContact);
@@ -440,10 +446,10 @@ export default class BulkMembersDialog extends Vue {
} }
showContactInfo() { showContactInfo() {
const message = // isOrganizer: true = admit mode, false = visibility mode
this.dialogType === "admit" const message = this.isOrganizer
? "This user is already your contact, but they are not yet admitted to the meeting." ? "This user is already your contact, but they are not yet admitted to the meeting."
: "This user is already your contact, but your activities are not visible to them yet."; : "This user is already your contact, but your activities are not visible to them yet.";
this.$notify( this.$notify(
{ {

View File

@@ -99,7 +99,7 @@
<font-awesome <font-awesome
v-if="member.did === activeDid" v-if="member.did === activeDid"
icon="hand" icon="hand"
class="fa-fw text-blue-500" class="fa-fw text-slate-500"
/> />
<font-awesome <font-awesome
v-if=" v-if="
@@ -113,10 +113,10 @@
</h3> </h3>
<div <div
v-if="!getContactFor(member.did) && member.did !== activeDid" v-if="!getContactFor(member.did) && member.did !== activeDid"
class="flex items-center gap-1.5 ms-1" class="flex items-center gap-1.5 ml-2 ms-1"
> >
<button <button
class="btn-add-contact" class="btn-add-contact ml-2"
title="Add as contact" title="Add as contact"
@click="addAsContact(member)" @click="addAsContact(member)"
> >
@@ -124,7 +124,7 @@
</button> </button>
<button <button
class="btn-info-contact" class="btn-info-contact ml-2"
title="Contact Info" title="Contact Info"
@click=" @click="
informAboutAddingContact( informAboutAddingContact(
@@ -135,6 +135,27 @@
<font-awesome icon="circle-info" /> <font-awesome icon="circle-info" />
</button> </button>
</div> </div>
<div
v-if="getContactFor(member.did) && member.did !== activeDid"
class="flex items-center gap-1.5 ms-1"
>
<router-link
:to="{ name: 'contact-edit', params: { did: member.did } }"
>
<font-awesome
icon="pen"
class="text-sm text-blue-500 ml-2 mb-1"
/>
</router-link>
<router-link
:to="{ name: 'did', params: { did: member.did } }"
>
<font-awesome
icon="arrow-up-right-from-square"
class="text-sm text-blue-500 ml-2 mb-1"
/>
</router-link>
</div>
</div> </div>
<span <span
v-if=" v-if="
@@ -202,7 +223,6 @@
ref="bulkMembersDialog" ref="bulkMembersDialog"
:active-did="activeDid" :active-did="activeDid"
:api-server="apiServer" :api-server="apiServer"
:dialog-type="isOrganizer ? 'admit' : 'visibility'"
:is-organizer="isOrganizer" :is-organizer="isOrganizer"
@close="closeBulkMembersDialogCallback" @close="closeBulkMembersDialogCallback"
/> />

View File

@@ -510,14 +510,6 @@ export const NOTIFY_REGISTER_CONTACT = {
text: "Do you want to register them?", text: "Do you want to register them?",
}; };
// Used in: ContactsView.vue (showOnboardMeetingDialog method - complex modal for onboarding meeting)
export const NOTIFY_ONBOARDING_MEETING = {
title: "Onboarding Meeting",
text: "Would you like to start a new meeting?",
yesText: "Start New Meeting",
noText: "Join Existing Meeting",
};
// TestView.vue specific constants // TestView.vue specific constants
// Used in: TestView.vue (executeSql method - SQL error handling) // Used in: TestView.vue (executeSql method - SQL error handling)
export const NOTIFY_SQL_ERROR = { export const NOTIFY_SQL_ERROR = {

View File

@@ -70,15 +70,6 @@ export interface AxiosErrorResponse {
[key: string]: unknown; [key: string]: unknown;
} }
export interface UserInfo {
did: string;
name: string;
publicEncKey: string;
registered: boolean;
profileImageUrl?: string;
nextPublicEncKeyHash?: string;
}
export interface CreateAndSubmitClaimResult { export interface CreateAndSubmitClaimResult {
success: boolean; success: boolean;
embeddedRecordError?: string; embeddedRecordError?: string;

View File

@@ -4,3 +4,4 @@ export * from "./common";
export * from "./deepLinks"; export * from "./deepLinks";
export * from "./limits"; export * from "./limits";
export * from "./records"; export * from "./records";
export * from "./user";

View File

@@ -42,9 +42,6 @@ import {
PlanActionClaim, PlanActionClaim,
RegisterActionClaim, RegisterActionClaim,
TenureClaim, TenureClaim,
} from "../interfaces/claims";
import {
GenericCredWrapper, GenericCredWrapper,
GenericVerifiableCredential, GenericVerifiableCredential,
AxiosErrorResponse, AxiosErrorResponse,
@@ -55,13 +52,11 @@ import {
QuantitativeValue, QuantitativeValue,
KeyMetaWithPrivate, KeyMetaWithPrivate,
KeyMetaMaybeWithPrivate, KeyMetaMaybeWithPrivate,
} from "../interfaces/common";
import {
OfferSummaryRecord, OfferSummaryRecord,
OfferToPlanSummaryRecord, OfferToPlanSummaryRecord,
PlanSummaryAndPreviousClaim, PlanSummaryAndPreviousClaim,
PlanSummaryRecord, PlanSummaryRecord,
} from "../interfaces/records"; } from "../interfaces";
import { logger, safeStringify } from "../utils/logger"; import { logger, safeStringify } from "../utils/logger";
import { PlatformServiceFactory } from "@/services/PlatformServiceFactory"; import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
import { APP_SERVER } from "@/constants/app"; import { APP_SERVER } from "@/constants/app";
@@ -1662,30 +1657,35 @@ export async function register(
message?: string; message?: string;
}>(url, { jwtEncoded: vcJwt }); }>(url, { jwtEncoded: vcJwt });
if (resp.data?.success?.handleId) { if (resp.data?.success?.embeddedRecordError) {
return { success: true };
} else if (resp.data?.success?.embeddedRecordError) {
let message = let message =
"There was some problem with the registration and so it may not be complete."; "There was some problem with the registration and so it may not be complete.";
if (typeof resp.data.success.embeddedRecordError === "string") { if (typeof resp.data.success.embeddedRecordError === "string") {
message += " " + resp.data.success.embeddedRecordError; message += " " + resp.data.success.embeddedRecordError;
} }
return { error: message }; return { error: message };
} else if (resp.data?.success?.handleId) {
return { success: true };
} else { } else {
logger.error("Registration error:", JSON.stringify(resp.data)); logger.error("Registration non-thrown error:", JSON.stringify(resp.data));
return { error: "Got a server error when registering." }; return {
error:
(resp.data?.error as { message?: string })?.message ||
(resp.data?.error as string) ||
"Got a server error when registering.",
};
} }
} catch (error: unknown) { } catch (error: unknown) {
if (error && typeof error === "object") { if (error && typeof error === "object") {
const err = error as AxiosErrorResponse; const err = error as AxiosErrorResponse;
const errorMessage = const errorMessage =
err.message || err.response?.data?.error?.message ||
(err.response?.data && err.response?.data?.error ||
typeof err.response.data === "object" && err.message;
"message" in err.response.data logger.error(
? (err.response.data as { message: string }).message "Registration thrown error:",
: undefined); errorMessage || JSON.stringify(err),
logger.error("Registration error:", errorMessage || JSON.stringify(err)); );
return { error: errorMessage || "Got a server error when registering." }; return { error: errorMessage || "Got a server error when registering." };
} }
return { error: "Got a server error when registering." }; return { error: "Got a server error when registering." };

View File

@@ -346,9 +346,7 @@ export default class ContactEditView extends Vue {
// Notify success and redirect // Notify success and redirect
this.notify.success(NOTIFY_CONTACT_SAVED.message, TIMEOUTS.STANDARD); this.notify.success(NOTIFY_CONTACT_SAVED.message, TIMEOUTS.STANDARD);
(this.$router as Router).push({ this.$router.back();
path: "/did/" + encodeURIComponent(this.contact?.did || ""),
});
} }
} }
</script> </script>

View File

@@ -171,9 +171,11 @@ import {
CONTACT_IMPORT_ONE_URL_PATH_TIME_SAFARI, CONTACT_IMPORT_ONE_URL_PATH_TIME_SAFARI,
CONTACT_URL_PATH_ENDORSER_CH_OLD, CONTACT_URL_PATH_ENDORSER_CH_OLD,
} from "../libs/endorserServer"; } from "../libs/endorserServer";
import { GiveSummaryRecord } from "@/interfaces/records"; import {
import { UserInfo } from "@/interfaces/common"; GiveSummaryRecord,
import { VerifiableCredential } from "@/interfaces/claims-result"; UserInfo,
VerifiableCredential,
} from "@/interfaces";
import * as libsUtil from "../libs/util"; import * as libsUtil from "../libs/util";
import { import {
generateSaveAndActivateIdentity, generateSaveAndActivateIdentity,

View File

@@ -12,20 +12,20 @@
</h1> </h1>
<!-- Back --> <!-- Back -->
<router-link <button
class="order-first text-lg text-center leading-none p-1" class="order-first text-lg text-center leading-none p-1"
:to="{ name: 'contacts' }" @click="goBack()"
> >
<font-awesome icon="chevron-left" class="block text-center w-[1em]" /> <font-awesome icon="chevron-left" class="block text-center w-[1em]" />
</router-link> </button>
<!-- Help button --> <!-- Help button -->
<router-link <button
:to="{ name: 'help' }"
class="block ms-auto text-sm text-center text-white bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] p-1.5 rounded-full" class="block ms-auto text-sm text-center text-white bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] p-1.5 rounded-full"
@click="goToHelp()"
> >
<font-awesome icon="question" class="block text-center w-[1em]" /> <font-awesome icon="question" class="block text-center w-[1em]" />
</router-link> </button>
</div> </div>
<!-- Identity Details --> <!-- Identity Details -->
@@ -476,7 +476,7 @@ export default class DIDView extends Vue {
* Navigation helper methods * Navigation helper methods
*/ */
goBack() { goBack() {
this.$router.go(-1); this.$router.back();
} }
/** /**

View File

@@ -473,6 +473,7 @@ export default class OnboardMeetingView extends Vue {
); );
return; return;
} }
const password: string = this.newOrUpdatedMeetingInputs.password;
// create content with user's name & DID encrypted with password // create content with user's name & DID encrypted with password
const content = { const content = {
@@ -482,7 +483,7 @@ export default class OnboardMeetingView extends Vue {
}; };
const encryptedContent = await encryptMessage( const encryptedContent = await encryptMessage(
JSON.stringify(content), JSON.stringify(content),
this.newOrUpdatedMeetingInputs.password, password,
); );
const headers = await getHeaders(this.activeDid); const headers = await getHeaders(this.activeDid);
@@ -505,6 +506,11 @@ export default class OnboardMeetingView extends Vue {
this.newOrUpdatedMeetingInputs = null; this.newOrUpdatedMeetingInputs = null;
this.notify.success(NOTIFY_MEETING_CREATED.message, TIMEOUTS.STANDARD); this.notify.success(NOTIFY_MEETING_CREATED.message, TIMEOUTS.STANDARD);
// redirect to the same page with the password parameter set
this.$router.push({
name: "onboard-meeting-setup",
query: { password: password },
});
} else { } else {
throw { response: response }; throw { response: response };
} }