Skip to content

Commit e338d51

Browse files
committed
Merge branch 'wip/fix-correct' into improvement/org-view
2 parents 8f3b45b + 878abc5 commit e338d51

13 files changed

Lines changed: 521 additions & 56 deletions

File tree

‎apps/sim/app/_shell/providers/get-query-client.ts‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,12 @@ export function makeQueryClient() {
2929
},
3030
mutations: {
3131
retry: false,
32+
/**
33+
* The default pauses a mutation started offline until reconnect. Nothing here persists or
34+
* shows that queue: the caller just waits, holding a pending dialog open, and the action
35+
* then runs long after the person gave up on it. Failing fast lets them see it and decide.
36+
*/
37+
networkMode: 'always',
3238
},
3339
},
3440
})

‎apps/sim/ee/data-retention/hooks/data-retention.ts‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ export function useOrganizationRetention(orgId: string | undefined) {
3737
queryFn: ({ signal }) => fetchDataRetention(orgId as string, signal),
3838
enabled: Boolean(orgId),
3939
staleTime: DATA_RETENTION_STALE_TIME,
40+
// A save re-reads this first: offline, that read must fail rather than wait and write on reconnect.
41+
networkMode: 'always',
4042
})
4143
}
4244

@@ -54,6 +56,9 @@ export function useUpdateOrganizationRetention() {
5456
params: { id: orgId },
5557
body: settings,
5658
}),
59+
onSuccess: ({ data }, { orgId }) => {
60+
queryClient.setQueryData(dataRetentionKeys.settings(orgId), data)
61+
},
5762
onSettled: (_data, _error, { orgId }) => {
5863
queryClient.invalidateQueries({ queryKey: dataRetentionKeys.settings(orgId) })
5964
},

‎apps/sim/lib/credential-groups/viewer-accounts.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ export async function listViewerOrganizationAccounts(input: {
1414
const accounts = await db
1515
.select({
1616
credentialId: credential.id,
17-
accountId: credential.accountId,
17+
providerSubjectId: credential.providerSubjectId,
1818
displayName: credential.displayName,
1919
status: credential.managedOauthStatus,
2020
groupId: credentialGroup.id,

‎apps/sim/lib/guardrails/custom-patterns.ts‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ export const MAX_CUSTOM_PII_PATTERNS = 20
99
* removed rather than kept.
1010
*/
1111
export function customPatternError(regex: string): string | undefined {
12-
const validation = regex.length > 0 ? validateRegexPattern(regex) : null
13-
return validation && !validation.valid ? validation.error : undefined
12+
if (regex.length === 0) return 'Enter a regular expression, or remove this pattern'
13+
const validation = validateRegexPattern(regex)
14+
return validation.valid ? undefined : validation.error
1415
}

‎apps/sim/lib/organizations/application/home-widgets.ts‎

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ interface WidgetReadResult<T> {
107107

108108
/** One account's read: which grant it is, and how many accounts this widget read is sharing. */
109109
interface ViewerAccountRead extends ProviderTokenInput {
110-
accountId: string | null
110+
providerSubjectId: string | null
111111
readableAccounts: number
112112
withToken: AccountTokenRead
113113
}
@@ -173,7 +173,8 @@ async function readViewerAccounts<T>(
173173

174174
const results = await Promise.all(
175175
candidates.map(async (candidate) => {
176-
const { credentialId, accountId, displayName, provider, providerId, status, read } = candidate
176+
const { credentialId, providerSubjectId, displayName, provider, providerId, status, read } =
177+
candidate
177178
const settled = (state: HomeWidgetAccount['state'], rows: T[] = []) => ({
178179
account: { credentialId, displayName, state },
179180
rows: rows.map((row) => ({ ...row, credentialId })),
@@ -186,7 +187,7 @@ async function readViewerAccounts<T>(
186187
const tokenInput = { context, credentialId, providerId }
187188
const account = {
188189
...tokenInput,
189-
accountId,
190+
providerSubjectId,
190191
readableAccounts: toRead.size,
191192
withToken: createAccountTokenRead(tokenInput),
192193
}
@@ -261,7 +262,7 @@ export const readOrganizationHomeEmail = defineOrganizationAccountsUseCase({
261262
accessToken,
262263
signal,
263264
credentialId: account.credentialId,
264-
accountId: account.accountId,
265+
providerSubjectId: account.providerSubjectId,
265266
count: Math.ceil(HOME_EMAIL_THREAD_LIMIT / account.readableAccounts),
266267
})
267268
cursors.set(account.credentialId, { ...cursor, account })

‎apps/sim/lib/organizations/home/provider-reads.test.ts‎

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ async function readGmailThreads(input: { accessToken: string; signal: AbortSigna
8383
const { threads } = await readGmailThreadsPage({
8484
...input,
8585
credentialId: `credential-${credentialCount}`,
86-
accountId: 'account',
86+
providerSubjectId: 'google-sub',
8787
count: HOME_EMAIL_THREAD_LIMIT,
8888
})
8989
return threads
@@ -296,7 +296,13 @@ describe('sharing a Gmail read across accounts', () => {
296296
: respond(gmailThread('t', [metadataMessage('Ada', 'S', 'P')]))
297297
)
298298
const read = (accessToken: string, credentialId: string) =>
299-
readGmailThreadsPage({ accessToken, signal, credentialId, accountId: 'account', count: 1 })
299+
readGmailThreadsPage({
300+
accessToken,
301+
signal,
302+
credentialId,
303+
providerSubjectId: 'google-sub',
304+
count: 1,
305+
})
300306
await read('token-a', 'mailbox-cached')
301307
const rotated = await read('token-b', 'mailbox-cached')
302308
expect(mailboxEmail).toHaveBeenCalledTimes(1)
@@ -305,23 +311,23 @@ describe('sharing a Gmail read across accounts', () => {
305311
expect(mailboxEmail).toHaveBeenCalledTimes(2)
306312
})
307313

308-
it('reads the mailbox again when a reconnect moves the credential to another account', async () => {
314+
it('reads the mailbox again when a reconnect binds a credential that had no subject', async () => {
309315
providerFetch.mockImplementation(async (url: string) =>
310316
url.includes('/threads?')
311317
? respond({ threads: [{ id: 't' }] })
312318
: respond(gmailThread('t', [metadataMessage('Ada', 'S', 'P')]))
313319
)
314-
const read = (accountId: string) =>
320+
const read = (providerSubjectId: string | null) =>
315321
readGmailThreadsPage({
316322
accessToken: 'token',
317323
signal,
318324
credentialId: 'mailbox-reconnected',
319-
accountId,
325+
providerSubjectId,
320326
count: 1,
321327
})
322-
await read('account-old')
328+
await read(null)
323329
mailboxEmail.mockResolvedValue('new@example.com')
324-
const reconnected = await read('account-new')
330+
const reconnected = await read('google-sub-new')
325331
expect(reconnected.threads[0].url).toBe(gmailThreadUrl('t', 'new@example.com'))
326332
})
327333

@@ -338,7 +344,7 @@ describe('sharing a Gmail read across accounts', () => {
338344
accessToken: 'token',
339345
signal,
340346
credentialId: 'mailbox-parallel',
341-
accountId: 'account',
347+
providerSubjectId: 'google-sub',
342348
count: 1,
343349
})
344350
await vi.waitFor(() => expect(mailboxEmail).toHaveBeenCalledOnce())
@@ -358,7 +364,7 @@ describe('sharing a Gmail read across accounts', () => {
358364
accessToken: 't',
359365
signal,
360366
credentialId: 'mailbox-failed',
361-
accountId: 'account',
367+
providerSubjectId: 'google-sub',
362368
count: 1,
363369
})
364370
await expect(read()).rejects.toMatchObject({ failure: 'unavailable' })

‎apps/sim/lib/organizations/home/provider-reads.ts‎

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -175,9 +175,9 @@ function mailboxReadFailure(error: unknown): HomeProviderReadError {
175175
}
176176

177177
/**
178-
* The address thread links open as. A reconnect can point a credential at another Google account, on
179-
* any instance, so the key names the account too. It holds only the address: the token itself is
180-
* resolved fresh for every read.
178+
* The address thread links open as. A reconnect can bind a credential to a Google account it had no
179+
* identity for, on any instance, so the key names the account's subject too. It holds only the
180+
* address: the token itself is resolved fresh for every read.
181181
*/
182182
const gmailMailboxes = new LRUCache<string, string>({
183183
max: 10_000,
@@ -188,9 +188,9 @@ async function readGmailMailbox({
188188
accessToken,
189189
signal,
190190
credentialId,
191-
accountId,
192-
}: ProviderReadInput & { credentialId: string; accountId: string | null }) {
193-
const key = `${credentialId}:${accountId}`
191+
providerSubjectId,
192+
}: ProviderReadInput & { credentialId: string; providerSubjectId: string | null }) {
193+
const key = `${credentialId}:${providerSubjectId}`
194194
const cached = gmailMailboxes.get(key)
195195
if (cached !== undefined) return cached
196196
const mailbox = await getGmailMailboxEmail(accessToken, { signal }).catch((error: unknown) => {
@@ -211,9 +211,13 @@ export async function readGmailThreads({
211211
accessToken,
212212
signal,
213213
credentialId,
214-
accountId,
214+
providerSubjectId,
215215
count,
216-
}: ProviderReadInput & { credentialId: string; accountId: string | null; count: number }): Promise<{
216+
}: ProviderReadInput & {
217+
credentialId: string
218+
providerSubjectId: string | null
219+
count: number
220+
}): Promise<{
217221
threads: HomeEmailThread[]
218222
cursor: GmailThreadCursor
219223
}> {
@@ -223,7 +227,7 @@ export async function readGmailThreads({
223227
{ accessToken, labelIds: 'INBOX', maxResults: HOME_EMAIL_THREAD_LIMIT },
224228
signal
225229
),
226-
readGmailMailbox({ accessToken, signal, credentialId, accountId }),
230+
readGmailMailbox({ accessToken, signal, credentialId, providerSubjectId }),
227231
])
228232
if (!list.success) throw new HomeProviderReadError('unavailable')
229233
const cursor: GmailThreadCursor = {

‎apps/sim/platform/app/(shell)/_components/use-server-draft.ts‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,9 @@ type DraftAction<T> =
3434
/**
3535
* An editing buffer over a value another tab or admin can change. A newer server value is adopted
3636
* while the draft is clean. While it is dirty the edits stay and `conflict` holds the choice, so a
37-
* refetch can neither overwrite an edit nor let a save overwrite a change the editor never saw.
37+
* refetch cannot overwrite an edit. It knows only the values that reach it and checks no version: a
38+
* save before a refetch lands can still overwrite a change the editor never saw, unless the caller
39+
* re-reads the value at save or the route compares versions.
3840
*/
3941
export function useServerDraft<T>({
4042
server,

0 commit comments

Comments
 (0)