diff --git a/apps/web-app/components/layout/navbar.vue b/apps/web-app/components/layout/navbar.vue
index d8da702c..9bac1110 100644
--- a/apps/web-app/components/layout/navbar.vue
+++ b/apps/web-app/components/layout/navbar.vue
@@ -70,10 +70,11 @@
const { $trpc } = useNuxtApp();
- const { data: userProfile } = $trpc.user.profile.getUserOrgProfile.useQuery(
- { orgSlug: orgSlug },
- { server: false, queryKey: 'getUserSingleProfileNav', lazy: true }
- );
+ const { data: userProfile } =
+ $trpc.user.profile.getUserOrgProfile.useLazyQuery(
+ { orgSlug: orgSlug },
+ { server: false, queryKey: 'getUserSingleProfileNav' }
+ );
const {
data: userOrgs,
@@ -97,7 +98,7 @@
}
if (!userOrgSlugs?.includes(orgSlug)) {
- navigateTo(`/login`);
+ navigateTo(`/redirect`);
}
});
@@ -330,7 +331,6 @@
:avatar-id="item.avatarId"
:type="'org'"
:alt="item.label"
- color="gray"
size="sm" />
diff --git a/apps/web-app/components/un/ui-avatar-list.vue b/apps/web-app/components/un/ui-avatar-list.vue
deleted file mode 100644
index 4a6f9164..00000000
--- a/apps/web-app/components/un/ui-avatar-list.vue
+++ /dev/null
@@ -1,119 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
- + {{ props.avatars.length - props.limit + 1 }}
-
-
-
-
-
-
-
-
-
-
-
diff --git a/apps/web-app/components/un/ui-avatar-plus.vue b/apps/web-app/components/un/ui-avatar-plus.vue
index acd3c97e..8dd58b0b 100644
--- a/apps/web-app/components/un/ui-avatar-plus.vue
+++ b/apps/web-app/components/un/ui-avatar-plus.vue
@@ -1,22 +1,10 @@
-
-
+
+
+
- {{
- primary.avatarId ? '' : primary.name ? primary.name?.charAt(0) : ''
- }}
+ class="bg-gray-50 h-[32px] w-[32px] flex items-center justify-center rounded-2 text-sm font-display shadow backdrop-blur-2xl">
+ + {{ props.avatars.length - props.limit + 1 }}
-
-
-
-
+
+
- + {{ props.avatars.length - props.limit + 1 }}
+ v-for="avatar in avatarArray"
+ :key="avatar.participantPublicId">
+
-
-
-
-
-
-
-
-
+
+
+
diff --git a/apps/web-app/components/un/ui-avatar.vue b/apps/web-app/components/un/ui-avatar.vue
index 1bd62816..6f4156d6 100644
--- a/apps/web-app/components/un/ui-avatar.vue
+++ b/apps/web-app/components/un/ui-avatar.vue
@@ -1,24 +1,31 @@
-
+
+
+
+
+ {{ tooltipText }}
+
+
diff --git a/apps/web-app/components/un/ui-popover.vue b/apps/web-app/components/un/ui-popover.vue
new file mode 100644
index 00000000..e14e917c
--- /dev/null
+++ b/apps/web-app/components/un/ui-popover.vue
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-app/components/un/ui-tooltip.vue b/apps/web-app/components/un/ui-tooltip.vue
index 14401a56..ca3e9f21 100644
--- a/apps/web-app/components/un/ui-tooltip.vue
+++ b/apps/web-app/components/un/ui-tooltip.vue
@@ -14,6 +14,9 @@
+
+
+
diff --git a/apps/web-app/composables/types.ts b/apps/web-app/composables/types.ts
index 095a63be..eb343739 100644
--- a/apps/web-app/composables/types.ts
+++ b/apps/web-app/composables/types.ts
@@ -1,5 +1,17 @@
+import { uiColors } from '@uninbox/types/ui';
+
const { $trpc } = useNuxtApp();
type PromiseType = T extends Promise ? U : never;
export type UserConvosDataType = PromiseType<
ReturnType
>['data'];
+
+export type ConvoParticipantEntry = {
+ participantPublicId: string;
+ typePublicId: string;
+ avatarPublicId: string;
+ name: string;
+ type: 'user' | 'group' | 'contact';
+ role: 'assigned' | 'contributor' | 'commenter' | 'watcher' | 'guest';
+ color: (typeof uiColors)[number] | null;
+};
diff --git a/apps/web-app/composables/utils.ts b/apps/web-app/composables/utils.ts
index 498269d5..ca4297a0 100644
--- a/apps/web-app/composables/utils.ts
+++ b/apps/web-app/composables/utils.ts
@@ -1,4 +1,6 @@
import { cva, type VariantProps } from 'class-variance-authority';
+import type { UserConvosDataType } from '~/composables/types';
+
function generateAvatarUrl(
type: 'user' | 'org' | 'group' | 'contact',
avatarId: string,
@@ -34,8 +36,67 @@ function generateAvatarUrl(
}`;
}
+function useParticipantData(
+ participant: UserConvosDataType[number]['participants'][0]
+) {
+ const {
+ publicId: participantPublicId,
+ contact,
+ userGroup,
+ orgMember,
+ role: participantRole
+ } = participant;
+
+ let participantType: 'user' | 'group' | 'contact',
+ participantTypePublicId,
+ avatarPublicId,
+ participantName,
+ participantColor;
+
+ switch (true) {
+ case !!contact?.publicId:
+ participantType = 'contact';
+ participantTypePublicId = contact.publicId;
+ avatarPublicId = contact.avatarId || '';
+ participantName =
+ contact.name || `${contact.emailUsername}@${contact.emailDomain}`;
+ participantColor = null;
+ break;
+ case !!userGroup?.name:
+ participantType = 'group';
+ participantTypePublicId = userGroup.publicId;
+ avatarPublicId = userGroup.avatarId || '';
+ participantName = userGroup.name;
+ participantColor = userGroup.color;
+ break;
+ case !!orgMember?.publicId:
+ participantType = 'user';
+ participantTypePublicId = orgMember.publicId;
+ avatarPublicId = orgMember.profile.avatarId || '';
+ participantName = `${orgMember.profile.firstName} ${orgMember.profile.lastName}`;
+ participantColor = null;
+ break;
+ default:
+ participantType = 'user';
+ participantTypePublicId = '';
+ avatarPublicId = '';
+ participantName = '';
+ participantColor = null;
+ }
+
+ return {
+ participantPublicId,
+ participantType,
+ participantTypePublicId,
+ avatarPublicId,
+ participantName,
+ participantColor,
+ participantRole
+ };
+}
+
export const useUtils = () => {
- return { cva, generateAvatarUrl };
+ return { cva, generateAvatarUrl, convos: { useParticipantData } };
};
// TODO: Fix exporting types under namespace UseUtilTypes
diff --git a/apps/web-app/emails/vercel-template.vue b/apps/web-app/emails/vercel-template.vue
deleted file mode 100644
index 1e55f567..00000000
--- a/apps/web-app/emails/vercel-template.vue
+++ /dev/null
@@ -1,117 +0,0 @@
-
-
-
-
-
-
- {{ previewText }}
-
-
-
-
-
-
-
- Join {{ teamName }} on Vercel
-
-
- Hello {{ username }},
-
-
- bukinoshita (
-
- {{ invitedByEmail }}
-
- ) has invited you to the {{ teamName }} team on
- Vercel.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Join the team
-
-
-
- or copy and paste this URL into your browser:
-
- {{ inviteLink }}
-
-
-
-
- This invitation was intended for
- {{ username }} .This invite was sent
- from {{ inviteFromIp }} located in
- {{ inviteFromLocation }}. If you were not expecting this invitation, you can ignore this
- email. If you are concerned about your account's safety, please
- reply to this email to get in touch with us.
-
-
-
-
-
-
diff --git a/apps/web-app/package.json b/apps/web-app/package.json
index 3db35e0b..0ff3dbf4 100644
--- a/apps/web-app/package.json
+++ b/apps/web-app/package.json
@@ -62,6 +62,7 @@
"radix-vue": "^1.3.2",
"superjson": "^2.2.1",
"trpc-nuxt": "^0.10.17",
+ "vue-virtual-scroller": "2.0.0-beta.8",
"zod": "^3.22.4"
},
"packageManager": "pnpm@8.5.1"
diff --git a/apps/web-app/pages/[orgSlug]/convo/[id].vue b/apps/web-app/pages/[orgSlug]/convo/[id].vue
index b5913978..a2a4a5e6 100644
--- a/apps/web-app/pages/[orgSlug]/convo/[id].vue
+++ b/apps/web-app/pages/[orgSlug]/convo/[id].vue
@@ -1,160 +1,434 @@
-
+
-
-
- {{
- convoDetails?.data?.subjects.length !== 1 ? 'Subjects' : 'Subject'
- }}:
-
-
+ class="max-w-full w-full flex flex-row items-center justify-between gap-2">
+
+
+ class="bg-gray-100 dark:bg-gray-800 truncate rounded-xl px-4 py-2 text-lg">
{{ subject.subject }}
-
+
+
+
+
+
+
+
+
+
+
+
-
+ class="h-full max-h-full min-w-[600px] w-[600px] flex flex-col gap-2">
+
-
-
{{ attachment.name }}
+ class="from-gray-100 z-20000 mb-[-12px] h-[12px] bg-gradient-to-b" />
+
+
+
+
+ replyingToBanner
+
+
+
+
-
-
-
-
-
+
+
+
+
+
+ PARTICIPANTS
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ASSIGNED
+
+
+
+
+
+ {{ participant.name }}
+
+
+
+
+
+
+ CONTRIBUTORS
+
+
+
+
+
+ {{ participant.name }}
+
+
+
+
+
+
+ COMMENTERS
+
+
+
+
+
+ {{ participant.name }}
+
+
+
+
+
+
+ WATCHERS
+
+
+
+
+
+ {{ participant.name }}
+
+
+
+
+
+
+ GUEST
+
+
+
+
+
+ {{ participant.name }}
+
+
+
+
+
+
+
+
+
+ ATTACHMENTS
+
+
+
+
+
+
+
+ {{ attachment.name }}
+
+
+
+
+
+
+
+ {{ attachment.name }}
+
+
+
+
+ No attachments
+
-
-
Started: {{ createDate }}
-
Updated: {{ updateDate }}
+
+
+
+
+ Started: {{ createdAgo }}
+
+
+
+
+ Updated: {{ updatedAgo }}
+
+
-
-
diff --git a/apps/web-app/pages/[orgSlug]/convo/new.vue b/apps/web-app/pages/[orgSlug]/convo/new.vue
index 22cefe75..048245d0 100644
--- a/apps/web-app/pages/[orgSlug]/convo/new.vue
+++ b/apps/web-app/pages/[orgSlug]/convo/new.vue
@@ -419,7 +419,7 @@
publicId: firstParticipantPublicId
};
const createNewConvoTrpc = $trpc.convos.createNewConvo.useMutation();
- await createNewConvoTrpc.mutate({
+ const createNewConvo = await createNewConvoTrpc.mutate({
firstMessageType: type,
to: convoToValue,
participantsOrgMembersPublicIds: convoParticipantsOrgMembersPublicIds,
@@ -431,20 +431,27 @@
message: stringify(messageEditorData.value)
});
- if (createNewConvoTrpc.error) {
+ if (createNewConvoTrpc.status.value === 'error') {
actionLoading.value = false;
- } else {
toast.add({
- title: 'Conversation created',
- icon: 'i-ph-thumbs-up',
+ id: 'create_new_convo_fail',
+ title: 'Conversation creation failed',
+ description: `Conversation could not be created.`,
+ color: 'red',
+ icon: 'i-ph-warning-circle',
timeout: 5000
});
- setTimeout(() => {
- navigateTo(
- `/${orgSlug}/convo/${createNewConvoTrpc.data.value?.publicId}`
- );
- }, 1500);
+ return;
}
+ toast.add({
+ title: 'Conversation created',
+ description: `Redirecting to new conversation...`,
+ icon: 'i-ph-thumbs-up',
+ timeout: 5000
+ });
+ setTimeout(() => {
+ navigateTo(`/${orgSlug}/convo/${createNewConvo?.publicId}`);
+ }, 1500);
}
@@ -540,7 +547,7 @@
:avatar-id="participant.avatarId?.toString()"
:type="'group'"
:alt="participant.name.toString()"
- :color="participant.color?.toString()"
+ :color="participant.color as UiColor"
size="xs" />
{{ participant.name }}
diff --git a/apps/web-app/plugins/ virtualScroller.ts b/apps/web-app/plugins/ virtualScroller.ts
new file mode 100644
index 00000000..e98ad5a4
--- /dev/null
+++ b/apps/web-app/plugins/ virtualScroller.ts
@@ -0,0 +1,12 @@
+import 'vue-virtual-scroller/dist/vue-virtual-scroller.css';
+import {
+ DynamicScroller,
+ DynamicScrollerItem,
+ RecycleScroller
+} from 'vue-virtual-scroller';
+
+export default defineNuxtPlugin((nuxtApp) => {
+ nuxtApp.vueApp.component('RecycleScroller', RecycleScroller);
+ nuxtApp.vueApp.component('DynamicScroller', DynamicScroller);
+ nuxtApp.vueApp.component('DynamicScrollerItem', DynamicScrollerItem);
+});
diff --git a/apps/web-app/plugins/trpcClient.ts b/apps/web-app/plugins/trpcClient.ts
index 215f0c15..6906a6ca 100644
--- a/apps/web-app/plugins/trpcClient.ts
+++ b/apps/web-app/plugins/trpcClient.ts
@@ -18,7 +18,7 @@ export const errorHandler: TRPCLink = () => {
err.message ===
'You are not a member of this organization, redirecting...'
) {
- navigateTo('/login');
+ navigateTo('/redirect');
return;
}
diff --git a/apps/web-app/server/trpc/index.ts b/apps/web-app/server/trpc/index.ts
index 8cda4b36..e61147b7 100644
--- a/apps/web-app/server/trpc/index.ts
+++ b/apps/web-app/server/trpc/index.ts
@@ -46,10 +46,6 @@ const trpcWebAppOrgRouter = router({
mail: trpcWebAppOrgMailRouter
});
-const trpcWebAppConvoRouter = router({
- convos: convoRouter
-});
-
export const trpcWebAppRouter = router({
signup: signupRouter,
auth: authRouter,
diff --git a/apps/web-app/server/trpc/routers/convoRouter/convoRouter.ts b/apps/web-app/server/trpc/routers/convoRouter/convoRouter.ts
index 03149604..71af297d 100644
--- a/apps/web-app/server/trpc/routers/convoRouter/convoRouter.ts
+++ b/apps/web-app/server/trpc/routers/convoRouter/convoRouter.ts
@@ -1,7 +1,15 @@
import { z } from 'zod';
import { parse } from 'superjson';
import { router, orgProcedure } from '../../trpc';
-import { type InferInsertModel, and, eq, inArray } from '@uninbox/database/orm';
+import {
+ type InferInsertModel,
+ and,
+ eq,
+ inArray,
+ desc,
+ or,
+ lt
+} from '@uninbox/database/orm';
import {
convos,
convoParticipants,
@@ -12,29 +20,37 @@ import {
contacts,
contactGlobalReputations,
convoEntries,
- emailIdentitiesAuthorizedUsers
+ emailIdentitiesAuthorizedUsers,
+ userGroupMembers
} from '@uninbox/database/schema';
-import { nanoId, nanoIdLength, nanoIdToken } from '@uninbox/utils';
+import {
+ nanoId,
+ nanoIdLength,
+ nanoIdSchema,
+ nanoIdToken
+} from '@uninbox/utils';
import { TRPCError } from '@trpc/server';
import type { JSONContent } from '@tiptap/vue-3';
import { generateText } from '@tiptap/core';
import { generateHTML } from '@tiptap/html';
import { tipTapExtensions } from '~/shared/editorConfig';
+import { convoEntryRouter } from './entryRouter';
export const convoRouter = router({
+ entries: convoEntryRouter,
createNewConvo: orgProcedure
.input(
z.object({
participantsOrgMembersPublicIds: z.array(
z.string().min(3).max(nanoIdLength)
),
- participantsGroupsPublicIds: z
- .array(z.string().min(3).max(nanoIdLength))
- .optional(),
- participantsContactsPublicIds: z
- .array(z.string().min(3).max(nanoIdLength))
- .optional(),
- participantsEmails: z.array(z.string()).optional(),
+ participantsGroupsPublicIds: z.array(
+ z.string().min(3).max(nanoIdLength)
+ ),
+ participantsContactsPublicIds: z.array(
+ z.string().min(3).max(nanoIdLength)
+ ),
+ participantsEmails: z.array(z.string()),
sendAsEmailIdentityPublicId: z
.string()
.min(3)
@@ -217,7 +233,7 @@ export const convoRouter = router({
}
}
let newConvoToEmailAddress: string;
- if (participantsContactsPublicIds || participantsEmails) {
+ if (participantsContactsPublicIds.length || participantsEmails.length) {
newConvoToEmailAddress = await getConvoToAddress();
} else {
newConvoToEmailAddress = '';
@@ -367,7 +383,8 @@ export const convoRouter = router({
const newConvoPublicId = nanoId();
const insertConvoResponse = await db.insert(convos).values({
publicId: newConvoPublicId,
- orgId: orgId
+ orgId: orgId,
+ lastUpdatedAt: new Date()
});
// create conversationSubject entry
@@ -381,12 +398,12 @@ export const convoRouter = router({
// create conversationParticipants Entries
if (orgMemberIds.length) {
- const convoMembersDbInsertValuesArray: InferInsertModel<
+ const convoParticipantsDbInsertValuesArray: InferInsertModel<
typeof convoParticipants
>[] = [];
orgMemberIds.forEach((orgMemberId) => {
const convoMemberPublicId = nanoId();
- convoMembersDbInsertValuesArray.push({
+ convoParticipantsDbInsertValuesArray.push({
orgId: orgId,
convoId: +insertConvoResponse.insertId,
publicId: convoMemberPublicId,
@@ -395,16 +412,16 @@ export const convoRouter = router({
});
await db
.insert(convoParticipants)
- .values(convoMembersDbInsertValuesArray);
+ .values(convoParticipantsDbInsertValuesArray);
}
if (orgGroupIds.length) {
- const convoMembersDbInsertValuesArray: InferInsertModel<
+ const convoParticipantsDbInsertValuesArray: InferInsertModel<
typeof convoParticipants
>[] = [];
orgGroupIds.forEach((groupId) => {
const convoMemberPublicId = nanoId();
- convoMembersDbInsertValuesArray.push({
+ convoParticipantsDbInsertValuesArray.push({
orgId: orgId,
convoId: +insertConvoResponse.insertId,
publicId: convoMemberPublicId,
@@ -413,16 +430,16 @@ export const convoRouter = router({
});
await db
.insert(convoParticipants)
- .values(convoMembersDbInsertValuesArray);
+ .values(convoParticipantsDbInsertValuesArray);
}
if (orgContactIds.length) {
- const convoMembersDbInsertValuesArray: InferInsertModel<
+ const convoParticipantsDbInsertValuesArray: InferInsertModel<
typeof convoParticipants
>[] = [];
orgContactIds.forEach((contactId) => {
const convoMemberPublicId = nanoId();
- convoMembersDbInsertValuesArray.push({
+ convoParticipantsDbInsertValuesArray.push({
orgId: orgId,
convoId: +insertConvoResponse.insertId,
publicId: convoMemberPublicId,
@@ -431,7 +448,7 @@ export const convoRouter = router({
});
await db
.insert(convoParticipants)
- .values(convoMembersDbInsertValuesArray);
+ .values(convoParticipantsDbInsertValuesArray);
}
const authorConvoParticipantPublicId = nanoId();
const insertAuthorConvoParticipantResponse = await db
@@ -620,286 +637,398 @@ export const convoRouter = router({
publicId: newConvoPublicId,
missingEmailIdentities: missingEmailIdentitiesWarnings
};
+ }),
+
+ //* get a specific conversation
+ getConvo: orgProcedure
+ .input(
+ z.object({
+ convoPublicId: nanoIdSchema
+ })
+ )
+ .query(async ({ ctx, input }) => {
+ const { db, user, org } = ctx;
+
+ if (!ctx.user || !ctx.org) {
+ throw new TRPCError({
+ code: 'UNPROCESSABLE_CONTENT',
+ message: 'User or Organization is not defined'
+ });
+ }
+ if (!org?.memberId) {
+ throw new TRPCError({
+ code: 'UNPROCESSABLE_CONTENT',
+ message: 'User is not a member of the organization'
+ });
+ }
+
+ const userId = user.id;
+ const orgId = org.id;
+ const userOrgMemberId = org.memberId;
+
+ const { convoPublicId } = input;
+
+ // check if the conversation belongs to the same org, early return if not before multiple db selects
+ const convoResponse = await db.query.convos.findFirst({
+ where: eq(convos.publicId, convoPublicId),
+ columns: {
+ id: true,
+ orgId: true
+ }
+ });
+ if (!convoResponse) {
+ throw new TRPCError({
+ code: 'NOT_FOUND',
+ message: 'Conversation not found'
+ });
+ }
+ if (Number(convoResponse.orgId) !== orgId) {
+ const convoOrgOwnerMembersIds = await db.query.orgMembers.findMany({
+ where: eq(orgMembers.orgId, convoResponse.orgId),
+ columns: {
+ userId: true
+ },
+ with: {
+ org: {
+ columns: {
+ name: true
+ }
+ }
+ }
+ });
+ const convoOrgOwnerUserIds = convoOrgOwnerMembersIds.map((member) =>
+ Number(member?.userId ?? 0)
+ );
+ if (!convoOrgOwnerUserIds.includes(userId)) {
+ throw new TRPCError({
+ code: 'NOT_FOUND',
+ message: 'Conversation not found'
+ });
+ }
+ throw new TRPCError({
+ code: 'UNAUTHORIZED',
+ message: `Conversation is owned by ${convoOrgOwnerMembersIds[0].org.name} organization.`
+ });
+ }
+
+ // TODO: Add filtering for org based on input.filterOrgPublicId
+ const convoDetails = await db.query.convos.findFirst({
+ columns: {
+ publicId: true,
+ lastUpdatedAt: true,
+ createdAt: true
+ },
+ where: eq(convos.id, convoResponse.id),
+ with: {
+ subjects: {
+ columns: {
+ publicId: true,
+ subject: true
+ }
+ },
+ participants: {
+ columns: {
+ publicId: true,
+ orgMemberId: true,
+ userGroupId: true,
+ contactId: true,
+ lastReadAt: true,
+ notifications: true,
+ active: true,
+ role: true
+ },
+ with: {
+ orgMember: {
+ columns: {
+ id: true,
+ publicId: true
+ },
+ with: {
+ profile: {
+ columns: {
+ avatarId: true,
+ firstName: true,
+ lastName: true,
+ publicId: true,
+ handle: true,
+ title: true
+ }
+ }
+ }
+ },
+ userGroup: {
+ columns: {
+ avatarId: true,
+ id: true,
+ name: true,
+ color: true,
+ publicId: true,
+ description: true
+ },
+ with: {
+ members: {
+ columns: {
+ orgMemberId: true
+ }
+ }
+ }
+ },
+ contact: {
+ columns: {
+ avatarId: true,
+ publicId: true,
+ name: true,
+ emailUsername: true,
+ emailDomain: true,
+ setName: true,
+ signature: true
+ }
+ }
+ }
+ },
+ attachments: {
+ columns: {
+ publicId: true,
+ fileName: true,
+ type: true,
+ storageId: true
+ }
+ }
+ }
+ });
+ if (!convoDetails) {
+ throw new TRPCError({
+ code: 'NOT_FOUND',
+ message: 'Conversation not found'
+ });
+ }
+
+ // Find the participant.publicId for the userOrgMemberId
+ let participantPublicId: string | undefined;
+
+ // Check if the user's orgMemberId is in the conversation participants
+ convoDetails?.participants.forEach((participant) => {
+ if (participant.orgMember?.id === userOrgMemberId) {
+ participantPublicId = participant.publicId;
+ }
+ });
+
+ // If not found, check if the user's orgMemberId is in any participant's userGroup members
+ if (!participantPublicId) {
+ convoDetails?.participants.forEach((participant) => {
+ participant.userGroup?.members.forEach((groupMember) => {
+ if (groupMember.orgMemberId === userOrgMemberId) {
+ participantPublicId = participant.publicId;
+ }
+ });
+ });
+ }
+
+ // If participantPublicId is still not found, the user is not a participant of this conversation
+ if (!participantPublicId) {
+ throw new TRPCError({
+ code: 'UNAUTHORIZED',
+ message: 'You are not a participant of this conversation'
+ });
+ }
+
+ // strip the user IDs from the response
+ convoDetails.participants.forEach((participant) => {
+ if (participant.orgMember?.id) participant.orgMember.id = 0;
+ participant.userGroup?.members.forEach((groupMember) => {
+ if (groupMember.orgMemberId) groupMember.orgMemberId = 0;
+ });
+ });
+ return {
+ data: convoDetails,
+ participantPublicId: participantPublicId
+ };
+ }),
+
+ //* get convo entries
+ getConvoEntries: orgProcedure
+ .input(
+ z.object({
+ convoPublicId: nanoIdSchema,
+ cursorLastUpdatedAt: z.date().optional(),
+ cursorLastPublicId: z.string().min(3).max(nanoIdLength).optional()
+ })
+ )
+ .query(async ({ ctx, input }) => {}),
+
+ getUserConvos: orgProcedure
+ .input(
+ z.object({
+ cursorLastUpdatedAt: z.date().optional(),
+ cursorLastPublicId: z.string().min(3).max(nanoIdLength).optional()
+ })
+ )
+ .query(async ({ ctx, input }) => {
+ const { db, user, org } = ctx;
+ const { cursorLastUpdatedAt, cursorLastPublicId } = input;
+
+ const userId = user.id;
+ const orgId = org.id;
+ const orgMemberId = org.memberId;
+
+ const inputLastUpdatedAt = cursorLastUpdatedAt
+ ? new Date(cursorLastUpdatedAt)
+ : new Date();
+
+ console.log('🔥', { inputLastUpdatedAt });
+ const inputLastPublicId = cursorLastPublicId || '';
+
+ const convoQuery = await db.query.convos.findMany({
+ orderBy: [desc(convos.lastUpdatedAt), desc(convos.publicId)],
+ limit: 15,
+ columns: {
+ publicId: true,
+ lastUpdatedAt: true
+ },
+ where: and(
+ or(
+ and(
+ eq(convos.lastUpdatedAt, inputLastUpdatedAt),
+ lt(convos.publicId, inputLastPublicId)
+ ),
+ lt(convos.lastUpdatedAt, inputLastUpdatedAt)
+ ),
+ inArray(
+ convos.id,
+ db
+ .select({ id: convoParticipants.convoId })
+ .from(convoParticipants)
+ .where(
+ or(
+ eq(convoParticipants.orgMemberId, orgMemberId),
+ inArray(
+ convoParticipants.userGroupId,
+ db
+ .select({ id: userGroupMembers.groupId })
+ .from(userGroupMembers)
+ .where(eq(userGroupMembers.orgMemberId, orgMemberId))
+ )
+ )
+ )
+ )
+ ),
+ with: {
+ subjects: {
+ columns: {
+ subject: true
+ }
+ },
+ participants: {
+ columns: {
+ role: true,
+ publicId: true
+ },
+ with: {
+ orgMember: {
+ columns: { publicId: true },
+ with: {
+ profile: {
+ columns: {
+ publicId: true,
+ firstName: true,
+ lastName: true,
+ avatarId: true,
+ handle: true
+ }
+ }
+ }
+ },
+ userGroup: {
+ columns: {
+ publicId: true,
+ name: true,
+ color: true,
+ avatarId: true
+ }
+ },
+ contact: {
+ columns: {
+ publicId: true,
+ name: true,
+ avatarId: true,
+ setName: true,
+ emailUsername: true,
+ emailDomain: true,
+ type: true
+ }
+ }
+ }
+ },
+ entries: {
+ orderBy: [desc(convoEntries.createdAt)],
+ limit: 1,
+ columns: {
+ bodyPlainText: true,
+ type: true
+ },
+ with: {
+ author: {
+ columns: {},
+ with: {
+ orgMember: {
+ columns: {
+ publicId: true
+ },
+ with: {
+ profile: {
+ columns: {
+ publicId: true,
+ firstName: true,
+ lastName: true,
+ avatarId: true,
+ handle: true
+ }
+ }
+ }
+ },
+ userGroup: {
+ columns: {
+ publicId: true,
+ name: true,
+ color: true,
+ avatarId: true
+ }
+ },
+ contact: {
+ columns: {
+ publicId: true,
+ name: true,
+ avatarId: true,
+ setName: true,
+ emailUsername: true,
+ emailDomain: true,
+ type: true
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ });
+
+ if (!convoQuery.length) {
+ return {
+ data: [],
+ cursor: null
+ };
+ }
+
+ const newCursorLastUpdatedAt =
+ convoQuery[convoQuery.length - 1].lastUpdatedAt;
+ const newCursorLastPublicId = convoQuery[convoQuery.length - 1].publicId;
+
+ return {
+ data: convoQuery,
+ cursor: {
+ lastUpdatedAt: newCursorLastUpdatedAt,
+ lastPublicId: newCursorLastPublicId
+ }
+ };
})
- // getUserConvos: orgProcedure
- // .input(
- // z.object({
- // cursorLastUpdatedAt: z.date().optional(),
- // cursorLastPublicId: z.string().min(3).max(nanoIdLength).optional()
- // })
- // )
- // .query(async ({ ctx, input }) => {
- // const { db, user, org } = ctx;
- // const { cursorLastUpdatedAt, cursorLastPublicId } = input;
- // const userId = user?.id || 0;
- // const orgId = org?.id || 0;
- // const inputLastUpdatedAt = cursorLastUpdatedAt
- // ? new Date(cursorLastUpdatedAt)
- // : new Date();
- // const inputLastPublicId = cursorLastPublicId || '';
- // // TODO: Add filtering for org based on input.filterOrgPublicId
- // const convoQuery = await db.query.convos.findMany({
- // orderBy: [desc(convos.lastUpdatedAt), desc(convos.publicId)],
- // limit: 15,
- // columns: {
- // publicId: true,
- // lastUpdatedAt: true
- // },
- // where: and(
- // or(
- // and(
- // eq(convos.lastUpdatedAt, inputLastUpdatedAt),
- // lt(convos.publicId, inputLastPublicId)
- // ),
- // lt(convos.lastUpdatedAt, inputLastUpdatedAt)
- // ),
- // inArray(
- // convos.id,
- // db
- // .select({ id: convoParticipants.convoId })
- // .from(convoParticipants)
- // .where(
- // or(
- // eq(convoParticipants.userId, userId),
- // inArray(
- // convoParticipants.userGroupId,
- // db
- // .select({ id: userGroupMembers.groupId })
- // .from(userGroupMembers)
- // .where(eq(userGroupMembers.userId, userId))
- // )
- // )
- // )
- // )
- // ),
- // with: {
- // org: {
- // columns: {
- // publicId: true,
- // name: true,
- // avatarId: true
- // }
- // },
- // subjects: {
- // columns: {
- // subject: true
- // }
- // },
- // members: {
- // with: {
- // orgMember: {
- // with: {
- // profile: {
- // columns: {
- // firstName: true,
- // lastName: true,
- // avatarId: true,
- // handle: true
- // }
- // }
- // }
- // },
- // userGroup: {
- // columns: {
- // name: true,
- // color: true,
- // avatarId: true
- // }
- // },
- // foreignEmailIdentity: {
- // columns: {
- // senderName: true,
- // avatarId: true,
- // username: true,
- // rootDomain: true
- // }
- // }
- // },
- // columns: {
- // id: true,
- // orgMemberId: true,
- // userGroupId: true,
- // foreignEmailIdentityId: true,
- // role: true
- // }
- // },
- // messages: {
- // orderBy: [desc(convoMessages.createdAt)],
- // limit: 1,
- // columns: {
- // body: true
- // },
- // with: {
- // author: {
- // with: {
- // orgMember: {
- // with: {
- // profile: {
- // columns: {
- // firstName: true,
- // lastName: true,
- // avatarId: true,
- // handle: true
- // }
- // }
- // }
- // },
- // userGroup: {
- // columns: {
- // name: true,
- // color: true,
- // avatarId: true
- // }
- // },
- // foreignEmailIdentity: {
- // columns: {
- // senderName: true,
- // avatarId: true
- // }
- // }
- // }
- // }
- // }
- // }
- // }
- // });
- // const newCursorLastUpdatedAt =
- // convoQuery[convoQuery.length - 1].lastUpdatedAt;
- // const newCursorLastPublicId = convoQuery[convoQuery.length - 1].publicId;
- // return {
- // data: convoQuery,
- // cursor: {
- // lastUpdatedAt: newCursorLastUpdatedAt,
- // lastPublicId: newCursorLastPublicId
- // }
- // };
- // }),
- // getConvo: orgProcedure
- // .input(
- // z.object({
- // convoPublicId: z.string().min(3).max(nanoIdLength)
- // })
- // )
- // .query(async ({ ctx, input }) => {
- // const { db, user, org } = ctx;
- // const { convoPublicId } = input;
- // const userId = user?.id || 0;
- // const orgId = org?.id || 0;
- // // TODO: Add filtering for org based on input.filterOrgPublicId
- // const convoDetails = await db.query.convos.findFirst({
- // columns: {
- // publicId: true,
- // lastUpdatedAt: true,
- // createdAt: true
- // },
- // where: eq(convos.publicId, convoPublicId),
- // with: {
- // org: {
- // columns: {
- // publicId: true,
- // name: true,
- // avatarId: true
- // }
- // },
- // subjects: {
- // columns: {
- // subject: true
- // }
- // },
- // members: {
- // with: {
- // orgMember: {
- // with: {
- // profile: {
- // columns: {
- // userId: true,
- // firstName: true,
- // lastName: true,
- // avatarId: true,
- // publicId: true,
- // handle: true
- // }
- // }
- // }
- // },
- // userGroup: {
- // columns: {
- // name: true,
- // color: true,
- // avatarId: true,
- // publicId: true
- // },
- // with: {
- // members: {
- // columns: {
- // userId: true
- // }
- // }
- // }
- // },
- // foreignEmailIdentity: {
- // columns: {
- // senderName: true,
- // avatarId: true,
- // username: true,
- // rootDomain: true,
- // publicId: true
- // }
- // }
- // },
- // columns: {
- // orgMemberId: true,
- // userGroupId: true,
- // foreignEmailIdentityId: true,
- // role: true
- // }
- // },
- // attachments: {
- // columns: {
- // publicId: true,
- // fileName: true,
- // type: true,
- // storageId: true
- // }
- // }
- // }
- // });
- // if (!convoDetails) {
- // console.log('Convo not found');
- // return {
- // data: null
- // };
- // }
- // //Check if the user is in the conversation
- // const convoParticipantsUserIds: number[] = [];
- // convoDetails?.members.forEach((member) => {
- // member.orgMember?.userId &&
- // convoParticipantsUserIds.push(member.orgMember?.userId);
- // member.orgMember?.profile?.userId &&
- // convoParticipantsUserIds.push(member.orgMember?.profile.userId);
- // member.userGroup?.members.forEach((groupMember) => {
- // groupMember.userId &&
- // convoParticipantsUserIds.push(groupMember.userId);
- // });
- // });
- // if (!convoParticipantsUserIds.includes(+userId)) {
- // console.log('User not in convo');
- // console.log({ userId, convoParticipantsUserIds });
- // return {
- // data: null
- // };
- // }
- // // strip the user IDs from the response
- // convoDetails.members.forEach((member) => {
- // if (member.orgMember?.userId) member.orgMember.userId = 0;
- // if (member.orgMember?.profile?.userId)
- // member.orgMember.profile.userId = 0;
- // member.userGroup?.members.forEach((groupMember) => {
- // if (groupMember.userId) groupMember.userId = 0;
- // });
- // });
- // return {
- // data: convoDetails
- // };
- // }),
});
diff --git a/apps/web-app/server/trpc/routers/convoRouter/entryRouter.ts b/apps/web-app/server/trpc/routers/convoRouter/entryRouter.ts
new file mode 100644
index 00000000..a9ed878e
--- /dev/null
+++ b/apps/web-app/server/trpc/routers/convoRouter/entryRouter.ts
@@ -0,0 +1,167 @@
+import { z } from 'zod';
+import { router, orgProcedure, limitedProcedure } from '../../trpc';
+import { and, desc, eq, inArray, lt, or } from '@uninbox/database/orm';
+import { convos, convoEntries } from '@uninbox/database/schema';
+import {
+ nanoId,
+ nanoIdLength,
+ nanoIdToken,
+ nanoIdSchema
+} from '@uninbox/utils';
+import { TRPCError } from '@trpc/server';
+
+export const convoEntryRouter = router({
+ getConvoEntries: orgProcedure
+ .input(
+ z.object({
+ convoPublicId: nanoIdSchema,
+ cursorLastCreatedAt: z.date().optional(),
+ cursorLastPublicId: nanoIdSchema.optional()
+ })
+ )
+ .query(async ({ ctx, input }) => {
+ const { db, user, org } = ctx;
+
+ if (!ctx.user || !ctx.org) {
+ throw new TRPCError({
+ code: 'UNPROCESSABLE_CONTENT',
+ message: 'User or Organization is not defined'
+ });
+ }
+ if (!org?.memberId) {
+ throw new TRPCError({
+ code: 'UNPROCESSABLE_CONTENT',
+ message: 'User is not a member of the organization'
+ });
+ }
+
+ const userId = user.id;
+ const orgId = org.id;
+ const userOrgMemberId = org.memberId;
+
+ const { convoPublicId, cursorLastCreatedAt, cursorLastPublicId } = input;
+ const inputLastCreatedAt = cursorLastCreatedAt
+ ? new Date(cursorLastCreatedAt)
+ : new Date();
+ const inputLastPublicId = cursorLastPublicId || '';
+
+ // check if the conversation belongs to the same org, early return if not before multiple db selects
+ const convoResponse = await db.query.convos.findFirst({
+ where: and(eq(convos.publicId, convoPublicId), eq(convos.orgId, orgId)),
+ columns: {
+ id: true
+ }
+ });
+ if (!convoResponse) {
+ throw new TRPCError({
+ code: 'NOT_FOUND',
+ message: 'Conversation not found or not in this organization'
+ });
+ }
+
+ // TODO: Add filtering for org based on input.filterOrgPublicId
+ const convoDetails = await db.query.convos.findFirst({
+ columns: {
+ id: true
+ },
+ where: eq(convos.id, convoResponse.id),
+ with: {
+ participants: {
+ columns: {
+ id: true
+ },
+ with: {
+ orgMember: {
+ columns: {
+ id: true
+ }
+ },
+ userGroup: {
+ columns: {
+ id: true
+ },
+ with: {
+ members: {
+ columns: {
+ orgMemberId: true
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ });
+ if (!convoDetails) {
+ throw new TRPCError({
+ code: 'NOT_FOUND',
+ message: 'Conversation not found'
+ });
+ }
+
+ //Check if the user's orgMemberId is in the conversation
+ const convoParticipantsOrgMemberIds: number[] = [];
+ convoDetails?.participants.forEach((participant) => {
+ participant.orgMember?.id &&
+ convoParticipantsOrgMemberIds.push(participant.orgMember?.id);
+ participant.userGroup?.members.forEach((groupMember) => {
+ groupMember.orgMemberId &&
+ convoParticipantsOrgMemberIds.push(groupMember.orgMemberId);
+ });
+ });
+
+ if (!convoParticipantsOrgMemberIds.includes(userOrgMemberId)) {
+ throw new TRPCError({
+ code: 'UNAUTHORIZED',
+ message: 'You are not a participant of this conversation'
+ });
+ }
+
+ // get the entries
+ const convoEntriesQuery = await db.query.convoEntries.findMany({
+ where: and(
+ or(
+ and(
+ eq(convoEntries.createdAt, inputLastCreatedAt),
+ lt(convoEntries.publicId, inputLastPublicId)
+ ),
+ lt(convoEntries.createdAt, inputLastCreatedAt)
+ ),
+ eq(convoEntries.convoId, convoDetails.id)
+ ),
+ orderBy: [desc(convoEntries.createdAt), desc(convoEntries.publicId)],
+ limit: 15,
+ columns: {
+ publicId: true,
+ createdAt: true,
+ body: true,
+ type: true
+ },
+ with: {
+ subject: {
+ columns: {
+ publicId: true,
+ subject: true
+ }
+ },
+ attachments: {
+ columns: {
+ publicId: true,
+ fileName: true,
+ type: true,
+ storageId: true
+ }
+ },
+ author: {
+ columns: {
+ publicId: true
+ }
+ }
+ }
+ });
+
+ return {
+ entries: convoEntriesQuery
+ };
+ })
+});
diff --git a/apps/web-app/server/trpc/routers/convoRouter/messageRouter.ts b/apps/web-app/server/trpc/routers/convoRouter/messageRouter.ts
deleted file mode 100644
index 20f616da..00000000
--- a/apps/web-app/server/trpc/routers/convoRouter/messageRouter.ts
+++ /dev/null
@@ -1,144 +0,0 @@
-import { z } from 'zod';
-import { router, orgProcedure, limitedProcedure } from '../../trpc';
-import { and, desc, eq, inArray, lt, or } from '@uninbox/database/orm';
-import {
- convos,
- convoMembers,
- convoSubjects,
- userProfiles,
- foreignEmailIdentities,
- userGroups,
- userGroupMembers,
- convoMessages
-} from '@uninbox/database/schema';
-import { nanoId, nanoIdLength, nanoIdToken, nanoIdSchema } from '@uninbox/utils';
-
-export const messageRouter = router({
- getConvoMessages: orgProcedure
- .input(
- z.object({
- convoPublicId: nanoIdSchema,
- cursorLastCreatedAt: z.date().optional(),
- cursorLastPublicId: nanoIdSchema.optional()
- })
- )
- .query(async ({ ctx, input }) => {
- if (!ctx.user || !ctx.org) {
- throw new TRPCError({
- code: 'UNPROCESSABLE_CONTENT',
- message: 'User or Organization is not defined'
- });
- }
- const { db, user, org } = ctx;
- const userId = +user?.id;
- const orgId = +org?.id;
- const { convoPublicId, cursorLastCreatedAt, cursorLastPublicId } = input;
-
- const inputLastCreatedAt = cursorLastCreatedAt
- ? new Date(cursorLastCreatedAt)
- : new Date();
- const inputLastPublicId = cursorLastPublicId || '';
-
- // TODO: Find a better way to do this
- // Verify the user is in the convo
- const convoDetails = await db.query.convos.findFirst({
- where: eq(convos.publicId, convoPublicId),
- columns: {
- id: true
- },
- with: {
- members: {
- with: {
- userProfile: {
- columns: {
- userId: true,
- firstName: true,
- lastName: true,
- publicId: true,
- handle: true
- }
- },
- userGroup: {
- columns: {
- name: true,
- color: true,
- publicId: true
- },
- with: {
- members: {
- columns: {
- userId: true
- }
- }
- }
- },
- foreignEmailIdentity: {
- columns: {
- senderName: true,
- username: true,
- rootDomain: true,
- publicId: true
- }
- }
- },
- columns: {
- userId: true,
- userGroupId: true,
- foreignEmailIdentityId: true,
- role: true
- }
- }
- }
- });
-
- if (!convoDetails) {
- console.log('Convo not found');
- return {
- error: 'Convo not found'
- };
- }
- //Check if the user is in the conversation
- const convoMembersUserIds: number[] = [];
- convoDetails?.members.forEach((member) => {
- member.userId && convoMembersUserIds.push(member.userId);
- member.userProfile?.userId &&
- convoMembersUserIds.push(member.userProfile.userId);
- member.userGroup?.members.forEach((groupMember) => {
- groupMember.userId && convoMembersUserIds.push(groupMember.userId);
- });
- });
-
- if (!convoMembersUserIds.includes(+userId)) {
- console.log('User not in convo');
- console.log({ userId, convoMembersUserIds });
- return {
- error: 'User not in convo'
- };
- }
-
- const convoMessagesReturn = await db.query.convoMessages.findMany({
- orderBy: [desc(convoMessages.createdAt), desc(convoMessages.publicId)],
- limit: 15,
- columns: {
- publicId: true,
- createdAt: true,
- author: true,
- body: true
- },
- where: and(
- or(
- and(
- eq(convoMessages.createdAt, inputLastCreatedAt),
- lt(convoMessages.publicId, inputLastPublicId)
- ),
- lt(convoMessages.createdAt, inputLastCreatedAt)
- ),
- eq(convoMessages.convoId, convoDetails.id)
- )
- });
-
- return {
- messages: convoMessagesReturn
- };
- })
-});
diff --git a/apps/web-app/tailwind.config.ts b/apps/web-app/tailwind.config.ts
index f96b517c..da49fd47 100644
--- a/apps/web-app/tailwind.config.ts
+++ b/apps/web-app/tailwind.config.ts
@@ -9,6 +9,7 @@ export default >{
darkMode: 'class',
plugins: [colors.plugin],
content: ['docs/content/**/*.md'],
+ safeList: ['items-end', 'items-start', 'rounded-br-none', 'rounded-bl-none'],
theme: {
extend: {
fontFamily: {
diff --git a/packages/database/schema.ts b/packages/database/schema.ts
index 5c547d97..2595d7d5 100644
--- a/packages/database/schema.ts
+++ b/packages/database/schema.ts
@@ -1074,7 +1074,7 @@ export const convos = mysqlTable(
id: serial('id').primaryKey(),
orgId: foreignKey('org_id').notNull(),
publicId: nanoId('public_id').notNull(),
- lastUpdatedAt: timestamp('last_updated_at'),
+ lastUpdatedAt: timestamp('last_updated_at').notNull(),
createdAt: timestamp('created_at')
.default(sql`CURRENT_TIMESTAMP`)
.notNull()
@@ -1089,7 +1089,7 @@ export const convosRelations = relations(convos, ({ one, many }) => ({
fields: [convos.orgId],
references: [orgs.id]
}),
- members: many(convoParticipants),
+ participants: many(convoParticipants),
attachments: many(convoAttachments),
entries: many(convoEntries),
subjects: many(convoSubjects)
@@ -1205,7 +1205,7 @@ export const convoAttachments = mysqlTable(
fileName: varchar('fileName', { length: 256 }).notNull(),
type: varchar('type', { length: 256 }).notNull(),
storageId: varchar('storageId', { length: 256 }).notNull(),
- convoMemberId: foreignKey('convo_members').notNull(),
+ convoParticipantId: foreignKey('convo_participant_id').notNull(),
createdAt: timestamp('created_at')
.default(sql`CURRENT_TIMESTAMP`)
.notNull()
@@ -1233,7 +1233,7 @@ export const convoAttachmentsRelations = relations(
references: [convoEntries.id]
}),
uploader: one(convoParticipants, {
- fields: [convoAttachments.convoMemberId],
+ fields: [convoAttachments.convoParticipantId],
references: [convoParticipants.id]
})
})
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 98f34164..24e7e777 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -224,6 +224,9 @@ importers:
trpc-nuxt:
specifier: ^0.10.17
version: 0.10.17(@trpc/client@10.45.0)(@trpc/server@10.45.0)
+ vue-virtual-scroller:
+ specifier: 2.0.0-beta.8
+ version: 2.0.0-beta.8(vue@3.4.15)
zod:
specifier: ^3.22.4
version: 3.22.4
@@ -3432,6 +3435,7 @@ packages:
dependencies:
is-glob: 4.0.3
micromatch: 4.0.5
+ napi-wasm: 1.1.0
bundledDependencies:
- napi-wasm
@@ -10816,6 +10820,10 @@ packages:
minipass: 3.3.6
yallist: 4.0.0
+ /mitt@2.1.0:
+ resolution: {integrity: sha512-ILj2TpLiysu2wkBbWjAmww7TkZb65aiQO+DkVdUTBpBXq+MHYiETENkKFMtsJZX1Lf4pe4QOrTSjIfUwN5lRdg==}
+ dev: false
+
/mitt@3.0.1:
resolution: {integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==}
dev: false
@@ -10944,6 +10952,9 @@ packages:
hasBin: true
dev: false
+ /napi-wasm@1.1.0:
+ resolution: {integrity: sha512-lHwIAJbmLSjF9VDRm9GoVOy9AGp3aIvkjv+Kvz9h16QR3uSVYH78PNQUnT2U4X53mhlnV2M7wrhibQ3GHicDmg==}
+
/natural-compare-lite@1.4.0:
resolution: {integrity: sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==}
dev: true
@@ -14889,6 +14900,22 @@ packages:
vue: 3.4.15(typescript@5.3.3)
dev: false
+ /vue-observe-visibility@2.0.0-alpha.1(vue@3.4.15):
+ resolution: {integrity: sha512-flFbp/gs9pZniXR6fans8smv1kDScJ8RS7rEpMjhVabiKeq7Qz3D9+eGsypncjfIyyU84saU88XZ0zjbD6Gq/g==}
+ peerDependencies:
+ vue: ^3.0.0
+ dependencies:
+ vue: 3.4.15(typescript@5.3.3)
+ dev: false
+
+ /vue-resize@2.0.0-alpha.1(vue@3.4.15):
+ resolution: {integrity: sha512-7+iqOueLU7uc9NrMfrzbG8hwMqchfVfSzpVlCMeJQe4pyibqyoifDNbKTZvwxZKDvGkB+PdFeKvnGZMoEb8esg==}
+ peerDependencies:
+ vue: ^3.0.0
+ dependencies:
+ vue: 3.4.15(typescript@5.3.3)
+ dev: false
+
/vue-router@4.2.5(vue@3.4.15):
resolution: {integrity: sha512-DIUpKcyg4+PTQKfFPX88UWhlagBEBEfJ5A8XDXRJLUnZOvcpMF8o/dnL90vpVkGaPbjvXazV/rC1qBKrZlFugw==}
peerDependencies:
@@ -14914,6 +14941,17 @@ packages:
semver: 7.5.4
typescript: 5.3.3
+ /vue-virtual-scroller@2.0.0-beta.8(vue@3.4.15):
+ resolution: {integrity: sha512-b8/f5NQ5nIEBRTNi6GcPItE4s7kxNHw2AIHLtDp+2QvqdTjVN0FgONwX9cr53jWRgnu+HRLPaWDOR2JPI5MTfQ==}
+ peerDependencies:
+ vue: ^3.2.0
+ dependencies:
+ mitt: 2.1.0
+ vue: 3.4.15(typescript@5.3.3)
+ vue-observe-visibility: 2.0.0-alpha.1(vue@3.4.15)
+ vue-resize: 2.0.0-alpha.1(vue@3.4.15)
+ dev: false
+
/vue-wrap-balancer@1.1.3(vue@3.4.15):
resolution: {integrity: sha512-9kTRwYIveWxV1FdaCJfRjIIRZOtwgnxypGS5mlAiXnih5+Cfaby9YDh3APMW1jWp0oCvL+gep0XCbcjBb7/ZXQ==}
peerDependencies: