diff --git a/packages/frontend-2/components/server-management/CardRow.vue b/packages/frontend-2/components/server-management/CardRow.vue
index 88cd69512..e6aa96180 100644
--- a/packages/frontend-2/components/server-management/CardRow.vue
+++ b/packages/frontend-2/components/server-management/CardRow.vue
@@ -21,12 +21,18 @@
{{ cta.label }}
+
+
+
+ {{ cta.label }}
+
+
diff --git a/packages/frontend-2/lib/common/generated/gql/graphql.ts b/packages/frontend-2/lib/common/generated/gql/graphql.ts
index 046dfe4ca..bfca57235 100644
--- a/packages/frontend-2/lib/common/generated/gql/graphql.ts
+++ b/packages/frontend-2/lib/common/generated/gql/graphql.ts
@@ -1305,6 +1305,7 @@ export type Project = {
/** Return metadata about resources being requested in the viewer */
viewerResources: Array;
visibility: ProjectVisibility;
+ webhooks?: Maybe;
};
@@ -1355,6 +1356,11 @@ export type ProjectViewerResourcesArgs = {
resourceIdString: Scalars['String'];
};
+
+export type ProjectWebhooksArgs = {
+ id?: InputMaybe;
+};
+
export type ProjectCollaborator = {
__typename?: 'ProjectCollaborator';
role: Scalars['String'];
diff --git a/packages/frontend-2/lib/server-management/helpers/types.ts b/packages/frontend-2/lib/server-management/helpers/types.ts
index b00f9bede..979fc40cf 100644
--- a/packages/frontend-2/lib/server-management/helpers/types.ts
+++ b/packages/frontend-2/lib/server-management/helpers/types.ts
@@ -20,9 +20,9 @@ export type InviteItem = NonNullable<
>
export interface CTA {
- type: 'button' | 'link'
+ type: 'button' | 'link' | 'text'
label: string
- action: () => MaybeAsync
+ action?: () => MaybeAsync
}
export interface Button {
@@ -35,11 +35,5 @@ export interface CardInfo {
title: string
value: string
icon: ConcreteComponent
- cta?:
- | {
- type: 'button' | 'link'
- label: string
- action: () => MaybeAsync
- }
- | undefined
+ cta?: CTA
}
diff --git a/packages/frontend-2/pages/server-management/active-users.vue b/packages/frontend-2/pages/server-management/active-users.vue
index 9b93e5213..a2698adf9 100644
--- a/packages/frontend-2/pages/server-management/active-users.vue
+++ b/packages/frontend-2/pages/server-management/active-users.vue
@@ -91,7 +91,10 @@
+
+
userToModify.value?.role as Optional)
diff --git a/packages/frontend-2/pages/server-management/index.vue b/packages/frontend-2/pages/server-management/index.vue
index ee3d7548d..914fc063e 100644
--- a/packages/frontend-2/pages/server-management/index.vue
+++ b/packages/frontend-2/pages/server-management/index.vue
@@ -66,6 +66,7 @@ const isLatestVersion = computed(() => {
!latestVersion.value ||
currentVersion.value === latestVersion.value ||
currentVersion.value === 'dev' ||
+ currentVersion.value?.includes('alpha') ||
currentVersion.value === 'N/A'
)
})
@@ -92,7 +93,10 @@ const serverData = computed((): CardInfo[] => [
label: 'Update is available',
action: openGithubReleasePage
}
- : undefined
+ : {
+ type: 'text',
+ label: 'Up-to-date'
+ }
}
])
const userData = computed((): CardInfo[] => [
diff --git a/packages/frontend-2/pages/server-management/pending-invitations.vue b/packages/frontend-2/pages/server-management/pending-invitations.vue
index f8d2da232..c68b06a87 100644
--- a/packages/frontend-2/pages/server-management/pending-invitations.vue
+++ b/packages/frontend-2/pages/server-management/pending-invitations.vue
@@ -85,7 +85,10 @@
:result-variables="resultVariables"
/>
+
+
({
limit: 50,
query: searchString.value
diff --git a/packages/frontend-2/pages/server-management/projects.vue b/packages/frontend-2/pages/server-management/projects.vue
index df2a05d54..36658550a 100644
--- a/packages/frontend-2/pages/server-management/projects.vue
+++ b/packages/frontend-2/pages/server-management/projects.vue
@@ -89,7 +89,10 @@
+
+
({
limit: 50,
query: searchString.value
diff --git a/packages/server/modules/core/tests/usersAdminList.spec.ts b/packages/server/modules/core/tests/usersAdminList.spec.ts
index 3642a3475..c0a975f91 100644
--- a/packages/server/modules/core/tests/usersAdminList.spec.ts
+++ b/packages/server/modules/core/tests/usersAdminList.spec.ts
@@ -12,6 +12,10 @@ import { expect } from 'chai'
import { ApolloServer } from 'apollo-server-express'
import { ServerInviteRecord } from '@/modules/serverinvites/helpers/types'
import { Optional } from '@/modules/shared/helpers/typeHelper'
+import { wait } from '@speckle/shared'
+
+// To ensure that the invites are created in the correct order, we need to wait a bit between each creation
+const WAIT_TIMEOUT = 5
function randomEl(array: T[]): T {
return array[Math.floor(Math.random() * array.length)]
@@ -90,18 +94,18 @@ describe('[Admin users list]', () => {
let remainingSearchQueryInviteCount = SEARCH_QUERY_RESULT_COUNT
// Create Users
- await Promise.all(
- // count - 1, cause `me` also exists
- times(USER_COUNT - 1, (i) =>
- createUser({
- name: `User #${i} - ${
- remainingSearchQueryUserCount-- >= 1 ? SEARCH_QUERY : ''
- }`,
- email: `speckleuser${i}@gmail.com`,
- password: 'sn3aky-1337-b1m'
- }).then((id) => userIds.push(id))
- )
- )
+ // count - 1, cause `me` also exists
+ for (let i = 0; i < USER_COUNT - 1; i++) {
+ const id = await createUser({
+ name: `User #${i} - ${
+ remainingSearchQueryUserCount-- >= 1 ? SEARCH_QUERY : ''
+ }`,
+ email: `speckleuser${i}@gmail.com`,
+ password: 'sn3aky-1337-b1m'
+ })
+ userIds.push(id)
+ await wait(WAIT_TIMEOUT)
+ }
// Create streams
const streamData: { id: string; ownerId: string }[] = []
@@ -117,34 +121,35 @@ describe('[Admin users list]', () => {
)
// Create invites
- await Promise.all([
- // Server invites
- ...times(SERVER_INVITE_COUNT, (i) =>
- createInviteDirectly(
- {
- email: `randominvitee${i}.${
- remainingSearchQueryInviteCount-- >= 1 ? SEARCH_QUERY : ''
- }@gmail.com`
- },
- randomEl(userIds)
- )
- ),
- // Stream invites
- ...times(STREAM_INVITE_COUNT, (i) => {
- const { id: streamId, ownerId } = randomEl(streamData)
- const email = `streamrandominvitee${i}.${
- remainingSearchQueryInviteCount-- >= 1 ? SEARCH_QUERY : ''
- }@gmail.com`
+ // Server invites
+ for (let i = 0; i < SERVER_INVITE_COUNT; i++) {
+ await createInviteDirectly(
+ {
+ email: `randominvitee${i}.${
+ remainingSearchQueryInviteCount-- >= 1 ? SEARCH_QUERY : ''
+ }@gmail.com`
+ },
+ randomEl(userIds)
+ )
+ await wait(WAIT_TIMEOUT)
+ }
- return createInviteDirectly(
- {
- streamId,
- email
- },
- ownerId
- )
- })
- ])
+ // Stream invites
+ for (let i = 0; i < STREAM_INVITE_COUNT; i++) {
+ const { id: streamId, ownerId } = randomEl(streamData)
+ const email = `streamrandominvitee${i}.${
+ remainingSearchQueryInviteCount-- >= 1 ? SEARCH_QUERY : ''
+ }@gmail.com`
+
+ await createInviteDirectly(
+ {
+ streamId,
+ email
+ },
+ ownerId
+ )
+ await wait(WAIT_TIMEOUT)
+ }
// Create a few more stream invites to registered users, which should not appear in
// the users list
diff --git a/packages/server/modules/serverinvites/migrations/20230907131636_migrate_invites_to_lower_precision_timestamps.ts b/packages/server/modules/serverinvites/migrations/20230907131636_migrate_invites_to_lower_precision_timestamps.ts
new file mode 100644
index 000000000..74ea3cf91
--- /dev/null
+++ b/packages/server/modules/serverinvites/migrations/20230907131636_migrate_invites_to_lower_precision_timestamps.ts
@@ -0,0 +1,31 @@
+import { Knex } from 'knex'
+
+/**
+ * MIGRATING STREAMS TIMESTAMP FIELDS TO A LOWER PRECISION, CAUSE JS CANT HANDLE
+ * IT BEING THAT HIGH AND THIS GENERATES BUGS
+ */
+
+const TABLE_NAME = 'server_invites'
+const TIMESTAMP_COLUMNS = ['createdAt']
+
+export async function up(knex: Knex): Promise {
+ await knex.schema.alterTable(TABLE_NAME, (table) => {
+ TIMESTAMP_COLUMNS.forEach((col) => {
+ table
+ .timestamp(col, { precision: 3, useTz: true })
+ .defaultTo(knex.fn.now())
+ .alter()
+ })
+ })
+}
+
+export async function down(knex: Knex): Promise {
+ await knex.schema.alterTable(TABLE_NAME, (table) => {
+ TIMESTAMP_COLUMNS.forEach((col) => {
+ table
+ .timestamp(col, { precision: 6, useTz: true })
+ .defaultTo(knex.fn.now())
+ .alter()
+ })
+ })
+}
diff --git a/packages/server/modules/serverinvites/repositories/index.js b/packages/server/modules/serverinvites/repositories/index.js
index 2c3d48463..d1fbf4b85 100644
--- a/packages/server/modules/serverinvites/repositories/index.js
+++ b/packages/server/modules/serverinvites/repositories/index.js
@@ -13,7 +13,7 @@ const { getStream } = require('@/modules/core/repositories/streams')
/**
* Use this wherever you're retrieving invites, not necessarily where you're writing to them
*/
-const getInvitesBaseQuery = () => {
+const getInvitesBaseQuery = (sort = 'asc') => {
const q = ServerInvites.knex().select(ServerInvites.cols)
// join just to ensure we don't retrieve invalid invites
@@ -25,7 +25,7 @@ const getInvitesBaseQuery = () => {
w1.whereNull(ServerInvites.col.resourceId).orWhereNotNull(Streams.col.id)
})
- q.orderBy(ServerInvites.col.createdAt)
+ q.orderBy(ServerInvites.col.createdAt, sort)
return q
}
@@ -225,8 +225,8 @@ async function deleteStreamInvite(inviteId) {
.delete()
}
-function findServerInvitesBaseQuery(searchQuery) {
- const q = getInvitesBaseQuery()
+function findServerInvitesBaseQuery(searchQuery, sort) {
+ const q = getInvitesBaseQuery(sort)
if (searchQuery) {
// TODO: Is this safe from SQL injection?
@@ -272,10 +272,9 @@ async function findServerInvites(searchQuery, limit, offset) {
* @returns {Promise}
*/
async function queryServerInvites(searchQuery, limit, cursor) {
- const q = findServerInvitesBaseQuery(searchQuery)
- .limit(limit)
- .orderBy(ServerInvites.col.createdAt, 'desc')
- if (cursor) q.where(ServerInvites.col.createdAt, '<', cursor)
+ const q = findServerInvitesBaseQuery(searchQuery, 'desc').limit(limit)
+
+ if (cursor) q.where(ServerInvites.col.createdAt, '<', cursor.toISOString())
return await q
}