Skip to content

Commit f0d2039

Browse files
committed
Bring Members, Requests and People to the Platform on live data
Members and Requests fill their Settings sections, and People is a gated route with a directory, a fixed-layout org chart, profiles, import and sync. Moves the people URL parsers, sheet reader, role lock reason, invite failure message and policy descriptions out of app and component folders so both surfaces read one copy. Legacy behavior is unchanged.
1 parent 2b2b7be commit f0d2039

67 files changed

Lines changed: 5772 additions & 151 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎apps/sim/app/o/[organizationId]/people/[memberId]/person.tsx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ import { useQueryStates } from 'nuqs'
88
import { isApiClientError } from '@/lib/api/client/errors'
99
import type { PersonSummary } from '@/lib/api/contracts/people'
1010
import { organizationRoutes } from '@/lib/navigation/paths'
11+
import { peopleParsers, serializePeopleView } from '@/lib/people/search-params'
1112
import { PersonModal } from '@/app/o/[organizationId]/people/[memberId]/person-modal'
12-
import { peopleParsers, serializePeopleView } from '@/app/o/[organizationId]/people/search-params'
1313
import { useOrganizationContext } from '@/app/o/[organizationId]/providers/organization-provider'
1414
import {
1515
CredentialDetailHeading,

‎apps/sim/app/o/[organizationId]/people/people-chart.tsx‎

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,14 +21,10 @@ import { ChevronLeft, ChevronRight, Search } from '@sim/emcn/icons'
2121
import { useQueryStates } from 'nuqs'
2222
import { EmptyState } from '@/components/empty-state/empty-state'
2323
import { isApiClientError } from '@/lib/api/client/errors'
24+
import { peopleParsers, peopleUrlKeys, serializePeopleView } from '@/lib/people/search-params'
2425
import { ChartCanvas } from '@/app/o/[organizationId]/people/chart-canvas'
2526
import { ChartEmptyState } from '@/app/o/[organizationId]/people/chart-empty-state'
2627
import { LoadingStatus } from '@/app/o/[organizationId]/people/loading-status'
27-
import {
28-
peopleParsers,
29-
peopleUrlKeys,
30-
serializePeopleView,
31-
} from '@/app/o/[organizationId]/people/search-params'
3228
import { usePeoplePicker } from '@/app/o/[organizationId]/people/use-people-picker'
3329
import { useOrganizationContext } from '@/app/o/[organizationId]/providers/organization-provider'
3430
import { ResourceLoadError } from '@/app/workspace/[workspaceId]/components/resource/components/resource-empty-state'

‎apps/sim/app/o/[organizationId]/people/people-directory.tsx‎

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,11 @@ import { useQueryStates } from 'nuqs'
88
import { EmptyState } from '@/components/empty-state/empty-state'
99
import type { PersonSummary } from '@/lib/api/contracts/people'
1010
import { organizationRoutes } from '@/lib/navigation/paths'
11+
import { peopleParsers, peopleUrlKeys, serializePeopleView } from '@/lib/people/search-params'
1112
import { PEOPLE_SEARCH_MAX_LENGTH } from '@/lib/people/validation'
1213
import { SEARCH_DEBOUNCE_MS } from '@/lib/url-state'
1314
import { PEOPLE_COLUMNS, PEOPLE_SEARCH_PLACEHOLDER } from '@/app/o/[organizationId]/people/columns'
1415
import { LoadingStatus } from '@/app/o/[organizationId]/people/loading-status'
15-
import {
16-
peopleParsers,
17-
peopleUrlKeys,
18-
serializePeopleView,
19-
} from '@/app/o/[organizationId]/people/search-params'
2016
import {
2117
ResourceLoadError,
2218
ResourceNoResults,

‎apps/sim/app/o/[organizationId]/people/people-import-modal.tsx‎

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import { isOrganizationSettingsSectionAvailable } from '@/components/settings/na
2020
import { isApiClientError } from '@/lib/api/client/errors'
2121
import type { PeopleImportBody, PeopleImportResult } from '@/lib/api/contracts/people'
2222
import { useDeploymentShape } from '@/lib/core/config/deployment-shape'
23+
import { type PeopleSheet, readPeopleSheet } from '@/lib/people/read-sheet'
2324
import { saveBlob } from '@/lib/uploads/client/download'
2425
import {
2526
IMPORT_SOURCES,
@@ -28,10 +29,6 @@ import {
2829
ProviderFields,
2930
ProvisioningSource,
3031
} from '@/app/o/[organizationId]/people/import-source'
31-
import {
32-
type PeopleSheet,
33-
readPeopleSheet,
34-
} from '@/app/o/[organizationId]/people/read-people-sheet'
3532
import { useOrganizationContext } from '@/app/o/[organizationId]/providers/organization-provider'
3633
import { useScimConnection } from '@/ee/scim/hooks/scim'
3734
import { useImportPeople } from '@/hooks/queries/people'

‎apps/sim/app/o/[organizationId]/people/people.tsx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,10 @@ import { ChipSwitch } from '@sim/emcn'
55
import { List, RefreshCw, Users, Workflow } from '@sim/emcn/icons'
66
import dynamic from 'next/dynamic'
77
import { useQueryStates } from 'nuqs'
8+
import { peopleParsers, peopleUrlKeys } from '@/lib/people/search-params'
89
import { IMPORT_PEOPLE_ACTION } from '@/app/o/[organizationId]/people/columns'
910
import { PeopleDirectory } from '@/app/o/[organizationId]/people/people-directory'
1011
import { PeopleImportModal } from '@/app/o/[organizationId]/people/people-import-modal'
11-
import { peopleParsers, peopleUrlKeys } from '@/app/o/[organizationId]/people/search-params'
1212
import { useOrganizationContext } from '@/app/o/[organizationId]/providers/organization-provider'
1313
import { Resource } from '@/app/workspace/[workspaceId]/components/resource/resource'
1414

‎apps/sim/app/workspace/[workspaceId]/components/invite-modal/invite-modal.tsx‎

Lines changed: 1 addition & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,10 @@ import {
1313
toast,
1414
} from '@sim/emcn'
1515
import { createLogger } from '@sim/logger'
16-
import type { BatchInvitationResult } from '@/lib/api/contracts/invitations'
1716
import { useSession } from '@/lib/auth/client'
1817
import { isEnterprise } from '@/lib/billing/plan-helpers'
1918
import { useDeploymentShape } from '@/lib/core/config/deployment-shape'
19+
import { buildInviteFailureMessage } from '@/lib/invitations/failure-message'
2020
import { quickValidateEmail } from '@/lib/messaging/email/validation'
2121
import type { PermissionType } from '@/lib/workspaces/permissions/utils'
2222
import { useOptionalWorkspaceHostContext } from '@/app/workspace/[workspaceId]/providers/workspace-host-provider'
@@ -47,40 +47,6 @@ const MEMBERSHIP_HINTS: Partial<Record<Membership, string>> = {
4747

4848
const EMPTY_WORKSPACE_IDS: string[] = []
4949

50-
/** Distinct failure reasons listed in full before the remainder is counted instead. */
51-
const MAX_LISTED_FAILURE_REASONS = 3
52-
53-
/**
54-
* Builds the submit error for a batch where some or all invitations failed.
55-
*
56-
* The server rejects per email and every reason names its own address, so the
57-
* reasons are listed rather than collapsed to the first one: inviting several
58-
* people who are all ineligible for the chosen membership is the common
59-
* multi-failure case, and reporting one of them hides who else needs attention.
60-
* Identical reasons are deduplicated, and beyond
61-
* {@link MAX_LISTED_FAILURE_REASONS} the tail is counted rather than listed so a
62-
* large batch cannot bury the modal in near-identical sentences.
63-
*/
64-
export function buildInviteFailureMessage(
65-
failures: BatchInvitationResult['failed'],
66-
attemptedCount: number
67-
): string {
68-
if (failures.length === 1) return failures[0].error
69-
70-
const headline =
71-
failures.length >= attemptedCount
72-
? `None of the ${failures.length} invitations could be sent.`
73-
: `${failures.length} of ${attemptedCount} invitations could not be sent.`
74-
75-
const reasons = [...new Set(failures.map((failure) => failure.error))]
76-
const listed = reasons.slice(0, MAX_LISTED_FAILURE_REASONS)
77-
const remaining = reasons.length - listed.length
78-
79-
return [headline, ...listed, remaining > 0 ? `And ${remaining} more.` : null]
80-
.filter(Boolean)
81-
.join(' ')
82-
}
83-
8450
interface InviteModalProps {
8551
open: boolean
8652
onOpenChange: (open: boolean) => void

‎apps/sim/components/permissions/index.ts‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
export {
2+
type WorkspaceRoleSource,
3+
workspaceRoleLockReason,
4+
} from '@/lib/workspaces/permissions/role-lock'
15
export { AddPeopleModal, type AddPeopleTarget } from './add-people-modal'
26
export {
37
MEMBER_ROLE_OPTIONS,
@@ -18,7 +22,5 @@ export {
1822
credentialRoleLockReason,
1923
RoleLockTooltip,
2024
skillEditorLockReason,
21-
type WorkspaceRoleSource,
2225
workspaceMemberRemovalLockReason,
23-
workspaceRoleLockReason,
2426
} from './role-lock'

‎apps/sim/components/permissions/role-lock.tsx‎

Lines changed: 0 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -3,27 +3,8 @@
33
import type { ReactNode } from 'react'
44
import { Tooltip } from '@sim/emcn'
55

6-
export type WorkspaceRoleSource = 'owner' | 'explicit' | 'org-admin'
76
export type CredentialRoleSource = 'explicit' | 'workspace-admin'
87

9-
/**
10-
* Explanation shown when a workspace member's role is fixed and cannot be
11-
* edited. Returns null for editable roles.
12-
*
13-
* Mirrors the server guards on `PATCH /api/workspaces/[id]/permissions`, which
14-
* refuse the same three cases — so every reason here must have a guard there and
15-
* vice versa, or the UI offers a control that can only fail.
16-
*/
17-
export function workspaceRoleLockReason(
18-
roleSource: WorkspaceRoleSource | undefined,
19-
options?: { isBilledAccount?: boolean }
20-
): string | null {
21-
if (roleSource === 'org-admin') return 'Organization admins are automatically workspace admins'
22-
if (roleSource === 'owner') return 'Workspace owner'
23-
if (options?.isBilledAccount) return 'Workspace billing account'
24-
return null
25-
}
26-
278
/**
289
* Explanation shown when a workspace member cannot be removed from the
2910
* workspace. Returns null when removal is allowed.

‎apps/sim/ee/access-requests/components/policy-changes.test.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { describe, expect, it } from 'vitest'
22
import {
33
describePolicyChange,
44
describePolicyValue,
5-
} from '@/ee/access-requests/components/policy-changes'
5+
} from '@/ee/access-requests/lib/policy-description'
66

77
const target = { kind: 'integration', id: 'slack_v2' } as const
88

‎apps/sim/ee/access-requests/components/policy-changes.tsx‎

Lines changed: 4 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,10 @@ import type {
77
AccessRequestPreviewResponse,
88
AccessRequestTarget,
99
} from '@/lib/api/contracts/access-requests'
10-
import { BLOCK_NAMES } from '@/lib/block-metadata/names.generated'
11-
import { PERMISSION_GROUP_FIELDS } from '@/lib/permission-groups/fields'
12-
import { resolveAccessControlBlockType } from '@/lib/permission-groups/integration-allowlist'
10+
import {
11+
describePolicyChange,
12+
describePolicyValue,
13+
} from '@/ee/access-requests/lib/policy-description'
1314

1415
interface PolicyChangesProps {
1516
changes: AccessRequestPolicyChange[]
@@ -18,74 +19,6 @@ interface PolicyChangesProps {
1819
targetLabel: string
1920
}
2021

21-
function describePolicyItems(
22-
values: string[],
23-
configKey: AccessRequestPolicyChange['configKey'],
24-
target: AccessRequestTarget,
25-
targetLabel: string
26-
): ReadonlyMap<string, string> {
27-
const items = new Map<string, string>()
28-
const integrationTarget =
29-
target.kind === 'integration'
30-
? resolveAccessControlBlockType(target.id.toLowerCase()).toLowerCase()
31-
: null
32-
for (const value of values) {
33-
if (configKey === 'allowedIntegrations') {
34-
const canonical = resolveAccessControlBlockType(value.toLowerCase()).toLowerCase()
35-
const label = Object.hasOwn(BLOCK_NAMES, canonical)
36-
? BLOCK_NAMES[canonical]!
37-
: canonical === integrationTarget
38-
? targetLabel
39-
: value
40-
items.set(canonical, label)
41-
} else {
42-
items.set(value, target.kind !== 'feature' && value === target.id ? targetLabel : value)
43-
}
44-
}
45-
return items
46-
}
47-
48-
export function describePolicyValue(
49-
value: AccessRequestPolicyChange['before'],
50-
configKey: AccessRequestPolicyChange['configKey'],
51-
target: AccessRequestTarget,
52-
targetLabel: string
53-
): string {
54-
if (value === null) return 'All allowed'
55-
if (typeof value === 'boolean') return value ? 'Restricted' : 'Allowed'
56-
return value.length
57-
? [...describePolicyItems(value, configKey, target, targetLabel).values()].join(', ')
58-
: 'None'
59-
}
60-
61-
export function describePolicyChange(
62-
change: AccessRequestPolicyChange,
63-
target: AccessRequestTarget,
64-
targetLabel: string
65-
): string {
66-
const { before, after } = change
67-
const describe = (value: AccessRequestPolicyChange['before']) =>
68-
describePolicyValue(value, change.configKey, target, targetLabel)
69-
if (typeof after === 'boolean') return `${describe(before)} → ${describe(after)}`
70-
if (after === null) return 'Allow all'
71-
const next = describePolicyItems(after, change.configKey, target, targetLabel)
72-
if (before === null)
73-
return next.size ? `Allow only ${[...next.values()].join(', ')}` : 'Allow none'
74-
if (!Array.isArray(before)) return `${describe(before)} → ${describe(after)}`
75-
const previous = describePolicyItems(before, change.configKey, target, targetLabel)
76-
const added = [...next].filter(([id]) => !previous.has(id)).map(([, label]) => label)
77-
const removed = [...previous].filter(([id]) => !next.has(id)).map(([, label]) => label)
78-
const denylist = PERMISSION_GROUP_FIELDS[change.configKey].kind === 'denylist'
79-
return (
80-
[
81-
added.length ? `${denylist ? 'Block' : 'Allow'} ${added.join(', ')}` : '',
82-
removed.length ? `${denylist ? 'Unblock' : 'Remove'} ${removed.join(', ')}` : '',
83-
]
84-
.filter(Boolean)
85-
.join('; ') || 'No membership change'
86-
)
87-
}
88-
8922
export function PolicyChanges({ changes, impact, target, targetLabel }: PolicyChangesProps) {
9023
const [expanded, setExpanded] = useState(false)
9124
const detailsId = useId()

0 commit comments

Comments
 (0)