Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions api/lib/local_ipc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,8 @@ const commands: Record<
teamId: 'local',
isPublic: true,
repositoryUrl: null,
discordChannelId: null,
jiraProjectKey: null,
})
}

Expand Down
10 changes: 10 additions & 0 deletions api/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,8 @@ const projectOutput = OBJ({
teamId: STR('The ID of the team that owns the project'),
isPublic: BOOL('Is the project public?'),
repositoryUrl: optional(STR('The URL of the project repository')),
jiraProjectKey: optional(STR('The Jira project key, e.g. "TNT"')),
discordChannelId: optional(STR('The ID of the project Discord channel')),
createdAt: optional(NUM('The creation date of the project')),
updatedAt: optional(NUM('The last update date of the project')),
})
Expand Down Expand Up @@ -349,6 +351,10 @@ const defs = {
teamId: STR('The ID of the team that owns the project'),
isPublic: BOOL('Is the project public?'),
repositoryUrl: optional(STR('The URL of the project repository')),
jiraProjectKey: optional(STR('The Jira project key, e.g. "TNT"')),
discordChannelId: optional(
STR('The ID of the project Discord channel'),
),
}, 'Create a new project'),
output: projectOutput,
description: 'Create a new project',
Expand All @@ -375,6 +381,10 @@ const defs = {
teamId: STR('The ID of the team that owns the project'),
isPublic: BOOL('Is the project public?'),
repositoryUrl: optional(STR('The URL of the project repository')),
jiraProjectKey: optional(STR('The Jira project key, e.g. "TNT"')),
discordChannelId: optional(
STR('The ID of the project Discord channel'),
),
}),
output: projectOutput,
description: 'Update a project by ID',
Expand Down
2 changes: 2 additions & 0 deletions api/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ export const ProjectDef = OBJ({
teamId: STR('The ID of the team that owns the project'),
isPublic: BOOL('Is the project public?'),
repositoryUrl: optional(STR('The URL of the project repository')),
jiraProjectKey: optional(STR('The Jira project key, e.g. "TNT"')),
discordChannelId: optional(STR('The ID of the project Discord channel')),
}, 'The project schema definition')
export type Project = Asserted<typeof ProjectDef>

Expand Down
101 changes: 101 additions & 0 deletions api/team-directory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import { get } from './lmdb-store.ts'

// Matches the `Person` shape from tickets.ts (issue 1, a separate branch as
// of this issue) — duplicated rather than imported since this issue has no
// dependency on it.
export type Person = {
id: string
name: string
emails: string[]
githubLogin?: string
discordId?: string
jiraAccountId?: string
}

type GoogleUser = {
id: string
primaryEmail: string
emails: string[]
name: string
}

type JiraUser = {
accountId: string
email: string | null
}

// `name`/`global_name` come back as `[]`, not `null`, when unset on some
// store records (an upstream sync quirk) — typed loosely and guarded with
// `typeof` rather than trusted.
type GithubUser = {
login: string
name: unknown
}

type DiscordUser = {
id: string
global_name: unknown
}

const GOOGLE_USER_QUERY =
'{id: .id, primaryEmail: .primaryEmail, emails: [.emails[]?.address], name: .name.fullName}'
const JIRA_USER_QUERY = '{accountId: .accountId, email: .emailAddress}'
const GITHUB_USER_QUERY = '{login: .login, name: .name}'
const DISCORD_USER_QUERY = '{id: .id, global_name: .global_name}'

// github/user and discord/user carry no email to join against google/user
// by, so this falls back to an exact, case-insensitive full-name match —
// the only signal shared with google/user's `name.fullName`. A name shared
// by more than one account is dropped rather than guessed at (e.g. two
// real github accounts both just named "Henri").
const uniqueByName = <T>(
items: T[],
getName: (item: T) => unknown,
): Map<string, T> => {
const byName = new Map<string, T>()
const ambiguous = new Set<string>()
for (const item of items) {
const name = getName(item)
if (typeof name !== 'string' || !name) continue
const key = name.toLowerCase()
if (byName.has(key)) ambiguous.add(key)
else byName.set(key, item)
}
for (const key of ambiguous) byName.delete(key)
return byName
}

export const mergeTeamDirectory = (
googleUsers: GoogleUser[],
jiraUsers: JiraUser[],
githubUsers: GithubUser[],
discordUsers: DiscordUser[],
): Person[] => {
const githubByName = uniqueByName(githubUsers, (u) => u.name)
const discordByName = uniqueByName(discordUsers, (u) => u.global_name)

return googleUsers.map((user) => {
const emails = [...new Set([user.primaryEmail, ...user.emails])]
const jiraUser = jiraUsers.find((j) => j.email && emails.includes(j.email))
const nameKey = user.name.toLowerCase()
return {
id: user.id,
name: user.name,
emails,
jiraAccountId: jiraUser?.accountId,
githubLogin: githubByName.get(nameKey)?.login,
discordId: discordByName.get(nameKey)?.id,
}
})
}

export const buildTeamDirectory = async (): Promise<Person[]> => {
const [googleUsers, jiraUsers, githubUsers, discordUsers] = await Promise
.all([
get<GoogleUser[]>('google/user', { q: GOOGLE_USER_QUERY }),
get<JiraUser[]>('jira/user', { q: JIRA_USER_QUERY }),
get<GithubUser[]>('github/user', { q: GITHUB_USER_QUERY }),
get<DiscordUser[]>('discord/user', { q: DISCORD_USER_QUERY }),
])
return mergeTeamDirectory(googleUsers, jiraUsers, githubUsers, discordUsers)
}
6 changes: 6 additions & 0 deletions tasks/seed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,20 +12,26 @@ const projects: Omit<Project, 'createdAt'>[] = [
teamId: 'frontend-devs',
isPublic: true,
repositoryUrl: 'https://github.com/example/website',
discordChannelId: null,
jiraProjectKey: null,
},
{
slug: 'api-refactor',
name: 'API Refactor',
teamId: 'backend-devs',
isPublic: false,
repositoryUrl: 'https://github.com/example/api',
discordChannelId: null,
jiraProjectKey: null,
},
{
slug: 'design-system',
name: 'Design System',
teamId: 'frontend-devs',
isPublic: true,
repositoryUrl: 'https://github.com/example/design-system',
discordChannelId: null,
jiraProjectKey: null,
},
]

Expand Down
2 changes: 2 additions & 0 deletions web/pages/ProjectsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@ async function saveProject(
teamId,
repositoryUrl,
isPublic: isPublic ?? false,
discordChannelId: null,
jiraProjectKey: null,
})
projects.fetch()
navigate({ params: { dialog: null, slug: null }, replace: true })
Expand Down
Loading