fix: add missing google native triggers to triggers panel (#7966)

This commit is contained in:
hugocasa
2026-02-17 07:34:45 +00:00
committed by GitHub
parent 72227d72f3
commit a82a538081
7 changed files with 145 additions and 113 deletions
+22 -1
View File
@@ -581,7 +581,28 @@ In `frontend/src/lib/components/graph/renderers/triggers/TriggersBadge.svelte`:
2. Add to `baseConfig` with `countKey` (the dynamic `availableNativeServices` loop does NOT set `countKey`)
3. Add to the `allTypes` array
### Step 14: Update OpenAPI Spec and Regenerate Types
### Step 14: Update TriggersWrapper.svelte
In `frontend/src/lib/components/triggers/TriggersWrapper.svelte`:
Add a `{:else if selectedTrigger.type === 'yourservice'}` case that renders `<NativeTriggersPanel service="yourservice" ...>` with the same props pattern as the existing native trigger cases (e.g., `nextcloud`).
### Step 15: Update AddTriggersButton.svelte
In `frontend/src/lib/components/triggers/AddTriggersButton.svelte`:
1. Add `yourserviceAvailable` state variable
2. Add `setYourserviceState()` async function using `isServiceAvailable('yourservice', $workspaceStore!)`
3. Call it at module level
4. Add a dropdown entry to `addTriggerItems` with `hidden: !yourserviceAvailable`
### Step 16: Update TriggersEditor.svelte Delete Handling
In `frontend/src/lib/components/triggers/TriggersEditor.svelte`:
Add your service to the `nativeTriggerServices` map in `deleteDeployedTrigger()`. Native triggers use `NativeTriggerService.deleteNativeTrigger({ workspace, serviceName, externalId })` instead of the standard `path`-based delete.
### Step 17: Update OpenAPI Spec and Regenerate Types
Add to `JobTriggerKind` enum in `backend/windmill-api/openapi.yaml`, then:
@@ -35,12 +35,18 @@
const cloudHosted = isCloudHosted()
let nextcloudAvailable = $state(false)
let googleAvailable = $state(false)
async function setNextcloudState() {
nextcloudAvailable = await isServiceAvailable('nextcloud', $workspaceStore!)
}
async function setGoogleState() {
googleAvailable = await isServiceAvailable('google', $workspaceStore!)
}
setNextcloudState()
setGoogleState()
const addTriggerItems = $derived(
[
@@ -112,6 +118,12 @@
action: () => onAddDraftTrigger?.('nextcloud'),
icon: triggerIconMap.nextcloud,
hidden: !nextcloudAvailable
},
{
displayName: 'Google',
action: () => onAddDraftTrigger?.('google'),
icon: triggerIconMap.google,
hidden: !googleAvailable
}
].filter((item) => !item.hidden)
)
@@ -29,7 +29,9 @@
HttpTriggerService,
GcpTriggerService,
SqsTriggerService,
EmailTriggerService
EmailTriggerService,
NativeTriggerService,
type NativeServiceName
} from '$lib/gen'
import { sendUserToast } from '$lib/toast'
import Alert from '../common/alert/Alert.svelte'
@@ -116,20 +118,37 @@
email: () => EmailTriggerService.deleteEmailTrigger
}
const nativeTriggerServices: Record<string, NativeServiceName> = {
nextcloud: 'nextcloud',
google: 'google'
}
const deleteHandler = deleteHandlers[triggerType as keyof typeof deleteHandlers]
if (deleteHandler && deletingTrigger !== triggerIndex) {
deletingTrigger = triggerIndex
try {
const nativeServiceName = nativeTriggerServices[triggerType]
if (deletingTrigger === triggerIndex) return
deletingTrigger = triggerIndex
try {
if (nativeServiceName) {
await NativeTriggerService.deleteNativeTrigger({
workspace: $workspaceStore ?? '',
serviceName: nativeServiceName,
externalId: triggerPath ?? ''
})
} else if (deleteHandler) {
await deleteHandler()({
workspace: $workspaceStore ?? '',
path: triggerPath ?? ''
})
sendUserToast(`Successfully deleted ${triggerType} trigger: ${triggerPath}`)
} catch (error) {
sendUserToast(error.body || error.message, true)
} finally {
deletingTrigger = undefined
} else {
return
}
sendUserToast(`Successfully deleted ${triggerType} trigger: ${triggerPath}`)
} catch (error) {
sendUserToast(error.body || error.message, true)
} finally {
deletingTrigger = undefined
}
}
@@ -182,6 +182,16 @@
{customLabel}
{...props}
/>
{:else if selectedTrigger.type === 'google'}
<NativeTriggersPanel
service="google"
{isFlow}
path={initialPath || fakeInitialPath}
{selectedTrigger}
defaultValues={selectedTrigger.draftConfig ?? selectedTrigger.captureConfig ?? undefined}
{customLabel}
{...props}
/>
{:else if selectedTrigger.type === 'cli'}
<div class="py-1 flex flex-col gap-6">
<ClipboardPanel content={selectedTrigger.extra?.cliCommand ?? ''} />
@@ -97,7 +97,7 @@
let externalId = $state<string | null>(null)
let can_write = $state(true)
let originalConfig = $state<Record<string, any> | undefined>(undefined)
let initialConfig: Record<string, any> | undefined = undefined
let initialConfig = $state<Record<string, any> | undefined>(undefined)
export function openNew(
nis_flow?: boolean,
@@ -119,6 +119,7 @@
itemKind = nis_flow ? 'flow' : 'script'
externalId = null
loadingConfig = false
loadingForm = false
can_write = true
originalConfig = undefined
initialConfig = undefined
@@ -166,6 +167,7 @@
externalId = externalIdOrPath
loadingConfig = true
loadingForm = true
originalConfig = undefined
initialConfig = undefined
itemKind = nis_flow ? 'flow' : 'script'
@@ -187,15 +189,10 @@
serviceConfig = { ...serviceConfig, ...defaultValues }
externalData = { ...externalData, ...defaultValues }
}
originalConfig = structuredClone($state.snapshot(getSaveCfg()))
} catch (err: any) {
sendUserToast(`Failed to load trigger configuration: ${err}`, true)
externalData = null
} finally {
// For drawer mode, set initialConfig here; for inline mode, the effect handles it after form settles
if (!defaultValues) {
initialConfig = structuredClone($state.snapshot(getSaveCfg()))
}
clearTimeout(loadingTimeout)
loadingConfig = false
showLoading = false
@@ -210,6 +207,15 @@
}
}
// Capture originalConfig after the form has settled (loadingConfig and loadingForm both false)
// This ensures we compare against the form's normalized config, not raw backend data
$effect(() => {
if (!loadingConfig && !loadingForm && originalConfig === undefined && !isNew) {
originalConfig = structuredClone($state.snapshot(getSaveCfg()))
initialConfig = structuredClone($state.snapshot(getSaveCfg()))
}
})
function close() {
drawer?.closeDrawer()
}
@@ -246,7 +252,7 @@
const saveCfg = $derived.by(getSaveCfg)
$effect(() => {
if (!loadingConfig && !loadingForm) {
if (!loadingConfig && !loadingForm && (isNew || initialConfig)) {
handleConfigChange(saveCfg, initialConfig, saveDisabled, !isNew, onConfigChange)
}
})
@@ -4,7 +4,7 @@
import ToggleButton from '$lib/components/common/toggleButton-v2/ToggleButton.svelte'
import GoogleDrivePicker from './GoogleDrivePicker.svelte'
import GoogleCalendarPicker from './GoogleCalendarPicker.svelte'
import { Loader2, Eye, FileText } from 'lucide-svelte'
import { Eye, FileText } from 'lucide-svelte'
import GoogleDriveIcon from '$lib/components/icons/GoogleDriveIcon.svelte'
import GoogleCalendarIcon from '$lib/components/icons/GoogleCalendarIcon.svelte'
@@ -13,15 +13,14 @@
errors: Record<string, string>
disabled?: boolean
externalData?: any
loading: boolean
loading?: boolean
}
let {
serviceConfig = $bindable(),
errors = $bindable(),
disabled = false,
externalData = undefined,
loading = $bindable()
loading = $bindable(false),
disabled = false
}: Props = $props()
let triggerType = $state<'drive' | 'calendar'>(serviceConfig.triggerType ?? 'drive')
@@ -37,6 +36,11 @@
let calendarId = $state<string>(serviceConfig.calendarId ?? 'primary')
let calendarName = $state<string>(serviceConfig.calendarName ?? '')
// No async loading — signal ready immediately
$effect(() => {
loading = false
})
// Sync form state back to serviceConfig
$effect(() => {
if (triggerType === 'drive') {
@@ -71,105 +75,65 @@
return serviceErrors
}
// Apply external data when editing existing trigger
let externalDataApplied = $state(false)
$effect(() => {
if (externalData && !externalDataApplied) {
if (externalData.triggerType) {
triggerType = externalData.triggerType
}
driveResourceId = externalData.resourceId ?? driveResourceId
driveResourceName = externalData.resourceName ?? driveResourceName
driveWatchMode = externalData.resourceId ? 'file' : 'all'
calendarId = externalData.calendarId ?? calendarId
calendarName = externalData.calendarName ?? calendarName
externalDataApplied = true
loading = false
}
})
$effect(() => {
if (!externalData) {
externalDataApplied = false
loading = false
}
})
</script>
<Section label="Google Trigger Configuration">
{#if loading}
<div class="flex items-center gap-2 text-secondary text-xs">
<Loader2 class="animate-spin" size={16} />
Loading configuration...
<div class="flex flex-col gap-4">
<div>
<p class="block text-xs font-semibold text-primary mb-1">Trigger Type</p>
<ToggleButtonGroup bind:selected={triggerType} {disabled}>
{#snippet children({ item })}
<ToggleButton value="drive" label="Drive" icon={GoogleDriveIcon} {item} />
<ToggleButton value="calendar" label="Calendar" icon={GoogleCalendarIcon} {item} />
{/snippet}
</ToggleButtonGroup>
</div>
{:else}
<div class="flex flex-col gap-4">
<div>
<p class="block text-xs font-semibold text-primary mb-1">Trigger Type</p>
<ToggleButtonGroup bind:selected={triggerType} {disabled}>
{#if triggerType === 'drive'}
<div class="flex flex-col gap-2">
<p class="block text-xs font-semibold text-primary">Watch Mode</p>
<ToggleButtonGroup bind:selected={driveWatchMode} {disabled}>
{#snippet children({ item })}
<ToggleButton value="drive" label="Drive" icon={GoogleDriveIcon} {item} />
<ToggleButton
value="calendar"
label="Calendar"
icon={GoogleCalendarIcon}
{item}
/>
<ToggleButton value="file" label="Specific file" icon={FileText} {item} />
<ToggleButton value="all" label="All changes" icon={Eye} {item} />
{/snippet}
</ToggleButtonGroup>
</div>
{#if triggerType === 'drive'}
<div class="flex flex-col gap-2">
<p class="block text-xs font-semibold text-primary">Watch Mode</p>
<ToggleButtonGroup bind:selected={driveWatchMode} {disabled}>
{#snippet children({ item })}
<ToggleButton value="file" label="Specific file" icon={FileText} {item} />
<ToggleButton value="all" label="All changes" icon={Eye} {item} />
{/snippet}
</ToggleButtonGroup>
{#if driveWatchMode === 'file'}
<p class="block text-xs font-semibold text-primary">File</p>
<GoogleDrivePicker
bind:resourceId={driveResourceId}
bind:resourceName={driveResourceName}
{disabled}
/>
{#if errors.resourceId}
<p class="text-red-500 text-xs">{errors.resourceId}</p>
{/if}
{:else}
<p class="text-tertiary text-xs">
Watches all changes across your Google Drive. The trigger will fire whenever any file is created, modified, or deleted.
</p>
{/if}
</div>
{:else if triggerType === 'calendar'}
<div class="flex flex-col gap-2">
<p class="block text-xs font-semibold text-primary">Calendar</p>
<GoogleCalendarPicker
bind:calendarId
bind:calendarName
{#if driveWatchMode === 'file'}
<p class="block text-xs font-semibold text-primary">File</p>
<GoogleDrivePicker
bind:resourceId={driveResourceId}
bind:resourceName={driveResourceName}
{disabled}
/>
{#if errors.calendarId}
<p class="text-red-500 text-xs">{errors.calendarId}</p>
{#if errors.resourceId}
<p class="text-red-500 text-xs">{errors.resourceId}</p>
{/if}
<div class="text-tertiary text-xs">
<p>
Requires the <a
href="https://console.cloud.google.com/apis/library/calendar-json.googleapis.com"
target="_blank"
rel="noopener"
class="underline">Google Calendar API</a
> to be enabled in your Google Cloud project.
</p>
</div>
{:else}
<p class="text-tertiary text-xs">
Watches all changes across your Google Drive. The trigger will fire whenever any file is
created, modified, or deleted.
</p>
{/if}
</div>
{:else if triggerType === 'calendar'}
<div class="flex flex-col gap-2">
<p class="block text-xs font-semibold text-primary">Calendar</p>
<GoogleCalendarPicker bind:calendarId bind:calendarName {disabled} />
{#if errors.calendarId}
<p class="text-red-500 text-xs">{errors.calendarId}</p>
{/if}
<div class="text-tertiary text-xs">
<p>
Requires the <a
href="https://console.cloud.google.com/apis/library/calendar-json.googleapis.com"
target="_blank"
rel="noopener"
class="underline">Google Calendar API</a
> to be enabled in your Google Cloud project.
</p>
</div>
{/if}
</div>
{/if}
</div>
{/if}
</div>
</Section>
@@ -529,10 +529,10 @@ export function getTriggerLabel(trigger: Trigger): string {
} else if (type === 'google' && path) {
const triggerType = config?.trigger_type ?? config?.triggerType
if (triggerType === 'calendar') {
const name = config?.calendar_name ?? config?.calendarName ?? config?.calendar_id ?? ''
const name = config?.resource_name ?? config?.calendar_id ?? ''
return `Calendar: ${name || path}`
} else {
const name = config?.resource_name ?? config?.resourceName ?? ''
const name = config?.resource_name ?? ''
return name ? `Drive: ${name}` : config?.resource_id ? `Drive: ${path}` : `Drive: All changes`
}
} else if (isDraft && draftConfig?.path) {