Incremental work on Workflows

--HG--
branch : 1.x
extra : rebase_source : 5ce381991d6e285619ad926bf527f6f5db1f425b
This commit is contained in:
Sebastien Ros
2012-11-29 11:06:45 -08:00
parent cd748154e2
commit 88f316a557
37 changed files with 1266 additions and 55 deletions

View File

@@ -0,0 +1,54 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Orchard.ContentManagement;
using Orchard.Localization;
using Orchard.Workflows.Models.Descriptors;
using Orchard.Workflows.Services;
namespace Orchard.Workflows.Activities {
public abstract class ContentActivity : BlockingActivity {
public Localizer T { get; set; }
public override bool CanTransition(ActivityContext context) {
string contenttypes = context.State.ContentTypes;
var content = context.Tokens["Content"] as IContent;
// "" means 'any'
if (String.IsNullOrEmpty(contenttypes)) {
return true;
}
if (content == null) {
return false;
}
var contentTypes = contenttypes.Split(new[] { ',' });
return contentTypes.Any(contentType => content.ContentItem.TypeDefinition.Name == contentType);
}
public override IEnumerable<LocalizedString> GetPossibleOutcomes(ActivityContext context) {
return new[] { T("Success") };
}
public override LocalizedString Transition(ActivityContext context) {
return T("True");
}
}
public class ContentCreatedActivity : ContentActivity {
public override string Name {
get { return "ContentCreated"; }
}
public override LocalizedString Category {
get { return T("Content Items"); }
}
public override LocalizedString Description {
get { return T("Content is actually created."); }
}
}
}

View File

@@ -0,0 +1,35 @@
using System.Collections.Generic;
using Orchard.Localization;
using Orchard.Workflows.Models.Descriptors;
using Orchard.Workflows.Services;
namespace Orchard.Workflows.Activities {
public class DecisionActivity : Activity {
public DecisionActivity() {
T = NullLocalizer.Instance;
}
public Localizer T { get; set; }
public override string Name {
get { return "Decision"; }
}
public override LocalizedString Category {
get { return T("Misc"); }
}
public override LocalizedString Description {
get { return T("Evaluates an expression."); }
}
public override IEnumerable<LocalizedString> GetPossibleOutcomes(ActivityContext context) {
return new[] { T("True"), T("False") };
}
public override LocalizedString Transition(ActivityContext context) {
return T("True");
}
}
}

View File

@@ -0,0 +1,54 @@
using System;
using System.Collections.Generic;
using Orchard.Localization;
using Orchard.Tokens;
using Orchard.UI.Notify;
using Orchard.Workflows.Models.Descriptors;
using Orchard.Workflows.Services;
namespace Orchard.Workflows.Activities {
public class NotificationActivity : Activity {
private readonly INotifier _notifier;
private readonly ITokenizer _tokenizer;
public NotificationActivity(INotifier notifier, ITokenizer tokenizer) {
_notifier = notifier;
_tokenizer = tokenizer;
T = NullLocalizer.Instance;
}
public Localizer T { get; set; }
public override string Name {
get { return "Notify"; }
}
public override LocalizedString Category {
get { return T("Notification"); }
}
public override LocalizedString Description {
get { return T("Display a message."); }
}
public override string Form {
get { return "ActivityNotify"; }
}
public override IEnumerable<LocalizedString> GetPossibleOutcomes(ActivityContext context) {
yield return T("Done");
}
public override LocalizedString Transition(ActivityContext context) {
string notification = context.State.Notification;
string message = context.State.Message;
message = _tokenizer.Replace(message, context.Tokens);
var notificationType = (NotifyType)Enum.Parse(typeof(NotifyType), notification);
_notifier.Add(notificationType, T(message));
return T("Done");
}
}
}

View File

@@ -0,0 +1,18 @@
using Orchard.Localization;
using Orchard.Security;
using Orchard.UI.Navigation;
namespace Orchard.Workflows {
public class AdminMenu : INavigationProvider {
public Localizer T { get; set; }
public string MenuName { get { return "admin"; } }
public void GetNavigation(NavigationBuilder builder) {
builder.Add(T("Workflows"), "10",
menu => menu
.Add(T("Workflows"), "1.0",
qi => qi.Action("Index", "Admin", new { area = "Orchard.Workflows" }).Permission(StandardPermissions.SiteOwner).LocalNav())
);
}
}
}

View File

@@ -0,0 +1,149 @@
using System.Collections.Generic;
using System.Linq;
using System.Web.Mvc;
using System.Web.Routing;
using Orchard.Core.Title.Models;
using Orchard.ContentManagement;
using Orchard.Core.Contents.Controllers;
using Orchard.Data;
using Orchard.DisplayManagement;
using Orchard.Localization;
using Orchard.Security;
using Orchard.UI.Notify;
using System;
using Orchard.Settings;
using Orchard.UI.Navigation;
using Orchard.Workflows.Models;
using Orchard.Workflows.Services;
using Orchard.Workflows.ViewModels;
namespace Orchard.Workflows.Controllers {
[ValidateInput(false)]
public class AdminController : Controller, IUpdateModel {
private readonly IOrchardServices _services;
private readonly ISiteService _siteService;
private readonly IRepository<WorkflowDefinitionRecord> _workflowDefinitionRecords;
private readonly IEnumerable<IActivity> _activities;
public AdminController(
IOrchardServices services,
IShapeFactory shapeFactory,
ISiteService siteService,
IRepository<WorkflowDefinitionRecord> workflowDefinitionRecords,
IEnumerable<IActivity> activities
) {
_services = services;
_siteService = siteService;
_workflowDefinitionRecords = workflowDefinitionRecords;
_activities = activities;
Services = services;
T = NullLocalizer.Instance;
Shape = shapeFactory;
}
dynamic Shape { get; set; }
public IOrchardServices Services { get; set; }
public Localizer T { get; set; }
public ActionResult Index(AdminIndexOptions options, PagerParameters pagerParameters) {
if (!Services.Authorizer.Authorize(StandardPermissions.SiteOwner, T("Not authorized to list workflows")))
return new HttpUnauthorizedResult();
var pager = new Pager(_siteService.GetSiteSettings(), pagerParameters);
// default options
if (options == null)
options = new AdminIndexOptions();
var queries = _workflowDefinitionRecords.Table;
switch (options.Filter) {
case WorkflowDefinitionFilter.All:
break;
default:
throw new ArgumentOutOfRangeException();
}
if (!String.IsNullOrWhiteSpace(options.Search)) {
queries = queries.Where(w => w.Name.Contains(options.Search));
}
var pagerShape = Shape.Pager(pager).TotalItemCount(queries.Count());
switch (options.Order) {
case WorkflowDefinitionOrder.Name:
queries = queries.OrderBy(u => u.Name);
break;
}
var results = queries
.Skip(pager.GetStartIndex())
.Take(pager.PageSize)
.ToList();
var model = new AdminIndexViewModel {
WorkflowDefinitions = results.Select(x => new WorkflowDefinitionEntry {
WorkflowDefinitionRecord = x,
WokflowDefinitionId = x.Id,
Name = x.Name
}).ToList(),
Options = options,
Pager = pagerShape
};
// maintain previous route data when generating page links
var routeData = new RouteData();
routeData.Values.Add("Options.Filter", options.Filter);
routeData.Values.Add("Options.Search", options.Search);
routeData.Values.Add("Options.Order", options.Order);
pagerShape.RouteData(routeData);
return View(model);
}
public ActionResult Create() {
if (!Services.Authorizer.Authorize(StandardPermissions.SiteOwner, T("Not authorized to create workflows")))
return new HttpUnauthorizedResult();
return View();
}
[HttpPost, ActionName("Create")]
public ActionResult CreatePost(string name) {
if (!Services.Authorizer.Authorize(StandardPermissions.SiteOwner, T("Not authorized to create workflows")))
return new HttpUnauthorizedResult();
var workflowDefinitionRecord = new WorkflowDefinitionRecord {
Name = name
};
_workflowDefinitionRecords.Create(workflowDefinitionRecord);
return RedirectToAction("Edit", new { workflowDefinitionRecord.Id });
}
public ActionResult Edit(int id) {
if (!Services.Authorizer.Authorize(StandardPermissions.SiteOwner, T("Not authorized to edit workflows")))
return new HttpUnauthorizedResult();
var workflowDefinitionRecord = _workflowDefinitionRecords.Get(id);
var viewModel = new AdminEditViewModel {
WorkflowDefinitionRecord = workflowDefinitionRecord,
AllActivities = _activities.ToList()
};
return View(viewModel);
}
bool IUpdateModel.TryUpdateModel<TModel>(TModel model, string prefix, string[] includeProperties, string[] excludeProperties) {
return TryUpdateModel(model, prefix, includeProperties, excludeProperties);
}
public void AddModelError(string key, LocalizedString errorMessage) {
ModelState.AddModelError(key, errorMessage.ToString());
}
}
}

View File

@@ -1,21 +1,46 @@
using Orchard.ContentManagement.MetaData;
using Orchard.Data.Migration;
namespace Orchard.Workflows {
public class WorkflowsDataMigration : DataMigrationImpl {
public int Create() {
SchemaBuilder.CreateTable("WorkflowPartRecord",
table => table
.ContentPartRecord()
);
public class Migrations : DataMigrationImpl {
ContentDefinitionManager.AlterTypeDefinition("Workflow",
cfg => cfg
.WithPart("WorkflowPart")
.WithPart("TitlePart")
.WithPart("IdentityPart")
.WithPart("CommonPart", p => p.WithSetting("OwnerEditorSettings.ShowOwnerEditor", "false"))
);
public int Create() {
// Creating table TransitionRecord
SchemaBuilder.CreateTable("TransitionRecord", table => table
.Column<int>("Id", column => column.PrimaryKey().Identity())
.Column<string>("SourceEndpoint")
.Column<string>("DestinationEndpoint")
.Column<int>("SourceActivityRecord_id")
.Column<int>("DestinationActivityRecord_id")
.Column<int>("WorkflowDefinitionRecord_id")
);
// Creating table WorkflowRecord
SchemaBuilder.CreateTable("WorkflowRecord", table => table
.Column<int>("Id", column => column.PrimaryKey().Identity())
.Column<string>("State", column => column.Unlimited())
.Column<int>("WorkflowDefinitionRecord_id")
);
// Creating table WorkflowDefinitionRecord
SchemaBuilder.CreateTable("WorkflowDefinitionRecord", table => table
.Column<int>("Id", column => column.PrimaryKey().Identity())
.Column<bool>("Enabled")
.Column<string>("Name", column => column.WithLength(1024))
);
// Creating table AwaitingActivityRecord
SchemaBuilder.CreateTable("AwaitingActivityRecord", table => table
.Column<int>("Id", column => column.PrimaryKey().Identity())
.Column<int>("ActivityRecord_id")
);
// Creating table ActivityRecord
SchemaBuilder.CreateTable("ActivityRecord", table => table
.Column<int>("Id", column => column.PrimaryKey().Identity())
.Column<string>("Type")
.Column<string>("Parameters")
.Column<int>("WorkflowDefinitionRecord_id")
);
return 1;
}

View File

@@ -1,16 +1,24 @@
using System.Collections.Generic;
namespace Orchard.Workflows.Models {
namespace Orchard.Workflows.Models {
/// <summary>
/// Represents an activity in a <see cref="WorkflowDefinitionRecord"/>
/// </summary>
public class ActivityRecord {
public virtual int Id { get; set; }
public virtual string Category { get; set; }
/// <summary>
/// The type of the activity.
/// </summary>
public virtual string Type { get; set; }
/// <summary>
/// The serialized parameters of the activity.
/// </summary>
public virtual string Parameters { get; set; }
// Parent property
/// <summary>
/// The parent <see cref="WorkflowDefinitionRecord"/>
/// containing this activity.
/// </summary>
public virtual WorkflowDefinitionRecord WorkflowDefinitionRecord { get; set; }
// Parent property
public virtual IList<ActivityRecord> RegisteredWorkflows { get; set; }
}
}

View File

@@ -0,0 +1,12 @@
using System.Collections.Generic;
namespace Orchard.Workflows.Models {
public class AwaitingActivityRecord {
public virtual int Id { get; set; }
public virtual ActivityRecord ActivityRecord { get; set; }
// Parent property
public virtual IList<WorkflowRecord> WorkflowRecords { get; set; }
}
}

View File

@@ -1,17 +0,0 @@
namespace Orchard.Workflows.Models {
public class ConnectionRecord {
public virtual int Id { get; set; }
// Source is null if it's a starting activity
public virtual ActivityRecord Source { get; set; }
public virtual string SourceEndpoint { get; set; }
// Destination is null if it's an ending activity
public virtual ActivityRecord Destination { get; set; }
public virtual string DestinationEndpoint { get; set; }
// Parent relationship
public virtual WorkflowDefinitionRecord WorkflowDefinition { get; set; }
}
}

View File

@@ -2,7 +2,7 @@
using Orchard.Localization;
namespace Orchard.Workflows.Models.Descriptors {
public class ActionDescriptor {
public class ActivityDescriptor {
public string Category { get; set; }
public string Type { get; set; }
public LocalizedString Name { get; set; }

View File

@@ -6,8 +6,8 @@ namespace Orchard.Workflows.Models.Descriptors {
public class DescribeActivityContext {
private readonly Dictionary<string, DescribeActivityFor> _describes = new Dictionary<string, DescribeActivityFor>();
public IEnumerable<TypeDescriptor<ActionDescriptor>> Describe() {
return _describes.Select(kp => new TypeDescriptor<ActionDescriptor> {
public IEnumerable<TypeDescriptor<ActivityDescriptor>> Describe() {
return _describes.Select(kp => new TypeDescriptor<ActivityDescriptor> {
Category = kp.Key,
Name = kp.Value.Name,
Description = kp.Value.Description,

View File

@@ -7,7 +7,7 @@ namespace Orchard.Workflows.Models.Descriptors {
private readonly string _category;
public DescribeActivityFor(string category, LocalizedString name, LocalizedString description) {
Types = new List<ActionDescriptor>();
Types = new List<ActivityDescriptor>();
_category = category;
Name = name;
Description = description;
@@ -15,10 +15,10 @@ namespace Orchard.Workflows.Models.Descriptors {
public LocalizedString Name { get; private set; }
public LocalizedString Description { get; private set; }
public List<ActionDescriptor> Types { get; private set; }
public List<ActivityDescriptor> Types { get; private set; }
public DescribeActivityFor Element(string type, LocalizedString name, LocalizedString description, Func<ActivityContext, bool> action, Func<ActivityContext, LocalizedString> display, string form = null) {
Types.Add(new ActionDescriptor { Type = type, Name = name, Description = description, Category = _category, Action = action, Display = display, Form = form });
Types.Add(new ActivityDescriptor { Type = type, Name = name, Description = description, Category = _category, Action = action, Display = display, Form = form });
return this;
}
}

View File

@@ -0,0 +1,34 @@
namespace Orchard.Workflows.Models {
/// <summary>
/// Reprensents a transition between two <see cref="ActivityRecord"/>
/// </summary>
public class TransitionRecord {
public virtual int Id { get; set; }
/// <summary>
/// The source <see cref="ActivityRecord"/>
/// </summary>
public virtual ActivityRecord SourceActivityRecord { get; set; }
/// <summary>
/// The name of the endpoint on the source <see cref="ActivityRecord"/>
/// </summary>
public virtual string SourceEndpoint { get; set; }
/// <summary>
/// The destination <see cref="ActivityRecord"/>
/// </summary>
public virtual ActivityRecord DestinationActivityRecord { get; set; }
/// <summary>
/// The name of the endpoint on the destination <see cref="ActivityRecord"/>
/// </summary>
public virtual string DestinationEndpoint { get; set; }
/// <summary>
/// The parent <see cref="WorkflowDefinitionRecord"/>
/// </summary>
public virtual WorkflowDefinitionRecord WorkflowDefinitionRecord { get; set; }
}
}

View File

@@ -3,22 +3,47 @@ using System.ComponentModel.DataAnnotations;
using Orchard.Data.Conventions;
namespace Orchard.Workflows.Models {
/// <summary>
/// Represent a workflow definition comprised of activities and transitions between them.
/// </summary>
public class WorkflowDefinitionRecord {
public WorkflowDefinitionRecord() {
Activities = new List<ActivityRecord>();
Connections = new List<ConnectionRecord>();
ActivityRecords = new List<ActivityRecord>();
TransitionRecords = new List<TransitionRecord>();
}
public virtual int Id { get; set; }
/// <summary>
/// Whether or not to enable workflows of this type.
/// </summary>
public virtual bool Enabled { get; set; }
/// <summary>
/// The name of the workflow definition.
/// </summary>
[Required, StringLength(1024)]
public virtual string Name { get; set; }
/// <summary>
/// List of <see cref="ActivityRecord"/> composing this workflow definition.
/// </summary>
[CascadeAllDeleteOrphan]
public virtual IList<ActivityRecord> Activities { get; set; }
public virtual IList<ActivityRecord> ActivityRecords { get; set; }
/// <summary>
/// List of <see cref="TransitionRecord"/> composing this workflow definition.
/// This is distinct from Activities as there could be activities without
/// any connection an any time of the design process, though they should
/// be synchronized.
/// </summary>
[CascadeAllDeleteOrphan]
public virtual IList<ConnectionRecord> Connections { get; set; }
public virtual IList<TransitionRecord> TransitionRecords { get; set; }
/// <summary>
/// List of <see cref="WorkflowRecord"/> associated with this workflow definition.
/// </summary>
[CascadeAllDeleteOrphan]
public virtual IList<WorkflowRecord> WorkflowRecords { get; set; }
}
}

View File

@@ -2,16 +2,33 @@
using Orchard.Data.Conventions;
namespace Orchard.Workflows.Models {
/// <summary>
/// Reprensents a running workflow instance.
/// </summary>
public class WorkflowRecord {
public WorkflowRecord() {
AwaitingActivities = new List<ActivityRecord>();
AwaitingActivities = new List<AwaitingActivityRecord>();
}
public virtual int Id { get; set; }
/// <summary>
/// Serialized state of the workflow.
/// </summary>
[StringLengthMax]
public virtual string State { get; set; }
public virtual IList<ActivityRecord> AwaitingActivities { get; set; }
/// <summary>
/// List of activities the current workflow instance is waiting on
/// for continuing its process.
/// </summary>
[CascadeAllDeleteOrphan]
public virtual IList<AwaitingActivityRecord> AwaitingActivities { get; set; }
/// <summary>
/// Parent <see cref="WorkflowDefinitionRecord"/>
/// </summary>
public virtual WorkflowDefinitionRecord WorkflowDefinitionRecord { get; set; }
}
}

View File

@@ -7,4 +7,5 @@ OrchardVersion: 1.0
Description: Description for the module
Features:
Orchard.Workflows:
Description: Description for feature Orchard.Workflows.
Description: Description for feature Orchard.Workflows.
Dependencies: Orchard.Tokens, Orchard.Forms, Orchard.jQuery

View File

@@ -64,6 +64,9 @@
<Reference Include="System.Xml.Linq" />
</ItemGroup>
<ItemGroup>
<Content Include="Scripts\jquery.jsPlumb-1.3.16-all-min.js" />
<Content Include="Styles\admin-workflows.css" />
<Content Include="Styles\images\blocking.png" />
<Content Include="Web.config" />
<Content Include="Views\Web.config" />
<Content Include="Scripts\Web.config" />
@@ -80,23 +83,63 @@
<Project>{9916839C-39FC-4CEB-A5AF-89CA7E87119F}</Project>
<Name>Orchard.Core</Name>
</ProjectReference>
<ProjectReference Include="..\Orchard.Forms\Orchard.Forms.csproj">
<Project>{642a49d7-8752-4177-80d6-bfbbcfad3de0}</Project>
<Name>Orchard.Forms</Name>
</ProjectReference>
<ProjectReference Include="..\Orchard.Tokens\Orchard.Tokens.csproj">
<Project>{6f759635-13d7-4e94-bcc9-80445d63f117}</Project>
<Name>Orchard.Tokens</Name>
</ProjectReference>
</ItemGroup>
<ItemGroup>
<Compile Include="Activities\ContentActivity.cs" />
<Compile Include="Activities\DecisionActivity.cs" />
<Compile Include="Activities\NotificationActivity.cs" />
<Compile Include="AdminMenu.cs" />
<Compile Include="Controllers\AdminController.cs" />
<Compile Include="Models\AwaitingActivityRecord.cs" />
<Compile Include="Models\Descriptors\ActivityContext.cs" />
<Compile Include="Models\Descriptors\ActivityDescriptor.cs" />
<Compile Include="Models\Descriptors\DescribeActivityContext.cs" />
<Compile Include="Models\Descriptors\DescribeActivityFor.cs" />
<Compile Include="Migrations.cs" />
<Compile Include="Models\ActivityRecord.cs" />
<Compile Include="Models\ConnectionRecord.cs" />
<Compile Include="Models\TransitionRecord.cs" />
<Compile Include="Models\Descriptors\TypeDescriptor.cs" />
<Compile Include="Models\WorkflowRecord.cs" />
<Compile Include="Migrations.cs" />
<Compile Include="Models\WorkflowDefinitionRecord.cs" />
<Compile Include="Providers\ContentActivityProvider.cs" />
<Compile Include="Providers\ContentActivityForms.cs" />
<Compile Include="Providers\NotificationActivityForms.cs" />
<Compile Include="Providers\NotificationActivityProvider.cs" />
<Compile Include="Services\Activity.cs" />
<Compile Include="Services\BlockingActivity.cs" />
<Compile Include="Services\IActivity.cs" />
<Compile Include="Services\IActivityProvider.cs" />
<Compile Include="Services\ActivitiesManager.cs" />
<Compile Include="Services\IActivitiesManager.cs" />
<Compile Include="ViewModels\AdminEditViewModel.cs" />
<Compile Include="ViewModels\AdminIndexViewModel.cs" />
</ItemGroup>
<ItemGroup>
<Folder Include="Controllers\" />
<Folder Include="Handlers\" />
</ItemGroup>
<ItemGroup>
<Content Include="Views\Admin\Index.cshtml" />
</ItemGroup>
<ItemGroup>
<Content Include="Views\Admin\Edit.cshtml" />
</ItemGroup>
<ItemGroup>
<Content Include="Views\Admin\Create.cshtml" />
</ItemGroup>
<ItemGroup>
<Content Include="Views\Activity-ContentCreated.cshtml" />
</ItemGroup>
<ItemGroup>
<Content Include="Views\Activity.cshtml" />
</ItemGroup>
<PropertyGroup>
<VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">10.0</VisualStudioVersion>
<VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath>

View File

@@ -0,0 +1,51 @@
using System;
using System.Linq;
using System.Web.Mvc;
using Orchard.ContentManagement.MetaData;
using Orchard.DisplayManagement;
using Orchard.Forms.Services;
using Orchard.Localization;
namespace Orchard.Workflows.Providers {
public class ContentForms : IFormProvider {
private readonly IContentDefinitionManager _contentDefinitionManager;
protected dynamic Shape { get; set; }
public Localizer T { get; set; }
public ContentForms(
IShapeFactory shapeFactory,
IContentDefinitionManager contentDefinitionManager) {
_contentDefinitionManager = contentDefinitionManager;
Shape = shapeFactory;
T = NullLocalizer.Instance;
}
public void Describe(DescribeContext context) {
Func<IShapeFactory, dynamic> form =
shape => {
var f = Shape.Form(
Id: "AnyOfContentTypes",
_Parts: Shape.SelectList(
Id: "contenttypes", Name: "contenttypes",
Title: T("Content types"),
Description: T("Select some content types."),
Size: 10,
Multiple: true
)
);
f._Parts.Add(new SelectListItem { Value = "", Text = T("Any").Text });
foreach (var contentType in _contentDefinitionManager.ListTypeDefinitions().OrderBy(x => x.DisplayName)) {
f._Parts.Add(new SelectListItem { Value = contentType.Name, Text = contentType.DisplayName });
}
return f;
};
context.Form("SelectContentTypes", form);
}
}
}

View File

@@ -0,0 +1,53 @@
using System;
using System.Linq;
using Orchard.ContentManagement;
using Orchard.Localization;
using Orchard.Workflows.Models.Descriptors;
using Orchard.Workflows.Services;
namespace Orchard.Workflows.Providers {
public class ContentActivityProvider : IActivityProvider {
public ContentActivityProvider() {
T = NullLocalizer.Instance;
}
public Localizer T { get; set; }
public void Describe(DescribeActivityContext describe) {
describe.For("Content", T("Content Items"), T("Content Items"))
.Element("Created", T("Content Created"), T("Content is actually created."), ContentHasPart, context => T("When content with types ({0}) is created.", FormatPartsList(context)), "SelectContentTypes")
.Element("Versioned", T("Content Versioned"), T("Content is actually versioned."), ContentHasPart, context => T("When content with types ({0}) is versioned.", FormatPartsList(context)), "SelectContentTypes")
.Element("Published", T("Content Published"), T("Content is actually published."), ContentHasPart, context => T("When content with types ({0}) is published.", FormatPartsList(context)), "SelectContentTypes")
.Element("Removed", T("Content Removed"), T("Content is actually removed."), ContentHasPart, context => T("When content with types ({0}) is removed.", FormatPartsList(context)), "SelectContentTypes");
}
private string FormatPartsList(ActivityContext context) {
string contenttypes = context.State.ContentTypes;
if (String.IsNullOrEmpty(contenttypes)) {
return T("Any").Text;
}
return contenttypes;
}
private bool ContentHasPart(ActivityContext context) {
string contenttypes = context.State.ContentTypes;
var content = context.Tokens["Content"] as IContent;
// "" means 'any'
if (String.IsNullOrEmpty(contenttypes)) {
return true;
}
if (content == null) {
return false;
}
var contentTypes = contenttypes.Split(new[] { ',' });
return contentTypes.Any(contentType => content.ContentItem.TypeDefinition.Name == contentType);
}
}
}

View File

@@ -0,0 +1,36 @@
using System.Web.Mvc;
using Orchard.DisplayManagement;
using Orchard.Forms.Services;
using Orchard.Localization;
namespace Orchard.Workflows.Providers {
public class NotificationActivityForms : IFormProvider {
protected dynamic Shape { get; set; }
public Localizer T { get; set; }
public NotificationActivityForms(IShapeFactory shapeFactory) {
Shape = shapeFactory;
T = NullLocalizer.Instance;
}
public void Describe(DescribeContext context) {
context.Form("ActivityNotify",
shape => Shape.Form(
Id: "ActivityNotify",
_Type: Shape.SelectList(
Id: "notification", Name: "notification",
Title: T("Notification type"),
Description: T("Select what type of notification should be displayed."))
.Add(new SelectListItem { Value = "Information", Text = T("Information").Text })
.Add(new SelectListItem { Value = "Warning", Text = T("Warning").Text })
.Add(new SelectListItem { Value = "Error", Text = T("Error").Text }),
_Message: Shape.Textbox(
Id: "message", Name: "message",
Title: T("Message"),
Description: T("The notification message to display."),
Classes: new[] { "textMedium", "tokenized" })
)
);
}
}
}

View File

@@ -0,0 +1,49 @@
using System;
using Orchard.Localization;
using Orchard.Tokens;
using Orchard.UI.Notify;
using Orchard.Workflows.Models.Descriptors;
using Orchard.Workflows.Services;
namespace Orchard.Workflows.Providers {
public class NotificationActivityProvider : IActivityProvider {
private readonly INotifier _notifier;
private readonly ITokenizer _tokenizer;
public NotificationActivityProvider(INotifier notifier, ITokenizer tokenizer) {
_notifier = notifier;
_tokenizer = tokenizer;
T = NullLocalizer.Instance;
}
public Localizer T { get; set; }
public void Describe(DescribeActivityContext describe) {
describe.For("Notification", T("Notification"), T("Notifications"))
.Element(
"Notify",
T("Notify"),
T("Display a message."),
ExecuteActivity,
DisplayActivity,
"ActivityNotify"
);
}
private bool ExecuteActivity(ActivityContext context) {
string notification = context.State.Notification;
string message = context.State.Message;
message = _tokenizer.Replace(message, context.Tokens);
var notificationType = (NotifyType)Enum.Parse(typeof(NotifyType), notification);
_notifier.Add(notificationType, T(message));
return true;
}
private LocalizedString DisplayActivity(ActivityContext context) {
return T("Displays \"{1}\" as {0}", T(context.State.Notification).Text, T(context.State.Message).Text);
}
}
}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,55 @@
using System.Collections.Generic;
using Orchard.Caching;
using Orchard.ContentManagement;
using Orchard.Localization;
using Orchard.Tokens;
using Orchard.Workflows.Models.Descriptors;
namespace Orchard.Workflows.Services {
public class ActivitiesManager : IActivitiesManager{
private const string SignalName = "Orchard.Workflows.Services.ActivitiesManager";
private readonly ITokenizer _tokenizer;
private readonly IEnumerable<IActivityProvider> _activityProviders;
private readonly IContentManager _contentManager;
private readonly ICacheManager _cacheManager;
private readonly ISignals _signals;
public ActivitiesManager(
ITokenizer tokenizer,
IEnumerable<IActivityProvider> activityProviders,
IContentManager contentManager,
ICacheManager cacheManager,
ISignals signals) {
_tokenizer = tokenizer;
_activityProviders = activityProviders;
_contentManager = contentManager;
_cacheManager = cacheManager;
_signals = signals;
T = NullLocalizer.Instance;
}
public Localizer T { get; set; }
public IEnumerable<TypeDescriptor<ActivityDescriptor>> DescribeActivities() {
return _cacheManager.Get("activities", ctx => {
MonitorSignal(ctx);
var context = new DescribeActivityContext();
foreach (var provider in _activityProviders) {
provider.Describe(context);
}
return context.Describe();
});
}
private void MonitorSignal(AcquireContext<string> ctx) {
ctx.Monitor(_signals.When(SignalName));
}
private void TriggerSignal() {
_signals.Trigger(SignalName);
}
}
}

View File

@@ -0,0 +1,28 @@
using System.Collections.Generic;
using Orchard.Localization;
using Orchard.Workflows.Models.Descriptors;
namespace Orchard.Workflows.Services {
public abstract class Activity : IActivity {
public abstract string Name { get; }
public abstract LocalizedString Category { get; }
public abstract LocalizedString Description { get; }
public virtual bool IsBlocking {
get { return false; }
}
public virtual string Form {
get { return null; }
}
public abstract IEnumerable<LocalizedString> GetPossibleOutcomes(ActivityContext context);
public virtual bool CanTransition(ActivityContext context) {
return true;
}
public abstract LocalizedString Transition(ActivityContext context);
}
}

View File

@@ -0,0 +1,28 @@
using System.Collections.Generic;
using Orchard.Localization;
using Orchard.Workflows.Models.Descriptors;
namespace Orchard.Workflows.Services {
public abstract class BlockingActivity : IActivity {
public abstract string Name { get; }
public abstract LocalizedString Category { get; }
public abstract LocalizedString Description { get; }
public virtual bool IsBlocking {
get { return true; }
}
public virtual string Form {
get { return null; }
}
public abstract IEnumerable<LocalizedString> GetPossibleOutcomes(ActivityContext context);
public virtual bool CanTransition(ActivityContext context) {
return true;
}
public abstract LocalizedString Transition(ActivityContext context);
}
}

View File

@@ -0,0 +1,10 @@
using System.Collections.Generic;
using Orchard.ContentManagement;
using Orchard.Workflows.Models.Descriptors;
namespace Orchard.Workflows.Services {
public interface IActivitiesManager : IDependency {
IEnumerable<TypeDescriptor<ActivityDescriptor>> DescribeActivities();
}
}

View File

@@ -0,0 +1,21 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using Orchard.Localization;
using Orchard.Workflows.Models.Descriptors;
namespace Orchard.Workflows.Services {
public interface IActivity : IDependency {
string Name { get; }
LocalizedString Category { get; }
LocalizedString Description { get; }
bool IsBlocking { get; }
string Form { get; }
IEnumerable<LocalizedString> GetPossibleOutcomes(ActivityContext context);
bool CanTransition(ActivityContext context);
LocalizedString Transition(ActivityContext context);
}
}

View File

@@ -0,0 +1,8 @@
using Orchard.Events;
using Orchard.Workflows.Models.Descriptors;
namespace Orchard.Workflows.Services {
public interface IActivityProvider : IEventHandler {
void Describe(DescribeActivityContext describe);
}
}

View File

@@ -0,0 +1,98 @@
.activity {
border: 1px solid #346789;
/*box-shadow: 2px 2px 19px #aaa;
-o-box-shadow: 2px 2px 19px #aaa;
-webkit-box-shadow: 2px 2px 19px #aaa;
-moz-box-shadow: 2px 2px 19px #aaa;
-moz-border-radius: 0.5em;*/
border-radius: 0.5em;
opacity: 0.8;
filter: alpha(opacity=80);
min-width: 5em;
min-height: 5em;
line-height: 5em;
text-align: center;
z-index: 20;
position: absolute;
background-color: #eeeeef;
color: black;
font-family: helvetica;
padding: 0.5em;
font-size: 0.9em;
}
.activity:hover, .dragHover {
box-shadow: 2px 2px 19px #444;
-o-box-shadow: 2px 2px 19px #444;
-webkit-box-shadow: 2px 2px 19px #444;
-moz-box-shadow: 2px 2px 19px #444;
opacity: 0.6;
filter: alpha(opacity=60);
}
.blocking {
background-image: url(images/blocking.png) ;
background-position: top right;
background-repeat: no-repeat;
}
._jsPlumb_connector { z-index:4; }
._jsPlumb_endpoint { z-index:21;cursor:pointer; }
.sourceEndpointLabel, .targetEndpointLabel {
z-index: 21;
cursor: pointer;
}
.active {
border:1px dotted green;
}
.hover {
border:1px dotted red;
}
/* attempt to make the editor section to take the full height */
#layout-main, #main, #content {
height: 100%
}
.editor {
position: relative;
height:auto !important; /* real browsers */
min-height:500px; /* real browsers */
}
.connection-label {
z-index: 10;
opacity:0.8;
filter:alpha(opacity=80);
background-color:white;
color:black;
padding:0 0.5em;
border:1px solid #346789;
}
/* toolbox */
#activity-toolbox {
border: 1px solid #E4E5E6;
background-color: #F3F4F5;
padding: 0 5px;
}
#activity-toolbox .activity-toolbox-item {
display: block;
padding: 0 10px;
width: 150px;
border: 1px solid #EAEAEA;
background-color: white;
margin: 5px 0;
}
#activity-toolbox .activity-toolbox-item h2 {
color: #333;
padding: 2px;
font-size: 1.077em;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 653 B

View File

@@ -0,0 +1,10 @@
using System.Collections.Generic;
using Orchard.Workflows.Models;
using Orchard.Workflows.Services;
namespace Orchard.Workflows.ViewModels {
public class AdminEditViewModel {
public WorkflowDefinitionRecord WorkflowDefinitionRecord { get; set; }
public IEnumerable<IActivity> AllActivities { get; set; }
}
}

View File

@@ -0,0 +1,40 @@
using System.Collections.Generic;
using Orchard.Workflows.Models;
namespace Orchard.Workflows.ViewModels {
public class AdminIndexViewModel {
public IList<WorkflowDefinitionEntry> WorkflowDefinitions { get; set; }
public AdminIndexOptions Options { get; set; }
public dynamic Pager { get; set; }
}
public class WorkflowDefinitionEntry {
public WorkflowDefinitionRecord WorkflowDefinitionRecord { get; set; }
public bool IsChecked { get; set; }
public int WokflowDefinitionId { get; set; }
public string Name { get; set; }
}
public class AdminIndexOptions {
public string Search { get; set; }
public WorkflowDefinitionOrder Order { get; set; }
public WorkflowDefinitionFilter Filter { get; set; }
public WorkflowDefinitionBulk BulkAction { get; set; }
}
public enum WorkflowDefinitionOrder {
Name,
Creation
}
public enum WorkflowDefinitionFilter {
All
}
public enum WorkflowDefinitionBulk {
None,
Delete
}
}

View File

@@ -0,0 +1,11 @@
@using Orchard.Utility.Extensions
@{
string name = Model.Name;
bool blocking = Model.IsBlocking;
string blockingClass = blocking ? "blocking" : null;
}
<div class="activity @blockingClass" style="background-color: pink">
@name.CamelFriendly()
</div>

View File

@@ -0,0 +1,11 @@
@using Orchard.Utility.Extensions
@{
string name = Model.Name;
bool blocking = Model.IsBlocking;
}
<div class="activity @blocking">
@name.CamelFriendly()
</div>

View File

@@ -0,0 +1,19 @@
@model Orchard.Workflows.ViewModels.AdminEditViewModel
@using Orchard.Workflows.Models;
@using Orchard.Workflows.ViewModels;
@using (Html.BeginFormAntiForgeryPost()) {
@Html.ValidationSummary()
<fieldset>
<div>
<label for="name">@T("Name")</label>
@Html.TextBox("name", String.Empty, new { @class= "textMedium text" })
<span class="hint">@T("The name of the Workflow")</span>
</div>
</fieldset>
<fieldset>
<button class="primaryAction" type="submit">@T("Save")</button>
</fieldset>
}

View File

@@ -0,0 +1,152 @@
@model Orchard.Workflows.ViewModels.AdminEditViewModel
@using ClaySharp.Implementation
@using Orchard.ContentManagement
@using Orchard.Localization
@using Orchard.Utility.Extensions
@using Orchard.Workflows.Models;
@using Orchard.Workflows.ViewModels;
@using Orchard.DisplayManagement;
@{
Layout.Title = @T("Edit Workflow");
Style.Include("admin-workflows.css");
Script.Require("jQueryUI");
Script.Include("jquery.jsPlumb-1.3.16-all-min.js");
// var editorShape = ((IShapeFactory)New).Create(activity.Name + "_Editor");
}
<!-- List of available activities -->
<div id="activity-toolbox">
<ul>
@foreach (var activity in Model.AllActivities) {
IShape shape = New.Activity(activity);
shape.Metadata.Alternates.Add("Activity__" + activity.Name);
<li class="activity-toolbox-item" data-activity-name="@activity.Name">
<h2>@activity.Name</h2>
</li>
}
</ul>
</div>
@using (Html.BeginFormAntiForgeryPost()) {
@Html.ValidationSummary()
<div class="editor">
</div>
}
@* Render script to initialize a new jsplumb shapes in the form of activities['activity-name'].create() *@
@using (Script.Foot()) {
<script type="text/javascript">
//<![CDATA[
var activities = {};
@foreach (var activity in Model.AllActivities) {
IShape shape = New.Activity(activity);
shape.Metadata.Alternates.Add("Activity__" + activity.Name);
<text>
activities['@activity.Name'] = {};
activities['@activity.Name']['create'] = function () {
var dom = $('@Html.Raw(HttpUtility.JavaScriptStringEncode(Display(shape).ToString()))');
var editor = $('.editor');
editor.append(dom);
jsPlumb.draggable(dom, { containment: "parent", scroll: true });
jsPlumb.makeTarget(dom, {
dropOptions: { hoverClass: "dragHover" },
anchor: "Continuous",
endpoint: "Blank",
paintStyle: { fillStyle: "#558822", radius: 3 },
});
</text>
var outcomes = activity.GetPossibleOutcomes(null);
foreach(var outcome in outcomes) {
<text>
jsPlumb.addEndpoint(dom, {
anchor: "Continuous",
connectorOverlays: [["Label", { label: "@HttpUtility.JavaScriptStringEncode(outcome.ToString())", cssClass: "connection-label" }]],
}, sourceEndpointOptions);
</text>
}
<text>
};
</text>
}
//]]>
</script>
}
@using (Script.Foot()) {
<script type="text/javascript">
//<![CDATA[
var connectorPaintStyle = {
lineWidth: 3,
strokeStyle: "grey",
joinstyle: "round",
//outlineColor: "white",
//outlineWidth: 7
};
var connectorHoverStyle = {
lineWidth: 3,
strokeStyle: "#2e2aF8"
};
var sourceEndpointOptions = {
endpoint: "Dot",
paintStyle: { fillStyle: "#225588", radius: 7 },
isSource: true,
isTarget: false,
connector: ["Flowchart"], // gap needs to be the same as makeTarget.paintStyle.radius
connectorStyle: connectorPaintStyle,
hoverPaintStyle: connectorHoverStyle,
connectorHoverStyle: connectorHoverStyle,
overlays: [["Label", { location: [0.5, 1.5], cssClass: "sourceEndpointLabel" }]]
};
jsPlumb.bind("ready", function() {
jsPlumb.importDefaults({
// default drag options
DragOptions : { cursor: 'pointer', zIndex:2000 },
// default to blue at one end and green at the other
EndpointStyles : [{ fillStyle:'#225588' }, { fillStyle:'#558822' }],
// blue endpoints 7 px; green endpoints 11.
Endpoints : [ [ "Dot", {radius:7} ], [ "Dot", { radius:7 } ]],
// the overlays to decorate each connection with. note that the label overlay uses a function to generate the label text; in this
// case it returns the 'labelText' member that we set on each connection in the 'init' method below.
ConnectionOverlays: [
["Arrow", { width: 12, length: 12, location: 1 }],
// ["Label", { location: 0.1, id: "label", cssClass: "aLabel" }]
],
ConnectorZIndex:5
});
//jsPlumb.bind("jsPlumbConnection", function (connInfo, originalEvent) {
// init(connInfo.connection);
//});
});
// a new connection is created
jsPlumb.bind("jsPlumbConnection", function(connectionInfo) {
// ...update your data model here. The contents of the 'connectionInfo' are described below.
});
// a connection is detached
jsPlumb.bind("jsPlumbConnectionDetached", function(connectionInfo) {
// ...update your data model here. The contents of the 'connectionInfo' are described below.
});
// create a new activity node on the editor
$('.activity-toolbox-item').on('click', function () {
var self = $(this);
var activityName = self.data('activity-name');
activities[activityName].create();
});
//]]>
</script>
}

View File

@@ -0,0 +1,72 @@
@model Orchard.Workflows.ViewModels.AdminIndexViewModel
@using Orchard.Workflows.Models;
@using Orchard.Workflows.ViewModels;
@{
var index = 0;
var pageSizes = new List<int?>() { 10, 50, 100 };
var defaultPageSize = WorkContext.CurrentSite.PageSize;
if(!pageSizes.Contains(defaultPageSize)) {
pageSizes.Add(defaultPageSize);
}
}
<h1>@Html.TitleForPage(T("Manage Workflows").ToString()) </h1>
@using (Html.BeginFormAntiForgeryPost()) {
@Html.ValidationSummary()
<div class="manage">@Html.ActionLink(T("Create new Wokflow").ToString(), "Create", new { Area = "Orchard.Workflows", returnurl = HttpContext.Current.Request.RawUrl }, new { @class = "button primaryAction" })</div>
<fieldset class="bulk-actions">
<label for="publishActions">@T("Actions:")</label>
<select id="publishActions" name="@Html.NameOf(m => m.Options.BulkAction)">
@Html.SelectOption(Model.Options.BulkAction, WorkflowDefinitionBulk.None, T("Choose action...").ToString())
@Html.SelectOption(Model.Options.BulkAction, WorkflowDefinitionBulk.Delete, T("Delete").ToString())
</select>
<button type="submit" name="submit.BulkEdit" value="@T("Apply")">@T("Apply")</button>
</fieldset>
<fieldset class="bulk-actions">
<label for="filterResults">@T("Sort by:")</label>
<select id="filterResults" name="@Html.NameOf(m => m.Options.Order)">
@Html.SelectOption(Model.Options.Order, WorkflowDefinitionOrder.Name, T("Name").ToString())
</select>
<input type="hidden" name="Page" value="1" />
<label for="pageSize">@T("Show:")</label>
<select id="pageSize" name="PageSize">
@Html.SelectOption((int)Model.Pager.PageSize, 0, T("All").ToString())
@foreach(int size in pageSizes.OrderBy(p => p)) {
@Html.SelectOption((int)Model.Pager.PageSize, size, size.ToString())
}
</select>
<button type="submit" name="submit.Filter" value="@T("Filter")">@T("Filter")</button>
</fieldset>
<fieldset>
<table class="items">
<thead>
<tr>
<th scope="col" class="checkbox">&nbsp;&darr;</th>
<th scope="col">@T("Name")</th>
<th scope="col" class="actions">&nbsp;</th>
</tr>
</thead>
@foreach (var entry in Model.WorkflowDefinitions) {
<tr>
<td>
<input type="hidden" value="@Model.WorkflowDefinitions[index].WokflowDefinitionId" name="@Html.NameOf(m => m.WorkflowDefinitions[index].WokflowDefinitionId)"/>
<input type="checkbox" value="true" name="@Html.NameOf(m => m.WorkflowDefinitions[index].IsChecked)"/>
</td>
<td>
@Html.ActionLink(entry.Name, "Edit", new { id = entry.WokflowDefinitionId })
</td>
<td>
@*@Html.ActionLink(T("Properties").ToString(), "Edit", new { Area = "Contents", id = entry.QueryId, returnurl = HttpContext.Current.Request.RawUrl })*@ |
@Html.ActionLink(T("Edit").ToString(), "Edit", new { id = entry.WokflowDefinitionId}) |
@*@Html.ActionLink(T("Delete").ToString(), "Delete", new { id = entry.QueryId }, new { itemprop = "RemoveUrl UnsafeUrl" })*@
</td>
</tr>
index++;
}
</table>
@Display(Model.Pager)
</fieldset>
}