chore: import upstream snapshot with attribution
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,28 @@
|
||||
import { requestJson } from '@/lib/api/client/request'
|
||||
import { oauthTokenContract } from '@/lib/api/contracts/selectors'
|
||||
|
||||
export interface OAuthTokenBundle {
|
||||
accessToken: string
|
||||
cloudId?: string
|
||||
domain?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the access token plus any provider-specific extras (e.g. `cloudId` for
|
||||
* Atlassian service accounts whose tokens cannot call api.atlassian.com/oauth/token/accessible-resources).
|
||||
*/
|
||||
export async function fetchOAuthToken(
|
||||
credentialId: string,
|
||||
workflowId?: string
|
||||
): Promise<OAuthTokenBundle | null> {
|
||||
if (!credentialId) return null
|
||||
const token = await requestJson(oauthTokenContract, {
|
||||
body: { credentialId, workflowId },
|
||||
})
|
||||
if (!token.accessToken) return null
|
||||
return {
|
||||
accessToken: token.accessToken,
|
||||
cloudId: token.cloudId,
|
||||
domain: token.domain,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
import { requestJson } from '@/lib/api/client/request'
|
||||
import * as selectorContracts from '@/lib/api/contracts/selectors'
|
||||
import { ensureCredential, SELECTOR_STALE } from '@/hooks/selectors/providers/shared'
|
||||
import type { SelectorDefinition, SelectorKey, SelectorQueryArgs } from '@/hooks/selectors/types'
|
||||
|
||||
export const airtableSelectors = {
|
||||
'airtable.bases': {
|
||||
key: 'airtable.bases',
|
||||
contracts: [selectorContracts.airtableBasesSelectorContract],
|
||||
staleTime: SELECTOR_STALE,
|
||||
getQueryKey: ({ context }: SelectorQueryArgs) => [
|
||||
'selectors',
|
||||
'airtable.bases',
|
||||
context.oauthCredential ?? 'none',
|
||||
],
|
||||
enabled: ({ context }) => Boolean(context.oauthCredential),
|
||||
fetchList: async ({ context, signal }: SelectorQueryArgs) => {
|
||||
const credentialId = ensureCredential(context, 'airtable.bases')
|
||||
const data = await requestJson(selectorContracts.airtableBasesSelectorContract, {
|
||||
body: { credential: credentialId, workflowId: context.workflowId },
|
||||
signal,
|
||||
})
|
||||
return (data.bases || []).map((base) => ({
|
||||
id: base.id,
|
||||
label: base.name,
|
||||
}))
|
||||
},
|
||||
fetchById: async ({ context, detailId, signal }: SelectorQueryArgs) => {
|
||||
if (!detailId) return null
|
||||
const credentialId = ensureCredential(context, 'airtable.bases')
|
||||
const data = await requestJson(selectorContracts.airtableBasesSelectorContract, {
|
||||
body: {
|
||||
credential: credentialId,
|
||||
workflowId: context.workflowId,
|
||||
baseId: detailId,
|
||||
},
|
||||
signal,
|
||||
})
|
||||
const base = (data.bases || []).find((b) => b.id === detailId) ?? null
|
||||
if (!base) return null
|
||||
return { id: base.id, label: base.name }
|
||||
},
|
||||
},
|
||||
'airtable.tables': {
|
||||
key: 'airtable.tables',
|
||||
contracts: [selectorContracts.airtableTablesSelectorContract],
|
||||
staleTime: SELECTOR_STALE,
|
||||
getQueryKey: ({ context }: SelectorQueryArgs) => [
|
||||
'selectors',
|
||||
'airtable.tables',
|
||||
context.oauthCredential ?? 'none',
|
||||
context.baseId ?? 'none',
|
||||
],
|
||||
enabled: ({ context }) => Boolean(context.oauthCredential && context.baseId),
|
||||
fetchList: async ({ context, signal }: SelectorQueryArgs) => {
|
||||
const credentialId = ensureCredential(context, 'airtable.tables')
|
||||
if (!context.baseId) {
|
||||
throw new Error('Missing base ID for airtable.tables selector')
|
||||
}
|
||||
const data = await requestJson(selectorContracts.airtableTablesSelectorContract, {
|
||||
body: {
|
||||
credential: credentialId,
|
||||
workflowId: context.workflowId,
|
||||
baseId: context.baseId,
|
||||
},
|
||||
signal,
|
||||
})
|
||||
return (data.tables || []).map((table) => ({
|
||||
id: table.id,
|
||||
label: table.name,
|
||||
}))
|
||||
},
|
||||
fetchById: async ({ context, detailId, signal }: SelectorQueryArgs) => {
|
||||
if (!detailId) return null
|
||||
const credentialId = ensureCredential(context, 'airtable.tables')
|
||||
if (!context.baseId) return null
|
||||
const data = await requestJson(selectorContracts.airtableTablesSelectorContract, {
|
||||
body: {
|
||||
credential: credentialId,
|
||||
workflowId: context.workflowId,
|
||||
baseId: context.baseId,
|
||||
},
|
||||
signal,
|
||||
})
|
||||
const table = (data.tables || []).find((t) => t.id === detailId) ?? null
|
||||
if (!table) return null
|
||||
return { id: table.id, label: table.name }
|
||||
},
|
||||
},
|
||||
} satisfies Record<Extract<SelectorKey, 'airtable.bases' | 'airtable.tables'>, SelectorDefinition>
|
||||
@@ -0,0 +1,37 @@
|
||||
import { requestJson } from '@/lib/api/client/request'
|
||||
import * as selectorContracts from '@/lib/api/contracts/selectors'
|
||||
import { ensureCredential, SELECTOR_STALE } from '@/hooks/selectors/providers/shared'
|
||||
import type { SelectorDefinition, SelectorKey, SelectorQueryArgs } from '@/hooks/selectors/types'
|
||||
|
||||
export const asanaSelectors = {
|
||||
'asana.workspaces': {
|
||||
key: 'asana.workspaces',
|
||||
contracts: [selectorContracts.asanaWorkspacesSelectorContract],
|
||||
staleTime: SELECTOR_STALE,
|
||||
getQueryKey: ({ context }: SelectorQueryArgs) => [
|
||||
'selectors',
|
||||
'asana.workspaces',
|
||||
context.oauthCredential ?? 'none',
|
||||
],
|
||||
enabled: ({ context }) => Boolean(context.oauthCredential),
|
||||
fetchList: async ({ context, signal }: SelectorQueryArgs) => {
|
||||
const credentialId = ensureCredential(context, 'asana.workspaces')
|
||||
const data = await requestJson(selectorContracts.asanaWorkspacesSelectorContract, {
|
||||
body: { credential: credentialId, workflowId: context.workflowId },
|
||||
signal,
|
||||
})
|
||||
return (data.workspaces || []).map((ws) => ({ id: ws.id, label: ws.name }))
|
||||
},
|
||||
fetchById: async ({ context, detailId, signal }: SelectorQueryArgs) => {
|
||||
if (!detailId) return null
|
||||
const credentialId = ensureCredential(context, 'asana.workspaces')
|
||||
const data = await requestJson(selectorContracts.asanaWorkspacesSelectorContract, {
|
||||
body: { credential: credentialId, workflowId: context.workflowId },
|
||||
signal,
|
||||
})
|
||||
const ws = (data.workspaces || []).find((w) => w.id === detailId) ?? null
|
||||
if (!ws) return null
|
||||
return { id: ws.id, label: ws.name }
|
||||
},
|
||||
},
|
||||
} satisfies Record<Extract<SelectorKey, 'asana.workspaces'>, SelectorDefinition>
|
||||
@@ -0,0 +1,73 @@
|
||||
import { requestJson } from '@/lib/api/client/request'
|
||||
import * as selectorContracts from '@/lib/api/contracts/selectors'
|
||||
import { ensureCredential, SELECTOR_STALE } from '@/hooks/selectors/providers/shared'
|
||||
import type { SelectorDefinition, SelectorKey, SelectorQueryArgs } from '@/hooks/selectors/types'
|
||||
|
||||
export const attioSelectors = {
|
||||
'attio.objects': {
|
||||
key: 'attio.objects',
|
||||
contracts: [selectorContracts.attioObjectsSelectorContract],
|
||||
staleTime: SELECTOR_STALE,
|
||||
getQueryKey: ({ context }: SelectorQueryArgs) => [
|
||||
'selectors',
|
||||
'attio.objects',
|
||||
context.oauthCredential ?? 'none',
|
||||
],
|
||||
enabled: ({ context }) => Boolean(context.oauthCredential),
|
||||
fetchList: async ({ context, signal }: SelectorQueryArgs) => {
|
||||
const credentialId = ensureCredential(context, 'attio.objects')
|
||||
const data = await requestJson(selectorContracts.attioObjectsSelectorContract, {
|
||||
body: { credential: credentialId, workflowId: context.workflowId },
|
||||
signal,
|
||||
})
|
||||
return (data.objects || []).map((obj) => ({
|
||||
id: obj.id,
|
||||
label: obj.name,
|
||||
}))
|
||||
},
|
||||
fetchById: async ({ context, detailId, signal }: SelectorQueryArgs) => {
|
||||
if (!detailId) return null
|
||||
const credentialId = ensureCredential(context, 'attio.objects')
|
||||
const data = await requestJson(selectorContracts.attioObjectsSelectorContract, {
|
||||
body: { credential: credentialId, workflowId: context.workflowId },
|
||||
signal,
|
||||
})
|
||||
const obj = (data.objects || []).find((o) => o.id === detailId) ?? null
|
||||
if (!obj) return null
|
||||
return { id: obj.id, label: obj.name }
|
||||
},
|
||||
},
|
||||
'attio.lists': {
|
||||
key: 'attio.lists',
|
||||
contracts: [selectorContracts.attioListsSelectorContract],
|
||||
staleTime: SELECTOR_STALE,
|
||||
getQueryKey: ({ context }: SelectorQueryArgs) => [
|
||||
'selectors',
|
||||
'attio.lists',
|
||||
context.oauthCredential ?? 'none',
|
||||
],
|
||||
enabled: ({ context }) => Boolean(context.oauthCredential),
|
||||
fetchList: async ({ context, signal }: SelectorQueryArgs) => {
|
||||
const credentialId = ensureCredential(context, 'attio.lists')
|
||||
const data = await requestJson(selectorContracts.attioListsSelectorContract, {
|
||||
body: { credential: credentialId, workflowId: context.workflowId },
|
||||
signal,
|
||||
})
|
||||
return (data.lists || []).map((list) => ({
|
||||
id: list.id,
|
||||
label: list.name,
|
||||
}))
|
||||
},
|
||||
fetchById: async ({ context, detailId, signal }: SelectorQueryArgs) => {
|
||||
if (!detailId) return null
|
||||
const credentialId = ensureCredential(context, 'attio.lists')
|
||||
const data = await requestJson(selectorContracts.attioListsSelectorContract, {
|
||||
body: { credential: credentialId, workflowId: context.workflowId },
|
||||
signal,
|
||||
})
|
||||
const list = (data.lists || []).find((l) => l.id === detailId) ?? null
|
||||
if (!list) return null
|
||||
return { id: list.id, label: list.name }
|
||||
},
|
||||
},
|
||||
} satisfies Record<Extract<SelectorKey, 'attio.objects' | 'attio.lists'>, SelectorDefinition>
|
||||
@@ -0,0 +1,111 @@
|
||||
import { requestJson } from '@/lib/api/client/request'
|
||||
import * as selectorContracts from '@/lib/api/contracts/selectors'
|
||||
import { ensureCredential, SELECTOR_STALE } from '@/hooks/selectors/providers/shared'
|
||||
import type { SelectorDefinition, SelectorKey, SelectorQueryArgs } from '@/hooks/selectors/types'
|
||||
|
||||
export const bigquerySelectors = {
|
||||
'bigquery.datasets': {
|
||||
key: 'bigquery.datasets',
|
||||
contracts: [selectorContracts.bigQueryDatasetsSelectorContract],
|
||||
staleTime: SELECTOR_STALE,
|
||||
getQueryKey: ({ context }: SelectorQueryArgs) => [
|
||||
'selectors',
|
||||
'bigquery.datasets',
|
||||
context.oauthCredential ?? 'none',
|
||||
context.projectId ?? 'none',
|
||||
context.impersonateUserEmail ?? 'none',
|
||||
],
|
||||
enabled: ({ context }) => Boolean(context.oauthCredential && context.projectId),
|
||||
fetchList: async ({ context, signal }: SelectorQueryArgs) => {
|
||||
const credentialId = ensureCredential(context, 'bigquery.datasets')
|
||||
if (!context.projectId) throw new Error('Missing project ID for bigquery.datasets selector')
|
||||
const data = await requestJson(selectorContracts.bigQueryDatasetsSelectorContract, {
|
||||
body: {
|
||||
credential: credentialId,
|
||||
workflowId: context.workflowId,
|
||||
projectId: context.projectId,
|
||||
impersonateEmail: context.impersonateUserEmail,
|
||||
},
|
||||
signal,
|
||||
})
|
||||
return (data.datasets || []).map((ds) => ({
|
||||
id: ds.datasetReference.datasetId,
|
||||
label: ds.friendlyName || ds.datasetReference.datasetId,
|
||||
}))
|
||||
},
|
||||
fetchById: async ({ context, detailId, signal }: SelectorQueryArgs) => {
|
||||
if (!detailId || !context.projectId) return null
|
||||
const credentialId = ensureCredential(context, 'bigquery.datasets')
|
||||
const data = await requestJson(selectorContracts.bigQueryDatasetsSelectorContract, {
|
||||
body: {
|
||||
credential: credentialId,
|
||||
workflowId: context.workflowId,
|
||||
projectId: context.projectId,
|
||||
impersonateEmail: context.impersonateUserEmail,
|
||||
},
|
||||
signal,
|
||||
})
|
||||
const ds =
|
||||
(data.datasets || []).find((d) => d.datasetReference.datasetId === detailId) ?? null
|
||||
if (!ds) return null
|
||||
return {
|
||||
id: ds.datasetReference.datasetId,
|
||||
label: ds.friendlyName || ds.datasetReference.datasetId,
|
||||
}
|
||||
},
|
||||
},
|
||||
'bigquery.tables': {
|
||||
key: 'bigquery.tables',
|
||||
contracts: [selectorContracts.bigQueryTablesSelectorContract],
|
||||
staleTime: SELECTOR_STALE,
|
||||
getQueryKey: ({ context }: SelectorQueryArgs) => [
|
||||
'selectors',
|
||||
'bigquery.tables',
|
||||
context.oauthCredential ?? 'none',
|
||||
context.projectId ?? 'none',
|
||||
context.datasetId ?? 'none',
|
||||
context.impersonateUserEmail ?? 'none',
|
||||
],
|
||||
enabled: ({ context }) =>
|
||||
Boolean(context.oauthCredential && context.projectId && context.datasetId),
|
||||
fetchList: async ({ context, signal }: SelectorQueryArgs) => {
|
||||
const credentialId = ensureCredential(context, 'bigquery.tables')
|
||||
if (!context.projectId) throw new Error('Missing project ID for bigquery.tables selector')
|
||||
if (!context.datasetId) throw new Error('Missing dataset ID for bigquery.tables selector')
|
||||
const data = await requestJson(selectorContracts.bigQueryTablesSelectorContract, {
|
||||
body: {
|
||||
credential: credentialId,
|
||||
workflowId: context.workflowId,
|
||||
projectId: context.projectId,
|
||||
datasetId: context.datasetId,
|
||||
impersonateEmail: context.impersonateUserEmail,
|
||||
},
|
||||
signal,
|
||||
})
|
||||
return (data.tables || []).map((t) => ({
|
||||
id: t.tableReference.tableId,
|
||||
label: t.friendlyName || t.tableReference.tableId,
|
||||
}))
|
||||
},
|
||||
fetchById: async ({ context, detailId, signal }: SelectorQueryArgs) => {
|
||||
if (!detailId || !context.projectId || !context.datasetId) return null
|
||||
const credentialId = ensureCredential(context, 'bigquery.tables')
|
||||
const data = await requestJson(selectorContracts.bigQueryTablesSelectorContract, {
|
||||
body: {
|
||||
credential: credentialId,
|
||||
workflowId: context.workflowId,
|
||||
projectId: context.projectId,
|
||||
datasetId: context.datasetId,
|
||||
impersonateEmail: context.impersonateUserEmail,
|
||||
},
|
||||
signal,
|
||||
})
|
||||
const t = (data.tables || []).find((tbl) => tbl.tableReference.tableId === detailId) ?? null
|
||||
if (!t) return null
|
||||
return { id: t.tableReference.tableId, label: t.friendlyName || t.tableReference.tableId }
|
||||
},
|
||||
},
|
||||
} satisfies Record<
|
||||
Extract<SelectorKey, 'bigquery.datasets' | 'bigquery.tables'>,
|
||||
SelectorDefinition
|
||||
>
|
||||
@@ -0,0 +1,76 @@
|
||||
import { requestJson } from '@/lib/api/client/request'
|
||||
import * as selectorContracts from '@/lib/api/contracts/selectors'
|
||||
import { ensureCredential, SELECTOR_STALE } from '@/hooks/selectors/providers/shared'
|
||||
import type { SelectorDefinition, SelectorKey, SelectorQueryArgs } from '@/hooks/selectors/types'
|
||||
|
||||
export const calcomSelectors = {
|
||||
'calcom.eventTypes': {
|
||||
key: 'calcom.eventTypes',
|
||||
contracts: [selectorContracts.calcomEventTypesSelectorContract],
|
||||
staleTime: SELECTOR_STALE,
|
||||
getQueryKey: ({ context }: SelectorQueryArgs) => [
|
||||
'selectors',
|
||||
'calcom.eventTypes',
|
||||
context.oauthCredential ?? 'none',
|
||||
],
|
||||
enabled: ({ context }) => Boolean(context.oauthCredential),
|
||||
fetchList: async ({ context, signal }: SelectorQueryArgs) => {
|
||||
const credentialId = ensureCredential(context, 'calcom.eventTypes')
|
||||
const data = await requestJson(selectorContracts.calcomEventTypesSelectorContract, {
|
||||
body: { credential: credentialId, workflowId: context.workflowId },
|
||||
signal,
|
||||
})
|
||||
return (data.eventTypes || []).map((et) => ({
|
||||
id: et.id,
|
||||
label: et.title || et.slug,
|
||||
}))
|
||||
},
|
||||
fetchById: async ({ context, detailId, signal }: SelectorQueryArgs) => {
|
||||
if (!detailId) return null
|
||||
const credentialId = ensureCredential(context, 'calcom.eventTypes')
|
||||
const data = await requestJson(selectorContracts.calcomEventTypesSelectorContract, {
|
||||
body: { credential: credentialId, workflowId: context.workflowId },
|
||||
signal,
|
||||
})
|
||||
const et = (data.eventTypes || []).find((e) => e.id === detailId) ?? null
|
||||
if (!et) return null
|
||||
return { id: et.id, label: et.title || et.slug }
|
||||
},
|
||||
},
|
||||
'calcom.schedules': {
|
||||
key: 'calcom.schedules',
|
||||
contracts: [selectorContracts.calcomSchedulesSelectorContract],
|
||||
staleTime: SELECTOR_STALE,
|
||||
getQueryKey: ({ context }: SelectorQueryArgs) => [
|
||||
'selectors',
|
||||
'calcom.schedules',
|
||||
context.oauthCredential ?? 'none',
|
||||
],
|
||||
enabled: ({ context }) => Boolean(context.oauthCredential),
|
||||
fetchList: async ({ context, signal }: SelectorQueryArgs) => {
|
||||
const credentialId = ensureCredential(context, 'calcom.schedules')
|
||||
const data = await requestJson(selectorContracts.calcomSchedulesSelectorContract, {
|
||||
body: { credential: credentialId, workflowId: context.workflowId },
|
||||
signal,
|
||||
})
|
||||
return (data.schedules || []).map((s) => ({
|
||||
id: s.id,
|
||||
label: s.name,
|
||||
}))
|
||||
},
|
||||
fetchById: async ({ context, detailId, signal }: SelectorQueryArgs) => {
|
||||
if (!detailId) return null
|
||||
const credentialId = ensureCredential(context, 'calcom.schedules')
|
||||
const data = await requestJson(selectorContracts.calcomSchedulesSelectorContract, {
|
||||
body: { credential: credentialId, workflowId: context.workflowId },
|
||||
signal,
|
||||
})
|
||||
const s = (data.schedules || []).find((sc) => sc.id === detailId) ?? null
|
||||
if (!s) return null
|
||||
return { id: s.id, label: s.name }
|
||||
},
|
||||
},
|
||||
} satisfies Record<
|
||||
Extract<SelectorKey, 'calcom.eventTypes' | 'calcom.schedules'>,
|
||||
SelectorDefinition
|
||||
>
|
||||
@@ -0,0 +1,94 @@
|
||||
import { requestJson } from '@/lib/api/client/request'
|
||||
import * as selectorContracts from '@/lib/api/contracts/selectors'
|
||||
import { SELECTOR_STALE } from '@/hooks/selectors/providers/shared'
|
||||
import type { SelectorDefinition, SelectorKey, SelectorQueryArgs } from '@/hooks/selectors/types'
|
||||
|
||||
function ensureAwsSelectorCredentials(context: SelectorQueryArgs['context'], key: SelectorKey) {
|
||||
if (!context.awsAccessKeyId || !context.awsSecretAccessKey || !context.awsRegion) {
|
||||
throw new Error(`Missing AWS credentials for selector ${key}`)
|
||||
}
|
||||
|
||||
return {
|
||||
accessKeyId: context.awsAccessKeyId,
|
||||
secretAccessKey: context.awsSecretAccessKey,
|
||||
region: context.awsRegion,
|
||||
}
|
||||
}
|
||||
|
||||
export const cloudwatchSelectors = {
|
||||
'cloudwatch.logGroups': {
|
||||
key: 'cloudwatch.logGroups',
|
||||
contracts: [selectorContracts.cloudwatchLogGroupsSelectorContract],
|
||||
staleTime: SELECTOR_STALE,
|
||||
getQueryKey: ({ context }: SelectorQueryArgs) => [
|
||||
'selectors',
|
||||
'cloudwatch.logGroups',
|
||||
context.awsAccessKeyId ?? 'none',
|
||||
context.awsRegion ?? 'none',
|
||||
],
|
||||
enabled: ({ context }) =>
|
||||
Boolean(context.awsAccessKeyId && context.awsSecretAccessKey && context.awsRegion),
|
||||
fetchList: async ({ context, search, signal }: SelectorQueryArgs) => {
|
||||
const awsCredentials = ensureAwsSelectorCredentials(context, 'cloudwatch.logGroups')
|
||||
const data = await requestJson(selectorContracts.cloudwatchLogGroupsSelectorContract, {
|
||||
body: {
|
||||
...awsCredentials,
|
||||
prefix: search,
|
||||
},
|
||||
signal,
|
||||
})
|
||||
return (data.output?.logGroups || []).map((lg) => ({
|
||||
id: lg.logGroupName,
|
||||
label: lg.logGroupName,
|
||||
}))
|
||||
},
|
||||
fetchById: async ({ detailId, signal }: SelectorQueryArgs) => {
|
||||
if (!detailId) return null
|
||||
return { id: detailId, label: detailId }
|
||||
},
|
||||
},
|
||||
'cloudwatch.logStreams': {
|
||||
key: 'cloudwatch.logStreams',
|
||||
contracts: [selectorContracts.cloudwatchLogStreamsSelectorContract],
|
||||
staleTime: SELECTOR_STALE,
|
||||
getQueryKey: ({ context }: SelectorQueryArgs) => [
|
||||
'selectors',
|
||||
'cloudwatch.logStreams',
|
||||
context.awsAccessKeyId ?? 'none',
|
||||
context.awsRegion ?? 'none',
|
||||
context.logGroupName ?? 'none',
|
||||
],
|
||||
enabled: ({ context }) =>
|
||||
Boolean(
|
||||
context.awsAccessKeyId &&
|
||||
context.awsSecretAccessKey &&
|
||||
context.awsRegion &&
|
||||
context.logGroupName
|
||||
),
|
||||
fetchList: async ({ context, search, signal }: SelectorQueryArgs) => {
|
||||
const awsCredentials = ensureAwsSelectorCredentials(context, 'cloudwatch.logStreams')
|
||||
if (!context.logGroupName) {
|
||||
throw new Error('Missing log group name for cloudwatch.logStreams selector')
|
||||
}
|
||||
const data = await requestJson(selectorContracts.cloudwatchLogStreamsSelectorContract, {
|
||||
body: {
|
||||
...awsCredentials,
|
||||
logGroupName: context.logGroupName,
|
||||
prefix: search,
|
||||
},
|
||||
signal,
|
||||
})
|
||||
return (data.output?.logStreams || []).map((ls) => ({
|
||||
id: ls.logStreamName,
|
||||
label: ls.logStreamName,
|
||||
}))
|
||||
},
|
||||
fetchById: async ({ detailId, signal }: SelectorQueryArgs) => {
|
||||
if (!detailId) return null
|
||||
return { id: detailId, label: detailId }
|
||||
},
|
||||
},
|
||||
} satisfies Record<
|
||||
Extract<SelectorKey, 'cloudwatch.logGroups' | 'cloudwatch.logStreams'>,
|
||||
SelectorDefinition
|
||||
>
|
||||
@@ -0,0 +1,134 @@
|
||||
import { requestJson } from '@/lib/api/client/request'
|
||||
import * as selectorContracts from '@/lib/api/contracts/selectors'
|
||||
import { fetchOAuthToken } from '@/hooks/selectors/helpers'
|
||||
import { ensureCredential, ensureDomain, SELECTOR_STALE } from '@/hooks/selectors/providers/shared'
|
||||
import type { SelectorDefinition, SelectorKey, SelectorQueryArgs } from '@/hooks/selectors/types'
|
||||
|
||||
function formatConfluenceSpaceLabel(space: { name: string; key: string; status?: string }): string {
|
||||
const base = `${space.name} (${space.key})`
|
||||
return space.status === 'archived' ? `${base} — archived` : base
|
||||
}
|
||||
|
||||
function toSpaceOption(space: { name: string; key: string; status?: string }): {
|
||||
id: string
|
||||
label: string
|
||||
} {
|
||||
return { id: space.key, label: formatConfluenceSpaceLabel(space) }
|
||||
}
|
||||
|
||||
export const confluenceSelectors = {
|
||||
'confluence.spaces': {
|
||||
key: 'confluence.spaces',
|
||||
contracts: [selectorContracts.confluenceSpacesSelectorContract],
|
||||
staleTime: SELECTOR_STALE,
|
||||
getQueryKey: ({ context }: SelectorQueryArgs) => [
|
||||
'selectors',
|
||||
'confluence.spaces',
|
||||
context.oauthCredential ?? 'none',
|
||||
context.domain ?? 'none',
|
||||
],
|
||||
enabled: ({ context }) => Boolean(context.oauthCredential && context.domain),
|
||||
/**
|
||||
* Drives pagination through {@link useSelectorOptions}, which drains every
|
||||
* page via this callback. No `fetchList` — the paged path supersedes it.
|
||||
*/
|
||||
fetchPage: async ({ context, cursor, signal }) => {
|
||||
const credentialId = ensureCredential(context, 'confluence.spaces')
|
||||
const domain = ensureDomain(context, 'confluence.spaces')
|
||||
const data = await requestJson(selectorContracts.confluenceSpacesSelectorContract, {
|
||||
body: {
|
||||
credential: credentialId,
|
||||
workflowId: context.workflowId,
|
||||
domain,
|
||||
cursor,
|
||||
},
|
||||
signal,
|
||||
})
|
||||
return {
|
||||
items: (data.spaces || []).map(toSpaceOption),
|
||||
nextCursor: data.nextCursor,
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Resolves a single space label. Hits only the first page — the dropdown's
|
||||
* `fetchPage` stream populates the options cache for spaces beyond page 1,
|
||||
* and `useSelectorOptionMap` merges them in. Walking all pages here would
|
||||
* double API load since the stream is already running in parallel.
|
||||
*/
|
||||
fetchById: async ({ context, detailId, signal }: SelectorQueryArgs) => {
|
||||
if (!detailId) return null
|
||||
const credentialId = ensureCredential(context, 'confluence.spaces')
|
||||
const domain = ensureDomain(context, 'confluence.spaces')
|
||||
const data = await requestJson(selectorContracts.confluenceSpacesSelectorContract, {
|
||||
body: {
|
||||
credential: credentialId,
|
||||
workflowId: context.workflowId,
|
||||
domain,
|
||||
},
|
||||
signal,
|
||||
})
|
||||
const space = (data.spaces || []).find((s) => s.key === detailId) ?? null
|
||||
if (!space) return null
|
||||
return toSpaceOption(space)
|
||||
},
|
||||
},
|
||||
'confluence.pages': {
|
||||
key: 'confluence.pages',
|
||||
contracts: [
|
||||
selectorContracts.confluencePagesSelectorContract,
|
||||
selectorContracts.confluencePageSelectorContract,
|
||||
],
|
||||
staleTime: SELECTOR_STALE,
|
||||
getQueryKey: ({ context, search }: SelectorQueryArgs) => [
|
||||
'selectors',
|
||||
'confluence.pages',
|
||||
context.oauthCredential ?? 'none',
|
||||
context.domain ?? 'none',
|
||||
search ?? '',
|
||||
],
|
||||
enabled: ({ context }) => Boolean(context.oauthCredential && context.domain),
|
||||
fetchList: async ({ context, search, signal }: SelectorQueryArgs) => {
|
||||
const credentialId = ensureCredential(context, 'confluence.pages')
|
||||
const domain = ensureDomain(context, 'confluence.pages')
|
||||
const bundle = await fetchOAuthToken(credentialId, context.workflowId)
|
||||
if (!bundle) {
|
||||
throw new Error('Missing Confluence access token')
|
||||
}
|
||||
const data = await requestJson(selectorContracts.confluencePagesSelectorContract, {
|
||||
body: {
|
||||
domain,
|
||||
accessToken: bundle.accessToken,
|
||||
cloudId: bundle.cloudId,
|
||||
title: search,
|
||||
},
|
||||
signal,
|
||||
})
|
||||
return (data.files || []).map((file) => ({
|
||||
id: file.id,
|
||||
label: file.name,
|
||||
}))
|
||||
},
|
||||
fetchById: async ({ context, detailId, signal }: SelectorQueryArgs) => {
|
||||
if (!detailId) return null
|
||||
const credentialId = ensureCredential(context, 'confluence.pages')
|
||||
const domain = ensureDomain(context, 'confluence.pages')
|
||||
const bundle = await fetchOAuthToken(credentialId, context.workflowId)
|
||||
if (!bundle) {
|
||||
throw new Error('Missing Confluence access token')
|
||||
}
|
||||
const data = await requestJson(selectorContracts.confluencePageSelectorContract, {
|
||||
body: {
|
||||
domain,
|
||||
accessToken: bundle.accessToken,
|
||||
cloudId: bundle.cloudId,
|
||||
pageId: detailId,
|
||||
},
|
||||
signal,
|
||||
})
|
||||
return { id: data.id, label: data.title }
|
||||
},
|
||||
},
|
||||
} satisfies Record<
|
||||
Extract<SelectorKey, 'confluence.spaces' | 'confluence.pages'>,
|
||||
SelectorDefinition
|
||||
>
|
||||
@@ -0,0 +1,188 @@
|
||||
import { requestJson } from '@/lib/api/client/request'
|
||||
import * as selectorContracts from '@/lib/api/contracts/selectors'
|
||||
import { ensureCredential, SELECTOR_STALE } from '@/hooks/selectors/providers/shared'
|
||||
import type { SelectorDefinition, SelectorKey, SelectorQueryArgs } from '@/hooks/selectors/types'
|
||||
|
||||
export const googleSelectors = {
|
||||
'google.tasks.lists': {
|
||||
key: 'google.tasks.lists',
|
||||
contracts: [selectorContracts.googleTasksTaskListsSelectorContract],
|
||||
staleTime: SELECTOR_STALE,
|
||||
getQueryKey: ({ context }: SelectorQueryArgs) => [
|
||||
'selectors',
|
||||
'google.tasks.lists',
|
||||
context.oauthCredential ?? 'none',
|
||||
context.impersonateUserEmail ?? 'none',
|
||||
],
|
||||
enabled: ({ context }) => Boolean(context.oauthCredential),
|
||||
fetchList: async ({ context, signal }: SelectorQueryArgs) => {
|
||||
const credentialId = ensureCredential(context, 'google.tasks.lists')
|
||||
const data = await requestJson(selectorContracts.googleTasksTaskListsSelectorContract, {
|
||||
body: {
|
||||
credential: credentialId,
|
||||
workflowId: context.workflowId,
|
||||
impersonateEmail: context.impersonateUserEmail,
|
||||
},
|
||||
signal,
|
||||
})
|
||||
return (data.taskLists || []).map((tl) => ({ id: tl.id, label: tl.title }))
|
||||
},
|
||||
fetchById: async ({ context, detailId, signal }: SelectorQueryArgs) => {
|
||||
if (!detailId) return null
|
||||
const credentialId = ensureCredential(context, 'google.tasks.lists')
|
||||
const data = await requestJson(selectorContracts.googleTasksTaskListsSelectorContract, {
|
||||
body: {
|
||||
credential: credentialId,
|
||||
workflowId: context.workflowId,
|
||||
impersonateEmail: context.impersonateUserEmail,
|
||||
},
|
||||
signal,
|
||||
})
|
||||
const tl = (data.taskLists || []).find((t) => t.id === detailId) ?? null
|
||||
if (!tl) return null
|
||||
return { id: tl.id, label: tl.title }
|
||||
},
|
||||
},
|
||||
'gmail.labels': {
|
||||
key: 'gmail.labels',
|
||||
contracts: [selectorContracts.gmailLabelsSelectorContract],
|
||||
staleTime: SELECTOR_STALE,
|
||||
getQueryKey: ({ context }: SelectorQueryArgs) => [
|
||||
'selectors',
|
||||
'gmail.labels',
|
||||
context.oauthCredential ?? 'none',
|
||||
context.impersonateUserEmail ?? 'none',
|
||||
],
|
||||
enabled: ({ context }) => Boolean(context.oauthCredential),
|
||||
fetchList: async ({ context, signal }: SelectorQueryArgs) => {
|
||||
const credentialId = ensureCredential(context, 'gmail.labels')
|
||||
const data = await requestJson(selectorContracts.gmailLabelsSelectorContract, {
|
||||
query: {
|
||||
credentialId,
|
||||
impersonateEmail: context.impersonateUserEmail,
|
||||
},
|
||||
signal,
|
||||
})
|
||||
return (data.labels || []).map((label) => ({
|
||||
id: label.id,
|
||||
label: label.name,
|
||||
}))
|
||||
},
|
||||
},
|
||||
'google.calendar': {
|
||||
key: 'google.calendar',
|
||||
contracts: [selectorContracts.googleCalendarSelectorContract],
|
||||
staleTime: SELECTOR_STALE,
|
||||
getQueryKey: ({ context }: SelectorQueryArgs) => [
|
||||
'selectors',
|
||||
'google.calendar',
|
||||
context.oauthCredential ?? 'none',
|
||||
context.impersonateUserEmail ?? 'none',
|
||||
],
|
||||
enabled: ({ context }) => Boolean(context.oauthCredential),
|
||||
fetchList: async ({ context, signal }: SelectorQueryArgs) => {
|
||||
const credentialId = ensureCredential(context, 'google.calendar')
|
||||
const data = await requestJson(selectorContracts.googleCalendarSelectorContract, {
|
||||
query: {
|
||||
credentialId,
|
||||
impersonateEmail: context.impersonateUserEmail,
|
||||
},
|
||||
signal,
|
||||
})
|
||||
return (data.calendars || []).map((calendar) => ({
|
||||
id: calendar.id,
|
||||
label: calendar.summary,
|
||||
}))
|
||||
},
|
||||
},
|
||||
'google.drive': {
|
||||
key: 'google.drive',
|
||||
contracts: [
|
||||
selectorContracts.googleDriveFilesSelectorContract,
|
||||
selectorContracts.googleDriveFileSelectorContract,
|
||||
],
|
||||
staleTime: 15 * 1000,
|
||||
getQueryKey: ({ context, search }: SelectorQueryArgs) => [
|
||||
'selectors',
|
||||
'google.drive',
|
||||
context.oauthCredential ?? 'none',
|
||||
context.mimeType ?? 'any',
|
||||
context.fileId ?? 'root',
|
||||
search ?? '',
|
||||
context.impersonateUserEmail ?? 'none',
|
||||
],
|
||||
enabled: ({ context }) => Boolean(context.oauthCredential),
|
||||
fetchList: async ({ context, search, signal }: SelectorQueryArgs) => {
|
||||
const credentialId = ensureCredential(context, 'google.drive')
|
||||
const data = await requestJson(selectorContracts.googleDriveFilesSelectorContract, {
|
||||
query: {
|
||||
credentialId,
|
||||
mimeType: context.mimeType,
|
||||
parentId: context.fileId,
|
||||
query: search,
|
||||
workflowId: context.workflowId,
|
||||
impersonateEmail: context.impersonateUserEmail,
|
||||
},
|
||||
signal,
|
||||
})
|
||||
return (data.files || []).map((file) => ({
|
||||
id: file.id,
|
||||
label: file.name,
|
||||
}))
|
||||
},
|
||||
fetchById: async ({ context, detailId, signal }: SelectorQueryArgs) => {
|
||||
if (!detailId) return null
|
||||
const credentialId = ensureCredential(context, 'google.drive')
|
||||
const data = await requestJson(selectorContracts.googleDriveFileSelectorContract, {
|
||||
query: {
|
||||
credentialId,
|
||||
fileId: detailId,
|
||||
workflowId: context.workflowId,
|
||||
impersonateEmail: context.impersonateUserEmail,
|
||||
},
|
||||
signal,
|
||||
})
|
||||
const file = data.file
|
||||
if (!file) return null
|
||||
return { id: file.id, label: file.name }
|
||||
},
|
||||
},
|
||||
'google.sheets': {
|
||||
key: 'google.sheets',
|
||||
contracts: [selectorContracts.googleSheetsSelectorContract],
|
||||
staleTime: SELECTOR_STALE,
|
||||
getQueryKey: ({ context }: SelectorQueryArgs) => [
|
||||
'selectors',
|
||||
'google.sheets',
|
||||
context.oauthCredential ?? 'none',
|
||||
context.spreadsheetId ?? 'none',
|
||||
context.impersonateUserEmail ?? 'none',
|
||||
],
|
||||
enabled: ({ context }) => Boolean(context.oauthCredential && context.spreadsheetId),
|
||||
fetchList: async ({ context, signal }: SelectorQueryArgs) => {
|
||||
const credentialId = ensureCredential(context, 'google.sheets')
|
||||
if (!context.spreadsheetId) {
|
||||
throw new Error('Missing spreadsheet ID for google.sheets selector')
|
||||
}
|
||||
const data = await requestJson(selectorContracts.googleSheetsSelectorContract, {
|
||||
query: {
|
||||
credentialId,
|
||||
spreadsheetId: context.spreadsheetId,
|
||||
workflowId: context.workflowId,
|
||||
impersonateEmail: context.impersonateUserEmail,
|
||||
},
|
||||
signal,
|
||||
})
|
||||
return (data.sheets || []).map((sheet) => ({
|
||||
id: sheet.id,
|
||||
label: sheet.name,
|
||||
}))
|
||||
},
|
||||
},
|
||||
} satisfies Record<
|
||||
Extract<
|
||||
SelectorKey,
|
||||
'google.tasks.lists' | 'gmail.labels' | 'google.calendar' | 'google.drive' | 'google.sheets'
|
||||
>,
|
||||
SelectorDefinition
|
||||
>
|
||||
@@ -0,0 +1,134 @@
|
||||
import { requestJson } from '@/lib/api/client/request'
|
||||
import * as selectorContracts from '@/lib/api/contracts/selectors'
|
||||
import { fetchOAuthToken } from '@/hooks/selectors/helpers'
|
||||
import { ensureCredential, ensureDomain, SELECTOR_STALE } from '@/hooks/selectors/providers/shared'
|
||||
import type { SelectorDefinition, SelectorKey, SelectorQueryArgs } from '@/hooks/selectors/types'
|
||||
|
||||
export const jiraSelectors = {
|
||||
'jira.projects': {
|
||||
key: 'jira.projects',
|
||||
contracts: [
|
||||
selectorContracts.jiraProjectsSelectorContract,
|
||||
selectorContracts.jiraProjectSelectorContract,
|
||||
],
|
||||
staleTime: SELECTOR_STALE,
|
||||
getQueryKey: ({ context, search }: SelectorQueryArgs) => [
|
||||
'selectors',
|
||||
'jira.projects',
|
||||
context.oauthCredential ?? 'none',
|
||||
context.domain ?? 'none',
|
||||
search ?? '',
|
||||
],
|
||||
enabled: ({ context }) => Boolean(context.oauthCredential && context.domain),
|
||||
fetchList: async ({ context, search, signal }: SelectorQueryArgs) => {
|
||||
const credentialId = ensureCredential(context, 'jira.projects')
|
||||
const domain = ensureDomain(context, 'jira.projects')
|
||||
const bundle = await fetchOAuthToken(credentialId, context.workflowId)
|
||||
if (!bundle) {
|
||||
throw new Error('Missing Jira access token')
|
||||
}
|
||||
const data = await requestJson(selectorContracts.jiraProjectsSelectorContract, {
|
||||
query: {
|
||||
domain,
|
||||
accessToken: bundle.accessToken,
|
||||
cloudId: bundle.cloudId,
|
||||
query: search,
|
||||
},
|
||||
signal,
|
||||
})
|
||||
return (data.projects || []).map((project) => ({
|
||||
id: project.id,
|
||||
label: project.name,
|
||||
}))
|
||||
},
|
||||
fetchById: async ({ context, detailId, signal }: SelectorQueryArgs) => {
|
||||
if (!detailId) return null
|
||||
const credentialId = ensureCredential(context, 'jira.projects')
|
||||
const domain = ensureDomain(context, 'jira.projects')
|
||||
const bundle = await fetchOAuthToken(credentialId, context.workflowId)
|
||||
if (!bundle) {
|
||||
throw new Error('Missing Jira access token')
|
||||
}
|
||||
const data = await requestJson(selectorContracts.jiraProjectSelectorContract, {
|
||||
body: {
|
||||
domain,
|
||||
accessToken: bundle.accessToken,
|
||||
cloudId: bundle.cloudId,
|
||||
projectId: detailId,
|
||||
},
|
||||
signal,
|
||||
})
|
||||
if (!data.project) return null
|
||||
return {
|
||||
id: data.project.id,
|
||||
label: data.project.name,
|
||||
}
|
||||
},
|
||||
},
|
||||
'jira.issues': {
|
||||
key: 'jira.issues',
|
||||
contracts: [
|
||||
selectorContracts.jiraIssuesSelectorContract,
|
||||
selectorContracts.jiraIssueSelectorContract,
|
||||
],
|
||||
staleTime: 15 * 1000,
|
||||
getQueryKey: ({ context, search }: SelectorQueryArgs) => [
|
||||
'selectors',
|
||||
'jira.issues',
|
||||
context.oauthCredential ?? 'none',
|
||||
context.domain ?? 'none',
|
||||
context.projectId ?? 'none',
|
||||
search ?? '',
|
||||
],
|
||||
enabled: ({ context }) => Boolean(context.oauthCredential && context.domain),
|
||||
fetchList: async ({ context, search, signal }: SelectorQueryArgs) => {
|
||||
const credentialId = ensureCredential(context, 'jira.issues')
|
||||
const domain = ensureDomain(context, 'jira.issues')
|
||||
const bundle = await fetchOAuthToken(credentialId, context.workflowId)
|
||||
if (!bundle) {
|
||||
throw new Error('Missing Jira access token')
|
||||
}
|
||||
const data = await requestJson(selectorContracts.jiraIssuesSelectorContract, {
|
||||
query: {
|
||||
domain,
|
||||
accessToken: bundle.accessToken,
|
||||
cloudId: bundle.cloudId,
|
||||
projectId: context.projectId,
|
||||
query: search,
|
||||
},
|
||||
signal,
|
||||
})
|
||||
const issues =
|
||||
data.sections?.flatMap((section) =>
|
||||
(section.issues || []).map((issue) => ({
|
||||
id: issue.id || issue.key || '',
|
||||
name: issue.summary || issue.key || '',
|
||||
}))
|
||||
) || []
|
||||
return issues
|
||||
.filter((issue) => issue.id)
|
||||
.map((issue) => ({ id: issue.id, label: issue.name || issue.id }))
|
||||
},
|
||||
fetchById: async ({ context, detailId, signal }: SelectorQueryArgs) => {
|
||||
if (!detailId) return null
|
||||
const credentialId = ensureCredential(context, 'jira.issues')
|
||||
const domain = ensureDomain(context, 'jira.issues')
|
||||
const bundle = await fetchOAuthToken(credentialId, context.workflowId)
|
||||
if (!bundle) {
|
||||
throw new Error('Missing Jira access token')
|
||||
}
|
||||
const data = await requestJson(selectorContracts.jiraIssueSelectorContract, {
|
||||
body: {
|
||||
domain,
|
||||
accessToken: bundle.accessToken,
|
||||
cloudId: bundle.cloudId,
|
||||
issueKeys: [detailId],
|
||||
},
|
||||
signal,
|
||||
})
|
||||
const issue = data.issues?.[0]
|
||||
if (!issue) return null
|
||||
return { id: issue.id, label: issue.name }
|
||||
},
|
||||
},
|
||||
} satisfies Record<Extract<SelectorKey, 'jira.projects' | 'jira.issues'>, SelectorDefinition>
|
||||
@@ -0,0 +1,104 @@
|
||||
import { requestJson } from '@/lib/api/client/request'
|
||||
import * as selectorContracts from '@/lib/api/contracts/selectors'
|
||||
import { ensureCredential, ensureDomain, SELECTOR_STALE } from '@/hooks/selectors/providers/shared'
|
||||
import type { SelectorDefinition, SelectorKey, SelectorQueryArgs } from '@/hooks/selectors/types'
|
||||
|
||||
export const jsmSelectors = {
|
||||
'jsm.serviceDesks': {
|
||||
key: 'jsm.serviceDesks',
|
||||
contracts: [selectorContracts.jsmServiceDesksSelectorContract],
|
||||
staleTime: SELECTOR_STALE,
|
||||
getQueryKey: ({ context }: SelectorQueryArgs) => [
|
||||
'selectors',
|
||||
'jsm.serviceDesks',
|
||||
context.oauthCredential ?? 'none',
|
||||
context.domain ?? 'none',
|
||||
],
|
||||
enabled: ({ context }) => Boolean(context.oauthCredential && context.domain),
|
||||
fetchList: async ({ context, signal }: SelectorQueryArgs) => {
|
||||
const credentialId = ensureCredential(context, 'jsm.serviceDesks')
|
||||
const domain = ensureDomain(context, 'jsm.serviceDesks')
|
||||
const data = await requestJson(selectorContracts.jsmServiceDesksSelectorContract, {
|
||||
body: {
|
||||
credential: credentialId,
|
||||
workflowId: context.workflowId,
|
||||
domain,
|
||||
},
|
||||
signal,
|
||||
})
|
||||
return (data.serviceDesks || []).map((sd) => ({
|
||||
id: sd.id,
|
||||
label: sd.name,
|
||||
}))
|
||||
},
|
||||
fetchById: async ({ context, detailId, signal }: SelectorQueryArgs) => {
|
||||
if (!detailId) return null
|
||||
const credentialId = ensureCredential(context, 'jsm.serviceDesks')
|
||||
const domain = ensureDomain(context, 'jsm.serviceDesks')
|
||||
const data = await requestJson(selectorContracts.jsmServiceDesksSelectorContract, {
|
||||
body: {
|
||||
credential: credentialId,
|
||||
workflowId: context.workflowId,
|
||||
domain,
|
||||
},
|
||||
signal,
|
||||
})
|
||||
const sd = (data.serviceDesks || []).find((s) => s.id === detailId) ?? null
|
||||
if (!sd) return null
|
||||
return { id: sd.id, label: sd.name }
|
||||
},
|
||||
},
|
||||
'jsm.requestTypes': {
|
||||
key: 'jsm.requestTypes',
|
||||
contracts: [selectorContracts.jsmRequestTypesSelectorContract],
|
||||
staleTime: SELECTOR_STALE,
|
||||
getQueryKey: ({ context }: SelectorQueryArgs) => [
|
||||
'selectors',
|
||||
'jsm.requestTypes',
|
||||
context.oauthCredential ?? 'none',
|
||||
context.domain ?? 'none',
|
||||
context.serviceDeskId ?? 'none',
|
||||
],
|
||||
enabled: ({ context }) =>
|
||||
Boolean(context.oauthCredential && context.domain && context.serviceDeskId),
|
||||
fetchList: async ({ context, signal }: SelectorQueryArgs) => {
|
||||
const credentialId = ensureCredential(context, 'jsm.requestTypes')
|
||||
const domain = ensureDomain(context, 'jsm.requestTypes')
|
||||
if (!context.serviceDeskId) throw new Error('Missing serviceDeskId for jsm.requestTypes')
|
||||
const data = await requestJson(selectorContracts.jsmRequestTypesSelectorContract, {
|
||||
body: {
|
||||
credential: credentialId,
|
||||
workflowId: context.workflowId,
|
||||
domain,
|
||||
serviceDeskId: context.serviceDeskId,
|
||||
},
|
||||
signal,
|
||||
})
|
||||
return (data.requestTypes || []).map((rt) => ({
|
||||
id: rt.id,
|
||||
label: rt.name,
|
||||
}))
|
||||
},
|
||||
fetchById: async ({ context, detailId, signal }: SelectorQueryArgs) => {
|
||||
if (!detailId) return null
|
||||
const credentialId = ensureCredential(context, 'jsm.requestTypes')
|
||||
const domain = ensureDomain(context, 'jsm.requestTypes')
|
||||
if (!context.serviceDeskId) return null
|
||||
const data = await requestJson(selectorContracts.jsmRequestTypesSelectorContract, {
|
||||
body: {
|
||||
credential: credentialId,
|
||||
workflowId: context.workflowId,
|
||||
domain,
|
||||
serviceDeskId: context.serviceDeskId,
|
||||
},
|
||||
signal,
|
||||
})
|
||||
const rt = (data.requestTypes || []).find((r) => r.id === detailId) ?? null
|
||||
if (!rt) return null
|
||||
return { id: rt.id, label: rt.name }
|
||||
},
|
||||
},
|
||||
} satisfies Record<
|
||||
Extract<SelectorKey, 'jsm.serviceDesks' | 'jsm.requestTypes'>,
|
||||
SelectorDefinition
|
||||
>
|
||||
@@ -0,0 +1,61 @@
|
||||
import { requestJson } from '@/lib/api/client/request'
|
||||
import * as selectorContracts from '@/lib/api/contracts/selectors'
|
||||
import { ensureKnowledgeBase, SELECTOR_STALE } from '@/hooks/selectors/providers/shared'
|
||||
import type { SelectorDefinition, SelectorKey, SelectorQueryArgs } from '@/hooks/selectors/types'
|
||||
|
||||
const KNOWLEDGE_DOCUMENTS_PAGE_LIMIT = 100
|
||||
|
||||
export const knowledgeSelectors = {
|
||||
'knowledge.documents': {
|
||||
key: 'knowledge.documents',
|
||||
contracts: [
|
||||
selectorContracts.listKnowledgeSelectorDocumentsContract,
|
||||
selectorContracts.getKnowledgeSelectorDocumentContract,
|
||||
],
|
||||
staleTime: SELECTOR_STALE,
|
||||
getQueryKey: ({ context, search }: SelectorQueryArgs) => [
|
||||
'selectors',
|
||||
'knowledge.documents',
|
||||
context.knowledgeBaseId ?? 'none',
|
||||
search ?? '',
|
||||
],
|
||||
enabled: ({ context }) => Boolean(context.knowledgeBaseId),
|
||||
/**
|
||||
* Drives pagination through {@link useSelectorOptions}, which drains every
|
||||
* page via this callback. The `pagination.hasMore` flag from the route
|
||||
* decides when to stop; `nextCursor` encodes the next `offset`.
|
||||
*/
|
||||
fetchPage: async ({ context, search, cursor, signal }) => {
|
||||
const knowledgeBaseId = ensureKnowledgeBase(context)
|
||||
const offset = cursor ? Number(cursor) : 0
|
||||
const result = await requestJson(selectorContracts.listKnowledgeSelectorDocumentsContract, {
|
||||
params: { id: knowledgeBaseId },
|
||||
query: {
|
||||
limit: KNOWLEDGE_DOCUMENTS_PAGE_LIMIT,
|
||||
offset,
|
||||
search,
|
||||
},
|
||||
signal,
|
||||
})
|
||||
const { pagination } = result.data
|
||||
const nextOffset = pagination.offset + pagination.limit
|
||||
return {
|
||||
items: result.data.documents.map((doc) => ({
|
||||
id: doc.id,
|
||||
label: doc.filename,
|
||||
})),
|
||||
nextCursor: pagination.hasMore ? String(nextOffset) : undefined,
|
||||
}
|
||||
},
|
||||
fetchById: async ({ context, detailId, signal }: SelectorQueryArgs) => {
|
||||
if (!detailId) return null
|
||||
const knowledgeBaseId = ensureKnowledgeBase(context)
|
||||
const result = await requestJson(selectorContracts.getKnowledgeSelectorDocumentContract, {
|
||||
params: { id: knowledgeBaseId, documentId: detailId },
|
||||
query: { includeDisabled: 'true' },
|
||||
signal,
|
||||
})
|
||||
return { id: result.data.id, label: result.data.filename }
|
||||
},
|
||||
},
|
||||
} satisfies Record<Extract<SelectorKey, 'knowledge.documents'>, SelectorDefinition>
|
||||
@@ -0,0 +1,59 @@
|
||||
import { requestJson } from '@/lib/api/client/request'
|
||||
import * as selectorContracts from '@/lib/api/contracts/selectors'
|
||||
import { ensureCredential, SELECTOR_STALE } from '@/hooks/selectors/providers/shared'
|
||||
import type { SelectorDefinition, SelectorKey, SelectorQueryArgs } from '@/hooks/selectors/types'
|
||||
|
||||
export const linearSelectors = {
|
||||
'linear.teams': {
|
||||
key: 'linear.teams',
|
||||
contracts: [selectorContracts.linearTeamsSelectorContract],
|
||||
staleTime: SELECTOR_STALE,
|
||||
getQueryKey: ({ context }: SelectorQueryArgs) => [
|
||||
'selectors',
|
||||
'linear.teams',
|
||||
context.oauthCredential ?? 'none',
|
||||
],
|
||||
enabled: ({ context }) => Boolean(context.oauthCredential),
|
||||
fetchList: async ({ context, signal }: SelectorQueryArgs) => {
|
||||
const credentialId = ensureCredential(context, 'linear.teams')
|
||||
const data = await requestJson(selectorContracts.linearTeamsSelectorContract, {
|
||||
body: { credential: credentialId, workflowId: context.workflowId },
|
||||
signal,
|
||||
})
|
||||
return (data.teams || []).map((team) => ({
|
||||
id: team.id,
|
||||
label: team.name,
|
||||
}))
|
||||
},
|
||||
},
|
||||
'linear.projects': {
|
||||
key: 'linear.projects',
|
||||
contracts: [selectorContracts.linearProjectsSelectorContract],
|
||||
staleTime: SELECTOR_STALE,
|
||||
getQueryKey: ({ context }: SelectorQueryArgs) => [
|
||||
'selectors',
|
||||
'linear.projects',
|
||||
context.oauthCredential ?? 'none',
|
||||
context.teamId ?? 'none',
|
||||
],
|
||||
enabled: ({ context }) => Boolean(context.oauthCredential && context.teamId),
|
||||
fetchList: async ({ context, signal }: SelectorQueryArgs) => {
|
||||
const credentialId = ensureCredential(context, 'linear.projects')
|
||||
if (!context.teamId) {
|
||||
throw new Error('Missing team ID for linear.projects selector')
|
||||
}
|
||||
const data = await requestJson(selectorContracts.linearProjectsSelectorContract, {
|
||||
body: {
|
||||
credential: credentialId,
|
||||
teamId: context.teamId,
|
||||
workflowId: context.workflowId,
|
||||
},
|
||||
signal,
|
||||
})
|
||||
return (data.projects || []).map((project) => ({
|
||||
id: project.id,
|
||||
label: project.name,
|
||||
}))
|
||||
},
|
||||
},
|
||||
} satisfies Record<Extract<SelectorKey, 'linear.teams' | 'linear.projects'>, SelectorDefinition>
|
||||
@@ -0,0 +1,378 @@
|
||||
import { requestJson } from '@/lib/api/client/request'
|
||||
import * as selectorContracts from '@/lib/api/contracts/selectors'
|
||||
import { ensureCredential, SELECTOR_STALE } from '@/hooks/selectors/providers/shared'
|
||||
import type { SelectorDefinition, SelectorKey, SelectorQueryArgs } from '@/hooks/selectors/types'
|
||||
|
||||
export const microsoftSelectors = {
|
||||
'microsoft.planner.plans': {
|
||||
key: 'microsoft.planner.plans',
|
||||
contracts: [selectorContracts.microsoftPlannerPlansSelectorContract],
|
||||
staleTime: SELECTOR_STALE,
|
||||
getQueryKey: ({ context }: SelectorQueryArgs) => [
|
||||
'selectors',
|
||||
'microsoft.planner.plans',
|
||||
context.oauthCredential ?? 'none',
|
||||
],
|
||||
enabled: ({ context }) => Boolean(context.oauthCredential),
|
||||
fetchList: async ({ context, signal }: SelectorQueryArgs) => {
|
||||
const credentialId = ensureCredential(context, 'microsoft.planner.plans')
|
||||
const data = await requestJson(selectorContracts.microsoftPlannerPlansSelectorContract, {
|
||||
body: { credential: credentialId, workflowId: context.workflowId },
|
||||
signal,
|
||||
})
|
||||
return (data.plans || []).map((plan) => ({ id: plan.id, label: plan.title }))
|
||||
},
|
||||
fetchById: async ({ context, detailId, signal }: SelectorQueryArgs) => {
|
||||
if (!detailId) return null
|
||||
const credentialId = ensureCredential(context, 'microsoft.planner.plans')
|
||||
const data = await requestJson(selectorContracts.microsoftPlannerPlansSelectorContract, {
|
||||
body: { credential: credentialId, workflowId: context.workflowId },
|
||||
signal,
|
||||
})
|
||||
const plan = (data.plans || []).find((p) => p.id === detailId) ?? null
|
||||
if (!plan) return null
|
||||
return { id: plan.id, label: plan.title }
|
||||
},
|
||||
},
|
||||
'outlook.folders': {
|
||||
key: 'outlook.folders',
|
||||
contracts: [selectorContracts.outlookFoldersSelectorContract],
|
||||
staleTime: SELECTOR_STALE,
|
||||
getQueryKey: ({ context }: SelectorQueryArgs) => [
|
||||
'selectors',
|
||||
'outlook.folders',
|
||||
context.oauthCredential ?? 'none',
|
||||
],
|
||||
enabled: ({ context }) => Boolean(context.oauthCredential),
|
||||
fetchList: async ({ context, signal }: SelectorQueryArgs) => {
|
||||
const credentialId = ensureCredential(context, 'outlook.folders')
|
||||
const data = await requestJson(selectorContracts.outlookFoldersSelectorContract, {
|
||||
query: { credentialId },
|
||||
signal,
|
||||
})
|
||||
return (data.folders || []).map((folder) => ({
|
||||
id: folder.id,
|
||||
label: folder.name,
|
||||
}))
|
||||
},
|
||||
},
|
||||
'microsoft.teams': {
|
||||
key: 'microsoft.teams',
|
||||
contracts: [selectorContracts.microsoftTeamsSelectorContract],
|
||||
staleTime: SELECTOR_STALE,
|
||||
getQueryKey: ({ context }: SelectorQueryArgs) => [
|
||||
'selectors',
|
||||
'microsoft.teams',
|
||||
context.oauthCredential ?? 'none',
|
||||
],
|
||||
enabled: ({ context }) => Boolean(context.oauthCredential),
|
||||
fetchList: async ({ context, signal }: SelectorQueryArgs) => {
|
||||
const credentialId = ensureCredential(context, 'microsoft.teams')
|
||||
const data = await requestJson(selectorContracts.microsoftTeamsSelectorContract, {
|
||||
body: { credential: credentialId, workflowId: context.workflowId },
|
||||
signal,
|
||||
})
|
||||
return (data.teams || []).map((team) => ({
|
||||
id: team.id,
|
||||
label: team.displayName,
|
||||
}))
|
||||
},
|
||||
},
|
||||
'microsoft.chats': {
|
||||
key: 'microsoft.chats',
|
||||
contracts: [selectorContracts.microsoftChatsSelectorContract],
|
||||
staleTime: SELECTOR_STALE,
|
||||
getQueryKey: ({ context }: SelectorQueryArgs) => [
|
||||
'selectors',
|
||||
'microsoft.chats',
|
||||
context.oauthCredential ?? 'none',
|
||||
],
|
||||
enabled: ({ context }) => Boolean(context.oauthCredential),
|
||||
fetchList: async ({ context, signal }: SelectorQueryArgs) => {
|
||||
const credentialId = ensureCredential(context, 'microsoft.chats')
|
||||
const data = await requestJson(selectorContracts.microsoftChatsSelectorContract, {
|
||||
body: { credential: credentialId, workflowId: context.workflowId },
|
||||
signal,
|
||||
})
|
||||
return (data.chats || []).map((chat) => ({
|
||||
id: chat.id,
|
||||
label: chat.displayName,
|
||||
}))
|
||||
},
|
||||
},
|
||||
'microsoft.channels': {
|
||||
key: 'microsoft.channels',
|
||||
contracts: [selectorContracts.microsoftChannelsSelectorContract],
|
||||
staleTime: SELECTOR_STALE,
|
||||
getQueryKey: ({ context }: SelectorQueryArgs) => [
|
||||
'selectors',
|
||||
'microsoft.channels',
|
||||
context.oauthCredential ?? 'none',
|
||||
context.teamId ?? 'none',
|
||||
],
|
||||
enabled: ({ context }) => Boolean(context.oauthCredential && context.teamId),
|
||||
fetchList: async ({ context, signal }: SelectorQueryArgs) => {
|
||||
const credentialId = ensureCredential(context, 'microsoft.channels')
|
||||
if (!context.teamId) {
|
||||
throw new Error('Missing team ID for microsoft.channels selector')
|
||||
}
|
||||
const data = await requestJson(selectorContracts.microsoftChannelsSelectorContract, {
|
||||
body: {
|
||||
credential: credentialId,
|
||||
workflowId: context.workflowId,
|
||||
teamId: context.teamId,
|
||||
},
|
||||
signal,
|
||||
})
|
||||
return (data.channels || []).map((channel) => ({
|
||||
id: channel.id,
|
||||
label: channel.displayName,
|
||||
}))
|
||||
},
|
||||
},
|
||||
'microsoft.planner': {
|
||||
key: 'microsoft.planner',
|
||||
contracts: [selectorContracts.microsoftPlannerTasksSelectorContract],
|
||||
staleTime: SELECTOR_STALE,
|
||||
getQueryKey: ({ context }: SelectorQueryArgs) => [
|
||||
'selectors',
|
||||
'microsoft.planner',
|
||||
context.oauthCredential ?? 'none',
|
||||
context.planId ?? 'none',
|
||||
],
|
||||
enabled: ({ context }) => Boolean(context.oauthCredential && context.planId),
|
||||
fetchList: async ({ context, signal }: SelectorQueryArgs) => {
|
||||
const credentialId = ensureCredential(context, 'microsoft.planner')
|
||||
if (!context.planId) {
|
||||
throw new Error('Missing plan ID for microsoft.planner selector')
|
||||
}
|
||||
const data = await requestJson(selectorContracts.microsoftPlannerTasksSelectorContract, {
|
||||
body: {
|
||||
credential: credentialId,
|
||||
workflowId: context.workflowId,
|
||||
planId: context.planId,
|
||||
},
|
||||
signal,
|
||||
})
|
||||
return (data.tasks || []).map((task) => ({
|
||||
id: task.id,
|
||||
label: task.title,
|
||||
}))
|
||||
},
|
||||
fetchById: async ({ context, detailId, signal }: SelectorQueryArgs) => {
|
||||
if (!detailId || !context.planId) return null
|
||||
const credentialId = ensureCredential(context, 'microsoft.planner')
|
||||
const data = await requestJson(selectorContracts.microsoftPlannerTasksSelectorContract, {
|
||||
body: {
|
||||
credential: credentialId,
|
||||
workflowId: context.workflowId,
|
||||
planId: context.planId,
|
||||
},
|
||||
signal,
|
||||
})
|
||||
const task = (data.tasks || []).find((t) => t.id === detailId) ?? null
|
||||
if (!task) return null
|
||||
return { id: task.id, label: task.title }
|
||||
},
|
||||
},
|
||||
'onedrive.files': {
|
||||
key: 'onedrive.files',
|
||||
contracts: [selectorContracts.onedriveFilesSelectorContract],
|
||||
staleTime: SELECTOR_STALE,
|
||||
getQueryKey: ({ context }: SelectorQueryArgs) => [
|
||||
'selectors',
|
||||
'onedrive.files',
|
||||
context.oauthCredential ?? 'none',
|
||||
],
|
||||
enabled: ({ context }) => Boolean(context.oauthCredential),
|
||||
fetchList: async ({ context, signal }: SelectorQueryArgs) => {
|
||||
const credentialId = ensureCredential(context, 'onedrive.files')
|
||||
const data = await requestJson(selectorContracts.onedriveFilesSelectorContract, {
|
||||
query: { credentialId },
|
||||
signal,
|
||||
})
|
||||
return (data.files || []).map((file) => ({
|
||||
id: file.id,
|
||||
label: file.name,
|
||||
}))
|
||||
},
|
||||
},
|
||||
'onedrive.folders': {
|
||||
key: 'onedrive.folders',
|
||||
contracts: [selectorContracts.onedriveFoldersSelectorContract],
|
||||
staleTime: SELECTOR_STALE,
|
||||
getQueryKey: ({ context }: SelectorQueryArgs) => [
|
||||
'selectors',
|
||||
'onedrive.folders',
|
||||
context.oauthCredential ?? 'none',
|
||||
],
|
||||
enabled: ({ context }) => Boolean(context.oauthCredential),
|
||||
fetchList: async ({ context, signal }: SelectorQueryArgs) => {
|
||||
const credentialId = ensureCredential(context, 'onedrive.folders')
|
||||
const data = await requestJson(selectorContracts.onedriveFoldersSelectorContract, {
|
||||
query: { credentialId },
|
||||
signal,
|
||||
})
|
||||
return (data.files || []).map((file) => ({
|
||||
id: file.id,
|
||||
label: file.name,
|
||||
}))
|
||||
},
|
||||
},
|
||||
'microsoft.excel.sheets': {
|
||||
key: 'microsoft.excel.sheets',
|
||||
contracts: [selectorContracts.microsoftExcelSheetsSelectorContract],
|
||||
staleTime: SELECTOR_STALE,
|
||||
getQueryKey: ({ context }: SelectorQueryArgs) => [
|
||||
'selectors',
|
||||
'microsoft.excel.sheets',
|
||||
context.oauthCredential ?? 'none',
|
||||
context.spreadsheetId ?? 'none',
|
||||
context.driveId ?? 'none',
|
||||
],
|
||||
enabled: ({ context }) => Boolean(context.oauthCredential && context.spreadsheetId),
|
||||
fetchList: async ({ context, signal }: SelectorQueryArgs) => {
|
||||
const credentialId = ensureCredential(context, 'microsoft.excel.sheets')
|
||||
if (!context.spreadsheetId) {
|
||||
throw new Error('Missing spreadsheet ID for microsoft.excel.sheets selector')
|
||||
}
|
||||
const data = await requestJson(selectorContracts.microsoftExcelSheetsSelectorContract, {
|
||||
query: {
|
||||
credentialId,
|
||||
spreadsheetId: context.spreadsheetId,
|
||||
driveId: context.driveId,
|
||||
workflowId: context.workflowId,
|
||||
},
|
||||
signal,
|
||||
})
|
||||
return (data.sheets || []).map((sheet) => ({
|
||||
id: sheet.id,
|
||||
label: sheet.name,
|
||||
}))
|
||||
},
|
||||
},
|
||||
'microsoft.excel.drives': {
|
||||
key: 'microsoft.excel.drives',
|
||||
contracts: [
|
||||
selectorContracts.microsoftExcelDrivesSelectorContract,
|
||||
selectorContracts.microsoftExcelDriveSelectorContract,
|
||||
],
|
||||
staleTime: SELECTOR_STALE,
|
||||
getQueryKey: ({ context }: SelectorQueryArgs) => [
|
||||
'selectors',
|
||||
'microsoft.excel.drives',
|
||||
context.oauthCredential ?? 'none',
|
||||
context.siteId ?? 'none',
|
||||
],
|
||||
enabled: ({ context }) => Boolean(context.oauthCredential && context.siteId),
|
||||
fetchList: async ({ context, signal }: SelectorQueryArgs) => {
|
||||
const credentialId = ensureCredential(context, 'microsoft.excel.drives')
|
||||
if (!context.siteId) {
|
||||
throw new Error('Missing site ID for microsoft.excel.drives selector')
|
||||
}
|
||||
const data = await requestJson(selectorContracts.microsoftExcelDrivesSelectorContract, {
|
||||
body: {
|
||||
credential: credentialId,
|
||||
workflowId: context.workflowId,
|
||||
siteId: context.siteId,
|
||||
},
|
||||
signal,
|
||||
})
|
||||
return data.drives.map((drive) => ({
|
||||
id: drive.id,
|
||||
label: drive.name,
|
||||
}))
|
||||
},
|
||||
fetchById: async ({ context, detailId, signal }: SelectorQueryArgs) => {
|
||||
if (!detailId || !context.siteId) return null
|
||||
const credentialId = ensureCredential(context, 'microsoft.excel.drives')
|
||||
const data = await requestJson(selectorContracts.microsoftExcelDriveSelectorContract, {
|
||||
body: {
|
||||
credential: credentialId,
|
||||
workflowId: context.workflowId,
|
||||
siteId: context.siteId,
|
||||
driveId: detailId,
|
||||
},
|
||||
signal,
|
||||
})
|
||||
const { drive } = data
|
||||
if (!drive) return null
|
||||
return { id: drive.id, label: drive.name }
|
||||
},
|
||||
},
|
||||
'microsoft.excel': {
|
||||
key: 'microsoft.excel',
|
||||
contracts: [selectorContracts.microsoftFilesSelectorContract],
|
||||
staleTime: SELECTOR_STALE,
|
||||
getQueryKey: ({ context, search }: SelectorQueryArgs) => [
|
||||
'selectors',
|
||||
'microsoft.excel',
|
||||
context.oauthCredential ?? 'none',
|
||||
context.driveId ?? 'none',
|
||||
search ?? '',
|
||||
],
|
||||
enabled: ({ context }) => Boolean(context.oauthCredential),
|
||||
fetchList: async ({ context, search, signal }: SelectorQueryArgs) => {
|
||||
const credentialId = ensureCredential(context, 'microsoft.excel')
|
||||
const data = await requestJson(selectorContracts.microsoftFilesSelectorContract, {
|
||||
query: {
|
||||
credentialId,
|
||||
query: search,
|
||||
driveId: context.driveId,
|
||||
workflowId: context.workflowId,
|
||||
fileType: 'excel',
|
||||
},
|
||||
signal,
|
||||
})
|
||||
return (data.files || []).map((file) => ({
|
||||
id: file.id,
|
||||
label: file.name,
|
||||
}))
|
||||
},
|
||||
},
|
||||
'microsoft.word': {
|
||||
key: 'microsoft.word',
|
||||
contracts: [selectorContracts.microsoftFilesSelectorContract],
|
||||
staleTime: SELECTOR_STALE,
|
||||
getQueryKey: ({ context, search }: SelectorQueryArgs) => [
|
||||
'selectors',
|
||||
'microsoft.word',
|
||||
context.oauthCredential ?? 'none',
|
||||
search ?? '',
|
||||
],
|
||||
enabled: ({ context }) => Boolean(context.oauthCredential),
|
||||
fetchList: async ({ context, search, signal }: SelectorQueryArgs) => {
|
||||
const credentialId = ensureCredential(context, 'microsoft.word')
|
||||
const data = await requestJson(selectorContracts.microsoftFilesSelectorContract, {
|
||||
query: {
|
||||
credentialId,
|
||||
query: search,
|
||||
workflowId: context.workflowId,
|
||||
fileType: 'word',
|
||||
},
|
||||
signal,
|
||||
})
|
||||
return (data.files || []).map((file) => ({
|
||||
id: file.id,
|
||||
label: file.name,
|
||||
}))
|
||||
},
|
||||
},
|
||||
} satisfies Record<
|
||||
Extract<
|
||||
SelectorKey,
|
||||
| 'microsoft.planner.plans'
|
||||
| 'outlook.folders'
|
||||
| 'microsoft.teams'
|
||||
| 'microsoft.chats'
|
||||
| 'microsoft.channels'
|
||||
| 'microsoft.planner'
|
||||
| 'onedrive.files'
|
||||
| 'onedrive.folders'
|
||||
| 'microsoft.excel.sheets'
|
||||
| 'microsoft.excel.drives'
|
||||
| 'microsoft.excel'
|
||||
| 'microsoft.word'
|
||||
>,
|
||||
SelectorDefinition
|
||||
>
|
||||
@@ -0,0 +1,86 @@
|
||||
import { requestJson } from '@/lib/api/client/request'
|
||||
import * as selectorContracts from '@/lib/api/contracts/selectors'
|
||||
import { ensureCredential, SELECTOR_STALE } from '@/hooks/selectors/providers/shared'
|
||||
import type { SelectorDefinition, SelectorKey, SelectorQueryArgs } from '@/hooks/selectors/types'
|
||||
|
||||
export const mondaySelectors = {
|
||||
'monday.boards': {
|
||||
key: 'monday.boards',
|
||||
contracts: [selectorContracts.mondayBoardsSelectorContract],
|
||||
staleTime: SELECTOR_STALE,
|
||||
getQueryKey: ({ context }: SelectorQueryArgs) => [
|
||||
'selectors',
|
||||
'monday.boards',
|
||||
context.oauthCredential ?? 'none',
|
||||
],
|
||||
enabled: ({ context }) => Boolean(context.oauthCredential),
|
||||
fetchList: async ({ context, signal }: SelectorQueryArgs) => {
|
||||
const credentialId = ensureCredential(context, 'monday.boards')
|
||||
const data = await requestJson(selectorContracts.mondayBoardsSelectorContract, {
|
||||
body: { credential: credentialId, workflowId: context.workflowId },
|
||||
signal,
|
||||
})
|
||||
return (data.boards || []).map((board) => ({
|
||||
id: board.id,
|
||||
label: board.name,
|
||||
}))
|
||||
},
|
||||
fetchById: async ({ context, detailId, signal }: SelectorQueryArgs) => {
|
||||
if (!detailId) return null
|
||||
const credentialId = ensureCredential(context, 'monday.boards')
|
||||
const data = await requestJson(selectorContracts.mondayBoardsSelectorContract, {
|
||||
body: { credential: credentialId, workflowId: context.workflowId },
|
||||
signal,
|
||||
})
|
||||
const board = (data.boards || []).find((b) => b.id === detailId) ?? null
|
||||
if (!board) return null
|
||||
return { id: board.id, label: board.name }
|
||||
},
|
||||
},
|
||||
'monday.groups': {
|
||||
key: 'monday.groups',
|
||||
contracts: [selectorContracts.mondayGroupsSelectorContract],
|
||||
staleTime: SELECTOR_STALE,
|
||||
getQueryKey: ({ context }: SelectorQueryArgs) => [
|
||||
'selectors',
|
||||
'monday.groups',
|
||||
context.oauthCredential ?? 'none',
|
||||
context.boardId ?? 'none',
|
||||
],
|
||||
enabled: ({ context }) => Boolean(context.oauthCredential && context.boardId),
|
||||
fetchList: async ({ context, signal }: SelectorQueryArgs) => {
|
||||
const credentialId = ensureCredential(context, 'monday.groups')
|
||||
if (!context.boardId) {
|
||||
throw new Error('Missing board ID for monday.groups selector')
|
||||
}
|
||||
const data = await requestJson(selectorContracts.mondayGroupsSelectorContract, {
|
||||
body: {
|
||||
credential: credentialId,
|
||||
boardId: context.boardId,
|
||||
workflowId: context.workflowId,
|
||||
},
|
||||
signal,
|
||||
})
|
||||
return (data.groups || []).map((group) => ({
|
||||
id: group.id,
|
||||
label: group.name,
|
||||
}))
|
||||
},
|
||||
fetchById: async ({ context, detailId, signal }: SelectorQueryArgs) => {
|
||||
if (!detailId) return null
|
||||
const credentialId = ensureCredential(context, 'monday.groups')
|
||||
if (!context.boardId) return null
|
||||
const data = await requestJson(selectorContracts.mondayGroupsSelectorContract, {
|
||||
body: {
|
||||
credential: credentialId,
|
||||
boardId: context.boardId,
|
||||
workflowId: context.workflowId,
|
||||
},
|
||||
signal,
|
||||
})
|
||||
const group = (data.groups || []).find((g) => g.id === detailId) ?? null
|
||||
if (!group) return null
|
||||
return { id: group.id, label: group.name }
|
||||
},
|
||||
},
|
||||
} satisfies Record<Extract<SelectorKey, 'monday.boards' | 'monday.groups'>, SelectorDefinition>
|
||||
@@ -0,0 +1,73 @@
|
||||
import { requestJson } from '@/lib/api/client/request'
|
||||
import * as selectorContracts from '@/lib/api/contracts/selectors'
|
||||
import { ensureCredential, SELECTOR_STALE } from '@/hooks/selectors/providers/shared'
|
||||
import type { SelectorDefinition, SelectorKey, SelectorQueryArgs } from '@/hooks/selectors/types'
|
||||
|
||||
export const notionSelectors = {
|
||||
'notion.databases': {
|
||||
key: 'notion.databases',
|
||||
contracts: [selectorContracts.notionDatabasesSelectorContract],
|
||||
staleTime: SELECTOR_STALE,
|
||||
getQueryKey: ({ context }: SelectorQueryArgs) => [
|
||||
'selectors',
|
||||
'notion.databases',
|
||||
context.oauthCredential ?? 'none',
|
||||
],
|
||||
enabled: ({ context }) => Boolean(context.oauthCredential),
|
||||
fetchList: async ({ context, signal }: SelectorQueryArgs) => {
|
||||
const credentialId = ensureCredential(context, 'notion.databases')
|
||||
const data = await requestJson(selectorContracts.notionDatabasesSelectorContract, {
|
||||
body: { credential: credentialId, workflowId: context.workflowId },
|
||||
signal,
|
||||
})
|
||||
return (data.databases || []).map((db) => ({
|
||||
id: db.id,
|
||||
label: db.name,
|
||||
}))
|
||||
},
|
||||
fetchById: async ({ context, detailId, signal }: SelectorQueryArgs) => {
|
||||
if (!detailId) return null
|
||||
const credentialId = ensureCredential(context, 'notion.databases')
|
||||
const data = await requestJson(selectorContracts.notionDatabasesSelectorContract, {
|
||||
body: { credential: credentialId, workflowId: context.workflowId },
|
||||
signal,
|
||||
})
|
||||
const db = (data.databases || []).find((d) => d.id === detailId) ?? null
|
||||
if (!db) return null
|
||||
return { id: db.id, label: db.name }
|
||||
},
|
||||
},
|
||||
'notion.pages': {
|
||||
key: 'notion.pages',
|
||||
contracts: [selectorContracts.notionPagesSelectorContract],
|
||||
staleTime: SELECTOR_STALE,
|
||||
getQueryKey: ({ context }: SelectorQueryArgs) => [
|
||||
'selectors',
|
||||
'notion.pages',
|
||||
context.oauthCredential ?? 'none',
|
||||
],
|
||||
enabled: ({ context }) => Boolean(context.oauthCredential),
|
||||
fetchList: async ({ context, signal }: SelectorQueryArgs) => {
|
||||
const credentialId = ensureCredential(context, 'notion.pages')
|
||||
const data = await requestJson(selectorContracts.notionPagesSelectorContract, {
|
||||
body: { credential: credentialId, workflowId: context.workflowId },
|
||||
signal,
|
||||
})
|
||||
return (data.pages || []).map((page) => ({
|
||||
id: page.id,
|
||||
label: page.name,
|
||||
}))
|
||||
},
|
||||
fetchById: async ({ context, detailId, signal }: SelectorQueryArgs) => {
|
||||
if (!detailId) return null
|
||||
const credentialId = ensureCredential(context, 'notion.pages')
|
||||
const data = await requestJson(selectorContracts.notionPagesSelectorContract, {
|
||||
body: { credential: credentialId, workflowId: context.workflowId },
|
||||
signal,
|
||||
})
|
||||
const page = (data.pages || []).find((p) => p.id === detailId) ?? null
|
||||
if (!page) return null
|
||||
return { id: page.id, label: page.name }
|
||||
},
|
||||
},
|
||||
} satisfies Record<Extract<SelectorKey, 'notion.databases' | 'notion.pages'>, SelectorDefinition>
|
||||
@@ -0,0 +1,40 @@
|
||||
import { requestJson } from '@/lib/api/client/request'
|
||||
import * as selectorContracts from '@/lib/api/contracts/selectors'
|
||||
import { ensureCredential, SELECTOR_STALE } from '@/hooks/selectors/providers/shared'
|
||||
import type { SelectorDefinition, SelectorKey, SelectorQueryArgs } from '@/hooks/selectors/types'
|
||||
|
||||
export const pipedriveSelectors = {
|
||||
'pipedrive.pipelines': {
|
||||
key: 'pipedrive.pipelines',
|
||||
contracts: [selectorContracts.pipedrivePipelinesSelectorContract],
|
||||
staleTime: SELECTOR_STALE,
|
||||
getQueryKey: ({ context }: SelectorQueryArgs) => [
|
||||
'selectors',
|
||||
'pipedrive.pipelines',
|
||||
context.oauthCredential ?? 'none',
|
||||
],
|
||||
enabled: ({ context }) => Boolean(context.oauthCredential),
|
||||
fetchList: async ({ context, signal }: SelectorQueryArgs) => {
|
||||
const credentialId = ensureCredential(context, 'pipedrive.pipelines')
|
||||
const data = await requestJson(selectorContracts.pipedrivePipelinesSelectorContract, {
|
||||
body: { credential: credentialId, workflowId: context.workflowId },
|
||||
signal,
|
||||
})
|
||||
return (data.pipelines || []).map((p) => ({
|
||||
id: p.id,
|
||||
label: p.name,
|
||||
}))
|
||||
},
|
||||
fetchById: async ({ context, detailId, signal }: SelectorQueryArgs) => {
|
||||
if (!detailId) return null
|
||||
const credentialId = ensureCredential(context, 'pipedrive.pipelines')
|
||||
const data = await requestJson(selectorContracts.pipedrivePipelinesSelectorContract, {
|
||||
body: { credential: credentialId, workflowId: context.workflowId },
|
||||
signal,
|
||||
})
|
||||
const p = (data.pipelines || []).find((pl) => pl.id === detailId) ?? null
|
||||
if (!p) return null
|
||||
return { id: p.id, label: p.name }
|
||||
},
|
||||
},
|
||||
} satisfies Record<Extract<SelectorKey, 'pipedrive.pipelines'>, SelectorDefinition>
|
||||
@@ -0,0 +1,24 @@
|
||||
import type { SelectorContext, SelectorKey } from '@/hooks/selectors/types'
|
||||
|
||||
export const SELECTOR_STALE = 60 * 1000
|
||||
|
||||
export const ensureCredential = (context: SelectorContext, key: SelectorKey): string => {
|
||||
if (!context.oauthCredential) {
|
||||
throw new Error(`Missing credential for selector ${key}`)
|
||||
}
|
||||
return context.oauthCredential
|
||||
}
|
||||
|
||||
export const ensureDomain = (context: SelectorContext, key: SelectorKey): string => {
|
||||
if (!context.domain) {
|
||||
throw new Error(`Missing domain for selector ${key}`)
|
||||
}
|
||||
return context.domain
|
||||
}
|
||||
|
||||
export const ensureKnowledgeBase = (context: SelectorContext): string => {
|
||||
if (!context.knowledgeBaseId) {
|
||||
throw new Error('Missing knowledge base id')
|
||||
}
|
||||
return context.knowledgeBaseId
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
import { requestJson } from '@/lib/api/client/request'
|
||||
import * as selectorContracts from '@/lib/api/contracts/selectors'
|
||||
import { ensureCredential, SELECTOR_STALE } from '@/hooks/selectors/providers/shared'
|
||||
import type { SelectorDefinition, SelectorKey, SelectorQueryArgs } from '@/hooks/selectors/types'
|
||||
|
||||
export const sharepointSelectors = {
|
||||
'sharepoint.lists': {
|
||||
key: 'sharepoint.lists',
|
||||
contracts: [selectorContracts.sharepointListsSelectorContract],
|
||||
staleTime: SELECTOR_STALE,
|
||||
getQueryKey: ({ context }: SelectorQueryArgs) => [
|
||||
'selectors',
|
||||
'sharepoint.lists',
|
||||
context.oauthCredential ?? 'none',
|
||||
context.siteId ?? 'none',
|
||||
],
|
||||
enabled: ({ context }) => Boolean(context.oauthCredential && context.siteId),
|
||||
fetchList: async ({ context, signal }: SelectorQueryArgs) => {
|
||||
const credentialId = ensureCredential(context, 'sharepoint.lists')
|
||||
if (!context.siteId) throw new Error('Missing site ID for sharepoint.lists selector')
|
||||
const data = await requestJson(selectorContracts.sharepointListsSelectorContract, {
|
||||
body: {
|
||||
credential: credentialId,
|
||||
workflowId: context.workflowId,
|
||||
siteId: context.siteId,
|
||||
},
|
||||
signal,
|
||||
})
|
||||
return (data.lists || []).map((list) => ({ id: list.id, label: list.displayName }))
|
||||
},
|
||||
fetchById: async ({ context, detailId, signal }: SelectorQueryArgs) => {
|
||||
if (!detailId || !context.siteId) return null
|
||||
const credentialId = ensureCredential(context, 'sharepoint.lists')
|
||||
const data = await requestJson(selectorContracts.sharepointListsSelectorContract, {
|
||||
body: {
|
||||
credential: credentialId,
|
||||
workflowId: context.workflowId,
|
||||
siteId: context.siteId,
|
||||
},
|
||||
signal,
|
||||
})
|
||||
const list = (data.lists || []).find((l) => l.id === detailId) ?? null
|
||||
if (!list) return null
|
||||
return { id: list.id, label: list.displayName }
|
||||
},
|
||||
},
|
||||
'sharepoint.sites': {
|
||||
key: 'sharepoint.sites',
|
||||
contracts: [selectorContracts.sharepointSitesSelectorContract],
|
||||
staleTime: SELECTOR_STALE,
|
||||
getQueryKey: ({ context }: SelectorQueryArgs) => [
|
||||
'selectors',
|
||||
'sharepoint.sites',
|
||||
context.oauthCredential ?? 'none',
|
||||
],
|
||||
enabled: ({ context }) => Boolean(context.oauthCredential),
|
||||
fetchList: async ({ context, signal }: SelectorQueryArgs) => {
|
||||
const credentialId = ensureCredential(context, 'sharepoint.sites')
|
||||
const data = await requestJson(selectorContracts.sharepointSitesSelectorContract, {
|
||||
body: {
|
||||
credential: credentialId,
|
||||
workflowId: context.workflowId,
|
||||
},
|
||||
signal,
|
||||
})
|
||||
return (data.files || []).map((file) => ({
|
||||
id: file.id,
|
||||
label: file.name,
|
||||
}))
|
||||
},
|
||||
fetchById: async ({ context, detailId, signal }: SelectorQueryArgs) => {
|
||||
if (!detailId) return null
|
||||
const credentialId = ensureCredential(context, 'sharepoint.sites')
|
||||
const data = await requestJson(selectorContracts.sharepointSitesSelectorContract, {
|
||||
body: {
|
||||
credential: credentialId,
|
||||
workflowId: context.workflowId,
|
||||
},
|
||||
signal,
|
||||
})
|
||||
const site = (data.files || []).find((f) => f.id === detailId) ?? null
|
||||
if (!site) return null
|
||||
return { id: site.id, label: site.name }
|
||||
},
|
||||
},
|
||||
} satisfies Record<
|
||||
Extract<SelectorKey, 'sharepoint.lists' | 'sharepoint.sites'>,
|
||||
SelectorDefinition
|
||||
>
|
||||
@@ -0,0 +1,119 @@
|
||||
import { getColumnId } from '@/lib/table/column-keys'
|
||||
import { getQueryClient } from '@/app/_shell/providers/get-query-client'
|
||||
import { getTableDetailQueryOptions } from '@/hooks/queries/tables'
|
||||
import { getFolderMap } from '@/hooks/queries/utils/folder-cache'
|
||||
import { getFolderPath } from '@/hooks/queries/utils/folder-tree'
|
||||
import { getWorkflowById, getWorkflows } from '@/hooks/queries/utils/workflow-cache'
|
||||
import { getWorkflowListQueryOptions } from '@/hooks/queries/utils/workflow-list-query'
|
||||
import { SELECTOR_STALE } from '@/hooks/selectors/providers/shared'
|
||||
import { selectorKeys } from '@/hooks/selectors/query-keys'
|
||||
import type {
|
||||
SelectorDefinition,
|
||||
SelectorKey,
|
||||
SelectorOption,
|
||||
SelectorQueryArgs,
|
||||
} from '@/hooks/selectors/types'
|
||||
import type { WorkflowFolder } from '@/stores/folders/types'
|
||||
import type { WorkflowMetadata } from '@/stores/workflows/registry/types'
|
||||
|
||||
/**
|
||||
* Builds a label for a workflow option, appending the folder path when another
|
||||
* workflow in the workspace shares the same display name. Avoids suffixing
|
||||
* every workflow so the dropdown stays readable for the common case.
|
||||
*/
|
||||
function buildDisambiguatedLabel(
|
||||
workflow: WorkflowMetadata,
|
||||
duplicateNames: Set<string>,
|
||||
folders: Record<string, WorkflowFolder>
|
||||
): string {
|
||||
const baseLabel = workflow.name || `Workflow ${workflow.id.slice(0, 8)}`
|
||||
if (!duplicateNames.has(baseLabel)) return baseLabel
|
||||
|
||||
const folderPath = getFolderPath(workflow.folderId, folders)
|
||||
return folderPath ? `${baseLabel} (${folderPath})` : `${baseLabel} (Root)`
|
||||
}
|
||||
|
||||
function collectDuplicateNames(workflows: WorkflowMetadata[]): Set<string> {
|
||||
const counts = new Map<string, number>()
|
||||
for (const workflow of workflows) {
|
||||
const label = workflow.name || `Workflow ${workflow.id.slice(0, 8)}`
|
||||
counts.set(label, (counts.get(label) ?? 0) + 1)
|
||||
}
|
||||
const duplicates = new Set<string>()
|
||||
for (const [label, count] of counts) {
|
||||
if (count > 1) duplicates.add(label)
|
||||
}
|
||||
return duplicates
|
||||
}
|
||||
|
||||
export const simSelectors = {
|
||||
'sim.workflows': {
|
||||
key: 'sim.workflows',
|
||||
staleTime: SELECTOR_STALE,
|
||||
getQueryKey: ({ context }: SelectorQueryArgs) =>
|
||||
context.workspaceId
|
||||
? selectorKeys.simWorkflows(context.workspaceId, context.excludeWorkflowId)
|
||||
: [...selectorKeys.all, 'sim.workflows', 'none', context.excludeWorkflowId ?? 'none'],
|
||||
enabled: ({ context }) => Boolean(context.workspaceId),
|
||||
fetchList: async ({ context, signal }: SelectorQueryArgs): Promise<SelectorOption[]> => {
|
||||
if (!context.workspaceId) return []
|
||||
await getQueryClient().ensureQueryData(getWorkflowListQueryOptions(context.workspaceId))
|
||||
const workflows = getWorkflows(context.workspaceId)
|
||||
const folders = getFolderMap(context.workspaceId)
|
||||
const duplicateNames = collectDuplicateNames(workflows)
|
||||
return workflows
|
||||
.filter((w) => w.id !== context.excludeWorkflowId)
|
||||
.map((w) => ({
|
||||
id: w.id,
|
||||
label: buildDisambiguatedLabel(w, duplicateNames, folders),
|
||||
}))
|
||||
.sort((a, b) => a.label.localeCompare(b.label))
|
||||
},
|
||||
fetchById: async ({
|
||||
context,
|
||||
detailId,
|
||||
signal,
|
||||
}: SelectorQueryArgs): Promise<SelectorOption | null> => {
|
||||
if (!detailId || !context.workspaceId) return null
|
||||
await getQueryClient().ensureQueryData(getWorkflowListQueryOptions(context.workspaceId))
|
||||
const workflow = getWorkflowById(context.workspaceId, detailId)
|
||||
if (!workflow) return null
|
||||
const workflows = getWorkflows(context.workspaceId)
|
||||
const folders = getFolderMap(context.workspaceId)
|
||||
const duplicateNames = collectDuplicateNames(workflows)
|
||||
return {
|
||||
id: detailId,
|
||||
label: buildDisambiguatedLabel(workflow, duplicateNames, folders),
|
||||
}
|
||||
},
|
||||
},
|
||||
'table.columns': {
|
||||
key: 'table.columns',
|
||||
staleTime: SELECTOR_STALE,
|
||||
getQueryKey: ({ context, search }: SelectorQueryArgs) => [
|
||||
...selectorKeys.all,
|
||||
'table.columns',
|
||||
context.workspaceId ?? 'none',
|
||||
context.tableId ?? 'none',
|
||||
search ?? '',
|
||||
],
|
||||
enabled: ({ context }) => Boolean(context.workspaceId && context.tableId),
|
||||
fetchList: async ({ context }: SelectorQueryArgs): Promise<SelectorOption[]> => {
|
||||
if (!context.workspaceId || !context.tableId) return []
|
||||
const table = await getQueryClient().ensureQueryData(
|
||||
getTableDetailQueryOptions(context.workspaceId, context.tableId)
|
||||
)
|
||||
return (table.schema?.columns ?? [])
|
||||
.filter((col) => col.unique)
|
||||
.map((col) => ({ id: getColumnId(col), label: col.name }))
|
||||
},
|
||||
fetchById: async ({ context, detailId }: SelectorQueryArgs): Promise<SelectorOption | null> => {
|
||||
if (!detailId || !context.workspaceId || !context.tableId) return null
|
||||
const table = await getQueryClient().ensureQueryData(
|
||||
getTableDetailQueryOptions(context.workspaceId, context.tableId)
|
||||
)
|
||||
const col = (table.schema?.columns ?? []).find((c) => getColumnId(c) === detailId)
|
||||
return col ? { id: getColumnId(col), label: col.name } : null
|
||||
},
|
||||
},
|
||||
} satisfies Record<Extract<SelectorKey, 'sim.workflows' | 'table.columns'>, SelectorDefinition>
|
||||
@@ -0,0 +1,57 @@
|
||||
import { requestJson } from '@/lib/api/client/request'
|
||||
import * as selectorContracts from '@/lib/api/contracts/selectors'
|
||||
import { ensureCredential, SELECTOR_STALE } from '@/hooks/selectors/providers/shared'
|
||||
import type { SelectorDefinition, SelectorKey, SelectorQueryArgs } from '@/hooks/selectors/types'
|
||||
|
||||
export const slackSelectors = {
|
||||
'slack.channels': {
|
||||
key: 'slack.channels',
|
||||
contracts: [selectorContracts.slackChannelsSelectorContract],
|
||||
staleTime: SELECTOR_STALE,
|
||||
getQueryKey: ({ context }: SelectorQueryArgs) => [
|
||||
'selectors',
|
||||
'slack.channels',
|
||||
context.oauthCredential ?? 'none',
|
||||
],
|
||||
enabled: ({ context }) => Boolean(context.oauthCredential),
|
||||
fetchList: async ({ context, signal }: SelectorQueryArgs) => {
|
||||
const credentialId = ensureCredential(context, 'slack.channels')
|
||||
const data = await requestJson(selectorContracts.slackChannelsSelectorContract, {
|
||||
body: {
|
||||
credential: credentialId,
|
||||
workflowId: context.workflowId,
|
||||
},
|
||||
signal,
|
||||
})
|
||||
return (data.channels || []).map((channel) => ({
|
||||
id: channel.id,
|
||||
label: `#${channel.name}`,
|
||||
}))
|
||||
},
|
||||
},
|
||||
'slack.users': {
|
||||
key: 'slack.users',
|
||||
contracts: [selectorContracts.slackUsersSelectorContract],
|
||||
staleTime: SELECTOR_STALE,
|
||||
getQueryKey: ({ context }: SelectorQueryArgs) => [
|
||||
'selectors',
|
||||
'slack.users',
|
||||
context.oauthCredential ?? 'none',
|
||||
],
|
||||
enabled: ({ context }) => Boolean(context.oauthCredential),
|
||||
fetchList: async ({ context, signal }: SelectorQueryArgs) => {
|
||||
const credentialId = ensureCredential(context, 'slack.users')
|
||||
const data = await requestJson(selectorContracts.slackUsersSelectorContract, {
|
||||
body: {
|
||||
credential: credentialId,
|
||||
workflowId: context.workflowId,
|
||||
},
|
||||
signal,
|
||||
})
|
||||
return data.users.map((user) => ({
|
||||
id: user.id,
|
||||
label: user.real_name || user.name,
|
||||
}))
|
||||
},
|
||||
},
|
||||
} satisfies Record<Extract<SelectorKey, 'slack.channels' | 'slack.users'>, SelectorDefinition>
|
||||
@@ -0,0 +1,39 @@
|
||||
import { requestJson } from '@/lib/api/client/request'
|
||||
import * as selectorContracts from '@/lib/api/contracts/selectors'
|
||||
import { ensureCredential, SELECTOR_STALE } from '@/hooks/selectors/providers/shared'
|
||||
import type { SelectorDefinition, SelectorKey, SelectorQueryArgs } from '@/hooks/selectors/types'
|
||||
|
||||
export const trelloSelectors = {
|
||||
'trello.boards': {
|
||||
key: 'trello.boards',
|
||||
contracts: [selectorContracts.trelloBoardsSelectorContract],
|
||||
staleTime: SELECTOR_STALE,
|
||||
getQueryKey: ({ context }: SelectorQueryArgs) => [
|
||||
'selectors',
|
||||
'trello.boards',
|
||||
context.oauthCredential ?? 'none',
|
||||
],
|
||||
enabled: ({ context }) => Boolean(context.oauthCredential),
|
||||
fetchList: async ({ context, signal }: SelectorQueryArgs) => {
|
||||
const credentialId = ensureCredential(context, 'trello.boards')
|
||||
const data = await requestJson(selectorContracts.trelloBoardsSelectorContract, {
|
||||
body: { credential: credentialId, workflowId: context.workflowId },
|
||||
signal,
|
||||
})
|
||||
return (data.boards || [])
|
||||
.filter((board) => !board.closed)
|
||||
.map((board) => ({ id: board.id, label: board.name }))
|
||||
},
|
||||
fetchById: async ({ context, detailId, signal }: SelectorQueryArgs) => {
|
||||
if (!detailId) return null
|
||||
const credentialId = ensureCredential(context, 'trello.boards')
|
||||
const data = await requestJson(selectorContracts.trelloBoardsSelectorContract, {
|
||||
body: { credential: credentialId, workflowId: context.workflowId },
|
||||
signal,
|
||||
})
|
||||
const board = (data.boards || []).find((b) => b.id === detailId) ?? null
|
||||
if (!board) return null
|
||||
return { id: board.id, label: board.name }
|
||||
},
|
||||
},
|
||||
} satisfies Record<Extract<SelectorKey, 'trello.boards'>, SelectorDefinition>
|
||||
@@ -0,0 +1,30 @@
|
||||
import { requestJson } from '@/lib/api/client/request'
|
||||
import * as selectorContracts from '@/lib/api/contracts/selectors'
|
||||
import { ensureCredential, SELECTOR_STALE } from '@/hooks/selectors/providers/shared'
|
||||
import type { SelectorDefinition, SelectorKey, SelectorQueryArgs } from '@/hooks/selectors/types'
|
||||
|
||||
export const wealthboxSelectors = {
|
||||
'wealthbox.contacts': {
|
||||
key: 'wealthbox.contacts',
|
||||
contracts: [selectorContracts.wealthboxItemsSelectorContract],
|
||||
staleTime: SELECTOR_STALE,
|
||||
getQueryKey: ({ context, search }: SelectorQueryArgs) => [
|
||||
'selectors',
|
||||
'wealthbox.contacts',
|
||||
context.oauthCredential ?? 'none',
|
||||
search ?? '',
|
||||
],
|
||||
enabled: ({ context }) => Boolean(context.oauthCredential),
|
||||
fetchList: async ({ context, search, signal }: SelectorQueryArgs) => {
|
||||
const credentialId = ensureCredential(context, 'wealthbox.contacts')
|
||||
const data = await requestJson(selectorContracts.wealthboxItemsSelectorContract, {
|
||||
query: { credentialId, type: 'contact', query: search ?? '' },
|
||||
signal,
|
||||
})
|
||||
return (data.items || []).map((item) => ({
|
||||
id: item.id,
|
||||
label: item.name,
|
||||
}))
|
||||
},
|
||||
},
|
||||
} satisfies Record<Extract<SelectorKey, 'wealthbox.contacts'>, SelectorDefinition>
|
||||
@@ -0,0 +1,94 @@
|
||||
import { requestJson } from '@/lib/api/client/request'
|
||||
import * as selectorContracts from '@/lib/api/contracts/selectors'
|
||||
import { ensureCredential, SELECTOR_STALE } from '@/hooks/selectors/providers/shared'
|
||||
import type { SelectorDefinition, SelectorKey, SelectorQueryArgs } from '@/hooks/selectors/types'
|
||||
|
||||
export const webflowSelectors = {
|
||||
'webflow.sites': {
|
||||
key: 'webflow.sites',
|
||||
contracts: [selectorContracts.webflowSitesSelectorContract],
|
||||
staleTime: SELECTOR_STALE,
|
||||
getQueryKey: ({ context }: SelectorQueryArgs) => [
|
||||
'selectors',
|
||||
'webflow.sites',
|
||||
context.oauthCredential ?? 'none',
|
||||
],
|
||||
enabled: ({ context }) => Boolean(context.oauthCredential),
|
||||
fetchList: async ({ context, signal }: SelectorQueryArgs) => {
|
||||
const credentialId = ensureCredential(context, 'webflow.sites')
|
||||
const data = await requestJson(selectorContracts.webflowSitesSelectorContract, {
|
||||
body: { credential: credentialId, workflowId: context.workflowId },
|
||||
signal,
|
||||
})
|
||||
return (data.sites || []).map((site) => ({
|
||||
id: site.id,
|
||||
label: site.name,
|
||||
}))
|
||||
},
|
||||
},
|
||||
'webflow.collections': {
|
||||
key: 'webflow.collections',
|
||||
contracts: [selectorContracts.webflowCollectionsSelectorContract],
|
||||
staleTime: SELECTOR_STALE,
|
||||
getQueryKey: ({ context }: SelectorQueryArgs) => [
|
||||
'selectors',
|
||||
'webflow.collections',
|
||||
context.oauthCredential ?? 'none',
|
||||
context.siteId ?? 'none',
|
||||
],
|
||||
enabled: ({ context }) => Boolean(context.oauthCredential && context.siteId),
|
||||
fetchList: async ({ context, signal }: SelectorQueryArgs) => {
|
||||
const credentialId = ensureCredential(context, 'webflow.collections')
|
||||
if (!context.siteId) {
|
||||
throw new Error('Missing site ID for webflow.collections selector')
|
||||
}
|
||||
const data = await requestJson(selectorContracts.webflowCollectionsSelectorContract, {
|
||||
body: {
|
||||
credential: credentialId,
|
||||
workflowId: context.workflowId,
|
||||
siteId: context.siteId,
|
||||
},
|
||||
signal,
|
||||
})
|
||||
return (data.collections || []).map((collection) => ({
|
||||
id: collection.id,
|
||||
label: collection.name,
|
||||
}))
|
||||
},
|
||||
},
|
||||
'webflow.items': {
|
||||
key: 'webflow.items',
|
||||
contracts: [selectorContracts.webflowItemsSelectorContract],
|
||||
staleTime: 15 * 1000,
|
||||
getQueryKey: ({ context, search }: SelectorQueryArgs) => [
|
||||
'selectors',
|
||||
'webflow.items',
|
||||
context.oauthCredential ?? 'none',
|
||||
context.collectionId ?? 'none',
|
||||
search ?? '',
|
||||
],
|
||||
enabled: ({ context }) => Boolean(context.oauthCredential && context.collectionId),
|
||||
fetchList: async ({ context, search, signal }: SelectorQueryArgs) => {
|
||||
const credentialId = ensureCredential(context, 'webflow.items')
|
||||
if (!context.collectionId) {
|
||||
throw new Error('Missing collection ID for webflow.items selector')
|
||||
}
|
||||
const data = await requestJson(selectorContracts.webflowItemsSelectorContract, {
|
||||
body: {
|
||||
credential: credentialId,
|
||||
workflowId: context.workflowId,
|
||||
collectionId: context.collectionId,
|
||||
search,
|
||||
},
|
||||
signal,
|
||||
})
|
||||
return (data.items || []).map((item) => ({
|
||||
id: item.id,
|
||||
label: item.name,
|
||||
}))
|
||||
},
|
||||
},
|
||||
} satisfies Record<
|
||||
Extract<SelectorKey, 'webflow.sites' | 'webflow.collections' | 'webflow.items'>,
|
||||
SelectorDefinition
|
||||
>
|
||||
@@ -0,0 +1,40 @@
|
||||
import { requestJson } from '@/lib/api/client/request'
|
||||
import * as selectorContracts from '@/lib/api/contracts/selectors'
|
||||
import { ensureCredential, SELECTOR_STALE } from '@/hooks/selectors/providers/shared'
|
||||
import type { SelectorDefinition, SelectorKey, SelectorQueryArgs } from '@/hooks/selectors/types'
|
||||
|
||||
export const zoomSelectors = {
|
||||
'zoom.meetings': {
|
||||
key: 'zoom.meetings',
|
||||
contracts: [selectorContracts.zoomMeetingsSelectorContract],
|
||||
staleTime: SELECTOR_STALE,
|
||||
getQueryKey: ({ context }: SelectorQueryArgs) => [
|
||||
'selectors',
|
||||
'zoom.meetings',
|
||||
context.oauthCredential ?? 'none',
|
||||
],
|
||||
enabled: ({ context }) => Boolean(context.oauthCredential),
|
||||
fetchList: async ({ context, signal }: SelectorQueryArgs) => {
|
||||
const credentialId = ensureCredential(context, 'zoom.meetings')
|
||||
const data = await requestJson(selectorContracts.zoomMeetingsSelectorContract, {
|
||||
body: { credential: credentialId, workflowId: context.workflowId },
|
||||
signal,
|
||||
})
|
||||
return (data.meetings || []).map((m) => ({
|
||||
id: m.id,
|
||||
label: m.name || `Meeting ${m.id}`,
|
||||
}))
|
||||
},
|
||||
fetchById: async ({ context, detailId, signal }: SelectorQueryArgs) => {
|
||||
if (!detailId) return null
|
||||
const credentialId = ensureCredential(context, 'zoom.meetings')
|
||||
const data = await requestJson(selectorContracts.zoomMeetingsSelectorContract, {
|
||||
body: { credential: credentialId, workflowId: context.workflowId },
|
||||
signal,
|
||||
})
|
||||
const meeting = (data.meetings || []).find((m) => m.id === detailId) ?? null
|
||||
if (!meeting) return null
|
||||
return { id: meeting.id, label: meeting.name || `Meeting ${meeting.id}` }
|
||||
},
|
||||
},
|
||||
} satisfies Record<Extract<SelectorKey, 'zoom.meetings'>, SelectorDefinition>
|
||||
@@ -0,0 +1,7 @@
|
||||
export const selectorKeys = {
|
||||
all: ['selectors'] as const,
|
||||
simWorkflowsPrefix: (workspaceId: string) =>
|
||||
[...selectorKeys.all, 'sim.workflows', workspaceId] as const,
|
||||
simWorkflows: (workspaceId: string, excludeWorkflowId?: string) =>
|
||||
[...selectorKeys.simWorkflowsPrefix(workspaceId), excludeWorkflowId ?? 'none'] as const,
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
/**
|
||||
* @vitest-environment node
|
||||
*/
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
const { mockEnsureQueryData, mockGetWorkflows, mockGetFolderMap } = vi.hoisted(() => ({
|
||||
mockEnsureQueryData: vi.fn().mockResolvedValue(undefined),
|
||||
mockGetWorkflows: vi.fn(),
|
||||
mockGetFolderMap: vi.fn().mockReturnValue({}),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/_shell/providers/get-query-client', () => ({
|
||||
getQueryClient: vi.fn(() => ({
|
||||
ensureQueryData: mockEnsureQueryData,
|
||||
})),
|
||||
}))
|
||||
|
||||
vi.mock('@/hooks/queries/utils/workflow-cache', () => ({
|
||||
getWorkflows: mockGetWorkflows,
|
||||
getWorkflowById: vi.fn((workspaceId: string, workflowId: string) =>
|
||||
mockGetWorkflows(workspaceId).find((workflow: { id: string }) => workflow.id === workflowId)
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('@/hooks/queries/utils/folder-cache', () => ({
|
||||
getFolderMap: mockGetFolderMap,
|
||||
}))
|
||||
|
||||
vi.mock('@/hooks/queries/utils/workflow-list-query', () => ({
|
||||
getWorkflowListQueryOptions: vi.fn((workspaceId: string) => ({
|
||||
queryKey: ['workflows', 'list', workspaceId, 'active'],
|
||||
})),
|
||||
}))
|
||||
|
||||
import { getSelectorDefinition } from '@/hooks/selectors/registry'
|
||||
|
||||
describe('sim.workflows selector', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockEnsureQueryData.mockResolvedValue(undefined)
|
||||
mockGetWorkflows.mockReturnValue([
|
||||
{ id: 'wf-1', name: 'Alpha Workflow', folderId: null },
|
||||
{ id: 'wf-2', name: 'Bravo Workflow', folderId: null },
|
||||
])
|
||||
mockGetFolderMap.mockReturnValue({})
|
||||
})
|
||||
|
||||
it('requires an explicit workspaceId in selector context', () => {
|
||||
const definition = getSelectorDefinition('sim.workflows')
|
||||
|
||||
expect(definition.enabled?.({ key: 'sim.workflows', context: {} })).toBe(false)
|
||||
expect(definition.staleTime).toBe(60_000)
|
||||
expect(
|
||||
definition.getQueryKey({
|
||||
key: 'sim.workflows',
|
||||
context: { workspaceId: 'ws-1', excludeWorkflowId: 'wf-2' },
|
||||
})
|
||||
).toEqual(['selectors', 'sim.workflows', 'ws-1', 'wf-2'])
|
||||
})
|
||||
|
||||
it('reads workflow options from the scoped workflow cache', async () => {
|
||||
const definition = getSelectorDefinition('sim.workflows')
|
||||
|
||||
const options = await definition.fetchList!({
|
||||
key: 'sim.workflows',
|
||||
context: { workspaceId: 'ws-1', excludeWorkflowId: 'wf-2' },
|
||||
})
|
||||
|
||||
expect(mockEnsureQueryData).toHaveBeenCalledWith({
|
||||
queryKey: ['workflows', 'list', 'ws-1', 'active'],
|
||||
})
|
||||
expect(mockGetWorkflows).toHaveBeenCalledWith('ws-1')
|
||||
expect(options).toEqual([{ id: 'wf-1', label: 'Alpha Workflow' }])
|
||||
})
|
||||
|
||||
it('resolves workflow labels by id using the same workspace scope', async () => {
|
||||
const definition = getSelectorDefinition('sim.workflows')
|
||||
|
||||
const option = await definition.fetchById?.({
|
||||
key: 'sim.workflows',
|
||||
context: { workspaceId: 'ws-1' },
|
||||
detailId: 'wf-2',
|
||||
})
|
||||
|
||||
expect(mockEnsureQueryData).toHaveBeenCalledWith({
|
||||
queryKey: ['workflows', 'list', 'ws-1', 'active'],
|
||||
})
|
||||
expect(mockGetWorkflows).toHaveBeenCalledWith('ws-1')
|
||||
expect(option).toEqual({ id: 'wf-2', label: 'Bravo Workflow' })
|
||||
})
|
||||
|
||||
it('disambiguates duplicate workflow names with their folder path', async () => {
|
||||
mockGetWorkflows.mockReturnValue([
|
||||
{ id: 'wf-root', name: 'Pipeline', folderId: null },
|
||||
{ id: 'wf-eng', name: 'Pipeline', folderId: 'folder-eng' },
|
||||
{ id: 'wf-eng-backend', name: 'Pipeline', folderId: 'folder-backend' },
|
||||
{ id: 'wf-unique', name: 'Solo Workflow', folderId: 'folder-eng' },
|
||||
])
|
||||
mockGetFolderMap.mockReturnValue({
|
||||
'folder-eng': {
|
||||
id: 'folder-eng',
|
||||
name: 'Engineering',
|
||||
parentId: null,
|
||||
workspaceId: 'ws-1',
|
||||
},
|
||||
'folder-backend': {
|
||||
id: 'folder-backend',
|
||||
name: 'Backend',
|
||||
parentId: 'folder-eng',
|
||||
workspaceId: 'ws-1',
|
||||
},
|
||||
})
|
||||
|
||||
const definition = getSelectorDefinition('sim.workflows')
|
||||
const options = await definition.fetchList!({
|
||||
key: 'sim.workflows',
|
||||
context: { workspaceId: 'ws-1' },
|
||||
})
|
||||
|
||||
const labelById = Object.fromEntries(options.map((o) => [o.id, o.label]))
|
||||
expect(labelById['wf-root']).toBe('Pipeline (Root)')
|
||||
expect(labelById['wf-eng']).toBe('Pipeline (Engineering)')
|
||||
expect(labelById['wf-eng-backend']).toBe('Pipeline (Engineering / Backend)')
|
||||
expect(labelById['wf-unique']).toBe('Solo Workflow')
|
||||
})
|
||||
|
||||
it('disambiguates a single workflow lookup when its name has duplicates', async () => {
|
||||
mockGetWorkflows.mockReturnValue([
|
||||
{ id: 'wf-1', name: 'Pipeline', folderId: 'folder-a' },
|
||||
{ id: 'wf-2', name: 'Pipeline', folderId: null },
|
||||
])
|
||||
mockGetFolderMap.mockReturnValue({
|
||||
'folder-a': {
|
||||
id: 'folder-a',
|
||||
name: 'Alpha',
|
||||
parentId: null,
|
||||
workspaceId: 'ws-1',
|
||||
},
|
||||
})
|
||||
|
||||
const definition = getSelectorDefinition('sim.workflows')
|
||||
const option = await definition.fetchById?.({
|
||||
key: 'sim.workflows',
|
||||
context: { workspaceId: 'ws-1' },
|
||||
detailId: 'wf-1',
|
||||
})
|
||||
|
||||
expect(option).toEqual({ id: 'wf-1', label: 'Pipeline (Alpha)' })
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,103 @@
|
||||
import { airtableSelectors } from '@/hooks/selectors/providers/airtable/selectors'
|
||||
import { asanaSelectors } from '@/hooks/selectors/providers/asana/selectors'
|
||||
import { attioSelectors } from '@/hooks/selectors/providers/attio/selectors'
|
||||
import { bigquerySelectors } from '@/hooks/selectors/providers/bigquery/selectors'
|
||||
import { calcomSelectors } from '@/hooks/selectors/providers/calcom/selectors'
|
||||
import { cloudwatchSelectors } from '@/hooks/selectors/providers/cloudwatch/selectors'
|
||||
import { confluenceSelectors } from '@/hooks/selectors/providers/confluence/selectors'
|
||||
import { googleSelectors } from '@/hooks/selectors/providers/google/selectors'
|
||||
import { jiraSelectors } from '@/hooks/selectors/providers/jira/selectors'
|
||||
import { jsmSelectors } from '@/hooks/selectors/providers/jsm/selectors'
|
||||
import { knowledgeSelectors } from '@/hooks/selectors/providers/knowledge/selectors'
|
||||
import { linearSelectors } from '@/hooks/selectors/providers/linear/selectors'
|
||||
import { microsoftSelectors } from '@/hooks/selectors/providers/microsoft/selectors'
|
||||
import { mondaySelectors } from '@/hooks/selectors/providers/monday/selectors'
|
||||
import { notionSelectors } from '@/hooks/selectors/providers/notion/selectors'
|
||||
import { pipedriveSelectors } from '@/hooks/selectors/providers/pipedrive/selectors'
|
||||
import { sharepointSelectors } from '@/hooks/selectors/providers/sharepoint/selectors'
|
||||
import { simSelectors } from '@/hooks/selectors/providers/sim/selectors'
|
||||
import { slackSelectors } from '@/hooks/selectors/providers/slack/selectors'
|
||||
import { trelloSelectors } from '@/hooks/selectors/providers/trello/selectors'
|
||||
import { wealthboxSelectors } from '@/hooks/selectors/providers/wealthbox/selectors'
|
||||
import { webflowSelectors } from '@/hooks/selectors/providers/webflow/selectors'
|
||||
import { zoomSelectors } from '@/hooks/selectors/providers/zoom/selectors'
|
||||
import type {
|
||||
SelectorDefinition,
|
||||
SelectorKey,
|
||||
SelectorOption,
|
||||
SelectorQueryArgs,
|
||||
} from '@/hooks/selectors/types'
|
||||
|
||||
export const selectorRegistry = {
|
||||
...airtableSelectors,
|
||||
...asanaSelectors,
|
||||
...attioSelectors,
|
||||
...bigquerySelectors,
|
||||
...calcomSelectors,
|
||||
...confluenceSelectors,
|
||||
...jsmSelectors,
|
||||
...googleSelectors,
|
||||
...microsoftSelectors,
|
||||
...notionSelectors,
|
||||
...pipedriveSelectors,
|
||||
...sharepointSelectors,
|
||||
...trelloSelectors,
|
||||
...zoomSelectors,
|
||||
...slackSelectors,
|
||||
...wealthboxSelectors,
|
||||
...jiraSelectors,
|
||||
...mondaySelectors,
|
||||
...linearSelectors,
|
||||
...knowledgeSelectors,
|
||||
...webflowSelectors,
|
||||
...cloudwatchSelectors,
|
||||
...simSelectors,
|
||||
} satisfies Record<SelectorKey, SelectorDefinition>
|
||||
|
||||
export function getSelectorDefinition(key: SelectorKey): SelectorDefinition {
|
||||
const definition = selectorRegistry[key]
|
||||
if (!definition) {
|
||||
throw new Error(`Missing selector definition for ${key}`)
|
||||
}
|
||||
return definition
|
||||
}
|
||||
|
||||
const MAX_LOAD_ALL_PAGES = 50
|
||||
|
||||
/**
|
||||
* Loads the complete option list for a selector outside the React Query hook —
|
||||
* for callers (search/replace, value resolution) that need every option in one
|
||||
* call. Uses `fetchList` when defined, otherwise drains `fetchPage` (bounded by
|
||||
* {@link MAX_LOAD_ALL_PAGES}). Returns an empty array for a selector that
|
||||
* provides neither.
|
||||
*/
|
||||
export async function loadAllSelectorOptions(
|
||||
definition: SelectorDefinition,
|
||||
args: SelectorQueryArgs
|
||||
): Promise<SelectorOption[]> {
|
||||
if (definition.fetchList) {
|
||||
return definition.fetchList(args)
|
||||
}
|
||||
|
||||
if (definition.fetchPage) {
|
||||
const items: SelectorOption[] = []
|
||||
let cursor: string | undefined
|
||||
for (let page = 0; page < MAX_LOAD_ALL_PAGES; page++) {
|
||||
const { items: pageItems, nextCursor } = await definition.fetchPage({ ...args, cursor })
|
||||
items.push(...pageItems)
|
||||
cursor = nextCursor
|
||||
if (!cursor) break
|
||||
}
|
||||
return items
|
||||
}
|
||||
|
||||
return []
|
||||
}
|
||||
|
||||
export function mergeOption(options: SelectorOption[], option?: SelectorOption | null) {
|
||||
if (!option) return options
|
||||
if (options.some((item) => item.id === option.id)) {
|
||||
return options
|
||||
}
|
||||
return [option, ...options]
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import type { SubBlockConfig } from '@/blocks/types'
|
||||
import type { SelectorContext, SelectorKey } from '@/hooks/selectors/types'
|
||||
|
||||
export interface SelectorResolution {
|
||||
key: SelectorKey | null
|
||||
context: SelectorContext
|
||||
allowSearch: boolean
|
||||
}
|
||||
|
||||
export function resolveSelectorForSubBlock(
|
||||
subBlock: SubBlockConfig,
|
||||
context: SelectorContext
|
||||
): SelectorResolution | null {
|
||||
if (!subBlock.selectorKey) return null
|
||||
return {
|
||||
key: subBlock.selectorKey,
|
||||
context: {
|
||||
...context,
|
||||
mimeType: subBlock.mimeType ?? context.mimeType,
|
||||
},
|
||||
allowSearch: subBlock.selectorAllowSearch ?? true,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
import type React from 'react'
|
||||
import type { QueryKey } from '@tanstack/react-query'
|
||||
import type { AnyApiRouteContract } from '@/lib/api/contracts/types'
|
||||
|
||||
export type SelectorKey =
|
||||
| 'airtable.bases'
|
||||
| 'airtable.tables'
|
||||
| 'asana.workspaces'
|
||||
| 'attio.lists'
|
||||
| 'attio.objects'
|
||||
| 'bigquery.datasets'
|
||||
| 'bigquery.tables'
|
||||
| 'calcom.eventTypes'
|
||||
| 'calcom.schedules'
|
||||
| 'confluence.spaces'
|
||||
| 'google.tasks.lists'
|
||||
| 'jsm.requestTypes'
|
||||
| 'jsm.serviceDesks'
|
||||
| 'microsoft.planner.plans'
|
||||
| 'notion.databases'
|
||||
| 'notion.pages'
|
||||
| 'pipedrive.pipelines'
|
||||
| 'sharepoint.lists'
|
||||
| 'trello.boards'
|
||||
| 'zoom.meetings'
|
||||
| 'slack.channels'
|
||||
| 'slack.users'
|
||||
| 'gmail.labels'
|
||||
| 'outlook.folders'
|
||||
| 'google.calendar'
|
||||
| 'jira.issues'
|
||||
| 'jira.projects'
|
||||
| 'linear.projects'
|
||||
| 'linear.teams'
|
||||
| 'confluence.pages'
|
||||
| 'microsoft.teams'
|
||||
| 'microsoft.chats'
|
||||
| 'microsoft.channels'
|
||||
| 'wealthbox.contacts'
|
||||
| 'onedrive.files'
|
||||
| 'onedrive.folders'
|
||||
| 'sharepoint.sites'
|
||||
| 'microsoft.excel'
|
||||
| 'microsoft.excel.drives'
|
||||
| 'microsoft.excel.sheets'
|
||||
| 'microsoft.word'
|
||||
| 'microsoft.planner'
|
||||
| 'google.drive'
|
||||
| 'google.sheets'
|
||||
| 'knowledge.documents'
|
||||
| 'webflow.sites'
|
||||
| 'webflow.collections'
|
||||
| 'webflow.items'
|
||||
| 'cloudwatch.logGroups'
|
||||
| 'cloudwatch.logStreams'
|
||||
| 'monday.boards'
|
||||
| 'monday.groups'
|
||||
| 'sim.workflows'
|
||||
| 'table.columns'
|
||||
|
||||
export interface SelectorOption {
|
||||
id: string
|
||||
label: string
|
||||
icon?: React.ComponentType<{ className?: string }>
|
||||
meta?: Record<string, unknown>
|
||||
}
|
||||
|
||||
export interface SelectorContext {
|
||||
workspaceId?: string
|
||||
workflowId?: string
|
||||
oauthCredential?: string
|
||||
serviceId?: string
|
||||
domain?: string
|
||||
teamId?: string
|
||||
projectId?: string
|
||||
knowledgeBaseId?: string
|
||||
planId?: string
|
||||
mimeType?: string
|
||||
fileId?: string
|
||||
siteId?: string
|
||||
collectionId?: string
|
||||
spreadsheetId?: string
|
||||
driveId?: string
|
||||
excludeWorkflowId?: string
|
||||
baseId?: string
|
||||
datasetId?: string
|
||||
serviceDeskId?: string
|
||||
impersonateUserEmail?: string
|
||||
boardId?: string
|
||||
awsAccessKeyId?: string
|
||||
awsSecretAccessKey?: string
|
||||
awsRegion?: string
|
||||
logGroupName?: string
|
||||
mcpServerId?: string
|
||||
tableId?: string
|
||||
}
|
||||
|
||||
export interface SelectorQueryArgs {
|
||||
key: SelectorKey
|
||||
context: SelectorContext
|
||||
search?: string
|
||||
detailId?: string
|
||||
signal?: AbortSignal
|
||||
}
|
||||
|
||||
export interface SelectorPage {
|
||||
items: SelectorOption[]
|
||||
nextCursor?: string
|
||||
}
|
||||
|
||||
interface SelectorPageArgs extends SelectorQueryArgs {
|
||||
cursor?: string
|
||||
}
|
||||
|
||||
export interface SelectorDefinition {
|
||||
key: SelectorKey
|
||||
contracts?: readonly AnyApiRouteContract[]
|
||||
getQueryKey: (args: SelectorQueryArgs) => QueryKey
|
||||
/**
|
||||
* Loads the full option list in a single call. Required unless `fetchPage` is
|
||||
* defined, in which case the hook drives pagination through `fetchPage` and
|
||||
* `fetchList` is never invoked — provide one or the other, not both.
|
||||
*/
|
||||
fetchList?: (args: SelectorQueryArgs) => Promise<SelectorOption[]>
|
||||
/**
|
||||
* Optional. When defined, the selector hook fetches one page at a time and
|
||||
* auto-drains remaining pages so the dropdown populates progressively.
|
||||
* Returns `{ items, nextCursor }`; `nextCursor: undefined` ends the stream.
|
||||
*/
|
||||
fetchPage?: (args: SelectorPageArgs) => Promise<SelectorPage>
|
||||
fetchById?: (args: SelectorQueryArgs) => Promise<SelectorOption | null>
|
||||
enabled?: (args: SelectorQueryArgs) => boolean
|
||||
staleTime?: number
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
import { useEffect, useMemo } from 'react'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { useInfiniteQuery, useQuery } from '@tanstack/react-query'
|
||||
import { extractEnvVarName, isEnvVarReference, isReference } from '@/executor/constants'
|
||||
import { usePersonalEnvironment } from '@/hooks/queries/environment'
|
||||
import { getSelectorDefinition, mergeOption } from '@/hooks/selectors/registry'
|
||||
import type {
|
||||
SelectorKey,
|
||||
SelectorOption,
|
||||
SelectorPage,
|
||||
SelectorQueryArgs,
|
||||
} from '@/hooks/selectors/types'
|
||||
|
||||
interface SelectorHookArgs extends Omit<SelectorQueryArgs, 'key'> {
|
||||
search?: string
|
||||
detailId?: string
|
||||
enabled?: boolean
|
||||
}
|
||||
|
||||
export interface SelectorOptionsResult {
|
||||
data: SelectorOption[] | undefined
|
||||
isLoading: boolean
|
||||
isFetching: boolean
|
||||
/**
|
||||
* True while paginated selectors are draining remaining pages in the
|
||||
* background. Always false for non-paginated selectors.
|
||||
*/
|
||||
isFetchingMore: boolean
|
||||
/**
|
||||
* True when the paginated selector still has more pages queued. Always false
|
||||
* for non-paginated selectors.
|
||||
*/
|
||||
hasMore: boolean
|
||||
/**
|
||||
* True when the paginated drain stopped at {@link MAX_AUTO_DRAIN_PAGES} with
|
||||
* pages still remaining, so the option list is a partial view. Always false
|
||||
* for non-paginated selectors.
|
||||
*/
|
||||
truncated: boolean
|
||||
error: Error | null
|
||||
}
|
||||
|
||||
const logger = createLogger('SelectorQuery')
|
||||
|
||||
const EMPTY_PAGE: SelectorPage = { items: [], nextCursor: undefined }
|
||||
|
||||
/**
|
||||
* Safety bound on the background auto-drain. Real dropdowns settle in a handful
|
||||
* of pages; this only trips for pathological result sets and prevents an
|
||||
* unbounded request loop when a provider keeps handing back cursors.
|
||||
*/
|
||||
const MAX_AUTO_DRAIN_PAGES = 50
|
||||
|
||||
export function useSelectorOptions(
|
||||
key: SelectorKey,
|
||||
args: SelectorHookArgs
|
||||
): SelectorOptionsResult {
|
||||
const definition = getSelectorDefinition(key)
|
||||
const queryArgs: SelectorQueryArgs = {
|
||||
key,
|
||||
context: args.context,
|
||||
search: args.search,
|
||||
}
|
||||
const isEnabled = args.enabled ?? (definition.enabled ? definition.enabled(queryArgs) : true)
|
||||
const supportsPagination = Boolean(definition.fetchPage)
|
||||
|
||||
const flatQuery = useQuery<SelectorOption[]>({
|
||||
queryKey: definition.getQueryKey(queryArgs),
|
||||
queryFn: ({ signal }) =>
|
||||
definition.fetchList?.({ ...queryArgs, signal }) ?? Promise.resolve([]),
|
||||
enabled: !supportsPagination && isEnabled,
|
||||
staleTime: definition.staleTime ?? 30_000,
|
||||
})
|
||||
|
||||
const pagedQuery = useInfiniteQuery<SelectorPage>({
|
||||
queryKey: [...definition.getQueryKey(queryArgs), 'paged'],
|
||||
queryFn: ({ pageParam, signal }) => {
|
||||
if (!definition.fetchPage) return Promise.resolve(EMPTY_PAGE)
|
||||
return definition.fetchPage({
|
||||
...queryArgs,
|
||||
cursor: pageParam as string | undefined,
|
||||
signal,
|
||||
})
|
||||
},
|
||||
getNextPageParam: (last) => last.nextCursor,
|
||||
initialPageParam: undefined as string | undefined,
|
||||
enabled: supportsPagination && isEnabled,
|
||||
staleTime: definition.staleTime ?? 30_000,
|
||||
})
|
||||
|
||||
const { hasNextPage, isFetchingNextPage, fetchNextPage, isError } = pagedQuery
|
||||
const pageCount = pagedQuery.data?.pages.length ?? 0
|
||||
const reachedDrainCap = pageCount >= MAX_AUTO_DRAIN_PAGES
|
||||
useEffect(() => {
|
||||
if (!supportsPagination) return
|
||||
if (isError) return
|
||||
if (reachedDrainCap) {
|
||||
if (hasNextPage) {
|
||||
logger.warn('Selector hit auto-drain cap; option list is truncated', {
|
||||
key,
|
||||
pages: pageCount,
|
||||
})
|
||||
}
|
||||
return
|
||||
}
|
||||
if (hasNextPage && !isFetchingNextPage) {
|
||||
void fetchNextPage()
|
||||
}
|
||||
}, [
|
||||
supportsPagination,
|
||||
hasNextPage,
|
||||
isFetchingNextPage,
|
||||
isError,
|
||||
fetchNextPage,
|
||||
reachedDrainCap,
|
||||
pageCount,
|
||||
key,
|
||||
])
|
||||
|
||||
const pagedOptions = useMemo<SelectorOption[] | undefined>(() => {
|
||||
if (!supportsPagination) return undefined
|
||||
if (!pagedQuery.data) return undefined
|
||||
return pagedQuery.data.pages.flatMap((page) => page.items)
|
||||
}, [supportsPagination, pagedQuery.data])
|
||||
|
||||
if (supportsPagination) {
|
||||
return {
|
||||
data: pagedOptions,
|
||||
isLoading: pagedQuery.isLoading,
|
||||
isFetching: pagedQuery.isFetching,
|
||||
isFetchingMore: pagedQuery.isFetchingNextPage,
|
||||
hasMore: (pagedQuery.hasNextPage ?? false) && !reachedDrainCap,
|
||||
truncated: reachedDrainCap && (pagedQuery.hasNextPage ?? false),
|
||||
error: (pagedQuery.error as Error | null) ?? null,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
data: flatQuery.data,
|
||||
isLoading: flatQuery.isLoading,
|
||||
isFetching: flatQuery.isFetching,
|
||||
isFetchingMore: false,
|
||||
hasMore: false,
|
||||
truncated: false,
|
||||
error: (flatQuery.error as Error | null) ?? null,
|
||||
}
|
||||
}
|
||||
|
||||
export function useSelectorOptionDetail(
|
||||
key: SelectorKey,
|
||||
args: SelectorHookArgs & { detailId?: string }
|
||||
) {
|
||||
const { data: envVariables = {} } = usePersonalEnvironment()
|
||||
const definition = getSelectorDefinition(key)
|
||||
|
||||
const resolvedDetailId = useMemo(() => {
|
||||
if (!args.detailId) return undefined
|
||||
if (isReference(args.detailId)) return undefined
|
||||
if (isEnvVarReference(args.detailId)) {
|
||||
const varName = extractEnvVarName(args.detailId)
|
||||
return envVariables[varName]?.value || undefined
|
||||
}
|
||||
return args.detailId
|
||||
}, [args.detailId, envVariables])
|
||||
|
||||
const queryArgs: SelectorQueryArgs = {
|
||||
key,
|
||||
context: args.context,
|
||||
detailId: resolvedDetailId,
|
||||
}
|
||||
const hasRealDetailId = Boolean(resolvedDetailId)
|
||||
const baseEnabled =
|
||||
hasRealDetailId && definition.fetchById !== undefined
|
||||
? definition.enabled
|
||||
? definition.enabled(queryArgs)
|
||||
: true
|
||||
: false
|
||||
const enabled = args.enabled ?? baseEnabled
|
||||
|
||||
const query = useQuery<SelectorOption | null>({
|
||||
queryKey: [...definition.getQueryKey(queryArgs), 'detail', resolvedDetailId ?? 'none'],
|
||||
queryFn: ({ signal }) => definition.fetchById!({ ...queryArgs, signal }),
|
||||
enabled,
|
||||
staleTime: definition.staleTime ?? 300_000,
|
||||
})
|
||||
|
||||
return query
|
||||
}
|
||||
|
||||
export function useSelectorOptionMap(options: SelectorOption[], extra?: SelectorOption | null) {
|
||||
return useMemo(() => {
|
||||
const merged = mergeOption(options, extra)
|
||||
return new Map(merged.map((option) => [option.id, option]))
|
||||
}, [options, extra])
|
||||
}
|
||||
Reference in New Issue
Block a user