Skip to content

Commit 1254d0b

Browse files
committed
fix(gitlab): address setup review and test expectations
1 parent 87b0288 commit 1254d0b

6 files changed

Lines changed: 119 additions & 41 deletions

File tree

apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/edit-connector-modal/use-connector-settings-form.ts

Lines changed: 37 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -316,20 +316,28 @@ export function useConnectorSettingsForm({
316316
!hiddenCapFieldIds.has(key) && !valuesEqual(connector.sourceConfig[key], value)
317317
)
318318

319-
const handleSave = () => {
319+
const {
320+
apiKey: permissionApiKey,
321+
complete: permissionsComplete,
322+
dirty: permissionsDirty,
323+
input: permissionInput,
324+
reset: resetPermissions,
325+
} = gitlabPermissions
326+
327+
const handleSave = useCallback(() => {
320328
if (
321329
!searchSettingsAllowed ||
322330
!settingsComplete ||
323331
accessDirty ||
324-
(showGitLabPermissions && !gitlabPermissions.complete)
332+
(showGitLabPermissions && !permissionsComplete)
325333
)
326334
return
327335
setError(null)
328336

329337
const updates: UpdateConnectorBody = {}
330-
if (showGitLabPermissions && gitlabPermissions.dirty) {
331-
updates.permissionConfig = gitlabPermissions.input
332-
if (gitlabPermissions.apiKey.trim()) updates.apiKey = gitlabPermissions.apiKey
338+
if (showGitLabPermissions && permissionsDirty) {
339+
updates.permissionConfig = permissionInput
340+
if (permissionApiKey.trim()) updates.apiKey = permissionApiKey
333341
}
334342

335343
if (syncInterval !== connector.syncIntervalMinutes) {
@@ -338,8 +346,9 @@ export function useConnectorSettingsForm({
338346

339347
const resolved = resolveSourceConfig()
340348
const changedEntries: Record<string, unknown> = {}
349+
const saveHiddenFieldIds = derivedAclCapFieldIds(connectorConfig, access.accessMode)
341350
for (const [key, value] of Object.entries(resolved)) {
342-
if (hiddenCapFieldIds.has(key)) continue
351+
if (saveHiddenFieldIds.has(key)) continue
343352
if (!valuesEqual(connector.sourceConfig[key], value)) changedEntries[key] = value
344353
}
345354

@@ -365,7 +374,7 @@ export function useConnectorSettingsForm({
365374
{ knowledgeBaseId, connectorId: connector.id, updates },
366375
{
367376
onSuccess: (updated) => {
368-
gitlabPermissions.reset(updated.permissionConfig)
377+
resetPermissions(updated.permissionConfig)
369378
onSaved(updated)
370379
},
371380
onError: (err) => {
@@ -374,7 +383,27 @@ export function useConnectorSettingsForm({
374383
},
375384
}
376385
)
377-
}
386+
}, [
387+
access.accessMode,
388+
accessDirty,
389+
canonicalModes,
390+
connector,
391+
connectorConfig,
392+
knowledgeBaseId,
393+
onSaved,
394+
permissionApiKey,
395+
permissionInput,
396+
permissionsComplete,
397+
permissionsDirty,
398+
persistedCanonicalModes,
399+
resetPermissions,
400+
resolveSourceConfig,
401+
searchSettingsAllowed,
402+
settingsComplete,
403+
showGitLabPermissions,
404+
syncInterval,
405+
updateConnector,
406+
])
378407

379408
/**
380409
* The mode switch is its own admin operation: it rewrites document access

apps/sim/app/workspace/[workspaceId]/search/components/search-source-setup.test.tsx

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1026,9 +1026,11 @@ describe('Search source setup with real connector dialogs', () => {
10261026
)
10271027
})
10281028

1029-
it('opens GitLab with its single central method and submits the custom host and PAT', async () => {
1029+
it('opens GitLab token tabs and submits the custom host and PAT with managed access', async () => {
10301030
await render(setup(), '?addConnector=gitlab')
1031-
expect(document.body.textContent).toContain('Admin or service account')
1031+
expect(button('Administrator token')).toHaveAttribute('aria-checked', 'true')
1032+
expect(button('Non-admin token')).toHaveAttribute('aria-checked', 'false')
1033+
expect(document.body.textContent).not.toContain('Connection method')
10321034
expect(document.body.textContent).not.toContain('Member accounts')
10331035
expect(button('Connect & Sync')).toBeDisabled()
10341036
await fill('Enter your GitLab PAT', 'test-pat')
@@ -1197,14 +1199,16 @@ describe('member content credentials in real add and edit dialogs', () => {
11971199
)
11981200
await click(card!)
11991201
expect(document.body.textContent).not.toContain('Connected members')
1200-
expect(button('Workspace')).toHaveAttribute('aria-checked', 'true')
1202+
expect(button('Administrator token')).toHaveAttribute('aria-checked', 'true')
1203+
expect(document.body.textContent).not.toContain('Connection method')
12011204
await fill('Enter your GitLab PAT', 'new-pat')
1205+
await fill('gitlab.example.com', 'gitlab.example.test')
12021206
await fill('group/project or numeric ID', '1')
12031207
expect(button('Connect & Sync')).toBeEnabled()
12041208
await click(button('Connect & Sync'))
12051209
expect(mocks.create.mock.calls[1][0]).toMatchObject({
12061210
connectorType: 'gitlab',
1207-
accessMode: 'workspace',
1211+
accessMode: 'admin',
12081212
apiKey: 'new-pat',
12091213
})
12101214
expect(mocks.create.mock.calls[1][0].sourceConfig).not.toHaveProperty('excludeChannels')
@@ -1978,8 +1982,9 @@ describe('canonical Search connector safety', () => {
19781982
)
19791983
expect(gitlab).toBeDefined()
19801984
await click(gitlab!)
1981-
expect(document.body.textContent).toContain('Admin or service account')
1982-
expect(document.querySelector('[role="radio"][aria-checked="true"]')).toBeNull()
1985+
expect(button('Administrator token')).toHaveAttribute('aria-checked', 'true')
1986+
expect(button('Non-admin token')).toHaveAttribute('aria-checked', 'false')
1987+
expect(document.body.textContent).not.toContain('Connection method')
19831988
expect(
19841989
Array.from(document.querySelectorAll('button')).some(
19851990
(node) => node.textContent === 'Workspace'
@@ -2027,8 +2032,9 @@ describe('resuming Search source setup', () => {
20272032

20282033
it('reopens the source from the URL even when the source filter hides its row', async () => {
20292034
await render(setup(), '?search=nothing-matches&addConnector=gitlab&credentialDraftId=draft-1')
2030-
expect(document.body.textContent).toContain('Admin or service account')
2031-
expect(document.querySelector('[role="radio"][aria-checked="true"]')).toBeNull()
2035+
expect(button('Administrator token')).toHaveAttribute('aria-checked', 'true')
2036+
expect(button('Non-admin token')).toHaveAttribute('aria-checked', 'false')
2037+
expect(document.body.textContent).not.toContain('Connection method')
20322038
expect(document.body.textContent).toContain('Add GitLab project')
20332039
expect(document.body.textContent).not.toContain('Sync Frequency')
20342040
expect(document.body.textContent).not.toContain('Sync automatically')

apps/sim/connectors/gitlab/permission-config/parser.test.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -80,13 +80,11 @@ describe('GitLab CSV permission uploads', () => {
8080
)
8181
).toThrow('4 MiB')
8282
})
83-
it('accepts 100,000 rows within a bounded parsing budget and rejects the next row', () => {
83+
it('accepts 100,000 rows and rejects the next row', () => {
8484
const content = Array.from({ length: 100_000 }, (_, i) => `${i + 1},u${i}@example.com`).join(
8585
'\n'
8686
)
87-
const start = performance.now()
8887
expect(parseGitLabCsv({ filename: 'users.csv', content }, 'userMapping')).toHaveLength(100_000)
89-
expect(performance.now() - start).toBeLessThan(5000)
9088
expect(() =>
9189
parseGitLabCsv(
9290
{ filename: 'users.csv', content: `${content}\n100001,last@example.com` },

apps/sim/connectors/gitlab/permission-config/use-permission-form.ts

Lines changed: 19 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
'use client'
22

3-
import { useRef, useState } from 'react'
3+
import { useCallback, useMemo, useRef, useState } from 'react'
44
import { getErrorMessage } from '@sim/utils/errors'
55
import type {
66
GitLabPermissionData,
@@ -41,18 +41,21 @@ export function useGitLabPermissionForm(initial?: GitLabPermissionData) {
4141
Boolean(apiKey) ||
4242
(mode === 'csv' &&
4343
Object.values(files).some((file) => file.upload || file.error || file.loading))
44-
const input: GitLabPermissionUploadInput = {
45-
provider: 'gitlab',
46-
mode,
47-
...(saved ? { expectedRevision: saved.revision } : {}),
48-
...(mode === 'csv'
49-
? {
50-
userMapping: files.userMapping?.upload,
51-
projectPermissions: files.projectPermissions?.upload,
52-
}
53-
: {}),
54-
}
55-
async function selectFile(kind: GitLabCsvKind, file: File) {
44+
const input = useMemo<GitLabPermissionUploadInput>(
45+
() => ({
46+
provider: 'gitlab',
47+
mode,
48+
...(saved ? { expectedRevision: saved.revision } : {}),
49+
...(mode === 'csv'
50+
? {
51+
userMapping: files.userMapping?.upload,
52+
projectPermissions: files.projectPermissions?.upload,
53+
}
54+
: {}),
55+
}),
56+
[files, mode, saved]
57+
)
58+
const selectFile = useCallback(async (kind: GitLabCsvKind, file: File) => {
5659
const generation = ++generations.current[kind]
5760
setFiles((previous) => ({ ...previous, [kind]: { loading: true } }))
5861
let selected: SelectedFile
@@ -69,15 +72,15 @@ export function useGitLabPermissionForm(initial?: GitLabPermissionData) {
6972
}
7073
if (generation === generations.current[kind])
7174
setFiles((previous) => ({ ...previous, [kind]: selected }))
72-
}
73-
function reset(next?: GitLabPermissionData) {
75+
}, [])
76+
const reset = useCallback((next?: GitLabPermissionData) => {
7477
generations.current.userMapping++
7578
generations.current.projectPermissions++
7679
setFiles({})
7780
setApiKey('')
7881
setSaved(next)
7982
setMode(next?.mode ?? 'administrator')
80-
}
83+
}, [])
8184
return {
8285
mode,
8386
setMode,

apps/sim/lib/knowledge/orchestration/connectors.test.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -430,6 +430,52 @@ describe('performUpdateKnowledgeConnector', () => {
430430
expect(dbChainMockFns.select).not.toHaveBeenCalled()
431431
})
432432

433+
it.each(['permissions', 'token'] as const)(
434+
'commits a %s-only change without dispatching a content sync',
435+
async (change) => {
436+
const existing = {
437+
id: 'conn-1',
438+
connectorType: 'gitlab',
439+
accessMode: 'admin',
440+
status: 'active',
441+
updatedAt: new Date('2026-01-01T00:00:00Z'),
442+
}
443+
dbChainMockFns.limit.mockResolvedValueOnce([existing])
444+
dbChainMockFns.returning.mockResolvedValueOnce([existing])
445+
const write = vi.fn().mockResolvedValue(undefined)
446+
const outcome = await performUpdateKnowledgeConnector({
447+
...ACTOR,
448+
knowledgeBase: KB,
449+
connectorId: existing.id,
450+
updates: {},
451+
permissionChange: {
452+
requiresAclReset: false,
453+
requiresContentSync: false,
454+
...(change === 'token' ? { encryptedApiKey: 'encrypted-fixture-pat' } : {}),
455+
populateSyncContext: vi.fn(),
456+
write,
457+
},
458+
resolveBillingAttribution,
459+
})
460+
461+
expect(outcome).toMatchObject({ success: true })
462+
expect(write).toHaveBeenCalledWith(expect.anything(), existing.id)
463+
expect(mockRecordAudit).toHaveBeenCalledWith(
464+
expect.objectContaining({
465+
metadata: expect.objectContaining({ updatedFields: ['permissionConfig'] }),
466+
})
467+
)
468+
expect(mockDispatchSync).not.toHaveBeenCalled()
469+
expect(mockDispatchMemberSync).not.toHaveBeenCalled()
470+
expect(resolveBillingAttribution).not.toHaveBeenCalled()
471+
if (change === 'token') {
472+
expect(dbChainMockFns.set).toHaveBeenCalledWith(
473+
expect.objectContaining({ encryptedApiKey: 'encrypted-fixture-pat' })
474+
)
475+
}
476+
}
477+
)
478+
433479
it('classifies a sub-hourly interval on an unentitled workspace as forbidden', async () => {
434480
dbChainMockFns.limit.mockResolvedValueOnce([
435481
{ id: 'conn-1', connectorType: 'notion', accessMode: 'workspace' },

apps/sim/lib/knowledge/orchestration/connectors.ts

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -722,13 +722,9 @@ export async function performUpdateKnowledgeConnector(
722722
const updatedFields = Object.keys(updates).filter(
723723
(key) => updates[key as keyof typeof updates] !== undefined
724724
)
725-
const changedFields: string[] = updatedFields
726-
if (params.permissionChange) changedFields.push('permissionConfig')
725+
if (params.permissionChange) updatedFields.push('permissionConfig')
727726
if (updatedFields.length === 0) {
728-
return fail(
729-
'At least one of sourceConfig, syncIntervalMinutes, or status is required',
730-
'validation'
731-
)
727+
return fail('At least one connector setting or permission change is required', 'validation')
732728
}
733729

734730
const existing = await getKnowledgeConnector(kb.id, connectorId)

0 commit comments

Comments
 (0)