Browse Source

Continued with refactoring.

pull/315/head
Sebastian Stehle 7 years ago
parent
commit
9c54e6d704
  1. 4
      Squidex.sln
  2. 4
      src/Squidex.Domain.Apps.Rules/Actions/Algolia/AlgoliaAction.cs
  3. 4
      src/Squidex.Domain.Apps.Rules/Actions/AzureQueue/AzureQueueAction.cs
  4. 4
      src/Squidex.Domain.Apps.Rules/Actions/ElasticSearch/ElasticSearchAction.cs
  5. 4
      src/Squidex.Domain.Apps.Rules/Actions/Fastly/FastlyAction.cs
  6. 4
      src/Squidex.Domain.Apps.Rules/Actions/Medium/MediumAction.cs
  7. 2
      src/Squidex.Domain.Apps.Rules/Actions/RuleActionAttribute.cs
  8. 2
      src/Squidex.Domain.Apps.Rules/Actions/RuleActionHandlerAttribute.cs
  9. 2
      src/Squidex.Domain.Apps.Rules/Actions/RuleElement.cs
  10. 18
      src/Squidex.Domain.Apps.Rules/Actions/RuleElementRegistry.cs
  11. 4
      src/Squidex.Domain.Apps.Rules/Actions/Slack/SlackAction.cs
  12. 4
      src/Squidex.Domain.Apps.Rules/Actions/Twitter/TweetAction.cs
  13. 4
      src/Squidex.Domain.Apps.Rules/Actions/WebhookAction/WebhookAction.cs
  14. 1
      src/Squidex/AppServices.cs
  15. 2
      src/Squidex/Areas/Api/Controllers/Rules/RulesController.cs
  16. 10
      src/Squidex/app/features/rules/declarations.ts
  17. 30
      src/Squidex/app/features/rules/module.ts
  18. 2
      src/Squidex/app/features/rules/pages/rules/actions/algolia-action.component.ts
  19. 2
      src/Squidex/app/features/rules/pages/rules/actions/azure-queue-action.component.ts
  20. 2
      src/Squidex/app/features/rules/pages/rules/actions/elastic-search-action.component.ts
  21. 2
      src/Squidex/app/features/rules/pages/rules/actions/fastly-action.component.ts
  22. 26
      src/Squidex/app/features/rules/pages/rules/actions/index.ts
  23. 2
      src/Squidex/app/features/rules/pages/rules/actions/medium-action.component.ts
  24. 2
      src/Squidex/app/features/rules/pages/rules/actions/slack-action.component.ts
  25. 2
      src/Squidex/app/features/rules/pages/rules/actions/tweet-action.component.ts
  26. 2
      src/Squidex/app/features/rules/pages/rules/actions/webhook-action.component.ts
  27. 59
      src/Squidex/app/features/rules/pages/rules/rule-action.component.ts
  28. 40
      src/Squidex/app/features/rules/pages/rules/rule-action.container.ts
  29. 64
      src/Squidex/app/features/rules/pages/rules/rule-wizard.component.html
  30. 8
      src/Squidex/app/shared/services/rules.service.ts

4
Squidex.sln

@ -65,6 +65,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Squidex.Tests", "tests\Squi
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Squidex.Domain.Apps.Rules", "src\Squidex.Domain.Apps.Rules\Squidex.Domain.Apps.Rules.csproj", "{99B4B165-9146-4406-87AA-A6CD722E33D6}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "extensions", "extensions", "{FB8BC3A2-2010-4C3C-A87D-D4A98C05EE52}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -363,7 +365,7 @@ Global
{AA003372-CD8D-4DBC-962C-F61E0C93CF05} = {C9809D59-6665-471E-AD87-5AC624C65892}
{7DA5B308-D950-4496-93D5-21D6C4D91644} = {C9809D59-6665-471E-AD87-5AC624C65892}
{A4823E14-C0E5-4A4D-B28F-27424C25C3C7} = {94207AA6-4923-4183-A558-E0F8196B8CA3}
{99B4B165-9146-4406-87AA-A6CD722E33D6} = {C9809D59-6665-471E-AD87-5AC624C65892}
{99B4B165-9146-4406-87AA-A6CD722E33D6} = {FB8BC3A2-2010-4C3C-A87D-D4A98C05EE52}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {02F2E872-3141-44F5-BD6A-33CD84E9FE08}

4
src/Squidex.Domain.Apps.Rules/Actions/Algolia/AlgoliaAction.cs

@ -12,7 +12,9 @@ using Squidex.Domain.Apps.Core.Rules;
namespace Squidex.Domain.Apps.Rules.Action.Algolia
{
[RuleActionHandler(typeof(AlgoliaActionHandler))]
[RuleAction(Description = "")]
[RuleAction(Link = "https://www.algolia.com/",
Display = "Populate Algolia index",
Description = "Populate and synchronize indices in Algolia for full text search.")]
public sealed class AlgoliaAction : RuleAction
{
[Required]

4
src/Squidex.Domain.Apps.Rules/Actions/AzureQueue/AzureQueueAction.cs

@ -15,7 +15,9 @@ using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Rules.Action.AzureQueue
{
[RuleActionHandler(typeof(AzureQueueActionHandler))]
[RuleAction(Description = "")]
[RuleAction(Link = "https://azure.microsoft.com/en-us/services/storage/queues/",
Display = "Send to Azure Queue",
Description = "Send an event to azure queue storage.")]
public sealed class AzureQueueAction : RuleAction
{
[Required]

4
src/Squidex.Domain.Apps.Rules/Actions/ElasticSearch/ElasticSearchAction.cs

@ -14,7 +14,9 @@ using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Rules.Action.ElasticSearch
{
[RuleActionHandler(typeof(ElasticSearchActionHandler))]
[RuleAction(Description = "")]
[RuleAction(Link = "https://www.elastic.co/",
Display = "Populate ElasticSearch index",
Description = "Populate and synchronize indices in ElasticSearch for full text search.")]
public sealed class ElasticSearchAction : RuleAction
{
[AbsoluteUrl]

4
src/Squidex.Domain.Apps.Rules/Actions/Fastly/FastlyAction.cs

@ -12,7 +12,9 @@ using Squidex.Domain.Apps.Core.Rules;
namespace Squidex.Domain.Apps.Rules.Action.Fastly
{
[RuleActionHandler(typeof(FastlyActionHandler))]
[RuleAction(Description = "")]
[RuleAction(Link = "https://www.fastly.com/",
Display = "Purge fastly cache",
Description = "Remove entries from the fastly CDN cache.")]
public sealed class FastlyAction : RuleAction
{
[Required]

4
src/Squidex.Domain.Apps.Rules/Actions/Medium/MediumAction.cs

@ -12,7 +12,9 @@ using Squidex.Domain.Apps.Core.Rules;
namespace Squidex.Domain.Apps.Rules.Action.Medium
{
[RuleActionHandler(typeof(MediumActionHandler))]
[RuleAction(Description = "")]
[RuleAction(Link = "https://medium.com/",
Display = "Post to Medium",
Description = "Create a new story or post at medium.")]
public sealed class MediumAction : RuleAction
{
[Required]

2
src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleActionAttribute.cs → src/Squidex.Domain.Apps.Rules/Actions/RuleActionAttribute.cs

@ -7,7 +7,7 @@
using System;
namespace Squidex.Domain.Apps.Core.HandleRules
namespace Squidex.Domain.Apps.Rules.Actions
{
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)]
public sealed class RuleActionAttribute : Attribute

2
src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleActionHandlerAttribute.cs → src/Squidex.Domain.Apps.Rules/Actions/RuleActionHandlerAttribute.cs

@ -8,7 +8,7 @@
using System;
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Core.HandleRules
namespace Squidex.Domain.Apps.Rules.Actions
{
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
public sealed class RuleActionHandlerAttribute : Attribute

2
src/Squidex.Domain.Apps.Rules/Actions/RuleElement.cs

@ -19,7 +19,7 @@ namespace Squidex.Domain.Apps.Rules.Actions
public string Description { get; }
public RuleElement(Type type, string display, string description, string link = null)
public RuleElement(Type type, string color, string display, string description, string link = null)
{
Type = type;

18
src/Squidex.Domain.Apps.Rules/Actions/RuleElementRegistry.cs

@ -21,18 +21,12 @@ namespace Squidex.Domain.Apps.Rules.Actions
private const string Suffix = "Action";
private static readonly HashSet<Type> ActionHandlerTypes = new HashSet<Type>();
private static readonly Dictionary<string, RuleElement> ActionTypes = new Dictionary<string, RuleElement>();
private static readonly Dictionary<string, RuleElement> TriggerTypes = new Dictionary<string, RuleElement>();
public static IReadOnlyDictionary<string, RuleElement> Actions
{
get { return ActionTypes; }
}
public static IReadOnlyDictionary<string, RuleElement> Triggers
{
get { return TriggerTypes; }
}
public static IReadOnlyCollection<Type> ActionHandlers
{
get { return ActionHandlerTypes; }
@ -40,18 +34,6 @@ namespace Squidex.Domain.Apps.Rules.Actions
static RuleElementRegistry()
{
TriggerTypes["ContentChanged"] =
new RuleElement(
typeof(ContentChangedTrigger),
"Content changed",
"Content changed like created, updated, published, unpublished...");
TriggerTypes["AssetChanged"] =
new RuleElement(
typeof(AssetChangedTrigger),
"Asset changed",
"Asset changed like created, updated, renamed...");
var actionTypes =
typeof(RuleElementRegistry).Assembly
.GetTypes()

4
src/Squidex.Domain.Apps.Rules/Actions/Slack/SlackAction.cs

@ -14,7 +14,9 @@ using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Rules.Action.Slack
{
[RuleActionHandler(typeof(SlackActionHandler))]
[RuleAction(Description = "")]
[RuleAction(Link = "https://slack.com",
Display = "Send to Slack",
Description = "Create a status update at slack to a channel you define.")]
public sealed class SlackAction : RuleAction
{
[AbsoluteUrl]

4
src/Squidex.Domain.Apps.Rules/Actions/Twitter/TweetAction.cs

@ -12,7 +12,9 @@ using Squidex.Domain.Apps.Core.Rules;
namespace Squidex.Domain.Apps.Rules.Action.Twitter
{
[RuleActionHandler(typeof(TweetActionHandler))]
[RuleAction(Description = "")]
[RuleAction(Link = "https://twitter.com",
Display = "Tweet",
Description = "Create a status update at Tweet to a your user account.")]
public sealed class TweetAction : RuleAction
{
[Required]

4
src/Squidex.Domain.Apps.Rules/Actions/WebhookAction/WebhookAction.cs

@ -14,7 +14,9 @@ using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Rules.Action.Webhook
{
[RuleActionHandler(typeof(WebhookActionHandler))]
[RuleAction(Description = "")]
[RuleAction(
Display = "Send Webhook",
Description = "Send events like ContentPublished to your webhook.")]
public sealed class WebhookAction : RuleAction
{
[AbsoluteUrl]

1
src/Squidex/AppServices.cs

@ -23,6 +23,7 @@ namespace Squidex
{
public static void AddAppServices(this IServiceCollection services, IConfiguration config)
{
services.AddHttpClient();
services.AddLogging();
services.AddMemoryCache();
services.AddOptions();

2
src/Squidex/Areas/Api/Controllers/Rules/RulesController.cs

@ -69,7 +69,7 @@ namespace Squidex.Areas.Api.Controllers.Rules
/// 200 => Rule triggers returned.
/// </returns>
[HttpGet]
[Route("rules/actions/")]
[Route("rules/triggers/")]
[ProducesResponseType(typeof(Dictionary<string, RuleElementDto>), 200)]
[ApiCosts(0)]
public IActionResult GetTriggers()

10
src/Squidex/app/features/rules/declarations.ts

@ -5,18 +5,10 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
export * from './pages/rules/actions/algolia-action.component';
export * from './pages/rules/actions/azure-queue-action.component';
export * from './pages/rules/actions/elastic-search-action.component';
export * from './pages/rules/actions/fastly-action.component';
export * from './pages/rules/actions/medium-action.component';
export * from './pages/rules/actions/slack-action.component';
export * from './pages/rules/actions/tweet-action.component';
export * from './pages/rules/actions/webhook-action.component';
export * from './pages/rules/triggers/asset-changed-trigger.component';
export * from './pages/rules/triggers/content-changed-trigger.component';
export * from './pages/rules/rule-action.component';
export * from './pages/rules/rule-wizard.component';
export * from './pages/rules/rules-page.component';

30
src/Squidex/app/features/rules/module.ts

@ -14,21 +14,18 @@ import {
SqxSharedModule
} from '@app/shared';
import actions from './pages/rules/actions';
const actionTypes: any[] = Object.values(actions);
import {
AlgoliaActionComponent,
AssetChangedTriggerComponent,
AzureQueueActionComponent,
ContentChangedTriggerComponent,
ElasticSearchActionComponent,
FastlyActionComponent,
MediumActionComponent,
RuleActionComponent,
RuleEventBadgeClassPipe,
RuleEventsPageComponent,
RulesPageComponent,
RuleWizardComponent,
SlackActionComponent,
TweetActionComponent,
WebhookActionComponent
RuleWizardComponent
} from './declarations';
const routes: Routes = [
@ -57,21 +54,18 @@ const routes: Routes = [
SqxSharedModule,
RouterModule.forChild(routes)
],
entryComponents: [
...actionTypes
],
declarations: [
AlgoliaActionComponent,
...actionTypes,
AssetChangedTriggerComponent,
AzureQueueActionComponent,
ContentChangedTriggerComponent,
ElasticSearchActionComponent,
FastlyActionComponent,
MediumActionComponent,
RuleActionComponent,
RuleEventBadgeClassPipe,
RuleEventsPageComponent,
RulesPageComponent,
RuleWizardComponent,
SlackActionComponent,
TweetActionComponent,
WebhookActionComponent
RuleWizardComponent
]
})
export class SqxFeatureRulesModule { }

2
src/Squidex/app/features/rules/pages/rules/actions/algolia-action.component.ts

@ -14,8 +14,6 @@ import { FormControl, FormGroup, Validators } from '@angular/forms';
templateUrl: './algolia-action.component.html'
})
export class AlgoliaActionComponent implements OnInit {
public static key = 'Algolia';
@Input()
public action: any;

2
src/Squidex/app/features/rules/pages/rules/actions/azure-queue-action.component.ts

@ -16,8 +16,6 @@ import { ValidatorsEx } from '@app/shared';
templateUrl: './azure-queue-action.component.html'
})
export class AzureQueueActionComponent implements OnInit {
public static key = 'AzureQueue';
@Input()
public action: any;

2
src/Squidex/app/features/rules/pages/rules/actions/elastic-search-action.component.ts

@ -14,8 +14,6 @@ import { FormControl, FormGroup, Validators } from '@angular/forms';
templateUrl: './elastic-search-action.component.html'
})
export class ElasticSearchActionComponent implements OnInit {
public static key = 'ElasticSearch';
@Input()
public action: any;

2
src/Squidex/app/features/rules/pages/rules/actions/fastly-action.component.ts

@ -14,8 +14,6 @@ import { FormControl, FormGroup, Validators } from '@angular/forms';
templateUrl: './fastly-action.component.html'
})
export class FastlyActionComponent implements OnInit {
public static key = 'Fastly';
@Input()
public action: any;

26
src/Squidex/app/features/rules/pages/rules/actions/index.ts

@ -0,0 +1,26 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { AlgoliaActionComponent } from './algolia-action.component';
import { AzureQueueActionComponent } from './azure-queue-action.component';
import { ElasticSearchActionComponent } from './elastic-search-action.component';
import { FastlyActionComponent } from './fastly-action.component';
import { MediumActionComponent } from './medium-action.component';
import { SlackActionComponent } from './slack-action.component';
import { TweetActionComponent } from './tweet-action.component';
import { WebhookActionComponent } from './webhook-action.component';
export default {
Algolia: AlgoliaActionComponent,
AzureQueue: AzureQueueActionComponent,
ElasticSearch: ElasticSearchActionComponent,
Fastly: FastlyActionComponent,
Medium: MediumActionComponent,
Slack: SlackActionComponent,
Tweet: TweetActionComponent,
Webhook: WebhookActionComponent
};

2
src/Squidex/app/features/rules/pages/rules/actions/medium-action.component.ts

@ -14,8 +14,6 @@ import { FormControl, FormGroup, Validators } from '@angular/forms';
templateUrl: './medium-action.component.html'
})
export class MediumActionComponent implements OnInit {
public static key = 'Medium';
@Input()
public action: any;

2
src/Squidex/app/features/rules/pages/rules/actions/slack-action.component.ts

@ -14,8 +14,6 @@ import { FormControl, FormGroup, Validators } from '@angular/forms';
templateUrl: './slack-action.component.html'
})
export class SlackActionComponent implements OnInit {
public static key = 'Slack';
@Input()
public action: any;

2
src/Squidex/app/features/rules/pages/rules/actions/tweet-action.component.ts

@ -17,8 +17,6 @@ import { DialogService } from '@app/shared';
templateUrl: './tweet-action.component.html'
})
export class TweetActionComponent implements OnInit {
public static key = 'Tweet';
private request: any;
@Input()

2
src/Squidex/app/features/rules/pages/rules/actions/webhook-action.component.ts

@ -14,8 +14,6 @@ import { FormControl, FormGroup, Validators } from '@angular/forms';
templateUrl: './webhook-action.component.html'
})
export class WebhookActionComponent implements OnInit {
public static key = 'Webhook';
@Input()
public action: any;

59
src/Squidex/app/features/rules/pages/rules/rule-action.component.ts

@ -0,0 +1,59 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { ChangeDetectionStrategy, Component, ComponentFactoryResolver, ComponentRef, Input, OnChanges, OnInit, ViewChild, ViewContainerRef } from '@angular/core';
import { FormGroup } from '@angular/forms';
import actions from './actions';
@Component({
selector: 'sqx-rule-action',
template: '<div #element></div>',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class RuleActionComponent implements OnChanges, OnInit {
@Input()
public actionType: string;
@Input()
public action: any;
@Input()
public actionForm: FormGroup;
@Input()
public actionFormSubmitted = false;
@ViewChild('element', { read: ViewContainerRef })
public viewContainer: ViewContainerRef;
private component: ComponentRef<any>;
constructor(
private readonly componentFactoryResolver: ComponentFactoryResolver
) {
}
public ngOnChanges() {
if (this.component) {
this.component.instance.action = this.action;
this.component.instance.actionForm = this.actionForm;
this.component.instance.actionFormSubmitted = this.actionFormSubmitted;
}
}
public ngOnInit() {
const factoryType: any = actions[this.actionType];
const factory: any = this.componentFactoryResolver.resolveComponentFactory(factoryType);
this.component = this.viewContainer.createComponent(factory);
this.component.instance.action = this.action;
this.component.instance.actionForm = this.actionForm;
this.component.instance.actionFormSubmitted = this.actionFormSubmitted;
}
}

40
src/Squidex/app/features/rules/pages/rules/rule-action.container.ts

@ -1,40 +0,0 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { ComponentFactoryResolver, ComponentRef, Input, OnInit, ViewChild, ViewContainerRef } from '@angular/core';
import { FormGroup } from '@angular/forms';
export class RuleActionContainer implements OnInit {
@Input()
public actionType: string;
@Input()
public action: any;
@Input()
public actionForm: FormGroup;
@Input()
public actionFormSubmitted = false;
@ViewChild('container', { read: ViewContainerRef })
public entry: ViewContainerRef;
private component: ComponentRef<any>;
constructor(
private readonly componentFactoryResolver: ComponentFactoryResolver
) {
}
public ngOnInit() {
const factories = Array.from(this.componentFactoryResolver['_factories'].values());
const factory: any = factories.find((x: any) => x.selector === this.actionType);
this.component = this.entry.createComponent(factory);
}
}

64
src/Squidex/app/features/rules/pages/rules/rule-wizard.component.html

@ -71,64 +71,12 @@
<ng-container *ngIf="step === 4">
<form [formGroup]="actionForm.form" (submit)="saveAction()">
<ng-container [ngSwitch]="actionType">
<ng-container *ngSwitchCase="'Algolia'">
<sqx-algolia-action
[action]="action"
[actionForm]="actionForm.form"
[actionFormSubmitted]="actionForm.submitted | async">
</sqx-algolia-action>
</ng-container>
<ng-container *ngSwitchCase="'AzureQueue'">
<sqx-azure-queue-action
[action]="action"
[actionForm]="actionForm.form"
[actionFormSubmitted]="actionForm.submitted | async">
</sqx-azure-queue-action>
</ng-container>
<ng-container *ngSwitchCase="'ElasticSearch'">
<sqx-elastic-search-action
[action]="action"
[actionForm]="actionForm.form"
[actionFormSubmitted]="actionForm.submitted | async">
</sqx-elastic-search-action>
</ng-container>
<ng-container *ngSwitchCase="'Fastly'">
<sqx-fastly-action
[action]="action"
[actionForm]="actionForm.form"
[actionFormSubmitted]="actionForm.submitted | async">
</sqx-fastly-action>
</ng-container>
<ng-container *ngSwitchCase="'Medium'">
<sqx-medium-action
[action]="action"
[actionForm]="actionForm.form"
[actionFormSubmitted]="actionForm.submitted | async">
</sqx-medium-action>
</ng-container>
<ng-container *ngSwitchCase="'Slack'">
<sqx-slack-action
[action]="action"
[actionForm]="actionForm.form"
[actionFormSubmitted]="actionForm.submitted | async">
</sqx-slack-action>
</ng-container>
<ng-container *ngSwitchCase="'Tweet'">
<sqx-tweet-action
[action]="action"
[actionForm]="actionForm.form"
[actionFormSubmitted]="actionForm.submitted | async">
</sqx-tweet-action>
</ng-container>
<ng-container *ngSwitchCase="'Webhook'">
<sqx-webhook-action
[action]="action"
[actionForm]="actionForm.form"
[actionFormSubmitted]="actionForm.submitted | async">
</sqx-webhook-action>
</ng-container>
</ng-container>
<sqx-rule-action
[actionType]="actionType"
[action]="action"
[actionForm]="actionForm.form"
[actionFormSubmitted]="actionForm.submitted | async">
</sqx-rule-action>
</form>
</ng-container>
</ng-container>

8
src/Squidex/app/shared/services/rules.service.ts

@ -103,7 +103,9 @@ export class RulesService {
}
public getActions(): Observable<{ [name: string]: RuleElementDto }> {
return HTTP.getVersioned<any>(this.http, 'rules/action').pipe(
const url = this.apiUrl.buildUrl('api/rules/actions');
return HTTP.getVersioned<any>(this.http, url).pipe(
map(response => {
const items: { [name: string]: any } = response.payload.body;
@ -123,7 +125,9 @@ export class RulesService {
}
public getTriggers(): Observable<{ [name: string]: RuleElementDto }> {
return HTTP.getVersioned<any>(this.http, 'rules/triggers').pipe(
const url = this.apiUrl.buildUrl('api/rules/triggers');
return HTTP.getVersioned<any>(this.http, url).pipe(
map(response => {
const items: { [name: string]: any } = response.payload.body;

Loading…
Cancel
Save