mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-10-26 08:02:31 +00:00
fix: add missing google native triggers to triggers panel (#7966)
This commit is contained in:
@@ -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)
|
||||
}
|
||||
})
|
||||
|
||||
+58
-94
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user