feat(fe2): invite + list workspace invites (#2629)

* list invites table

* invites list works

* update last reminded date on resend

* fix FE

* WIP invitedialog + updated debounced utility

* invite create works

* exclude users correctly

* more adjustments

* minor cleanup

* using workspace invite server role

* test fix

* fixed multiple root eslint issues

* minor adjustments
This commit is contained in:
Kristaps Fabians Geikins
2024-08-12 11:30:01 +03:00
committed by GitHub
parent 03db1cca94
commit 4dae1569cd
69 changed files with 1903 additions and 1327 deletions
@@ -9,7 +9,10 @@ import { ServerInviteResourceFilter } from '@/modules/serverinvites/repositories
export type FindUserByTarget = (target: string) => Promise<UserWithOptionalRole | null>
export type ServerInviteRecordInsertModel = Omit<ServerInviteRecord, 'createdAt'>
export type ServerInviteRecordInsertModel = Omit<
ServerInviteRecord,
'createdAt' | 'updatedAt'
>
export type InsertInviteAndDeleteOld = (
invite: ServerInviteRecordInsertModel,
@@ -57,7 +60,7 @@ export type QueryAllResourceInvites = <
filter: Pick<
InviteResourceTarget<TargetType, RoleType>,
'resourceId' | 'resourceType'
>
> & { search?: string }
) => Promise<ServerInviteRecord<InviteResourceTarget<TargetType, RoleType>>[]>
export type DeleteAllResourceInvites = <
@@ -104,3 +107,5 @@ export type CreateInviteParams = {
message?: string | null
primaryResourceTarget: PrimaryInviteResourceTarget
}
export type MarkInviteUpdated = (params: { inviteId: string }) => Promise<boolean>
@@ -54,6 +54,7 @@ export type ServerInviteRecord<
target: string
inviterId: string
createdAt: Date
updatedAt: Date
message: Nullable<string>
resource: PrimaryInviteResourceTarget<Resource>
token: string
@@ -28,7 +28,8 @@ import {
insertInviteAndDeleteOldFactory,
deleteInviteFactory as deleteInviteFromDbFactory,
queryAllUserResourceInvitesFactory,
queryAllResourceInvitesFactory
queryAllResourceInvitesFactory,
markInviteUpdatedfactory
} from '@/modules/serverinvites/repositories/serverInvites'
import {
createProjectInviteFactory,
@@ -301,7 +302,8 @@ export = {
getStream
}),
findUserByTarget: findUserByTargetFactory(),
findInvite: findInviteFactory({ db })
findInvite: findInviteFactory({ db }),
markInviteUpdated: markInviteUpdatedfactory({ db })
})
await resendInviteEmail({ inviteId })
@@ -0,0 +1,21 @@
import { Knex } from 'knex'
const TABLE_NAME = 'server_invites'
export async function up(knex: Knex): Promise<void> {
await knex.schema.alterTable(TABLE_NAME, (table) => {
table
.timestamp('updatedAt', { precision: 3, useTz: true })
.defaultTo(knex.fn.now())
.notNullable()
})
// set updatedAt to be same value as createdAt
await knex(TABLE_NAME).update({ updatedAt: knex.raw('??', ['createdAt']) })
}
export async function down(knex: Knex): Promise<void> {
await knex.schema.alterTable(TABLE_NAME, (table) => {
table.dropColumn('updatedAt')
})
}
@@ -1,4 +1,4 @@
import { knex, ServerInvites, Streams } from '@/modules/core/dbSchema'
import { knex, ServerInvites, Streams, Users } from '@/modules/core/dbSchema'
import {
getUserByEmail,
getUser,
@@ -24,6 +24,7 @@ import {
FindServerInvite,
FindServerInvites,
InsertInviteAndDeleteOld,
MarkInviteUpdated,
QueryAllResourceInvites,
QueryAllUserResourceInvites,
QueryInvites,
@@ -91,7 +92,7 @@ const buildInvitesBaseQuery =
const q = db(ServerInvites.name)
.select<Result>(ServerInvites.cols)
.orderBy(ServerInvites.col.createdAt, sort)
.orderBy(ServerInvites.col.updatedAt, sort)
// single built in filter
projectInviteValidityFilter(q)
@@ -241,13 +242,31 @@ export const queryAllResourceInvitesFactory =
filter: Pick<
InviteResourceTarget<TargetType, RoleType>,
'resourceId' | 'resourceType'
>
> & { search?: string }
) => {
if (!filter.resourceId) return []
return await buildInvitesBaseQuery({ db })<
const q = buildInvitesBaseQuery({ db })<
ServerInviteRecord<InviteResourceTarget<TargetType, RoleType>>[]
>({ filterQuery }).where((q) => filterByResource(q, filter))
>({ filterQuery })
q.where((q) => filterByResource(q, filter))
if (filter.search) {
q.leftJoin(
Users.name,
Users.col.id,
knex.raw('SUBSTRING(?? FROM 2)', [ServerInvites.col.target])
).where((w1) => {
w1.where(ServerInvites.col.target, 'ILIKE', `%${filter.search}%`).orWhere(
Users.col.name,
'ILIKE',
`%${filter.search}%`
)
})
}
return await q
}
export const deleteAllResourceInvitesFactory =
@@ -481,3 +500,13 @@ export const findInviteByTokenFactory =
return (await q) || null
}
export const markInviteUpdatedfactory =
({ db }: { db: Knex }): MarkInviteUpdated =>
async ({ inviteId }) => {
const cols = ServerInvites.with({ withoutTablePrefix: true }).col
const ret = await db(ServerInvites.name)
.where(ServerInvites.col.id, inviteId)
.update(cols.updatedAt, new Date())
return !!ret
}
@@ -13,6 +13,7 @@ import {
FindInvite,
FindUserByTarget,
InsertInviteAndDeleteOld,
MarkInviteUpdated,
ServerInviteRecordInsertModel
} from '@/modules/serverinvites/domain/operations'
import {
@@ -188,11 +189,13 @@ export const resendInviteEmailFactory =
({
buildInviteEmailContents,
findUserByTarget,
findInvite
findInvite,
markInviteUpdated
}: {
buildInviteEmailContents: BuildInviteEmailContents
findUserByTarget: FindUserByTarget
findInvite: FindInvite
markInviteUpdated: MarkInviteUpdated
}): ResendInviteEmail =>
async (params: { inviteId: string }) => {
const sendInviteEmail = sendInviteEmailFactory({ buildInviteEmailContents })
@@ -221,4 +224,6 @@ export const resendInviteEmailFactory =
targetUser,
targetData
})
await markInviteUpdated({ inviteId })
}
@@ -41,6 +41,8 @@ import {
UseStreamInviteDocument
} from '@/test/graphql/generated/graphql'
import { grantStreamPermissions } from '@/modules/core/repositories/streams'
import { ServerInviteRecord } from '@/modules/serverinvites/domain/types'
import { reduce } from 'lodash'
async function cleanup() {
await truncateTables([ServerInvites.name, Streams.name, Users.name])
@@ -503,6 +505,17 @@ describe('[Stream & Server Invites]', () => {
)
const inviteIds = invites.map((i) => i.inviteId)
const inviteLastRemindedDates = reduce(
await ServerInvites.knex<ServerInviteRecord[]>().whereIn(
ServerInvites.col.id,
inviteIds
),
(res, item) => {
res[item.id] = item.updatedAt
return res
},
{} as Record<string, Date>
)
const results = await Promise.all(
inviteIds.map((inviteId) =>
@@ -516,6 +529,22 @@ describe('[Stream & Server Invites]', () => {
}
expect(sendEmailInvocations.length()).to.eq(inviteIds.length)
const newInviteLastRemindedDates = reduce(
await ServerInvites.knex<ServerInviteRecord[]>().whereIn(
ServerInvites.col.id,
inviteIds
),
(res, item) => {
res[item.id] = item.updatedAt
return res
},
{} as Record<string, Date>
)
for (const [id, newDate] of Object.entries(newInviteLastRemindedDates)) {
expect(newDate).to.be.greaterThan(inviteLastRemindedDates[id])
}
})
it('they can delete pre-existing invites irregardless of type', async () => {