diff --git a/Controller/ArticleController.php b/Controller/ArticleController.php index 499e18db4..df2ced226 100644 --- a/Controller/ArticleController.php +++ b/Controller/ArticleController.php @@ -23,6 +23,7 @@ use ONGR\ElasticsearchDSL\Query\TermQuery; use ONGR\ElasticsearchDSL\Sort\FieldSort; use Sulu\Bundle\ArticleBundle\Admin\ArticleAdmin; +use Sulu\Bundle\ArticleBundle\Document\ArticleDocument; use Sulu\Bundle\ArticleBundle\Document\Form\ArticleDocumentType; use Sulu\Bundle\ArticleBundle\Metadata\ArticleViewDocumentIdTrait; use Sulu\Component\Content\Form\Exception\InvalidFormException; @@ -342,6 +343,14 @@ public function postTriggerAction($uuid, Request $request) $destLocales = $this->getRequestParameter($request, 'dest', true); $data = $this->getMapper()->copyLanguage($uuid, $userId, null, $locale, explode(',', $destLocales)); break; + case 'copy': + /** @var ArticleDocument $document */ + $document = $this->getDocumentManager()->find($uuid, $locale); + $copiedPath = $this->getDocumentManager()->copy($document, dirname($document->getPath())); + $this->getDocumentManager()->flush(); + + $data = $this->getDocumentManager()->find($copiedPath, $locale); + break; default: throw new RestException('Unrecognized action: ' . $action); } diff --git a/Document/ArticleDocument.php b/Document/ArticleDocument.php index e69493dda..0fe9da26d 100644 --- a/Document/ArticleDocument.php +++ b/Document/ArticleDocument.php @@ -244,9 +244,7 @@ public function getRoute() } /** - * Set route. - * - * @param RouteInterface $route + * {@inheritdoc} */ public function setRoute(RouteInterface $route) { @@ -254,6 +252,15 @@ public function setRoute(RouteInterface $route) $this->routePath = $route->getPath(); } + /** + * {@inheritdoc} + */ + public function removeRoute() + { + $this->route = null; + $this->routePath = null; + } + /** * {@inheritdoc} */ diff --git a/Document/Behavior/RoutableBehavior.php b/Document/Behavior/RoutableBehavior.php index f86904c35..3fe990f94 100644 --- a/Document/Behavior/RoutableBehavior.php +++ b/Document/Behavior/RoutableBehavior.php @@ -27,6 +27,11 @@ interface RoutableBehavior extends RoutableInterface, UuidBehavior, LocaleBehavi */ public function getRoutePath(); + /** + * Remove route. + */ + public function removeRoute(); + /** * Set route-path. * diff --git a/Document/Subscriber/ArticleSubscriber.php b/Document/Subscriber/ArticleSubscriber.php index 2ae4e135d..f96b753a6 100644 --- a/Document/Subscriber/ArticleSubscriber.php +++ b/Document/Subscriber/ArticleSubscriber.php @@ -15,6 +15,7 @@ use Sulu\Bundle\ArticleBundle\Document\Index\IndexerInterface; use Sulu\Component\DocumentManager\DocumentManagerInterface; use Sulu\Component\DocumentManager\Event\AbstractMappingEvent; +use Sulu\Component\DocumentManager\Event\CopyEvent; use Sulu\Component\DocumentManager\Event\FlushEvent; use Sulu\Component\DocumentManager\Event\RemoveEvent; use Sulu\Component\DocumentManager\Event\UnpublishEvent; @@ -78,6 +79,7 @@ public static function getSubscribedEvents() Events::UNPUBLISH => 'handleUnpublish', Events::REMOVE_DRAFT => ['handleScheduleIndex', -1024], Events::FLUSH => [['handleFlush', -2048], ['handleFlushLive', -2048]], + Events::COPY => ['handleCopy'], ]; } @@ -228,4 +230,23 @@ public function handleRemoveLive($event) $this->liveIndexer->remove($document); $this->liveIndexer->flush(); } + + /** + * Schedule document to index. + * + * @param CopyEvent $event + */ + public function handleCopy(CopyEvent $event) + { + $document = $event->getDocument(); + if (!$document instanceof ArticleDocument) { + return; + } + + $uuid = $event->getCopiedNode()->getIdentifier(); + $this->documents[$uuid] = [ + 'uuid' => $uuid, + 'locale' => $document->getLocale(), + ]; + } } diff --git a/Document/Subscriber/RoutableSubscriber.php b/Document/Subscriber/RoutableSubscriber.php index be460b429..837bfa126 100644 --- a/Document/Subscriber/RoutableSubscriber.php +++ b/Document/Subscriber/RoutableSubscriber.php @@ -12,11 +12,16 @@ namespace Sulu\Bundle\ArticleBundle\Document\Subscriber; use Doctrine\ORM\EntityManagerInterface; +use Sulu\Bundle\ArticleBundle\Document\ArticleDocument; use Sulu\Bundle\ArticleBundle\Document\Behavior\RoutableBehavior; +use Sulu\Bundle\DocumentManagerBundle\Bridge\DocumentInspector; +use Sulu\Bundle\DocumentManagerBundle\Bridge\PropertyEncoder; use Sulu\Bundle\RouteBundle\Entity\RouteRepositoryInterface; use Sulu\Bundle\RouteBundle\Manager\RouteManagerInterface; +use Sulu\Component\DocumentManager\DocumentManagerInterface; use Sulu\Component\DocumentManager\Event\AbstractMappingEvent; use Sulu\Component\DocumentManager\Event\ConfigureOptionsEvent; +use Sulu\Component\DocumentManager\Event\CopyEvent; use Sulu\Component\DocumentManager\Event\MetadataLoadEvent; use Sulu\Component\DocumentManager\Event\RemoveEvent; use Sulu\Component\DocumentManager\Events; @@ -27,6 +32,8 @@ */ class RoutableSubscriber implements EventSubscriberInterface { + const ROUTE_FIELD = 'routePath'; + /** * @var RouteManagerInterface */ @@ -42,16 +49,43 @@ class RoutableSubscriber implements EventSubscriberInterface */ private $entityManager; + /** + * @var DocumentManagerInterface + */ + private $documentManager; + + /** + * @var DocumentInspector + */ + private $documentInspector; + + /** + * @var PropertyEncoder + */ + private $propertyEncoder; + /** * @param RouteManagerInterface $routeManager * @param RouteRepositoryInterface $routeRepository * @param EntityManagerInterface $entityManager + * @param DocumentManagerInterface $documentManager + * @param DocumentInspector $documentInspector + * @param PropertyEncoder $propertyEncoder */ - public function __construct(RouteManagerInterface $routeManager, RouteRepositoryInterface $routeRepository, EntityManagerInterface $entityManager) - { + public function __construct( + RouteManagerInterface $routeManager, + RouteRepositoryInterface $routeRepository, + EntityManagerInterface $entityManager, + DocumentManagerInterface $documentManager, + DocumentInspector $documentInspector, + PropertyEncoder $propertyEncoder + ) { $this->routeManager = $routeManager; $this->routeRepository = $routeRepository; $this->entityManager = $entityManager; + $this->documentManager = $documentManager; + $this->documentInspector = $documentInspector; + $this->propertyEncoder = $propertyEncoder; } /** @@ -63,6 +97,7 @@ public static function getSubscribedEvents() Events::HYDRATE => [['handleHydrate', -500]], Events::PERSIST => [['handleRouteUpdate', 1], ['handleRoute', 0]], Events::REMOVE => [['handleRemove', -500]], + Events::COPY => ['handleCopy', -2000], Events::METADATA_LOAD => 'handleMetadataLoad', Events::CONFIGURE_OPTIONS => 'configureOptions', ]; @@ -149,6 +184,37 @@ public function handleRemove(RemoveEvent $event) $this->entityManager->flush(); } + /** + * Update routes for copied article. + * + * @param CopyEvent $event + */ + public function handleCopy(CopyEvent $event) + { + $document = $event->getDocument(); + if (!$document instanceof RoutableBehavior) { + return; + } + + $locales = $this->documentInspector->getLocales($document); + foreach ($locales as $locale) { + /** @var ArticleDocument $localizedDocument */ + $localizedDocument = $this->documentManager->find($event->getCopiedPath(), $locale); + + $localizedDocument->removeRoute(); + $route = $this->routeManager->create($localizedDocument); + $document->setRoutePath($route->getPath()); + $this->entityManager->persist($route); + + $event->getCopiedNode()->setProperty( + $this->propertyEncoder->localizedSystemName(self::ROUTE_FIELD, $locale), + $route->getPath() + ); + } + + $this->entityManager->flush(); + } + /** * Add route to metadata. * @@ -162,10 +228,10 @@ public function handleMetadataLoad(MetadataLoadEvent $event) $metadata = $event->getMetadata(); $metadata->addFieldMapping( - 'routePath', + self::ROUTE_FIELD, [ 'encoding' => 'system_localized', - 'property' => 'routePath', + 'property' => self::ROUTE_FIELD, ] ); } diff --git a/Resources/config/services.xml b/Resources/config/services.xml index 868f53258..ee95de05b 100644 --- a/Resources/config/services.xml +++ b/Resources/config/services.xml @@ -110,6 +110,9 @@ + + + diff --git a/Resources/public/dist/components/articles/edit/main.js b/Resources/public/dist/components/articles/edit/main.js index b255cfb2a..542a6150e 100644 --- a/Resources/public/dist/components/articles/edit/main.js +++ b/Resources/public/dist/components/articles/edit/main.js @@ -1 +1 @@ -define(["jquery","underscore","config","services/suluarticle/article-manager","sulusecurity/services/user-manager","services/sulupreview/preview","sulusecurity/services/security-checker","sulucontent/components/copy-locale-overlay/main","sulucontent/components/open-ghost-overlay/main"],function(a,b,c,d,e,f,g,h,i){"use strict";return{defaults:{options:{config:{}},templates:{url:"/admin/api/articles<% if (!!id) { %>/<%= id %><% } %>?locale=<%= locale %>"},translations:{headline:"sulu_article.edit.title",draftLabel:"sulu-document-manager.draft-label",removeDraft:"sulu-content.delete-draft",unpublish:"sulu-document-manager.unpublish",unpublishConfirmTextNoDraft:"sulu-content.unpublish-confirm-text-no-draft",unpublishConfirmTextWithDraft:"sulu-content.unpublish-confirm-text-with-draft",unpublishConfirmTitle:"sulu-content.unpublish-confirm-title",deleteDraftConfirmTitle:"sulu-content.delete-draft-confirm-title",deleteDraftConfirmText:"sulu-content.delete-draft-confirm-text",openGhostOverlay:{info:"sulu_article.settings.open-ghost-overlay.info","new":"sulu_article.settings.open-ghost-overlay.new",copy:"sulu_article.settings.open-ghost-overlay.copy",ok:"sulu_article.settings.open-ghost-overlay.ok"},copyLocaleOverlay:{info:"sulu_article.settings.copy-locale-overlay.info"}}},layout:function(){return{navigation:{collapsed:!0},content:{shrinkable:!!this.options.id},sidebar:!!this.options.id&&"max"}},header:function(){var a={},d={},e={};return g.hasPermission(this.data,"edit")&&(e.saveDraft={},g.hasPermission(this.data,"live")&&(e.savePublish={},e.publish={}),c.has("sulu_automation.enabled")&&(e.automationInfo={options:{entityId:this.options.id,entityClass:"Sulu\\Bundle\\ArticleBundle\\Document\\ArticleDocument",handlerClass:["Sulu\\Bundle\\ContentBundle\\Automation\\DocumentPublishHandler","Sulu\\Bundle\\ContentBundle\\Automation\\DocumentUnpublishHandler"]}}),a.save={parent:"saveWithDraft",options:{callback:function(){this.sandbox.emit("sulu.toolbar.save","publish")}.bind(this),dropdownItems:e}},a.template={options:{dropdownOptions:{url:"/admin/articles/templates?type="+(this.options.type||this.data.articleType),callback:function(a){this.template=a.template,this.sandbox.emit("sulu.tab.template-change",a)}.bind(this)}}}),g.hasPermission(this.data,"live")&&(d.unpublish={options:{title:this.translations.unpublish,disabled:!this.data.published,callback:this.unpublish.bind(this)}},d.divider={options:{divider:!0}}),g.hasPermission(this.data,"delete")&&(d["delete"]={options:{disabled:!this.options.id,callback:this.deleteArticle.bind(this)}}),d.copyLocale={options:{title:this.sandbox.translate("toolbar.copy-locale"),callback:function(){h.startCopyLocalesOverlay.call(this,this.translations.copyLocaleOverlay).then(function(a){return b.contains(a,this.options.locale)?void this.toEdit(this.options.locale):(this.data.concreteLanguages=b.uniq(this.data.concreteLanguages.concat(a)),void this.sandbox.emit("sulu.labels.success.show","labels.success.copy-locale-desc","labels.success"))}.bind(this))}.bind(this)}},this.sandbox.util.isEmpty(d)||(a.edit={options:{dropdownItems:d}}),a.statePublished={},a.stateTest={},{tabs:{url:"/admin/content-navigations?alias=article&id="+this.options.id+"&locale="+this.options.locale,options:{data:function(){return this.sandbox.util.deepCopy(this.data)}.bind(this),url:function(){return this.templates.url({id:this.options.id,locale:this.options.locale})}.bind(this),config:this.options.config,preview:this.preview},componentOptions:{values:b.defaults(this.data,{type:null})}},toolbar:{buttons:a,languageChanger:{data:this.options.config.languageChanger,preSelected:this.options.locale}}}},initialize:function(){this.bindCustomEvents(),this.showDraftLabel(),this.setHeaderBar(!0),this.loadLocalizations(),this.options.language=this.options.locale},bindCustomEvents:function(){this.sandbox.on("sulu.header.back",this.toList.bind(this)),this.sandbox.on("sulu.tab.dirty",this.setHeaderBar.bind(this)),this.sandbox.on("sulu.toolbar.save",this.save.bind(this)),this.sandbox.on("sulu.tab.data-changed",this.setData.bind(this)),this.sandbox.on("sulu.article.error",this.handleError.bind(this)),this.sandbox.on("husky.tabs.header.item.select",this.tabChanged.bind(this)),this.sandbox.on("sulu.header.language-changed",this.languageChanged.bind(this))},languageChanged:function(a){a.id!==this.options.locale&&(this.sandbox.sulu.saveUserSetting(this.options.config.settingsKey,a.id),-1===b(this.data.concreteLanguages).indexOf(a.id)?i.openGhost.call(this,this.data,this.translations.openGhostOverlay).then(function(b,c){b?h.copyLocale.call(this,this.data.id,c,[a.id],function(){this.toEdit(a.id)}.bind(this)):this.toEdit(a.id)}.bind(this)).fail(function(){this.sandbox.emit("sulu.header.change-language",this.options.language)}.bind(this)):this.toEdit(a.id))},tabChanged:function(a){this.options.content=a.id},handleError:function(a,b,c){switch(a){default:this.sandbox.emit("sulu.labels.error.show","labels.error.content-save-desc","labels.error"),this.sandbox.emit("sulu.header.toolbar.item.enable","save")}},deleteArticle:function(){this.sandbox.sulu.showDeleteDialog(function(a){a&&d.remove(this.options.id,this.options.locale).then(function(){this.toList()}.bind(this))}.bind(this))},toEdit:function(a,b){this.sandbox.emit("sulu.router.navigate","articles/"+(a||this.options.locale)+"/edit:"+(b||this.options.id)+"/"+(this.options.content||"details"),!0,!0)},toList:function(){1===this.options.config.typeNames.length?this.sandbox.emit("sulu.router.navigate","articles/"+this.options.locale):this.sandbox.emit("sulu.router.navigate","articles:"+(this.options.type||this.data.articleType)+"/"+this.options.locale)},toAdd:function(){1===this.options.config.typeNames.length?this.sandbox.emit("sulu.router.navigate","articles/"+this.options.locale+"/add",!0,!0):this.sandbox.emit("sulu.router.navigate","articles/"+this.options.locale+"/add:"+(this.options.type||this.data.articleType),!0,!0)},save:function(a){this.loadingSave(),this.saveTab(a).then(function(b){this.saved(b.id,b,a)}.bind(this))},setData:function(a){this.data=a},saveTab:function(c){var d=a.Deferred();return this.sandbox.emit("sulu.header.toolbar.item.loading","save"),this.sandbox.once("sulu.tab.saved",function(a,c){d.resolve(b.defaults(c,{type:null}))}.bind(this)),this.sandbox.emit("sulu.tab.save",c),d},setHeaderBar:function(a){var b=!a,c=!a,d=!!a&&!this.data.publishedState;this.setSaveToolbarItems.call(this,"saveDraft",b),this.setSaveToolbarItems.call(this,"savePublish",c),this.setSaveToolbarItems.call(this,"publish",d),this.setSaveToolbarItems.call(this,"unpublish",!!this.data.published),b||c||d?this.sandbox.emit("sulu.header.toolbar.item.enable","save",!1):this.sandbox.emit("sulu.header.toolbar.item.disable","save",!1),this.showState(!!this.data.published)},setSaveToolbarItems:function(a,b){this.sandbox.emit("sulu.header.toolbar.item."+(b?"enable":"disable"),a,!1)},loadingSave:function(){this.sandbox.emit("sulu.header.toolbar.item.loading","save")},afterSaveAction:function(a,b){"back"===a?this.toList():"new"===a?this.toAdd():b&&this.toEdit(this.options.locale,this.data.id)},showDraftLabel:function(){this.sandbox.emit("sulu.header.tabs.label.hide"),this.hasDraft(this.data)||e.find(this.data.changer).then(function(a){this.sandbox.emit("sulu.header.tabs.label.show",this.sandbox.util.sprintf(this.translations.draftLabel,{changed:this.sandbox.date.format(this.data.changed,!0),user:a.username}),[{id:"delete-draft",title:this.translations.removeDraft,skin:"critical",onClick:this.deleteDraft.bind(this)}])}.bind(this))},deleteDraft:function(){this.sandbox.sulu.showDeleteDialog(function(a){a&&(this.sandbox.emit("husky.label.header.loading"),d.removeDraft(this.data.id,this.options.locale).always(function(){this.sandbox.emit("sulu.header.toolbar.item.enable","edit")}.bind(this)).then(function(a){this.sandbox.emit("sulu.router.navigate",this.sandbox.mvc.history.fragment,!0,!0),this.saved(a.id,a)}.bind(this)).fail(function(){this.sandbox.emit("husky.label.header.reset"),this.sandbox.emit("sulu.labels.error.show","labels.error.remove-draft-desc","labels.error")}.bind(this)))}.bind(this),this.translations.deleteDraftConfirmTitle,this.translations.deleteDraftConfirmText)},hasDraft:function(a){return!a.id||!!a.publishedState||!a.published},getUrl:function(a){var c=b.template(this.defaults.templates.url,{id:this.options.id,locale:this.options.locale});return a&&(c+="&action="+a),c},loadComponentData:function(){var b=a.Deferred();return this.options.id?(this.sandbox.util.load(this.getUrl()).done(function(a){this.preview=f.initialize({}),this.preview.start("Sulu\\Bundle\\ArticleBundle\\Document\\ArticleDocument",this.options.id,this.options.locale,a),b.resolve(a)}.bind(this)),b):(b.resolve({}),b)},destroy:function(){this.preview&&f.destroy(this.preview)},showState:function(a){a?(this.sandbox.emit("sulu.header.toolbar.item.hide","stateTest"),this.sandbox.emit("sulu.header.toolbar.item.show","statePublished")):(this.sandbox.emit("sulu.header.toolbar.item.hide","statePublished"),this.sandbox.emit("sulu.header.toolbar.item.show","stateTest"))},unpublish:function(){this.sandbox.sulu.showConfirmationDialog({callback:function(a){a&&(this.sandbox.emit("sulu.header.toolbar.item.loading","edit"),d.unpublish(this.data.id,this.options.locale).always(function(){this.sandbox.emit("sulu.header.toolbar.item.enable","edit")}.bind(this)).then(function(a){this.sandbox.emit("sulu.labels.success.show","labels.success.content-unpublish-desc","labels.success"),this.saved(a.id,a)}.bind(this)).fail(function(){this.sandbox.emit("sulu.labels.error.show","labels.error.content-unpublish-desc","labels.error")}.bind(this)))}.bind(this),title:this.translations.unpublishConfirmTitle,description:this.hasDraft(this.data)?this.translations.unpublishConfirmTextNoDraft:this.translations.unpublishConfirmTextWithDraft})},saved:function(a,b,c){this.setData(b),this.options.id?(this.setHeaderBar(!0),this.showDraftLabel(),this.sandbox.emit("sulu.header.saved",b),this.sandbox.emit("sulu.labels.success.show","labels.success.content-save-desc","labels.success")):this.sandbox.sulu.viewStates.justSaved=!0,this.afterSaveAction(c,!this.options.id)},loadLocalizations:function(){this.sandbox.util.load("/admin/api/localizations").then(function(a){this.localizations=a._embedded.localizations.map(function(a){return{id:a.localization,title:a.localization}})}.bind(this))},getCopyLocaleUrl:function(a,b,c){return d.getCopyLocaleUrl(a,b,c)}}}); \ No newline at end of file +define(["jquery","underscore","config","services/suluarticle/article-manager","sulusecurity/services/user-manager","services/sulupreview/preview","sulusecurity/services/security-checker","sulucontent/components/copy-locale-overlay/main","sulucontent/components/open-ghost-overlay/main"],function(a,b,c,d,e,f,g,h,i){"use strict";return{defaults:{options:{config:{}},templates:{url:"/admin/api/articles<% if (!!id) { %>/<%= id %><% } %>?locale=<%= locale %>"},translations:{headline:"sulu_article.edit.title",draftLabel:"sulu-document-manager.draft-label",removeDraft:"sulu-content.delete-draft",unpublish:"sulu-document-manager.unpublish",unpublishConfirmTextNoDraft:"sulu-content.unpublish-confirm-text-no-draft",unpublishConfirmTextWithDraft:"sulu-content.unpublish-confirm-text-with-draft",unpublishConfirmTitle:"sulu-content.unpublish-confirm-title",deleteDraftConfirmTitle:"sulu-content.delete-draft-confirm-title",deleteDraftConfirmText:"sulu-content.delete-draft-confirm-text",copy:"sulu_article.edit.copy",openGhostOverlay:{info:"sulu_article.settings.open-ghost-overlay.info","new":"sulu_article.settings.open-ghost-overlay.new",copy:"sulu_article.settings.open-ghost-overlay.copy",ok:"sulu_article.settings.open-ghost-overlay.ok"},copyLocaleOverlay:{info:"sulu_article.settings.copy-locale-overlay.info"}}},layout:function(){return{navigation:{collapsed:!0},content:{shrinkable:!!this.options.id},sidebar:!!this.options.id&&"max"}},header:function(){var a={},d={},e={};return g.hasPermission(this.data,"edit")&&(e.saveDraft={},g.hasPermission(this.data,"live")&&(e.savePublish={},e.publish={}),c.has("sulu_automation.enabled")&&(e.automationInfo={options:{entityId:this.options.id,entityClass:"Sulu\\Bundle\\ArticleBundle\\Document\\ArticleDocument",handlerClass:["Sulu\\Bundle\\ContentBundle\\Automation\\DocumentPublishHandler","Sulu\\Bundle\\ContentBundle\\Automation\\DocumentUnpublishHandler"]}}),a.save={parent:"saveWithDraft",options:{callback:function(){this.sandbox.emit("sulu.toolbar.save","publish")}.bind(this),dropdownItems:e}},a.template={options:{dropdownOptions:{url:"/admin/articles/templates?type="+(this.options.type||this.data.articleType),callback:function(a){this.template=a.template,this.sandbox.emit("sulu.tab.template-change",a)}.bind(this)}}}),g.hasPermission(this.data,"live")&&(d.unpublish={options:{title:this.translations.unpublish,disabled:!this.data.published,callback:this.unpublish.bind(this)}},d.divider={options:{divider:!0}}),g.hasPermission(this.data,"delete")&&(d["delete"]={options:{disabled:!this.options.id,callback:this.deleteArticle.bind(this)}}),d.copyLocale={options:{title:this.sandbox.translate("toolbar.copy-locale"),callback:function(){h.startCopyLocalesOverlay.call(this,this.translations.copyLocaleOverlay).then(function(a){return b.contains(a,this.options.locale)?void this.toEdit(this.options.locale):(this.data.concreteLanguages=b.uniq(this.data.concreteLanguages.concat(a)),void this.sandbox.emit("sulu.labels.success.show","labels.success.copy-locale-desc","labels.success"))}.bind(this))}.bind(this)}},g.hasPermission(this.data,"edit")&&(d.copy={options:{title:this.translations.copy,callback:this.copy.bind(this)}}),this.sandbox.util.isEmpty(d)||(a.edit={options:{dropdownItems:d}}),a.statePublished={},a.stateTest={},{tabs:{url:"/admin/content-navigations?alias=article&id="+this.options.id+"&locale="+this.options.locale,options:{data:function(){return this.sandbox.util.deepCopy(this.data)}.bind(this),url:function(){return this.templates.url({id:this.options.id,locale:this.options.locale})}.bind(this),config:this.options.config,preview:this.preview},componentOptions:{values:b.defaults(this.data,{type:null})}},toolbar:{buttons:a,languageChanger:{data:this.options.config.languageChanger,preSelected:this.options.locale}}}},initialize:function(){this.bindCustomEvents(),this.showDraftLabel(),this.setHeaderBar(!0),this.loadLocalizations(),this.options.language=this.options.locale},bindCustomEvents:function(){this.sandbox.on("sulu.header.back",this.toList.bind(this)),this.sandbox.on("sulu.tab.dirty",this.setHeaderBar.bind(this)),this.sandbox.on("sulu.toolbar.save",this.save.bind(this)),this.sandbox.on("sulu.tab.data-changed",this.setData.bind(this)),this.sandbox.on("sulu.article.error",this.handleError.bind(this)),this.sandbox.on("husky.tabs.header.item.select",this.tabChanged.bind(this)),this.sandbox.on("sulu.header.language-changed",this.languageChanged.bind(this))},languageChanged:function(a){a.id!==this.options.locale&&(this.sandbox.sulu.saveUserSetting(this.options.config.settingsKey,a.id),-1===b(this.data.concreteLanguages).indexOf(a.id)?i.openGhost.call(this,this.data,this.translations.openGhostOverlay).then(function(b,c){b?h.copyLocale.call(this,this.data.id,c,[a.id],function(){this.toEdit(a.id)}.bind(this)):this.toEdit(a.id)}.bind(this)).fail(function(){this.sandbox.emit("sulu.header.change-language",this.options.language)}.bind(this)):this.toEdit(a.id))},tabChanged:function(a){this.options.content=a.id},handleError:function(a,b,c){switch(a){default:this.sandbox.emit("sulu.labels.error.show","labels.error.content-save-desc","labels.error"),this.sandbox.emit("sulu.header.toolbar.item.enable","save")}},deleteArticle:function(){this.sandbox.sulu.showDeleteDialog(function(a){a&&d.remove(this.options.id,this.options.locale).then(function(){this.toList()}.bind(this))}.bind(this))},toEdit:function(a,b){this.sandbox.emit("sulu.router.navigate","articles/"+(a||this.options.locale)+"/edit:"+(b||this.options.id)+"/"+(this.options.content||"details"),!0,!0)},toList:function(){1===this.options.config.typeNames.length?this.sandbox.emit("sulu.router.navigate","articles/"+this.options.locale):this.sandbox.emit("sulu.router.navigate","articles:"+(this.options.type||this.data.articleType)+"/"+this.options.locale)},toAdd:function(){1===this.options.config.typeNames.length?this.sandbox.emit("sulu.router.navigate","articles/"+this.options.locale+"/add",!0,!0):this.sandbox.emit("sulu.router.navigate","articles/"+this.options.locale+"/add:"+(this.options.type||this.data.articleType),!0,!0)},save:function(a){this.loadingSave(),this.saveTab(a).then(function(b){this.saved(b.id,b,a)}.bind(this))},setData:function(a){this.data=a},saveTab:function(c){var d=a.Deferred();return this.sandbox.emit("sulu.header.toolbar.item.loading","save"),this.sandbox.once("sulu.tab.saved",function(a,c){d.resolve(b.defaults(c,{type:null}))}.bind(this)),this.sandbox.emit("sulu.tab.save",c),d},setHeaderBar:function(a){var b=!a,c=!a,d=!!a&&!this.data.publishedState;this.setSaveToolbarItems.call(this,"saveDraft",b),this.setSaveToolbarItems.call(this,"savePublish",c),this.setSaveToolbarItems.call(this,"publish",d),this.setSaveToolbarItems.call(this,"unpublish",!!this.data.published),b||c||d?this.sandbox.emit("sulu.header.toolbar.item.enable","save",!1):this.sandbox.emit("sulu.header.toolbar.item.disable","save",!1),this.showState(!!this.data.published)},setSaveToolbarItems:function(a,b){this.sandbox.emit("sulu.header.toolbar.item."+(b?"enable":"disable"),a,!1)},loadingSave:function(){this.sandbox.emit("sulu.header.toolbar.item.loading","save")},afterSaveAction:function(a,b){"back"===a?this.toList():"new"===a?this.toAdd():b&&this.toEdit(this.options.locale,this.data.id)},showDraftLabel:function(){this.sandbox.emit("sulu.header.tabs.label.hide"),this.hasDraft(this.data)||e.find(this.data.changer).then(function(a){this.sandbox.emit("sulu.header.tabs.label.show",this.sandbox.util.sprintf(this.translations.draftLabel,{changed:this.sandbox.date.format(this.data.changed,!0),user:a.username}),[{id:"delete-draft",title:this.translations.removeDraft,skin:"critical",onClick:this.deleteDraft.bind(this)}])}.bind(this))},deleteDraft:function(){this.sandbox.sulu.showDeleteDialog(function(a){a&&(this.sandbox.emit("husky.label.header.loading"),d.removeDraft(this.data.id,this.options.locale).always(function(){this.sandbox.emit("sulu.header.toolbar.item.enable","edit")}.bind(this)).then(function(a){this.sandbox.emit("sulu.router.navigate",this.sandbox.mvc.history.fragment,!0,!0),this.saved(a.id,a)}.bind(this)).fail(function(){this.sandbox.emit("husky.label.header.reset"),this.sandbox.emit("sulu.labels.error.show","labels.error.remove-draft-desc","labels.error")}.bind(this)))}.bind(this),this.translations.deleteDraftConfirmTitle,this.translations.deleteDraftConfirmText)},hasDraft:function(a){return!a.id||!!a.publishedState||!a.published},getUrl:function(a){var c=b.template(this.defaults.templates.url,{id:this.options.id,locale:this.options.locale});return a&&(c+="&action="+a),c},loadComponentData:function(){var b=a.Deferred();return this.options.id?(this.sandbox.util.load(this.getUrl()).done(function(a){this.preview=f.initialize({}),this.preview.start("Sulu\\Bundle\\ArticleBundle\\Document\\ArticleDocument",this.options.id,this.options.locale,a),b.resolve(a)}.bind(this)),b):(b.resolve({}),b)},destroy:function(){this.preview&&f.destroy(this.preview)},showState:function(a){a?(this.sandbox.emit("sulu.header.toolbar.item.hide","stateTest"),this.sandbox.emit("sulu.header.toolbar.item.show","statePublished")):(this.sandbox.emit("sulu.header.toolbar.item.hide","statePublished"),this.sandbox.emit("sulu.header.toolbar.item.show","stateTest"))},unpublish:function(){this.sandbox.sulu.showConfirmationDialog({callback:function(a){a&&(this.sandbox.emit("sulu.header.toolbar.item.loading","edit"),d.unpublish(this.data.id,this.options.locale).always(function(){this.sandbox.emit("sulu.header.toolbar.item.enable","edit")}.bind(this)).then(function(a){this.sandbox.emit("sulu.labels.success.show","labels.success.content-unpublish-desc","labels.success"),this.saved(a.id,a)}.bind(this)).fail(function(){this.sandbox.emit("sulu.labels.error.show","labels.error.content-unpublish-desc","labels.error")}.bind(this)))}.bind(this),title:this.translations.unpublishConfirmTitle,description:this.hasDraft(this.data)?this.translations.unpublishConfirmTextNoDraft:this.translations.unpublishConfirmTextWithDraft})},copy:function(){d.copy(this.data.id,this.options.locale).done(function(a){this.toEdit(this.options.locale,a.id)}.bind(this))},saved:function(a,b,c){this.setData(b),this.options.id?(this.setHeaderBar(!0),this.showDraftLabel(),this.sandbox.emit("sulu.header.saved",b),this.sandbox.emit("sulu.labels.success.show","labels.success.content-save-desc","labels.success")):this.sandbox.sulu.viewStates.justSaved=!0,this.afterSaveAction(c,!this.options.id)},loadLocalizations:function(){this.sandbox.util.load("/admin/api/localizations").then(function(a){this.localizations=a._embedded.localizations.map(function(a){return{id:a.localization,title:a.localization}})}.bind(this))},getCopyLocaleUrl:function(a,b,c){return d.getCopyLocaleUrl(a,b,c)}}}); \ No newline at end of file diff --git a/Resources/public/dist/services/manager.js b/Resources/public/dist/services/manager.js index 87d3fd2d7..05617b9a5 100644 --- a/Resources/public/dist/services/manager.js +++ b/Resources/public/dist/services/manager.js @@ -1 +1 @@ -define(["jquery","services/husky/util"],function(a,b){"use strict";var c={url:_.template('/admin/api/articles<% if (typeof id !== "undefined") { %>/<%= id %><% } %><% if (typeof postfix !== "undefined") { %>/<%= postfix %><% } %><% if (typeof version !== "undefined") { %>/<%= version %><% } %>?locale=<%= locale %><% if (typeof action !== "undefined") { %>&action=<%= action %><% } %><% if (typeof ids !== "undefined") { %>&ids=<%= ids.join(",") %><% } %>')};return{url:c.url,load:function(a,d){return b.load(c.url({id:a,locale:d}))},save:function(a,d,e,f){return b.save(c.url({id:d,locale:e,action:f}),d?"PUT":"POST",a)},remove:function(a,d){return"string"==typeof a?b.save(c.url({id:a,locale:d}),"DELETE"):b.save(c.url({ids:a,locale:d}),"DELETE")},unpublish:function(a,d){return b.save(c.url({id:a,locale:d,action:"unpublish"}),"POST")},removeDraft:function(a,d){return b.save(c.url({id:a,locale:d,action:"remove-draft"}),"POST")},restoreVersion:function(a,d,e){return b.save(c.url({id:a,postfix:"versions",locale:e,version:d,action:"restore"}),"POST")},getCopyLocaleUrl:function(a,b,d){return[c.url({id:a,locale:b,action:"copy-locale"}),"&dest=",d].join("")},getVersionsUrl:function(a,b){return c.url({id:a,postfix:"versions",locale:b})}}}); \ No newline at end of file +define(["jquery","services/husky/util"],function(a,b){"use strict";var c={url:_.template('/admin/api/articles<% if (typeof id !== "undefined") { %>/<%= id %><% } %><% if (typeof postfix !== "undefined") { %>/<%= postfix %><% } %><% if (typeof version !== "undefined") { %>/<%= version %><% } %>?locale=<%= locale %><% if (typeof action !== "undefined") { %>&action=<%= action %><% } %><% if (typeof ids !== "undefined") { %>&ids=<%= ids.join(",") %><% } %>')};return{url:c.url,load:function(a,d){return b.load(c.url({id:a,locale:d}))},save:function(a,d,e,f){return b.save(c.url({id:d,locale:e,action:f}),d?"PUT":"POST",a)},remove:function(a,d){return"string"==typeof a?b.save(c.url({id:a,locale:d}),"DELETE"):b.save(c.url({ids:a,locale:d}),"DELETE")},unpublish:function(a,d){return b.save(c.url({id:a,locale:d,action:"unpublish"}),"POST")},removeDraft:function(a,d){return b.save(c.url({id:a,locale:d,action:"remove-draft"}),"POST")},restoreVersion:function(a,d,e){return b.save(c.url({id:a,postfix:"versions",locale:e,version:d,action:"restore"}),"POST")},copy:function(a,d){return b.save(c.url({id:a,locale:d,action:"copy"}),"POST")},getCopyLocaleUrl:function(a,b,d){return[c.url({id:a,locale:b,action:"copy-locale"}),"&dest=",d].join("")},getVersionsUrl:function(a,b){return c.url({id:a,postfix:"versions",locale:b})}}}); \ No newline at end of file diff --git a/Resources/public/js/components/articles/edit/main.js b/Resources/public/js/components/articles/edit/main.js index c3390c21a..156c6303f 100644 --- a/Resources/public/js/components/articles/edit/main.js +++ b/Resources/public/js/components/articles/edit/main.js @@ -42,6 +42,7 @@ define([ unpublishConfirmTitle: 'sulu-content.unpublish-confirm-title', deleteDraftConfirmTitle: 'sulu-content.delete-draft-confirm-title', deleteDraftConfirmText: 'sulu-content.delete-draft-confirm-text', + copy: 'sulu_article.edit.copy', openGhostOverlay: { info: 'sulu_article.settings.open-ghost-overlay.info', new: 'sulu_article.settings.open-ghost-overlay.new', @@ -161,6 +162,16 @@ define([ } }; + if (SecurityChecker.hasPermission(this.data, 'edit')) { + editDropdown.copy = { + options: { + title: this.translations.copy, + callback: this.copy.bind(this) + } + }; + } + + if (!this.sandbox.util.isEmpty(editDropdown)) { buttons.edit = { options: { @@ -534,6 +545,12 @@ define([ }); }, + copy: function() { + ArticleManager.copy(this.data.id, this.options.locale).done(function(data) { + this.toEdit(this.options.locale, data.id); + }.bind(this)); + }, + saved: function(id, data, action) { this.setData(data); diff --git a/Resources/public/js/services/manager.js b/Resources/public/js/services/manager.js index 029edb636..cfa69d318 100644 --- a/Resources/public/js/services/manager.js +++ b/Resources/public/js/services/manager.js @@ -101,6 +101,16 @@ define(['jquery', 'services/husky/util'], function($, Util) { ); }, + /** + * Copy given article. + * + * @param {String} id + * @param {String} locale + */ + copy: function(id, locale) { + return Util.save(templates.url({id: id, locale: locale, action: 'copy'}), 'POST'); + }, + /** * Returns copy article from a given locale to a array of other locales url. * diff --git a/Resources/translations/sulu/backend.de.xlf b/Resources/translations/sulu/backend.de.xlf index 9cdb1ba72..5fa6691a4 100644 --- a/Resources/translations/sulu/backend.de.xlf +++ b/Resources/translations/sulu/backend.de.xlf @@ -134,6 +134,10 @@ sulu_article.contact-selection-overlay.title Artikel nach Kontakt filtern + + sulu_article.edit.copy + Artikel kopieren + diff --git a/Resources/translations/sulu/backend.en.xlf b/Resources/translations/sulu/backend.en.xlf index 14a4a5004..aa79dc426 100644 --- a/Resources/translations/sulu/backend.en.xlf +++ b/Resources/translations/sulu/backend.en.xlf @@ -134,6 +134,10 @@ sulu_article.contact-selection-overlay.title Filter article by contact + + sulu_article.edit.copy + Copy Article + diff --git a/Tests/Unit/Document/Subscriber/RoutableSubscriberTest.php b/Tests/Unit/Document/Subscriber/RoutableSubscriberTest.php index c16777b76..ff078ef71 100644 --- a/Tests/Unit/Document/Subscriber/RoutableSubscriberTest.php +++ b/Tests/Unit/Document/Subscriber/RoutableSubscriberTest.php @@ -15,9 +15,13 @@ use PHPCR\NodeInterface; use Sulu\Bundle\ArticleBundle\Document\Behavior\RoutableBehavior; use Sulu\Bundle\ArticleBundle\Document\Subscriber\RoutableSubscriber; +use Sulu\Bundle\DocumentManagerBundle\Bridge\DocumentInspector; +use Sulu\Bundle\DocumentManagerBundle\Bridge\PropertyEncoder; use Sulu\Bundle\RouteBundle\Entity\RouteRepositoryInterface; use Sulu\Bundle\RouteBundle\Manager\RouteManagerInterface; use Sulu\Bundle\RouteBundle\Model\RouteInterface; +use Sulu\Component\DocumentManager\DocumentManagerInterface; +use Sulu\Component\DocumentManager\Event\CopyEvent; use Sulu\Component\DocumentManager\Event\HydrateEvent; use Sulu\Component\DocumentManager\Event\PersistEvent; use Sulu\Component\DocumentManager\Event\RemoveEvent; @@ -39,6 +43,21 @@ class RoutableSubscriberTest extends \PHPUnit_Framework_TestCase */ private $entityManager; + /** + * @var DocumentManagerInterface + */ + private $documentManager; + + /** + * @var DocumentInspector + */ + private $documentInspector; + + /** + * @var PropertyEncoder + */ + private $propertyEncoder; + /** * @var RoutableSubscriber */ @@ -54,13 +73,19 @@ protected function setUp() $this->routeManager = $this->prophesize(RouteManagerInterface::class); $this->routeRepository = $this->prophesize(RouteRepositoryInterface::class); $this->entityManager = $this->prophesize(EntityManagerInterface::class); + $this->documentManager = $this->prophesize(DocumentManagerInterface::class); + $this->documentInspector = $this->prophesize(DocumentInspector::class); + $this->propertyEncoder = $this->prophesize(PropertyEncoder::class); $this->document = $this->prophesize(RoutableBehavior::class); $this->routableSubscriber = new RoutableSubscriber( $this->routeManager->reveal(), $this->routeRepository->reveal(), - $this->entityManager->reveal() + $this->entityManager->reveal(), + $this->documentManager->reveal(), + $this->documentInspector->reveal(), + $this->propertyEncoder->reveal() ); } @@ -148,4 +173,35 @@ public function testHandleRemove() $this->routableSubscriber->handleRemove($event->reveal()); } + + public function testCopy() + { + $event = $this->prophesize(CopyEvent::class); + $event->getDocument()->willReturn($this->document->reveal()); + $event->getCopiedPath()->willReturn('/cmf/articles/2017/04/test-article'); + + $this->documentInspector->getLocales($this->document->reveal())->willReturn(['de']); + + $this->documentManager->find('/cmf/articles/2017/04/test-article', 'de')->willReturn($this->document->reveal()); + $this->document->removeRoute()->shouldBeCalled(); + + $route = $this->prophesize(RouteInterface::class); + $route->getPath()->willReturn('/test'); + $this->routeManager->create($this->document->reveal())->willReturn($route->reveal()); + + $this->document->setRoutePath('/test')->shouldBeCalled(); + + $this->entityManager->persist($route->reveal())->shouldBeCalled(); + $this->entityManager->flush()->shouldBeCalled(); + + $node = $this->prophesize(NodeInterface::class); + $event->getCopiedNode()->willReturn($node->reveal()); + + $this->propertyEncoder->localizedSystemName(RoutableSubscriber::ROUTE_FIELD, 'de') + ->willReturn('i18n:de-routePath'); + + $node->setProperty('i18n:de-routePath', '/test'); + + $this->routableSubscriber->handleCopy($event->reveal()); + } }