feat: add fileset resource type support

Add a new "fileset" resource type that represents a collection of files
stored as a relpath→content map. This enables resource types to manage
multiple files (e.g., config directories, template sets) instead of just
a single file.

Backend:
- Add is_fileset column to resource_type table
- Update CRUD operations and workspace duplication to handle is_fileset
- Add integration tests for fileset resource types

Frontend:
- Add FilesetEditor component with file explorer + Monaco editor
- Extract shared FileExplorer component from RawAppSidebar (dedup)
- Add fileset toggle to EditableSchemaWrapper
- Show fileset editor in ResourceEditor and ApiConnectForm
- Show folder icon for fileset resource types in IconedResourceType

CLI:
- Support fileset resources in sync pull (expand to .fileset/ directory)
- Support fileset resources in sync push (reconstruct from directory)
- Handle !inline_fileset YAML tag in resource resolution

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Ruben Fiszel
2026-02-23 17:50:37 +00:00
co-authored by Claude Opus 4.6
parent 710809ac64
commit d4dc0a088f
575 changed files with 1159 additions and 14866 deletions
@@ -30,7 +30,6 @@ use uuid::Uuid;
use windmill_audit::audit_oss::{audit_log, AuditAuthorable};
use windmill_audit::ActionKind;
use windmill_common::db::UserDB;
use windmill_types::s3::LargeFileStorage;
use windmill_common::users::username_to_permissioned_as;
use windmill_common::variables::{build_crypt, decrypt, encrypt, WORKSPACE_CRYPT_CACHE};
use windmill_common::worker::{to_raw_value, CLOUD_HOSTED};
@@ -55,6 +54,7 @@ use windmill_dep_map::scoped_dependency_map::{
DependencyDependent, DependencyMap, ScopedDependencyMap,
};
use windmill_git_sync::{handle_deployment_metadata, handle_fork_branch_creation, DeployedObject};
use windmill_types::s3::LargeFileStorage;
use hyper::StatusCode;
use serde::{Deserialize, Serialize};
@@ -3010,8 +3010,8 @@ async fn clone_resource_types(
target_workspace_id: &str,
) -> Result<()> {
sqlx::query!(
"INSERT INTO resource_type (workspace_id, name, schema, description, edited_at, created_by, format_extension)
SELECT $2, name, schema, description, edited_at, created_by, format_extension
"INSERT INTO resource_type (workspace_id, name, schema, description, edited_at, created_by, format_extension, is_fileset)
SELECT $2, name, schema, description, edited_at, created_by, format_extension, is_fileset
FROM resource_type
WHERE workspace_id = $1",
source_workspace_id,
@@ -5254,7 +5254,7 @@ async fn compare_two_resource_types(
) -> Result<ItemComparison> {
// Get resource type from each workspace
let source_resource_type = sqlx::query!(
"SELECT schema, description, format_extension
"SELECT schema, description, format_extension, is_fileset
FROM resource_type
WHERE workspace_id = $1 AND name = $2",
source_workspace_id,
@@ -5264,7 +5264,7 @@ async fn compare_two_resource_types(
.await?;
let target_resource_type = sqlx::query!(
"SELECT schema, description, format_extension
"SELECT schema, description, format_extension, is_fileset
FROM resource_type
WHERE workspace_id = $1 AND name = $2",
fork_workspace_id,
@@ -5280,6 +5280,7 @@ async fn compare_two_resource_types(
if source.schema != target.schema
|| source.description != target.description
|| source.format_extension != target.format_extension
|| source.is_fileset != target.is_fileset
{
has_changes = true;
}