diff --git a/src/modules/Elsa.Labels/Contracts/IWorkflowDefinitionLabelQuery.cs b/src/modules/Elsa.Labels/Contracts/IWorkflowDefinitionLabelQuery.cs
new file mode 100644
index 000000000..0c77828be
--- /dev/null
+++ b/src/modules/Elsa.Labels/Contracts/IWorkflowDefinitionLabelQuery.cs
@@ -0,0 +1,14 @@
+using Elsa.Labels.Entities;
+
+namespace Elsa.Labels.Contracts;
+
+///
+/// Queries workflow definition label relationships for workflow-definition filter providers.
+///
+public interface IWorkflowDefinitionLabelQuery
+{
+ ///
+ /// Returns workflow definition versions associated with any of the specified label IDs. Matching is based on the workflow definition version ID; an empty or unknown set returns no associations.
+ ///
+ Task> FindByLabelIdsAsync(IEnumerable labelIds, CancellationToken cancellationToken = default);
+}
diff --git a/src/modules/Elsa.Labels/Features/LabelsFeature.cs b/src/modules/Elsa.Labels/Features/LabelsFeature.cs
index 48c924818..6978af6eb 100644
--- a/src/modules/Elsa.Labels/Features/LabelsFeature.cs
+++ b/src/modules/Elsa.Labels/Features/LabelsFeature.cs
@@ -6,6 +6,7 @@ using Elsa.Features.Services;
using Elsa.Labels.Contracts;
using Elsa.Labels.Entities;
using Elsa.Labels.Services;
+using Elsa.Workflows.Management;
using Microsoft.Extensions.DependencyInjection;
namespace Elsa.Labels.Features;
@@ -45,6 +46,7 @@ public class LabelsFeature : FeatureBase
.AddMemoryStore()
.AddScoped(LabelStore)
.AddScoped(WorkflowDefinitionLabelStore)
+ .AddScoped()
;
Services.AddNotificationHandlersFrom(GetType());
diff --git a/src/modules/Elsa.Labels/Services/InMemoryWorkflowDefinitionLabelStore.cs b/src/modules/Elsa.Labels/Services/InMemoryWorkflowDefinitionLabelStore.cs
index b7b9e9fe1..943ab5110 100644
--- a/src/modules/Elsa.Labels/Services/InMemoryWorkflowDefinitionLabelStore.cs
+++ b/src/modules/Elsa.Labels/Services/InMemoryWorkflowDefinitionLabelStore.cs
@@ -7,7 +7,7 @@ namespace Elsa.Labels.Services;
///
/// An in-memory store of workflow-label associations.
///
-public class InMemoryWorkflowDefinitionLabelStore : IWorkflowDefinitionLabelStore
+public class InMemoryWorkflowDefinitionLabelStore : IWorkflowDefinitionLabelStore, IWorkflowDefinitionLabelQuery
{
private readonly MemoryStore _store;
@@ -47,6 +47,14 @@ public class InMemoryWorkflowDefinitionLabelStore : IWorkflowDefinitionLabelStor
return Task.FromResult(result);
}
+ ///
+ public Task> FindByLabelIdsAsync(IEnumerable labelIds, CancellationToken cancellationToken = default)
+ {
+ var ids = labelIds.ToHashSet();
+ var result = _store.FindMany(x => ids.Contains(x.LabelId));
+ return Task.FromResult(result);
+ }
+
///
public Task ReplaceAsync(IEnumerable removed, IEnumerable added, CancellationToken cancellationToken = default)
{
diff --git a/src/modules/Elsa.Labels/Services/WorkflowDefinitionLabelFilterProvider.cs b/src/modules/Elsa.Labels/Services/WorkflowDefinitionLabelFilterProvider.cs
new file mode 100644
index 000000000..f3783f3c5
--- /dev/null
+++ b/src/modules/Elsa.Labels/Services/WorkflowDefinitionLabelFilterProvider.cs
@@ -0,0 +1,46 @@
+using Elsa.Authorization;
+using Elsa.Labels.Contracts;
+using Elsa.Labels.Permissions;
+using Elsa.Workflows.Management;
+using Elsa.Workflows.Management.Exceptions;
+using Elsa.Workflows.Management.Filters;
+
+namespace Elsa.Labels.Services;
+
+///
+/// Applies label relationships to workflow definition filters. A version qualifies when it has any requested label ID; an unknown label produces no matches. A caller filtering by labels must hold the workflow-definition-label view permission. The configured label store must implement to support this filter.
+///
+public class WorkflowDefinitionLabelFilterProvider(IWorkflowDefinitionLabelStore store) : IWorkflowDefinitionFilterProvider
+{
+ ///
+ public bool CanApply(WorkflowDefinitionFilter filter) => filter.LabelIds is { Count: > 0 };
+
+ /// Requires workflow-definition-label view permission when a label filter is present.
+ public IEnumerable GetRequiredPermissions(WorkflowDefinitionFilter filter) =>
+ CanApply(filter) ? [new(LabelPermissions.WorkflowDefinitionLabels, CoreVerbs.View)] : [];
+
+ ///
+ public async Task ApplyAsync(WorkflowDefinitionFilter filter, CancellationToken cancellationToken = default)
+ {
+ if (filter.LabelIds is not { Count: > 0 })
+ {
+ return;
+ }
+
+ if (store is not IWorkflowDefinitionLabelQuery query)
+ {
+ throw new WorkflowDefinitionFilterNotSupportedException("Filtering workflow definitions by labels is not supported by the configured label store.");
+ }
+
+ var labelIds = filter.LabelIds.Distinct().ToList();
+ var matchingVersionIds = (await query.FindByLabelIdsAsync(labelIds, cancellationToken))
+ .Select(x => x.WorkflowDefinitionVersionId)
+ .Distinct()
+ .ToList();
+
+ filter.Ids = filter.Ids == null
+ ? matchingVersionIds
+ : filter.Ids.Intersect(matchingVersionIds).ToList();
+ filter.LabelIds = null;
+ }
+}
diff --git a/src/modules/Elsa.Labels/ShellFeatures/LabelsFeature.cs b/src/modules/Elsa.Labels/ShellFeatures/LabelsFeature.cs
index 37d8e9247..60e8a2269 100644
--- a/src/modules/Elsa.Labels/ShellFeatures/LabelsFeature.cs
+++ b/src/modules/Elsa.Labels/ShellFeatures/LabelsFeature.cs
@@ -5,6 +5,7 @@ using Elsa.Labels.Contracts;
using Elsa.Labels.Entities;
using Elsa.Labels.Services;
using Elsa.Platform.PackageManifest.Generator.Hints;
+using Elsa.Workflows.Management;
using JetBrains.Annotations;
using Microsoft.Extensions.DependencyInjection;
@@ -37,7 +38,8 @@ public class LabelsFeature : IShellFeature
.AddMemoryStore