Complete InviteOneView.vue Enhanced Triple Migration Pattern with human validation
- Database migration: databaseUtil → PlatformServiceMixin methods - SQL abstraction: Raw contact insertion → $insertContact() service method - Notification migration: 7 patterns → helper system + constants - Template streamlining: 5 computed properties + helper methods added - Human testing: Complete invitation lifecycle validated - Time: 9m 5s (50% faster than estimate) - Project: 43% complete (40/92 components migrated)
This commit is contained in:
@@ -68,11 +68,8 @@
|
||||
>
|
||||
<td>
|
||||
<span
|
||||
v-if="
|
||||
!invite.redeemedAt &&
|
||||
invite.expiresAt > new Date().toISOString()
|
||||
"
|
||||
class="text-center text-blue-500 cursor-pointer"
|
||||
v-if="isInviteActive(invite)"
|
||||
:class="activeInviteClass"
|
||||
:title="inviteLink(invite.jwt)"
|
||||
@click="
|
||||
copyInviteAndNotify(invite.inviteIdentifier, invite.jwt)
|
||||
@@ -82,13 +79,13 @@
|
||||
</span>
|
||||
<span
|
||||
v-else
|
||||
class="text-center text-slate-500 cursor-pointer"
|
||||
:class="inactiveInviteClass"
|
||||
:title="invite.inviteIdentifier"
|
||||
@click="
|
||||
showInvite(
|
||||
invite.inviteIdentifier,
|
||||
!!invite.redeemedAt,
|
||||
invite.expiresAt < new Date().toISOString(),
|
||||
isInviteExpired(invite),
|
||||
)
|
||||
"
|
||||
>
|
||||
@@ -99,10 +96,10 @@
|
||||
{{ invite.notes }}
|
||||
</td>
|
||||
<td class="text-center">
|
||||
{{ invite.redeemedAt ? "" : invite.expiresAt.substring(0, 10) }}
|
||||
{{ formatExpirationDate(invite) }}
|
||||
</td>
|
||||
<td class="text-center">
|
||||
{{ invite.redeemedAt?.substring(0, 10) }}
|
||||
{{ formatRedemptionDate(invite) }}
|
||||
<br />
|
||||
{{ getTruncatedRedeemedBy(invite.redeemedBy) }}
|
||||
<br />
|
||||
@@ -140,10 +137,40 @@ import TopMessage from "../components/TopMessage.vue";
|
||||
import InviteDialog from "../components/InviteDialog.vue";
|
||||
import { APP_SERVER, AppString, NotificationIface } from "../constants/app";
|
||||
import { Contact } from "../db/tables/contacts";
|
||||
import * as databaseUtil from "../db/databaseUtil";
|
||||
import { createInviteJwt, getHeaders } from "../libs/endorserServer";
|
||||
import { PlatformServiceFactory } from "../services/PlatformServiceFactory";
|
||||
import { logger } from "../utils/logger";
|
||||
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
|
||||
import { createNotifyHelpers, TIMEOUTS } from "@/utils/notify";
|
||||
import {
|
||||
NOTIFY_INVITE_LOAD_ERROR,
|
||||
NOTIFY_INVITE_DELETED,
|
||||
createInviteLinkCopyMessage,
|
||||
createInviteIdCopyMessage,
|
||||
createContactAddedMessage,
|
||||
createInviteDeleteConfirmMessage,
|
||||
} from "@/constants/notifications";
|
||||
|
||||
/**
|
||||
* InviteOneView Component
|
||||
*
|
||||
* Manages user invitations with comprehensive invite lifecycle management.
|
||||
* Handles creation, tracking, deletion, and redemption of invitations.
|
||||
* Integrates with contact management for redeemed invitations.
|
||||
*
|
||||
* Key features:
|
||||
* - Invitation lifecycle management (create, track, delete)
|
||||
* - Contact integration for redeemed invitations
|
||||
* - Link generation and clipboard functionality
|
||||
* - Comprehensive error handling and user feedback
|
||||
* - Expiration and status tracking
|
||||
*
|
||||
* Database Operations:
|
||||
* - Account settings retrieval via PlatformServiceMixin
|
||||
* - Contact queries and management via mixin methods
|
||||
* - Contact insertion for redeemed invitations
|
||||
*
|
||||
* Migration Status: Phase 1 Complete - Database patterns modernized
|
||||
*/
|
||||
|
||||
interface Invite {
|
||||
inviteIdentifier: string;
|
||||
@@ -156,6 +183,7 @@ interface Invite {
|
||||
|
||||
@Component({
|
||||
components: { ContactNameDialog, QuickNav, TopMessage, InviteDialog },
|
||||
mixins: [PlatformServiceMixin],
|
||||
})
|
||||
export default class InviteOneView extends Vue {
|
||||
$notify!: (notification: NotificationIface, timeout?: number) => void;
|
||||
@@ -168,9 +196,90 @@ export default class InviteOneView extends Vue {
|
||||
isRegistered: boolean = false;
|
||||
showAppleWarning = false;
|
||||
|
||||
notify!: ReturnType<typeof createNotifyHelpers>;
|
||||
|
||||
/**
|
||||
* Initializes notification helpers
|
||||
*/
|
||||
created() {
|
||||
this.notify = createNotifyHelpers(this.$notify);
|
||||
}
|
||||
|
||||
// =================================================
|
||||
// COMPUTED PROPERTIES
|
||||
// =================================================
|
||||
|
||||
/**
|
||||
* CSS classes for active invite links
|
||||
*/
|
||||
get activeInviteClass(): string {
|
||||
return "text-center text-blue-500 cursor-pointer";
|
||||
}
|
||||
|
||||
/**
|
||||
* CSS classes for inactive invite links
|
||||
*/
|
||||
get inactiveInviteClass(): string {
|
||||
return "text-center text-slate-500 cursor-pointer";
|
||||
}
|
||||
|
||||
// =================================================
|
||||
// HELPER METHODS
|
||||
// =================================================
|
||||
|
||||
/**
|
||||
* Checks if an invite is currently active (not redeemed and not expired)
|
||||
* @param invite - The invite to check
|
||||
* @returns True if invite is active
|
||||
*/
|
||||
isInviteActive(invite: Invite): boolean {
|
||||
return !invite.redeemedAt && invite.expiresAt > new Date().toISOString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if an invite has expired
|
||||
* @param invite - The invite to check
|
||||
* @returns True if invite is expired
|
||||
*/
|
||||
isInviteExpired(invite: Invite): boolean {
|
||||
return invite.expiresAt < new Date().toISOString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats expiration date for display
|
||||
* @param invite - The invite to format
|
||||
* @returns Formatted date string or empty if redeemed
|
||||
*/
|
||||
formatExpirationDate(invite: Invite): string {
|
||||
return invite.redeemedAt ? "" : invite.expiresAt.substring(0, 10);
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats redemption date for display
|
||||
* @param invite - The invite to format
|
||||
* @returns Formatted date string or empty if not redeemed
|
||||
*/
|
||||
formatRedemptionDate(invite: Invite): string {
|
||||
return invite.redeemedAt?.substring(0, 10) || "";
|
||||
}
|
||||
|
||||
// =================================================
|
||||
// LIFECYCLE METHODS
|
||||
// =================================================
|
||||
|
||||
/**
|
||||
* Initializes component with user invitations and contact data
|
||||
*
|
||||
* Workflow:
|
||||
* 1. Retrieves account settings via PlatformServiceMixin
|
||||
* 2. Loads user invitations from server API
|
||||
* 3. Loads contact data for redeemed invitations
|
||||
* 4. Maps redeemed contacts for display
|
||||
*/
|
||||
async mounted() {
|
||||
try {
|
||||
const settings = await databaseUtil.retrieveSettingsForActiveAccount();
|
||||
// Use PlatformServiceMixin for account settings
|
||||
const settings = await this.$accountSettings();
|
||||
this.activeDid = settings.activeDid || "";
|
||||
this.apiServer = settings.apiServer || "";
|
||||
this.isRegistered = !!settings.isRegistered;
|
||||
@@ -182,15 +291,12 @@ export default class InviteOneView extends Vue {
|
||||
);
|
||||
this.invites = response.data.data;
|
||||
|
||||
const platformService = PlatformServiceFactory.getInstance();
|
||||
const queryResult = await platformService.dbQuery(
|
||||
"SELECT * FROM contacts",
|
||||
);
|
||||
const baseContacts = databaseUtil.mapQueryResultToValues(
|
||||
queryResult,
|
||||
) as unknown as Contact[];
|
||||
// Use PlatformServiceMixin for contact retrieval
|
||||
const allContacts = await this.$getAllContacts();
|
||||
|
||||
// Map redeemed contacts for display
|
||||
for (const invite of this.invites) {
|
||||
const contact = baseContacts.find(
|
||||
const contact = allContacts.find(
|
||||
(contact) => contact.did === invite.redeemedBy,
|
||||
);
|
||||
if (contact && invite.redeemedBy) {
|
||||
@@ -199,15 +305,7 @@ export default class InviteOneView extends Vue {
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error("Error fetching invites:", error);
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
title: "Load Error",
|
||||
text: "Got an error loading your invites.",
|
||||
},
|
||||
5000,
|
||||
);
|
||||
this.notify.error(NOTIFY_INVITE_LOAD_ERROR.message, TIMEOUTS.LONG);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -233,33 +331,14 @@ export default class InviteOneView extends Vue {
|
||||
|
||||
copyInviteAndNotify(inviteId: string, jwt: string) {
|
||||
useClipboard().copy(this.inviteLink(jwt));
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "success",
|
||||
title: "Copied",
|
||||
text: "Your clipboard now contains the link for invite " + inviteId,
|
||||
},
|
||||
5000,
|
||||
);
|
||||
this.notify.success(createInviteLinkCopyMessage(inviteId), TIMEOUTS.LONG);
|
||||
}
|
||||
|
||||
showInvite(inviteId: string, redeemed: boolean, expired: boolean) {
|
||||
let message = `Your clipboard now contains the invite ID ${inviteId}`;
|
||||
if (redeemed) {
|
||||
message += " (This invite has been used.)";
|
||||
} else if (expired) {
|
||||
message += " (This invite has expired.)";
|
||||
}
|
||||
useClipboard().copy(inviteId);
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "success",
|
||||
title: "Copied",
|
||||
text: message,
|
||||
},
|
||||
5000,
|
||||
this.notify.success(
|
||||
createInviteIdCopyMessage(inviteId, redeemed, expired),
|
||||
TIMEOUTS.LONG,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -274,15 +353,7 @@ export default class InviteOneView extends Vue {
|
||||
message = error.response.data.error;
|
||||
}
|
||||
}
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
title: title,
|
||||
text: message,
|
||||
},
|
||||
5000,
|
||||
);
|
||||
this.notify.error(message, TIMEOUTS.LONG);
|
||||
}
|
||||
|
||||
async createInvite() {
|
||||
@@ -335,33 +406,31 @@ export default class InviteOneView extends Vue {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new contact from redeemed invitation
|
||||
*
|
||||
* @param did - DID of the person who redeemed the invitation
|
||||
* @param notes - Notes from the original invitation
|
||||
*/
|
||||
async addNewContact(did: string, notes: string) {
|
||||
(this.$refs.contactNameDialog as ContactNameDialog).open(
|
||||
"To Whom Did You Send The Invite?",
|
||||
"Their name will be added to your contact list.",
|
||||
async (name) => {
|
||||
// the person obviously registered themselves and this user already granted visibility, so we just add them
|
||||
// The person registered and user granted visibility, so add them
|
||||
const contact = {
|
||||
did: did,
|
||||
name: name,
|
||||
registered: true,
|
||||
};
|
||||
const platformService = PlatformServiceFactory.getInstance();
|
||||
const columns = Object.keys(contact);
|
||||
const values = Object.values(contact);
|
||||
const placeholders = values.map(() => "?").join(", ");
|
||||
const sql = `INSERT INTO contacts (${columns.join(", ")}) VALUES (${placeholders})`;
|
||||
await platformService.dbExec(sql, values);
|
||||
|
||||
// Use PlatformServiceMixin service method for contact insertion
|
||||
await this.$insertContact(contact);
|
||||
|
||||
this.contactsRedeemed[did] = contact;
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "success",
|
||||
title: "Contact Added",
|
||||
text: `${name} has been added to your contacts.`,
|
||||
},
|
||||
3000,
|
||||
this.notify.success(
|
||||
createContactAddedMessage(name || "Contact"),
|
||||
TIMEOUTS.STANDARD,
|
||||
);
|
||||
},
|
||||
() => {},
|
||||
@@ -370,45 +439,28 @@ export default class InviteOneView extends Vue {
|
||||
}
|
||||
|
||||
deleteInvite(inviteId: string, notes: string) {
|
||||
this.$notify(
|
||||
{
|
||||
group: "modal",
|
||||
type: "confirm",
|
||||
title: "Delete Invite?",
|
||||
text: `Are you sure you want to erase the invite for "${notes}"? (There is no undo.)`,
|
||||
onYes: async () => {
|
||||
const headers = await getHeaders(this.activeDid);
|
||||
try {
|
||||
const result = await axios.delete(
|
||||
this.apiServer + "/api/userUtil/invite/" + inviteId,
|
||||
{ headers },
|
||||
);
|
||||
if (result.status !== 204) {
|
||||
throw result.data;
|
||||
}
|
||||
this.invites = this.invites.filter(
|
||||
(invite) => invite.inviteIdentifier !== inviteId,
|
||||
);
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "success",
|
||||
title: "Deleted",
|
||||
text: "Invite deleted.",
|
||||
},
|
||||
3000,
|
||||
);
|
||||
} catch (e) {
|
||||
this.lookForErrorAndNotify(
|
||||
e,
|
||||
"Error Deleting Invite",
|
||||
"Got an error deleting your invite.",
|
||||
);
|
||||
}
|
||||
},
|
||||
},
|
||||
-1,
|
||||
);
|
||||
this.notify.confirm(createInviteDeleteConfirmMessage(notes), async () => {
|
||||
const headers = await getHeaders(this.activeDid);
|
||||
try {
|
||||
const result = await axios.delete(
|
||||
this.apiServer + "/api/userUtil/invite/" + inviteId,
|
||||
{ headers },
|
||||
);
|
||||
if (result.status !== 204) {
|
||||
throw result.data;
|
||||
}
|
||||
this.invites = this.invites.filter(
|
||||
(invite) => invite.inviteIdentifier !== inviteId,
|
||||
);
|
||||
this.notify.success(NOTIFY_INVITE_DELETED.message, TIMEOUTS.STANDARD);
|
||||
} catch (e) {
|
||||
this.lookForErrorAndNotify(
|
||||
e,
|
||||
"Error Deleting Invite",
|
||||
"Got an error deleting your invite.",
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user