Skip to content

Commit d7e26a1

Browse files
committed
Merge branch 'wip/org-people' into wip/platform-org-wiring
2 parents 8d35586 + 2c73588 commit d7e26a1

68 files changed

Lines changed: 5777 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)