mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-21 00:02:30 +00:00
feat: badge chat-input flows on the home list (#11164)
* feat: badge chat-input flows on the home list Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix: keep a malformed draft value from aborting the runnables list Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix: only a JSON boolean marks a draft flow as chat-enabled Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
c3e11bd223
commit
3d08197182
@@ -154,6 +154,7 @@ async fn list_flows(
|
||||
"favorite.path IS NOT NULL as starred",
|
||||
"ws_error_handler_muted",
|
||||
"o.labels",
|
||||
"(o.value->>'chat_input_enabled')::bool as chat_input_enabled",
|
||||
"draft.email IS NOT NULL as is_draft",
|
||||
// Per-path draft owners as a JSON array; see scripts.rs for the rationale
|
||||
// (non-member superadmin identity fallback via `password`, legacy NULL-email row).
|
||||
@@ -301,6 +302,10 @@ async fn list_flows(
|
||||
ws_error_handler_muted: None,
|
||||
deployment_msg: None,
|
||||
labels: None,
|
||||
chat_input_enabled: v
|
||||
.get("value")
|
||||
.and_then(|fv| fv.get("chat_input_enabled"))
|
||||
.and_then(|b| b.as_bool()),
|
||||
// No deployed row to inherit folder labels from.
|
||||
inherited_labels: None,
|
||||
is_draft: true,
|
||||
|
||||
@@ -12010,6 +12010,13 @@ paths:
|
||||
properties:
|
||||
draft_only:
|
||||
type: boolean
|
||||
chat_input_enabled:
|
||||
type: boolean
|
||||
description: |
|
||||
`chat_input_enabled` of the flow's value,
|
||||
projected so the list can mark flows that open
|
||||
as a chat. Omitted when the value has no such
|
||||
field.
|
||||
is_draft:
|
||||
type: boolean
|
||||
description: |
|
||||
@@ -28951,6 +28958,12 @@ components:
|
||||
type: boolean
|
||||
has_deploy_errors:
|
||||
type: boolean
|
||||
chat_input_enabled:
|
||||
type: boolean
|
||||
description: >-
|
||||
flow-only. `chat_input_enabled` of the flow's value, projected so
|
||||
the list can mark flows that open as a chat. Omitted when the
|
||||
value has no such field.
|
||||
raw_app:
|
||||
type: boolean
|
||||
execution_mode:
|
||||
|
||||
@@ -116,6 +116,10 @@ struct RunnableItem {
|
||||
use_codebase: Option<bool>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
has_deploy_errors: Option<bool>,
|
||||
// flow-only: projected from the value so the home list can badge flows that
|
||||
// open as a chat without fetching every flow's value.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
chat_input_enabled: Option<bool>,
|
||||
// app-only
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
raw_app: Option<bool>,
|
||||
@@ -274,7 +278,7 @@ fn branch_sqls() -> Branches {
|
||||
o.ws_error_handler_muted, o.created_at as edited_at, \
|
||||
o.hash, o.language::text as language, o.kind::text as script_kind, o.auto_kind, \
|
||||
o.codebase IS NOT NULL as use_codebase, \
|
||||
(o.lock_error_logs IS NOT NULL) as has_deploy_errors, \
|
||||
(o.lock_error_logs IS NOT NULL) as has_deploy_errors, NULL::bool as chat_input_enabled, \
|
||||
NULL::bool as raw_app, NULL::text as execution_mode, NULL::bigint as id, NULL::bigint as version, \
|
||||
o.created_at as sort_time, lower(COALESCE(NULLIF(o.summary, ''), o.path)) as sort_name, o.hash as tiebreak \
|
||||
FROM script o \
|
||||
@@ -291,6 +295,7 @@ fn branch_sqls() -> Branches {
|
||||
o.ws_error_handler_muted, o.edited_at, \
|
||||
NULL::bigint as hash, NULL::text as language, NULL::text as script_kind, NULL::text as auto_kind, \
|
||||
NULL::bool as use_codebase, NULL::bool as has_deploy_errors, \
|
||||
(o.value->>'chat_input_enabled')::bool as chat_input_enabled, \
|
||||
NULL::bool as raw_app, NULL::text as execution_mode, NULL::bigint as id, NULL::bigint as version, \
|
||||
o.edited_at as sort_time, lower(COALESCE(NULLIF(o.summary, ''), o.path)) as sort_name, 0::bigint as tiebreak \
|
||||
FROM flow o \
|
||||
@@ -306,7 +311,7 @@ fn branch_sqls() -> Branches {
|
||||
{draft_users}, o.labels, folder_labels(o.workspace_id, o.path) as inherited_labels, \
|
||||
NULL::bool as ws_error_handler_muted, av.created_at as edited_at, \
|
||||
NULL::bigint as hash, NULL::text as language, NULL::text as script_kind, NULL::text as auto_kind, \
|
||||
NULL::bool as use_codebase, NULL::bool as has_deploy_errors, \
|
||||
NULL::bool as use_codebase, NULL::bool as has_deploy_errors, NULL::bool as chat_input_enabled, \
|
||||
av.raw_app, o.policy->>'execution_mode' as execution_mode, o.id, \
|
||||
o.versions[array_upper(o.versions, 1)] as version, \
|
||||
COALESCE(av.created_at, 'epoch'::timestamptz) as sort_time, lower(COALESCE(NULLIF(o.summary, ''), o.path)) as sort_name, 0::bigint as tiebreak \
|
||||
@@ -344,11 +349,21 @@ fn draft_branch_sql(kind: &str) -> String {
|
||||
let kind_cols = match kind {
|
||||
"script" => {
|
||||
"d.value->>'language' as language, d.value->>'kind' as script_kind, \
|
||||
d.value->>'auto_kind' as auto_kind, false as raw_app"
|
||||
d.value->>'auto_kind' as auto_kind, false as raw_app, \
|
||||
NULL::bool as chat_input_enabled"
|
||||
}
|
||||
// Type-guarded rather than a bare `::bool` cast: draft JSON is stored
|
||||
// unvalidated, so a malformed value must yield NULL, not abort the list,
|
||||
// and the string "true" must not count as enabled.
|
||||
"flow" => {
|
||||
"NULL::text as language, NULL::text as script_kind, NULL::text as auto_kind, \
|
||||
false as raw_app, \
|
||||
CASE WHEN json_typeof(d.value->'value'->'chat_input_enabled') = 'boolean' \
|
||||
THEN (d.value->'value'->>'chat_input_enabled')::bool END as chat_input_enabled"
|
||||
}
|
||||
_ => {
|
||||
"NULL::text as language, NULL::text as script_kind, NULL::text as auto_kind, \
|
||||
(d.typ = 'raw_app') as raw_app"
|
||||
(d.typ = 'raw_app') as raw_app, NULL::bool as chat_input_enabled"
|
||||
}
|
||||
};
|
||||
format!(
|
||||
@@ -359,7 +374,7 @@ fn draft_branch_sql(kind: &str) -> String {
|
||||
NULL::text[] as labels, NULL::text[] as inherited_labels, \
|
||||
NULL::bool as ws_error_handler_muted, o.created_at as edited_at, \
|
||||
NULL::bigint as hash, o.language, o.script_kind, o.auto_kind, \
|
||||
NULL::bool as use_codebase, NULL::bool as has_deploy_errors, \
|
||||
NULL::bool as use_codebase, NULL::bool as has_deploy_errors, o.chat_input_enabled, \
|
||||
o.raw_app, NULL::text as execution_mode, NULL::bigint as id, NULL::bigint as version, \
|
||||
o.created_at as sort_time, lower(COALESCE(NULLIF(o.summary, ''), o.draft_path, o.path)) as sort_name, 0::bigint as tiebreak \
|
||||
FROM ( \
|
||||
|
||||
@@ -98,6 +98,11 @@ pub struct ListableFlow {
|
||||
pub deployment_msg: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub labels: Option<Vec<String>>,
|
||||
/// Projected from the flow value so a list can mark a flow that opens as a
|
||||
/// chat without fetching every flow's value.
|
||||
#[sqlx(default)]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub chat_input_enabled: Option<bool>,
|
||||
/// True when the authed user has a draft for this flow (draft-only or layered
|
||||
/// over the deployed row). See ListableScript in scripts.rs.
|
||||
#[serde(default)]
|
||||
|
||||
@@ -35,6 +35,7 @@
|
||||
HistoryIcon
|
||||
} from 'lucide-svelte'
|
||||
import FlowHistory from '$lib/components/flows/FlowHistory.svelte'
|
||||
import ChatFlowBadge from '$lib/components/flows/ChatFlowBadge.svelte'
|
||||
import InheritedLabels from '$lib/components/InheritedLabels.svelte'
|
||||
import { getDeployUiSettings } from '$lib/components/home/deploy_ui'
|
||||
import { editInForkAllowed, editInForkLabel, onEditInForkClick } from '$lib/utils/editInFork'
|
||||
@@ -48,6 +49,9 @@
|
||||
draft_path?: string
|
||||
draft_users?: { username?: string | null }[]
|
||||
canWrite: boolean
|
||||
/** Projected from the flow value by the listing; a chat-input flow opens
|
||||
* as a conversation and is badged as such. */
|
||||
chat_input_enabled?: boolean
|
||||
}
|
||||
marked: string | undefined
|
||||
shareModal: ShareModal
|
||||
@@ -124,6 +128,10 @@
|
||||
<FlowHistory bind:this={flowHistory} path={flow.path} />
|
||||
{/if}
|
||||
|
||||
{#snippet chatBadge()}
|
||||
<ChatFlowBadge />
|
||||
{/snippet}
|
||||
|
||||
<Row
|
||||
aiId={`flow-row-${flow.path}`}
|
||||
aiDescription={`Button to access the form to run the flow ${flow.summary ?? flow.path}`}
|
||||
@@ -140,6 +148,7 @@
|
||||
canFavorite={!flow.draft_only}
|
||||
{depth}
|
||||
{rowSelection}
|
||||
titleBadge={flow.chat_input_enabled ? chatBadge : undefined}
|
||||
>
|
||||
{#snippet badges()}
|
||||
{#if flow.archived}
|
||||
|
||||
@@ -67,6 +67,9 @@
|
||||
badges?: import('svelte').Snippet
|
||||
actions?: import('svelte').Snippet
|
||||
customSummary?: import('svelte').Snippet
|
||||
/** Rendered inline right after the title, unlike `badges`, which sit in
|
||||
* their own column and are hidden below `lg`. */
|
||||
titleBadge?: import('svelte').Snippet
|
||||
/** Overrides the secondary path line (e.g. to strike a renamed path).
|
||||
* Falls back to the plain `path` string when not provided. */
|
||||
pathDisplay?: import('svelte').Snippet
|
||||
@@ -101,6 +104,7 @@
|
||||
badges,
|
||||
actions,
|
||||
customSummary,
|
||||
titleBadge,
|
||||
pathDisplay,
|
||||
onSelect = () => {}
|
||||
}: Props = $props()
|
||||
@@ -275,7 +279,12 @@
|
||||
</div>
|
||||
{/if}
|
||||
<div class="grow min-w-0">
|
||||
<div class="text-emphasis flex-wrap text-left text-xs font-semibold">
|
||||
<div
|
||||
class={twMerge(
|
||||
'text-emphasis flex-wrap text-left text-xs font-semibold',
|
||||
titleBadge ? 'inline-flex items-center gap-2' : ''
|
||||
)}
|
||||
>
|
||||
{#if customSummary}
|
||||
{@render customSummary?.()}
|
||||
{:else if marked}
|
||||
@@ -283,6 +292,7 @@
|
||||
{:else}
|
||||
{!summary || summary.length == 0 ? displayPath : summary}
|
||||
{/if}
|
||||
{@render titleBadge?.()}
|
||||
</div>
|
||||
<div class="text-hint text-3xs truncate text-left font-normal" title={path}>
|
||||
{#if pathDisplay}
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
<script lang="ts">
|
||||
import { MessageSquare } from 'lucide-svelte'
|
||||
import Badge from '$lib/components/common/badge/Badge.svelte'
|
||||
|
||||
interface Props {
|
||||
title?: string
|
||||
}
|
||||
|
||||
let { title = 'Chat input enabled: this flow opens as a conversation' }: Props = $props()
|
||||
</script>
|
||||
|
||||
<Badge
|
||||
small
|
||||
color="gray"
|
||||
class="px-1.5 shrink-0"
|
||||
icon={{ icon: MessageSquare, position: 'left' }}
|
||||
{title}
|
||||
>
|
||||
Chat
|
||||
</Badge>
|
||||
Reference in New Issue
Block a user