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
6 changes: 6 additions & 0 deletions .github/scripts/http-e2e.sh
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@ case "$group" in
export DB_TX_TRIPWIRE=throw
export NEXT_PUBLIC_CHAT_DISABLED=true
export SIM_LINKS=true
export PEOPLE=true
start_app scim 3017 SCIM record-http-status
SCIM_E2E_BASE_URL="$local_app_url" \
SCIM_E2E_DATABASE_URL="$DATABASE_URL" \
Expand All @@ -161,6 +162,11 @@ case "$group" in
SIM_LINKS_E2E_AUTH_SECRET="$BETTER_AUTH_SECRET" \
SIM_LINKS_E2E_REPORT_PATH="$report_dir/sim-links-e2e-report.json" \
bun run test:sim-links:e2e
PEOPLE_E2E_BASE_URL="$local_app_url" \
PEOPLE_E2E_DATABASE_URL="$DATABASE_URL" \
PEOPLE_E2E_AUTH_SECRET="$BETTER_AUTH_SECRET" \
PEOPLE_E2E_REPORT_PATH="$report_dir/people-e2e-report.json" \
bun run test:people:e2e
;;

# The search suites serve their own fixtures in-process and need no app. They share this group
Expand Down
1 change: 1 addition & 0 deletions apps/docs/content/docs/search/meta.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"find-content",
"ask-questions",
"sim-links",
"people",
"troubleshooting",
"---Connect sources---",
"connect-your-account",
Expand Down
43 changes: 43 additions & 0 deletions apps/docs/content/docs/search/people.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
---
title: People
description: Your organization's people, with titles, departments, locations, and who reports to whom
---

**People** in the organization sidebar lists everyone active in your organization. Search by name, email, title, department, or location, or filter by department. Select a person to see their profile, the managers above them, and the people who report to them.

Organization admins keep this information up to date by importing it from your HR system or editing a profile.

## Import people data

1. Export your people from your HR system as CSV or Excel.
2. Open **People** and select **Import**, then choose the file.
3. Review the preview, then select **Import**.

Each row is matched to a member by email. The file needs an `email` column and any of:

| Column | Also recognized as |
| --- | --- |
| `title` | `job title`, `position` |
| `department` | `dept` |
| `location` | `office`; or `city`, `state`, and `country`, which are combined |
| `start_date` | `hire date`. Use `2024-01-31` or `1/31/2024`. |
| `manager_email` | `manager`, `reports to`, `supervisor`. Use the manager's email. |

**Download template** in the import dialog gives a file with these columns.

- A column the file includes replaces that field for every listed person; a blank cell clears it. A column the file leaves out is not changed.
- Rows are skipped when the person isn't a member, the email or date is invalid, a title, department, or location is over 200 characters, the person is named as their own manager, or an earlier row already updated the same person. The preview lists the first 200 issues with their row numbers.
- A manager who isn't a member, or who would make a reporting loop, is left as it was, and the preview says so.
- Nothing changes until you confirm the preview. Import up to 10,000 rows at a time.

## Edit a profile

Organization admins can open a person and select **Edit** to change their title, department, location, start date, or manager.

When someone leaves your organization, their profile is removed, and anyone who reported to them no longer has a manager recorded.

Imports and edits are recorded in your organization's [audit logs](/platform/enterprise/audit-logs).

## Related docs

- [Sim Links](/search/sim-links)
38 changes: 38 additions & 0 deletions apps/sim/app/api/organizations/[id]/people/[memberId]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { getPersonContract, updatePersonContract } from '@/lib/api/contracts/people'
import { presentPerson } from '@/lib/api/server/people-presenters'
import {
defineInternalJsonRoute,
internalOrchestrationErrorPolicy,
internalRateLimits,
internalSessionAuth,
} from '@/lib/api/server/routes'
import { getPerson, updatePerson } from '@/lib/people/application/use-cases'

const policy = {
auth: internalSessionAuth,
rateLimit: internalRateLimits.user({ bucketName: 'people' }),
errorPolicy: internalOrchestrationErrorPolicy,
staticResponseHeaders: { 'Cache-Control': 'private, no-store' },
}

export const GET = defineInternalJsonRoute({
...policy,
contract: getPersonContract,
operation: getPerson.operation,
useCase: getPerson,
mapInput: ({ params }) => ({ organizationId: params.id, memberId: params.memberId }),
present: ({ person, chain, reports }) => ({ person: presentPerson(person), chain, reports }),
})

export const PUT = defineInternalJsonRoute({
...policy,
contract: updatePersonContract,
operation: updatePerson.operation,
useCase: updatePerson,
mapInput: ({ params, body }) => ({
organizationId: params.id,
memberId: params.memberId,
...body,
}),
present: (person) => ({ person: presentPerson(person) }),
})
20 changes: 20 additions & 0 deletions apps/sim/app/api/organizations/[id]/people/departments/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { listDepartmentsContract } from '@/lib/api/contracts/people'
import {
defineInternalJsonRoute,
internalOrchestrationErrorPolicy,
internalRateLimits,
internalSessionAuth,
} from '@/lib/api/server/routes'
import { listDepartments } from '@/lib/people/application/use-cases'

export const GET = defineInternalJsonRoute({
contract: listDepartmentsContract,
operation: listDepartments.operation,
useCase: listDepartments,
auth: internalSessionAuth,
rateLimit: internalRateLimits.user({ bucketName: 'people' }),
errorPolicy: internalOrchestrationErrorPolicy,
staticResponseHeaders: { 'Cache-Control': 'private, no-store' },
mapInput: ({ params }) => ({ organizationId: params.id }),
present: (departments) => ({ departments }),
})
20 changes: 20 additions & 0 deletions apps/sim/app/api/organizations/[id]/people/import/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { importPeopleContract } from '@/lib/api/contracts/people'
import {
defineInternalJsonRoute,
internalOrchestrationErrorPolicy,
internalRateLimits,
internalSessionAuth,
} from '@/lib/api/server/routes'
import { importPeople } from '@/lib/people/application/use-cases'

export const POST = defineInternalJsonRoute({
contract: importPeopleContract,
operation: importPeople.operation,
useCase: importPeople,
auth: internalSessionAuth,
rateLimit: internalRateLimits.user({ bucketName: 'people-import' }),
errorPolicy: internalOrchestrationErrorPolicy,
staticResponseHeaders: { 'Cache-Control': 'private, no-store' },
mapInput: ({ params, body }) => ({ organizationId: params.id, ...body }),
present: (result) => result,
})
46 changes: 46 additions & 0 deletions apps/sim/app/api/organizations/[id]/people/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { listPeopleContract } from '@/lib/api/contracts/people'
import { cursorRoute, cursorScopeKey } from '@/lib/api/cursor-binding'
import { presentPerson } from '@/lib/api/server/people-presenters'
import {
defineInternalJsonRoute,
internalOrchestrationErrorPolicy,
internalRateLimits,
internalSessionAuth,
} from '@/lib/api/server/routes'
import { listPeople } from '@/lib/people/application/use-cases'
import { readSortedCursor, writeSortedCursor } from '@/app/api/v2/lib/response'

/** A cursor resumes only the list it came from: the same organization, search, and department. */
function cursorScope(id: string, search: string | undefined, department: string | undefined) {
return cursorScopeKey(cursorRoute(listPeopleContract, { id }), { search, department })
}

export const GET = defineInternalJsonRoute({
contract: listPeopleContract,
operation: listPeople.operation,
useCase: listPeople,
auth: internalSessionAuth,
rateLimit: internalRateLimits.user({ bucketName: 'people' }),
errorPolicy: internalOrchestrationErrorPolicy,
staticResponseHeaders: { 'Cache-Control': 'private, no-store' },
mapInput: ({ params, query }) => ({
organizationId: params.id,
search: query.search,
department: query.department,
cursorKeys: readSortedCursor(
query.cursor,
'name',
'asc',
cursorScope(params.id, query.search, query.department)
),
}),
present: ({ data, nextCursorKeys }, { input }) => ({
people: data.map(presentPerson),
nextCursor: writeSortedCursor(
nextCursorKeys,
'name',
'asc',
cursorScope(input.organizationId, input.search, input.department)
),
}),
})
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { ChartColumn, Home, Integration, Link, Search } from '@sim/emcn/icons'
import { ChartColumn, Home, Integration, Link, Search, Users } from '@sim/emcn/icons'
import { organizationRoutes } from '@/lib/navigation/paths'
import type { SidebarNavItemData } from '@/app/workspace/[workspaceId]/w/components/sidebar/components'

type OrganizationNavRoute = 'home' | 'search' | 'integrations' | 'simLinks' | 'benchmark'
type OrganizationNavRoute = 'home' | 'search' | 'people' | 'integrations' | 'simLinks' | 'benchmark'

interface OrganizationNavEntry {
id: string
Expand All @@ -18,6 +18,7 @@ interface OrganizationNavEntry {
const ORGANIZATION_NAV_ENTRIES: readonly OrganizationNavEntry[] = [
{ id: 'home', label: 'Home', icon: Home, route: 'home' },
{ id: 'search', label: 'Search', icon: Search, route: 'search' },
{ id: 'people', label: 'People', icon: Users, route: 'people' },
{ id: 'integrations', label: 'Integrations', icon: Integration, route: 'integrations' },
{ id: 'sim-links', label: 'Sim Links', icon: Link, route: 'simLinks' },
{ id: 'benchmark', label: 'Benchmark', icon: ChartColumn, route: 'benchmark' },
Expand All @@ -28,6 +29,7 @@ interface OrganizationNavAvailability {
mothershipAvailable: boolean
benchmarkEnabled?: boolean
simLinksEnabled?: boolean
peopleEnabled?: boolean
}

export function buildOrganizationNavItems(
Expand All @@ -37,12 +39,14 @@ export function buildOrganizationNavItems(
mothershipAvailable,
benchmarkEnabled = false,
simLinksEnabled = false,
peopleEnabled = false,
}: OrganizationNavAvailability
): SidebarNavItemData[] {
const routes = organizationRoutes(organizationId)
const available: Record<OrganizationNavRoute, boolean> = {
home: mothershipAvailable,
search: searchAvailable,
people: peopleEnabled,
Comment thread
waleedlatif1 marked this conversation as resolved.
Resolved
integrations: searchAvailable,
simLinks: simLinksEnabled,
benchmark: benchmarkEnabled,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ interface OrganizationChatsProps
interface OrganizationSidebarProps {
benchmarkEnabled?: boolean
simLinksEnabled?: boolean
peopleEnabled?: boolean
}

function OrganizationChats({ organizationId, ...props }: OrganizationChatsProps) {
Expand All @@ -77,6 +78,7 @@ function OrganizationChats({ organizationId, ...props }: OrganizationChatsProps)
export const OrganizationSidebar = memo(function OrganizationSidebar({
benchmarkEnabled = false,
simLinksEnabled = false,
peopleEnabled = false,
}: OrganizationSidebarProps) {
const { isCollapsed: railCollapsed, isPeeking, onToggle: toggleCollapsed } = useSidebarChrome()
/** The peek card always renders the expanded layout, whatever the rail's state. */
Expand All @@ -103,6 +105,7 @@ export const OrganizationSidebar = memo(function OrganizationSidebar({
mothershipAvailable: canUseHome,
benchmarkEnabled,
simLinksEnabled,
peopleEnabled,
})
const settingsPath = organizationRoutes(organization.id).settings
const isSettings = pathname === settingsPath || pathname?.startsWith(`${settingsPath}/`)
Expand Down
5 changes: 4 additions & 1 deletion apps/sim/app/o/[organizationId]/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { isDashboardsEnabled } from '@/lib/dashboards/feature-flag'
import { isMothershipModelSelectorEnabled } from '@/lib/mothership/feature-flags'
import { organizationRoutes, WORKSPACE_SETTINGS_PATH } from '@/lib/navigation/paths'
import { getOrganizationSurfaceContext } from '@/lib/organizations/surface'
import { canBrowsePeople } from '@/lib/people/feature-flag'
import { isProjectsEnabled } from '@/lib/projects/rollout.server'
import { isSimLinksEnabled } from '@/lib/sim-links/feature-flag'
import { isWorkflowTestsEnabled } from '@/lib/workflow-tests/feature-flag'
Expand Down Expand Up @@ -64,7 +65,8 @@ export default async function OrganizationLayout({
if (!context) {
return <OrganizationAccessDenied />
}
if (!context.mothershipAvailable && !context.searchAccess.memberScoped)
const peopleEnabled = await canBrowsePeople(organizationId, context.viewer.role)
if (!context.mothershipAvailable && !context.searchAccess.memberScoped && !peopleEnabled)
redirect(WORKSPACE_SETTINGS_PATH)

const [
Expand Down Expand Up @@ -123,6 +125,7 @@ export default async function OrganizationLayout({
<OrganizationSidebar
benchmarkEnabled={benchmarkEnabled}
simLinksEnabled={simLinksEnabled}
peopleEnabled={peopleEnabled}
/>
}
initialSidebarCollapsed={initialSidebarCollapsed}
Expand Down
8 changes: 3 additions & 5 deletions apps/sim/app/o/[organizationId]/links/sim-links.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
'use client'

import { useMemo, useState } from 'react'
import { Avatar, Chip, ChipConfirmModal, ChipModalError, writeTextToClipboard } from '@sim/emcn'
import { Avatar, ChipConfirmModal, ChipModalError, writeTextToClipboard } from '@sim/emcn'
import { Link } from '@sim/emcn/icons'
import { createLogger } from '@sim/logger'
import { useQueryStates } from 'nuqs'
Expand All @@ -23,6 +23,7 @@ import { SimLinkContextMenu } from '@/app/o/[organizationId]/links/sim-link-cont
import { SimLinkIcon } from '@/app/o/[organizationId]/links/sim-link-icon'
import { SimLinkModal } from '@/app/o/[organizationId]/links/sim-link-modal'
import {
ResourceLoadError,
ResourceNoResults,
SimLinksEmptyState,
} from '@/app/workspace/[workspaceId]/components/resource/components/resource-empty-state'
Expand Down Expand Up @@ -157,10 +158,7 @@ export function SimLinks({ organizationId }: SimLinksProps) {
loadMoreFailed={links.isFetchNextPageError}
emptyState={
links.isLoadingError ? (
<div role='alert' className='flex flex-col items-center gap-3 p-6'>
<p className='text-[var(--text-error)] text-small'>{links.error.message}</p>
<Chip onClick={() => void links.refetch()}>Retry</Chip>
</div>
<ResourceLoadError message={links.error.message} onRetry={() => void links.refetch()} />
) : query ? (
<ResourceNoResults
search={query}
Expand Down
11 changes: 6 additions & 5 deletions apps/sim/app/o/[organizationId]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { notFound, redirect } from 'next/navigation'
import { getSession } from '@/lib/auth'
import { organizationRoutes, WORKSPACE_SETTINGS_PATH } from '@/lib/navigation/paths'
import { getOrganizationSurfaceContext } from '@/lib/organizations/surface'
import { canBrowsePeople } from '@/lib/people/feature-flag'
import { buildAuthCrossLink } from '@/app/(auth)/auth-redirect'

export default async function OrganizationPage({
Expand All @@ -17,11 +18,11 @@ export default async function OrganizationPage({
}
const context = await getOrganizationSurfaceContext(organizationId, session.user.id)
if (!context) notFound()
if (context.mothershipAvailable && context.canBuild) redirect(routes.home)
if (context.searchAccess.memberScoped) redirect(routes.search)
redirect(
context.mothershipAvailable && context.canBuild
? routes.home
: context.searchAccess.memberScoped
? routes.search
: WORKSPACE_SETTINGS_PATH
(await canBrowsePeople(organizationId, context.viewer.role))
? routes.people
: WORKSPACE_SETTINGS_PATH
)
}
5 changes: 5 additions & 0 deletions apps/sim/app/o/[organizationId]/people/[memberId]/loading.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { ApplicationLoading } from '@/app/_shell/application-loading'

export default function Loading() {
return <ApplicationLoading />
}
27 changes: 27 additions & 0 deletions apps/sim/app/o/[organizationId]/people/[memberId]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import type { Metadata } from 'next'
import { notFound, redirect } from 'next/navigation'
import { getSession } from '@/lib/auth'
import { organizationRoutes } from '@/lib/navigation/paths'
import { isPeopleEnabled } from '@/lib/people/feature-flag'
import { buildAuthCrossLink } from '@/app/(auth)/auth-redirect'
import { Person } from '@/app/o/[organizationId]/people/[memberId]/person'

export const metadata: Metadata = { title: 'People' }

interface PersonPageProps {
params: Promise<{ organizationId: string; memberId: string }>
}

export default async function PersonPage({ params }: PersonPageProps) {
const { organizationId, memberId } = await params
const session = await getSession()
if (!session?.user)
Comment thread
waleedlatif1 marked this conversation as resolved.
Resolved
redirect(
buildAuthCrossLink('/login', {
callbackUrl: organizationRoutes(organizationId).person(memberId),
isInviteFlow: false,
})
)
if (!(await isPeopleEnabled(organizationId))) notFound()
return <Person organizationId={organizationId} memberId={memberId} />
}
Loading