Browse Source

Merge pull request #28246 from overleaf/td-ts-project-dashboard-jsdoc

Working JSDoc type annotations on project list controller

GitOrigin-RevId: b26833affb0fc2ecd38e869c2523e914eabe6548
Tim Down 11 tháng trước cách đây
mục cha
commit
36cbe840dd

+ 1 - 0
services/web/app/src/Features/Authorization/PermissionsController.js

@@ -1,3 +1,4 @@
+// @ts-check
 const { ForbiddenError, UserNotFoundError } = require('../Errors/Errors')
 const {
   getUserCapabilities,

+ 3 - 0
services/web/app/src/Features/Authorization/PrivilegeLevels.js

@@ -1,3 +1,6 @@
+// @ts-check
+
+/** @type {import('./types').PrivilegeLevelsType} */
 const PrivilegeLevels = {
   NONE: false,
   READ_ONLY: 'readOnly',

+ 4 - 0
services/web/app/src/Features/Authorization/PublicAccessLevels.js

@@ -1,3 +1,5 @@
+// @ts-check
+
 /**
  * Note:
  * It used to be that `project.publicAccessLevel` could be set to `private`,
@@ -9,6 +11,8 @@
  * `publicAccessLevel` to the legacy values, there are projects in the system
  * that already have those values set.
  */
+
+/** @type {import('./types').PublicAccessLevelsType} */
 module.exports = {
   READ_ONLY: 'readOnly', // LEGACY
   READ_AND_WRITE: 'readAndWrite', // LEGACY

+ 3 - 0
services/web/app/src/Features/Authorization/Sources.js

@@ -1,3 +1,6 @@
+// @ts-check
+
+/** @type {import('./types').SourcesType} */
 module.exports = {
   INVITE: 'invite',
   TOKEN: 'token',

+ 28 - 0
services/web/app/src/Features/Authorization/types.d.ts

@@ -0,0 +1,28 @@
+type ValueOf<T> = T[keyof T]
+
+export const SourcesType = {
+  INVITE: 'invite',
+  TOKEN: 'token',
+  OWNER: 'owner',
+} as const
+
+export type Source = ValueOf<typeof SourcesType>
+
+export const PrivilegeLevelsType = {
+  NONE: false,
+  READ_ONLY: 'readOnly',
+  READ_AND_WRITE: 'readAndWrite',
+  REVIEW: 'reviewer',
+  OWNER: 'owner',
+} as const
+
+export type PrivilegeLevel = ValueOf<typeof PrivilegeLevelsType>
+
+export const PublicAccessLevelsType = {
+  READ_ONLY: 'readOnly', // LEGACY
+  READ_AND_WRITE: 'readAndWrite', // LEGACY
+  PRIVATE: 'private',
+  TOKEN_BASED: 'tokenBased',
+} as const
+
+export type PublicAccessLevel = ValueOf<typeof PublicAccessLevelsType>

+ 10 - 8
services/web/app/src/Features/Collaborators/CollaboratorsGetter.js

@@ -12,18 +12,20 @@ const ProjectEditorHandler = require('../Project/ProjectEditorHandler')
 const Sources = require('../Authorization/Sources')
 const PrivilegeLevels = require('../Authorization/PrivilegeLevels')
 
+/** @import { PrivilegeLevel, Source, PublicAccessLevel } from "../Authorization/types" */
+
 /**
  * @typedef ProjectMember
  * @property {string} id
- * @property {typeof PrivilegeLevels[keyof PrivilegeLevels]} privilegeLevel
- * @property {typeof Sources[keyof Sources]} source
+ * @property {PrivilegeLevel} privilegeLevel
+ * @property {Source} source
  * @property {boolean} [pendingEditor]
  * @property {boolean} [pendingReviewer]
  */
 
 /**
  * @typedef LoadedProjectMember
- * @property {typeof PrivilegeLevels[keyof PrivilegeLevels]} privilegeLevel
+ * @property {PrivilegeLevel} privilegeLevel
  * @property {{_id: ObjectId, email: string, features: any, first_name: string, last_name: string, signUpDate: Date}} user
  * @property {boolean} [pendingEditor]
  * @property {boolean} [pendingReviewer]
@@ -34,11 +36,11 @@ class ProjectAccess {
   /** @type {ProjectMember[]} */
   #members
 
-  /** @type {typeof PublicAccessLevels[keyof PublicAccessLevels]} */
+  /** @type {PublicAccessLevel} */
   #publicAccessLevel
 
   /**
-   * @param {{ owner_ref: ObjectId; collaberator_refs: ObjectId[]; readOnly_refs: ObjectId[]; tokenAccessReadAndWrite_refs: ObjectId[]; tokenAccessReadOnly_refs: ObjectId[]; publicAccesLevel: typeof PublicAccessLevels[keyof PublicAccessLevels]; pendingEditor_refs: ObjectId[]; reviewer_refs: ObjectId[]; pendingReviewer_refs: ObjectId[]; }} project
+   * @param {{ owner_ref: ObjectId; collaberator_refs: ObjectId[]; readOnly_refs: ObjectId[]; tokenAccessReadAndWrite_refs: ObjectId[]; tokenAccessReadOnly_refs: ObjectId[]; publicAccesLevel: PublicAccessLevel; pendingEditor_refs: ObjectId[]; reviewer_refs: ObjectId[]; pendingReviewer_refs: ObjectId[]; }} project
    */
   constructor(project) {
     this.#members = _getMemberIdsWithPrivilegeLevelsFromFields(
@@ -99,7 +101,7 @@ class ProjectAccess {
   }
 
   /**
-   * @return {typeof PublicAccessLevels[keyof PublicAccessLevels]}
+   * @return {PublicAccessLevel}
    */
   publicAccessLevel() {
     return this.#publicAccessLevel
@@ -121,7 +123,7 @@ class ProjectAccess {
 
   /**
    * @param {string | ObjectId} userId
-   * @return {typeof PrivilegeLevels[keyof PrivilegeLevels]}
+   * @return {PrivilegeLevel}
    */
   privilegeLevelForUser(userId) {
     if (!userId) return PrivilegeLevels.NONE
@@ -427,7 +429,7 @@ async function userIsReadWriteTokenMember(userId, projectId) {
  * @param {ObjectId[]} readOnlyIds
  * @param {ObjectId[]} tokenAccessIds
  * @param {ObjectId[]} tokenAccessReadOnlyIds
- * @param {typeof PublicAccessLevels[keyof PublicAccessLevels]} publicAccessLevel
+ * @param {PublicAccessLevel} publicAccessLevel
  * @param {ObjectId[]} pendingEditorIds
  * @param {ObjectId[]} reviewerIds
  * @param {ObjectId[]} pendingReviewerIds

+ 81 - 52
services/web/app/src/Features/Project/ProjectListController.mjs

@@ -1,4 +1,4 @@
-// ts-check
+// @ts-check
 import _ from 'lodash'
 
 import Metrics from '@overleaf/metrics'
@@ -31,11 +31,19 @@ import PermissionsManager from '../Authorization/PermissionsManager.js'
 import AnalyticsManager from '../Analytics/AnalyticsManager.js'
 
 /**
- * @import { GetProjectsRequest, GetProjectsResponse, AllUsersProjects, MongoProject } from "./types"
- * @import { ProjectApi, Filters, Page, Sort } from "../../../../types/project/dashboard/api"
- * @import { Tag } from "../Tags/types"
+ * @import { GetProjectsRequest, GetProjectsResponse, AllUsersProjects, MongoProject, FormattedProject, MongoTag } from "./types"
+ * @import { Project, ProjectApi, ProjectAccessLevel, Filters, Page, Sort, UserRef } from "../../../../types/project/dashboard/api"
+ * @import { Affiliation } from "../../../../types/affiliation"
+ * @import { Source } from "../Authorization/types"
  */
 
+/**
+ * @param {Affiliation} affiliation
+ * @param session
+ * @param linkedInstitutionIds
+ * @returns {boolean}
+ * @private
+ */
 const _ssoAvailable = (affiliation, session, linkedInstitutionIds) => {
   if (!affiliation.institution) return false
 
@@ -55,6 +63,10 @@ const _ssoAvailable = (affiliation, session, linkedInstitutionIds) => {
   return false
 }
 
+/**
+ * @param {Affiliation[]} affiliations
+ * @returns {Array<{ name: string, url: string }>}
+ */
 const _buildPortalTemplatesList = affiliations => {
   if (affiliations == null) {
     affiliations = []
@@ -64,7 +76,7 @@ const _buildPortalTemplatesList = affiliations => {
   const uniqueAffiliations = _.uniqBy(affiliations, 'institution.id')
   for (const aff of uniqueAffiliations) {
     const hasSlug = aff.portal?.slug
-    const hasTemplates = aff.portal?.templates_count > 0
+    const hasTemplates = (aff.portal?.templates_count || 0) > 0
 
     if (hasSlug && hasTemplates) {
       const portalPath = aff.institution.isUniversity ? '/edu/' : '/org/'
@@ -198,14 +210,25 @@ async function projectListPage(req, res, next) {
       logger.err({ err: error, userId }, 'Failed to load the active survey')
     }
 
-    if (user && UserPrimaryEmailCheckHandler.requiresPrimaryEmailCheck(user)) {
+    if (
+      user &&
+      UserPrimaryEmailCheckHandler.requiresPrimaryEmailCheck({
+        email: user.email,
+        emails: user.emails,
+        lastPrimaryEmailCheck: user.lastPrimaryEmailCheck,
+        signUpDate: user.signUpDate,
+      })
+    ) {
       return res.redirect('/user/emails/primary-email-check')
     }
   }
 
   const tags = await TagsHandler.promises.getAllTags(userId)
 
-  let userEmailsData = { list: [], allInReconfirmNotificationPeriods: [] }
+  /** @type {{ list: any[], allInReconfirmNotificationPeriods?: any[], error?: any }} */
+  let userEmailsData = {
+    list: [],
+  }
 
   try {
     const fullEmails = await UserGetter.promises.getUserFullEmails(userId)
@@ -226,7 +249,7 @@ async function projectListPage(req, res, next) {
           allInReconfirmNotificationPeriods,
         }
       } catch (error) {
-        userEmailsData = error
+        userEmailsData.error = error
       }
     }
   } catch (error) {
@@ -526,7 +549,7 @@ async function getProjectsJson(req, res) {
  * @param {Filters} filters
  * @param {Sort} sort
  * @param {Page} page
- * @returns {Promise<{totalSize: number, projects: ProjectApi[]}>}
+ * @returns {Promise<{totalSize: number, projects: Project[]}>}
  * @private
  */
 async function _getProjects(
@@ -535,16 +558,15 @@ async function _getProjects(
   sort = { by: 'lastUpdated', order: 'desc' },
   page = { size: 20 }
 ) {
-  const [
-    /** @type {AllUsersProjects} **/ allProjects,
-    /** @type {Tag[]} **/ tags,
-  ] = await Promise.all([
+  /** @type {[AllUsersProjects, MongoTag[]]} */
+  const results = await Promise.all([
     ProjectGetter.promises.findAllUsersProjects(
       userId,
       'name lastUpdated lastUpdatedBy publicAccesLevel archived trashed owner_ref tokens'
     ),
     TagsHandler.promises.getAllTags(userId),
   ])
+  const [allProjects, tags] = results
   const formattedProjects = _formatProjects(allProjects, userId)
   const filteredProjects = _applyFilters(
     formattedProjects,
@@ -554,18 +576,18 @@ async function _getProjects(
   )
   const pagedProjects = _sortAndPaginate(filteredProjects, sort, page)
 
-  await _injectProjectUsers(pagedProjects)
+  const projects = await _injectProjectUsers(pagedProjects)
 
   return {
     totalSize: filteredProjects.length,
-    projects: pagedProjects,
+    projects,
   }
 }
 
 /**
  * @param {AllUsersProjects} projects
  * @param {string} userId
- * @returns {Project[]}
+ * @returns {FormattedProject[]}
  * @private
  */
 function _formatProjects(projects, userId) {
@@ -578,7 +600,7 @@ function _formatProjects(projects, userId) {
     tokenReadOnly,
   } = projects
 
-  const formattedProjects = /** @type {Project[]} **/ []
+  const formattedProjects = /** @type {FormattedProject[]} **/ []
   for (const project of owned) {
     formattedProjects.push(
       _formatProjectInfo(project, 'owner', Sources.OWNER, userId)
@@ -622,11 +644,11 @@ function _formatProjects(projects, userId) {
 }
 
 /**
- * @param {Project[]} projects
- * @param {Tag[]} tags
+ * @param {FormattedProject[]} projects
+ * @param {MongoTag[]} tags
  * @param {Filters} filters
  * @param {string} userId
- * @returns {Project[]}
+ * @returns {FormattedProject[]}
  * @private
  */
 function _applyFilters(projects, tags, filters, userId) {
@@ -637,10 +659,10 @@ function _applyFilters(projects, tags, filters, userId) {
 }
 
 /**
- * @param {Project[]} projects
+ * @param {FormattedProject[]} projects
  * @param {Sort} sort
  * @param {Page} page
- * @returns {Project[]}
+ * @returns {FormattedProject[]}
  * @private
  */
 function _sortAndPaginate(projects, sort, page) {
@@ -661,38 +683,35 @@ function _sortAndPaginate(projects, sort, page) {
 
 /**
  * @param {MongoProject} project
- * @param {string} accessLevel
- * @param {'owner' | 'invite' | 'token'} source
+ * @param {ProjectAccessLevel} accessLevel
+ * @param {Source} source
  * @param {string} userId
- * @returns {object}
+ * @returns {FormattedProject}
  * @private
  */
 function _formatProjectInfo(project, accessLevel, source, userId) {
   const archived = ProjectHelper.isArchived(project, userId)
   // If a project is simultaneously trashed and archived, we will consider it archived but not trashed.
   const trashed = ProjectHelper.isTrashed(project, userId) && !archived
+  const readOnlyTokenAccess =
+    accessLevel === PrivilegeLevels.READ_ONLY && source === Sources.TOKEN
 
-  const model = {
+  return {
     id: project._id.toString(),
     name: project.name,
-    owner_ref: project.owner_ref,
+    owner_ref: readOnlyTokenAccess ? null : project.owner_ref,
     lastUpdated: project.lastUpdated,
-    lastUpdatedBy: project.lastUpdatedBy,
+    lastUpdatedBy: readOnlyTokenAccess ? null : project.lastUpdatedBy,
     accessLevel,
     source,
     archived,
     trashed,
   }
-  if (accessLevel === PrivilegeLevels.READ_ONLY && source === Sources.TOKEN) {
-    model.owner_ref = null
-    model.lastUpdatedBy = null
-  }
-  return model
 }
 
 /**
- * @param {Project[]} projects
- * @returns {Promise<void>}
+ * @param {FormattedProject[]} projects
+ * @returns {Promise<Project[]>}
  * @private
  */
 async function _injectProjectUsers(projects) {
@@ -711,6 +730,7 @@ async function _injectProjectUsers(projects) {
     last_name: 1,
     email: 1,
   }
+  /** @type {Record<string, UserRef>} */
   const users = {}
   for (const user of await UserGetter.promises.getUsers(userIds, projection)) {
     const userId = user._id.toString()
@@ -721,21 +741,30 @@ async function _injectProjectUsers(projects) {
       lastName: user.last_name,
     }
   }
-  for (const project of projects) {
-    if (project.owner_ref != null) {
-      project.owner = users[project.owner_ref.toString()]
-    }
-    if (project.lastUpdatedBy != null) {
-      project.lastUpdatedBy = users[project.lastUpdatedBy.toString()] || null
-    }
 
-    delete project.owner_ref
-  }
+  return projects.map(project => ({
+    id: project.id,
+    name: project.name,
+    archived: project.archived,
+    trashed: project.trashed,
+    accessLevel: project.accessLevel,
+    source: project.source,
+    lastUpdated: project.lastUpdated.toISOString(),
+    lastUpdatedBy:
+      project.lastUpdatedBy == null
+        ? null
+        : users[project.lastUpdatedBy.toString()] || null,
+    owner:
+      project.owner_ref == null
+        ? undefined
+        : users[project.owner_ref.toString()],
+    owner_ref: undefined,
+  }))
 }
 
 /**
  * @param {any} project
- * @param {Tag[]} tags
+ * @param {MongoTag[]} tags
  * @param {Filters} filters
  * @private
  */
@@ -777,14 +806,14 @@ function _matchesFilters(project, tags, filters) {
  * @private
  */
 function _hasActiveFilter(filters) {
-  return (
+  return Boolean(
     filters.ownedByUser ||
-    filters.sharedWithUser ||
-    filters.archived ||
-    filters.trashed ||
-    filters.tag === null ||
-    filters.tag?.length ||
-    filters.search?.length
+      filters.sharedWithUser ||
+      filters.archived ||
+      filters.trashed ||
+      filters.tag === null ||
+      filters.tag?.length ||
+      filters.search?.length
   )
 }
 

+ 24 - 0
services/web/app/src/Features/Project/types.d.ts

@@ -2,8 +2,11 @@ import express from 'express'
 import {
   GetProjectsRequestBody,
   GetProjectsResponseBody,
+  ProjectAccessLevel,
+  UserRef,
 } from '../../../../types/project/dashboard/api'
 import { ObjectId } from 'mongodb-legacy'
+import { Source } from '../Authorization/types'
 
 export type GetProjectsRequest = express.Request<
   unknown,
@@ -30,10 +33,31 @@ export type MongoProject = {
   }[]
 }
 
+export type MongoTag = {
+  user_id: string
+  name: string
+  color?: string | null
+  project_ids?: string[]
+}
+
 export type AllUsersProjects = {
   owned: MongoProject[]
   readAndWrite: MongoProject[]
   readOnly: MongoProject[]
   tokenReadAndWrite: MongoProject[]
   tokenReadOnly: MongoProject[]
+  review: MongoProject[]
+}
+
+export type FormattedProject = {
+  id: string
+  name: string
+  owner_ref?: string | null
+  owner?
+  lastUpdated: Date
+  lastUpdatedBy: string | null | UserRef
+  archived: boolean
+  trashed: boolean
+  accessLevel: ProjectAccessLevel
+  source: Source
 }

+ 14 - 12
services/web/app/src/Features/User/UserGetter.js

@@ -377,17 +377,19 @@ const decorateFullEmails = (
   return emailsData
 }
 
-UserGetter.promises = promisifyAll(UserGetter, {
-  without: [
-    'getSsoUsersAtInstitution',
-    'getUserFullEmails',
-    'getUserFeatures',
-    'getWritefullData',
-  ],
-})
-UserGetter.promises.getUserFullEmails = getUserFullEmails
-UserGetter.promises.getSsoUsersAtInstitution = getSsoUsersAtInstitution
-UserGetter.promises.getUserFeatures = getUserFeatures
-UserGetter.promises.getWritefullData = getWritefullData
+UserGetter.promises = {
+  ...promisifyAll(UserGetter, {
+    without: [
+      'getSsoUsersAtInstitution',
+      'getUserFullEmails',
+      'getUserFeatures',
+      'getWritefullData',
+    ],
+  }),
+  getUserFullEmails,
+  getSsoUsersAtInstitution,
+  getUserFeatures,
+  getWritefullData,
+}
 
 module.exports = UserGetter

+ 25 - 15
services/web/test/unit/src/Project/ProjectListController.test.mjs

@@ -298,19 +298,25 @@ describe('ProjectListController', function () {
   describe('projectListPage', function () {
     beforeEach(function (ctx) {
       ctx.projects = [
-        { _id: 1, lastUpdated: 1, owner_ref: 'user-1' },
+        { _id: 1, lastUpdated: new Date(1), owner_ref: 'user-1' },
         {
           _id: 2,
-          lastUpdated: 2,
+          lastUpdated: new Date(2),
           owner_ref: 'user-2',
           lastUpdatedBy: 'user-1',
         },
       ]
-      ctx.readAndWrite = [{ _id: 5, lastUpdated: 5, owner_ref: 'user-1' }]
-      ctx.readOnly = [{ _id: 3, lastUpdated: 3, owner_ref: 'user-1' }]
-      ctx.tokenReadAndWrite = [{ _id: 6, lastUpdated: 5, owner_ref: 'user-4' }]
-      ctx.tokenReadOnly = [{ _id: 7, lastUpdated: 4, owner_ref: 'user-5' }]
-      ctx.review = [{ _id: 8, lastUpdated: 4, owner_ref: 'user-6' }]
+      ctx.readAndWrite = [
+        { _id: 5, lastUpdated: new Date(5), owner_ref: 'user-1' },
+      ]
+      ctx.readOnly = [{ _id: 3, lastUpdated: new Date(3), owner_ref: 'user-1' }]
+      ctx.tokenReadAndWrite = [
+        { _id: 6, lastUpdated: new Date(5), owner_ref: 'user-4' },
+      ]
+      ctx.tokenReadOnly = [
+        { _id: 7, lastUpdated: new Date(4), owner_ref: 'user-5' },
+      ]
+      ctx.review = [{ _id: 8, lastUpdated: new Date(4), owner_ref: 'user-6' }]
       ctx.allProjects = {
         owned: ctx.projects,
         readAndWrite: ctx.readAndWrite,
@@ -854,17 +860,21 @@ describe('ProjectListController', function () {
   describe('projectListReactPage with duplicate projects', function () {
     beforeEach(function (ctx) {
       ctx.projects = [
-        { _id: 1, lastUpdated: 1, owner_ref: 'user-1' },
-        { _id: 2, lastUpdated: 2, owner_ref: 'user-2' },
+        { _id: 1, lastUpdated: new Date(1), owner_ref: 'user-1' },
+        { _id: 2, lastUpdated: new Date(2), owner_ref: 'user-2' },
+      ]
+      ctx.readAndWrite = [
+        { _id: 5, lastUpdated: new Date(5), owner_ref: 'user-1' },
+      ]
+      ctx.readOnly = [{ _id: 3, lastUpdated: new Date(3), owner_ref: 'user-1' }]
+      ctx.tokenReadAndWrite = [
+        { _id: 6, lastUpdated: new Date(5), owner_ref: 'user-4' },
       ]
-      ctx.readAndWrite = [{ _id: 5, lastUpdated: 5, owner_ref: 'user-1' }]
-      ctx.readOnly = [{ _id: 3, lastUpdated: 3, owner_ref: 'user-1' }]
-      ctx.tokenReadAndWrite = [{ _id: 6, lastUpdated: 5, owner_ref: 'user-4' }]
       ctx.tokenReadOnly = [
-        { _id: 6, lastUpdated: 5, owner_ref: 'user-4' }, // Also in tokenReadAndWrite
-        { _id: 7, lastUpdated: 4, owner_ref: 'user-5' },
+        { _id: 6, lastUpdated: new Date(5), owner_ref: 'user-4' }, // Also in tokenReadAndWrite
+        { _id: 7, lastUpdated: new Date(4), owner_ref: 'user-5' },
       ]
-      ctx.review = [{ _id: 8, lastUpdated: 5, owner_ref: 'user-6' }]
+      ctx.review = [{ _id: 8, lastUpdated: new Date(5), owner_ref: 'user-6' }]
       ctx.allProjects = {
         owned: ctx.projects,
         readAndWrite: ctx.readAndWrite,

+ 2 - 1
services/web/tsconfig.backend.json

@@ -9,6 +9,7 @@
     "scripts/**/*",
     "test/acceptance/**/*",
     "test/smoke/**/*",
-    "test/unit/**/*"
+    "test/unit/**/*",
+    "types/backend/**/*"
   ]
 }

+ 14 - 0
services/web/types/backend/express/request.d.ts

@@ -0,0 +1,14 @@
+import 'express'
+import OAuth2Server from '@node-oauth/oauth2-server'
+import type SessionData from 'express-session'
+
+// Add properties to Express's Request object that are defined in JS middleware
+// or controllers and expected to be present in controllers.
+declare module 'express' {
+  // eslint-disable-next-line no-unused-vars
+  interface Request {
+    session: SessionData
+    userRestrictions?: Set
+    oauth_user?: OAuth2Server.User
+  }
+}

+ 29 - 0
services/web/types/backend/express/session-data.d.ts

@@ -0,0 +1,29 @@
+import 'express-session'
+
+// Add properties to Express's SessionData object that are expected to be
+// present in controllers.
+declare module 'express-session' {
+  // eslint-disable-next-line no-unused-vars
+  interface SessionData {
+    postCheckoutRedirect?: string
+    postLoginRedirect?: string
+    postOnboardingRedirect?: string
+    sharedProjectData?: any
+    templateData?: any
+    saml?: {
+      reconfirmed?: boolean
+      linked?: {
+        universityName?: string
+        providerName?: string
+      }
+      linkedGroup?: any
+      requestedEmail?: string
+      emailNonCanonical?: string
+      institutionEmail?: string
+      registerIntercept?: boolean
+      error?: any
+    }
+    samlBeta?: boolean
+    // Add further properties as needed
+  }
+}

+ 10 - 0
services/web/types/backend/i18next.d.ts

@@ -0,0 +1,10 @@
+import 'i18next'
+
+// Add our custom translate function from Translations.js into the i18next i18n
+// object type definition
+declare module 'i18next' {
+  // eslint-disable-next-line no-unused-vars
+  interface i18n {
+    translate(key: string, vars?: Record<string, any>, components?: any): string
+  }
+}

+ 10 - 2
services/web/types/project/dashboard/api.d.ts

@@ -1,5 +1,6 @@
 import { SortingOrder } from '../../sorting-order'
 import { MergeAndOverride } from '../../utils'
+import { Source } from '../../../app/src/Features/Authorization/types'
 
 export type Page = {
   size: number
@@ -33,6 +34,13 @@ export type UserRef = {
   lastName: string
 }
 
+export type ProjectAccessLevel =
+  | 'owner'
+  | 'readWrite'
+  | 'readOnly'
+  | 'readAndWrite'
+  | 'review'
+
 export type ProjectApi = {
   id: string
   name: string
@@ -41,8 +49,8 @@ export type ProjectApi = {
   lastUpdatedBy: UserRef | null
   archived: boolean
   trashed: boolean
-  accessLevel: 'owner' | 'readWrite' | 'readOnly' | 'readAndWrite'
-  source: 'owner' | 'invite' | 'token'
+  accessLevel: ProjectAccessLevel
+  source: Source
 }
 
 export type Project = MergeAndOverride<