Jump to content
gebeer

Custom Page Actions

Recommended Posts

As of 2.6.5dev there are these neat extra action buttons 'unpub', 'hide' etc in the page tree.

post-1920-0-84696500-1435224026_thumb.pn

In ListerPro we can define our own actions as modules that extend the new PageAction class.

Ryan mentions in the ListerPro forum that the page actions are a new feature of PW and that they are independent from ListerPro.

It would be great if we could define our own page actions and attach them to the page tree or to lister results.

Looking at the code I am not quite sure how we can achieve that through a hook or the like.

So for all not so code savvy PW "hookers" (pun intended) it would be great to get some documentation on this new feature.  

  • Like 1

Share this post


Link to post
Share on other sites

Not tested, but should be a sufficient blueprint.

<?php

/**
 * Add own PageList actions
 * 
 * ProcessWire 2.x 
 * Copyright (C) 2014 by Ryan Cramer 
 * Licensed under GNU/GPL v2, see LICENSE.TXT
 * 
 * http://processwire.com
 *
 */

class AddPageActionsToPageList extends WireData implements Module {

    public static function getModuleInfo() {

        return array(
            'title' => 'Page Actions to Page List', 
            'version' => 1, 
            'summary' => 'Adds PageActions to Page List actions',
            'singular' => true, // Limit the module to a single instance
            'autoload' => true, // Load the module with every call to ProcessWire 
            );
    }

    public function init() {
        $this->addHookAfter('ProcessPageListActions::getExtraActions', $this, 'addActions'); 
        $this->addHookBefore('ProcessPageListActions::processAction', $this, 'exeActions');
    }

    /**
     * Add the new actions
     */
    public function addActions($event) {
        $page = $event->arguments[0]; 
        $actions = $event->return;
        // We do not overwrite existing core actions
        $actions = array_merge($this->createActions($page), $actions);

        // $event->return = $actions // Shouldn't be needed
    }

    /**
     * Create the actions
     */
    protected function createActions($page){
        $actions = array();

        if($page->id == 1 || $page->template == 'admin') return $actions;
        if($page->template->noSettings || !$page->editable('status', false)) return $actions;
        $adminUrl = $this->wire('config')->urls->admin . 'page/';

        if($this->wire('user')->hasPermission('action-email', $page) && !$page->isTrash()) {
            $actions['action-email'] = array(
                'cn'    => 'Email',
                'name'  => 'Email',
                'url'   => "$adminUrl?action=PageActionEmail&id=$page->id",
                'ajax' => true, 
            );
        }

        return $actions;
    }

    /**
     * This is run when an action is initiated
     * 
     * This can only been called if a page is editable.
     */
    public function exeActions($event) {
        list($page, $action) = $event->arguments;

        $actions = $this->createActions($page);

        // This way checking for roles or other access rules is not duplicated.
        // If the action is still created, it's also actionable.
        if(!isset($actions[$action]) || !$page->editable()) return;

        $success = false;
        $needSave = true; 
        $message = '';
        $remove = false;
        $refreshChildren = 0;

        // If $action is the name of the module
        if(strpos($action, "PageAction") === 0){
            $module = $this->modules->get($action);
            if($module){
                $module->action($page);
                $success = true;
                $message = $this->_("Email sent");
            }
        }

        // If no module was supplied select manually
        if(!$success){
            switch($action){
                case "SomeOtherAction"
                    // Do stuff
                    break;
            }
        }

        // Return if success, otherwise move on to the hook function
        if(!$success) return;
        else $event->replace = true; 

        // Return information
        $event->return = array(
            'action' => $action,
            'success' => true, // Fails are managed later by hooked function
            'message' => $message,
            'updateItem' => $page->id, // id of page to update in output
            'remove' => $remove, 
            'refreshChildren' => $refreshChildren,
            // also available: 'appendItem' => $page->id, which adds a new item below the existing
        );
    }

}
  • Like 4

Share this post


Link to post
Share on other sites

Hi @LostKobrakai,

I'm trying to get a custom action to work, based on this code, but I'm having some difficulties... The action label appears in the page list tree, with the right link and action, but when I click it, instead of executing the action, it just displays the single page in tree view. It seems that the action is getting ignored, some how. In the Debug Tools, it seems that the right function is being called:

$this->addHookBefore('ProcessPageListActions::processAction', $this, 'executeActions');

But nothing happens. Any ideas?

Thanks.

Share this post


Link to post
Share on other sites

Nope. Maybe there was some change with the new persistently open page-tree and I haven't yet used one of those with it.

Share this post


Link to post
Share on other sites

I couldn't get @LostKobrakai's blueprint to work.

I putted together some of @LostKobrakai code with some investigated source code from ProcessWire.

Here's a full working solution:

<?php

class PayActions extends Process {

	public static function getModuleInfo() {
		return array(
			'title' => __('Quick Pay/Unpay Method', __FILE__),
			'summary' => __('Adds Pay/Unpay actions to pages that use reservation template.', __FILE__),
			'version' => 102,
			'singular' => true,
			'autoload' => 'template=admin',
		);
	}

	// MODULE INITIALIZATION
	public function ready() {
		// Default actions can't use AJAX
		// $this->addHookAfter("ProcessPageListActions::getActions", $this, 'hookPageListActions');
		// Use Extra Actions if you need AJAX
		$this->addHookAfter('ProcessPageListActions::getExtraActions', $this, 'hookPageListActions');
		$this->addHookAfter('ProcessPageListActions::processAction', $this, 'hookProcessExtraAction');
	}

	// ADD ACTIONS
	public function hookPageListActions(HookEvent $event) {

		$page = $event->arguments[0];
		$actions = array();

		// Apply only to reservation pages
		if($page->template == 'reservation') {

			if($page->reservation_paid) {
				$actions['unpay'] = array(
					'cn' => 'Unpay',
					'name' => 'Unpay',
					'url' => $this->config->urls->admin . "page/?action=unpay&id={$page->id}",
					'ajax' => true,
				);
			}
			else {
				$actions['pay'] = array(
					'cn' => 'Pay',
					'name' => 'Pay',
					'url' => $this->config->urls->admin . "page/?action=pay&id={$page->id}",
					'ajax' => true,
				);
			}

		}

		if(count($actions)) $event->return = $actions + $event->return;

	}

	// PREPARE ACTION
	public function hookProcessExtraAction(HookEvent $event) {

		$page = $event->arguments(0);
		$action = $event->arguments(1);
		$success = false;

		switch($action) {
			case 'pay':
				$page->setAndSave('reservation_paid', 1);
				$success = true;
				$message = $this->_('Paid');
				break;
			case 'unpay':
				$page->setAndSave('reservation_paid', 0);
				$success = true;
				$message = $this->_('Unpaid');
				break;
			default:
				$message = $this->_('Failed');
		}

		// If this action was successfull return result
		if($success) {

			$result = array(
				'action' => $action,
				'success' => $success,
				'message' => $message,
				'page' => $page->id,
				'updateItem' => $page->id,
				'remove'          => false,
				'refreshChildren' => false,
			);

			$event->return = $result;

		}

	}

}

What this module does:

For each page with the template "reservation" you're able to mark it as "Payed" or "Unpayed" without having to edit it:

print.jpg

 

Edited by Xonox
Clearer result.
  • Like 4

Share this post


Link to post
Share on other sites

You cannot return things from hooks via the return statement. Instead pass the return value into $event->return like I did it in my example.

  • Like 2

Share this post


Link to post
Share on other sites
15 hours ago, LostKobrakai said:

You cannot return things from hooks via the return statement. Instead pass the return value into $event->return like I did it in my example.

With return via $event->return, the module stops working. The used return is inside the Ajax process, not the hook. If we change the return, the module doesn't get an ajax response and doesn't update the page (keeps the ajax spinner forever). However, I'm not that knowledgeable in ProcessWire. Does this make sense?

  • Like 1

Share this post


Link to post
Share on other sites

Yeah sure, you're right. I usually do not call class methods from within the hooks executed function, which made me miss that.

  • Like 1

Share this post


Link to post
Share on other sites

After some testing I came to the conclusion that we don't need the AJAX code. In fact it makes the code incompatible with the other extra actions. At the end of the day, the code becomes even simpler! :) It's been edited to show the final, simpler version. 

  • Like 1

Share this post


Link to post
Share on other sites
57 minutes ago, Xonox said:

After some testing I came to the conclusion that we don't need the AJAX code. In fact it makes the code incompatible with the other extra actions. At the end of the day, the code becomes even simpler! :) It's been edited to show the final, simpler version. 

Cool, thanks for sharing!

Could you please also edit the first sentence of the post above the code? Maybe by crossing it out, and explaining the situation right after it. Leaving it like this is misleading when skimming through the posts and not reading every single character.

  • Like 1

Share this post


Link to post
Share on other sites

@Xonox code works here but I always got these notices (using TracyDebugger):

1×	
PHP Notice: Undefined variable: message in .../Process/ProcessPageList/ProcessPageListActions.php:279
1×	
PHP Notice: Undefined variable: remove in .../Process/ProcessPageList/ProcessPageListActions.php:281
1×	
PHP Notice: Undefined variable: refreshChildren in .../Process/ProcessPageList/ProcessPageListActions.php:282

I couldn't get rid of these.  Here is my code:

Spoiler

            $this->addHookAfter("ProcessPageListActions::getExtraActions", function (HookEvent $event) {

                $page = $event->arguments("page");

                if(!$page->trashable() || !$this->user->isSuperUser()) {
                    return false;
                }
                
                $actions = array();
                $adminUrl = $this->wire('config')->urls->admin . 'page/';
                $deleteIcon = "<i class='fa fa-trash'></i>&nbsp;";

                $actions['delete'] = array(
                    'cn'   => 'Delete aos',
                    'name' => $deleteIcon . 'Wipe',
                    'url'  => $adminUrl . '?action=wipe&id='  .$page->id,
                    'ajax' => true
                );

                $event->return += $actions;
            });

            $this->addHookAfter('ProcessPageListActions::processAction', function(HookEvent $event) {
                
                $page = $event->arguments(0);
                $action = $event->arguments(1);

                if($action == 'wipe') {

                    $page->setAndSave('title', $page->title . '-hello');

                    $event->return = array(
                        'action' => $action,
                        'success' => true,
                        'page' => $page->id,
                        'updateItem' => $page->id,
                        'message' => 'Wiped!',
                        'remove'          => false,
                        'refreshChildren' => false
                    );
                }
            });

 

Share this post


Link to post
Share on other sites
9 hours ago, tpr said:

I couldn't get rid of these.

The name (array key) you set in your getExtraActions hook needs to match what is passed as $action in the processAction hook. You're using "delete" in one place and "wipe" in the other.

  • Like 1

Share this post


Link to post
Share on other sites

Oh great, thanks! I've spent a lot of time to figure this out but haven't spot that. What a relief! :)

Apparently everything worked fine but there were these PHP notices that showed something's not right.

  • Like 1

Share this post


Link to post
Share on other sites

Here is the above code (with @BitPoet's correction) in action:

 

  • Like 1

Share this post


Link to post
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now

  • Recently Browsing   0 members

    No registered users viewing this page.

  • Similar Content

    • By Robin S
      A new module that hasn't had a lot of testing yet. Please do your own testing before deploying on any production website.
      Custom Paths
      Allows any page to have a custom path/URL.
      Screenshot

      Usage
      The module creates a field named custom_path on install. Add the custom_path field to the template of any page you want to set a custom path for. Whatever path is entered into this field determines the path and URL of the page ($page->path and $page->url). Page numbers and URL segments are supported if these are enabled for the template, and previous custom paths are managed by PagePathHistory if that module is installed.
      The custom_path field appears on the Settings tab in Page Edit by default but there is an option in the module configuration to disable this if you want to position the field among the other template fields.
      If the custom_path field is populated for a page it should be a path that is relative to the site root and that starts with a forward slash. The module prevents the same custom path being set for more than one page.
      The custom_path value takes precedence over any ProcessWire path. You can even override the Home page by setting a custom path of "/" for a page.
      It is highly recommended to set access controls on the custom_path field so that only privileged roles can edit it: superuser-only is recommended.
      It is up to the user to set and maintain suitable custom paths for any pages where the module is in use. Make sure your custom paths are compatible with ProcessWire's $config and .htaccess settings, and if you are basing the custom path on the names of parent pages you will probably want to have a strategy for updating custom paths if parent pages are renamed or moved.
      Example hooks to Pages::saveReady
      You might want to use a Pages::saveReady hook to automatically set the custom path for some pages. Below are a couple of examples.
      1. In this example the start of the custom path is fixed but the end of the path will update dynamically according to the name of the page:
      $pages->addHookAfter('saveReady', function(HookEvent $event) { $page = $event->arguments(0); if($page->template == 'my_template') { $page->custom_path = "/some-custom/path-segments/$page->name/"; } }); 2. The Custom Paths module adds a new Page::realPath method/property that can be used to get the "real" ProcessWire path to a page that might have a custom path set. In this example the custom path for news items is derived from the real ProcessWire path but a parent named "news-items" is removed:
      $pages->addHookAfter('saveReady', function(HookEvent $event) { $page = $event->arguments(0); if($page->template == 'news_item') { $page->custom_path = str_replace('/news-items/', '/', $page->realPath); } }); Caveats
      The custom paths will be used automatically for links created in CKEditor fields, but if you have the "link abstraction" option enabled for CKEditor fields (Details > Markup/HTML (Content Type) > HTML Options) then you will see notices from MarkupQA warning you that it is unable to resolve the links.
      Installation
      Install the Custom Paths module.
      Uninstallation
      The custom_path field is not automatically deleted when the module is uninstalled. You can delete it manually if the field is no longer needed.
       
      https://github.com/Toutouwai/CustomPaths
      https://modules.processwire.com/modules/custom-paths/
    • By sambadave
      Hi everyone, here's the problem I'm trying to solve.
      I have a config area in my PW admin that is locked down for admin use. I use pages to store a bunch of settings that I'll use for my clients website. It's mostly used for visual things like colours and theming. This list could be tiny or large, depending on the sites requirements, but its great because I can store any information I want to. So the page tree could look something like this:
      ADMIN SETUP
      Home Config Aesthetics Colours Red Field: Custom Label - "Red" Field: Class name - "theme--red' Green Field: Custom Label - "Green" Field: Class name - "theme--green' Blue Field: Custom Label - "Blue" Field: Class name - "theme--blue' etc... Sizes Small Field: Custom Label - "Small" Field: Class name - "sm' Medium Field: Custom Label - "Medium" Field: Class name - "md Large Field: Custom Label - "Medium" Field: Class name - "lg" etc... Icons Target Field: Custom Label - "Hands shaking Icon" Field: Icon SVG - "[svg code]' Target Field: Custom Label - "Target icon" Field: Icon SVG - "[svg code]' Success Field: Custom Label - "Happy face icon" Field: Icon SVG - "[svg code]' etc... HOW I USE THIS
      I'm then able to set up page reference fields for colour, size and icons. I'll use these fields on particular pages so that my clients can select a particular colour, size or icon, or anything really. Currently, with the page reference field I can create a custom label for the options. So for something like colour I can label the field "Theme" and present a list of colours like "Red, "Green" and "Blue" using the custom page label label format of the colours page.
      This of course means that I can use this to do some lovely presentation on the front end of the site. With a colour selected I'll then be able to use the page reference to get the class name for that colour theme so that I can update the page's look and feel. So in my markup I'd end up with "theme--red" or "theme--green" etc.
      THE PROBLEM
      I have this all working which is great and it's really flexible for the client which they love. However, I'd love to be able to make things more visual for the client.
      Is there a way to be able to output more than just text in the page reference field? I might have a bunch of different blue colours, so instead of a list like so:
      Navy Blue Deep Blue Bright Blue Sea Blue ... it would be great to be able to output actual colour swatches, which is a lot mor visual for the client.
      Taking the icon selector, I would ideally like to show the actual svg that I've stored against that icon as a selectable image instead of seeing text options like:
      Hands shaking icon Target icon Happy face icon Just a couple of scenarios here, but as you can see there could be any number of reasons to display a more visual method of selection.
      ANY SOLUTIONS?
      I've looked at modules like FieldtypeColorPicker which could help in solving the colour issue, but it doesn't allow me to select a colour and then use a particular class name assigned to it the way I describe above.
      Considering the other use cases I mention above, does anyone know if anything exists already that would help me to create custom presentaion for page reference lists, or if there's anything planned?
      Thank you in advance for anyone who's read this far and has any words of wisdom!
      Dave
       
    • By gebeer
      Hello,
      I have a user page template with many fields that are organised in tabs. Tabs do not work on the profile edit screen. So I had to find a way how to let users edit their page with tabs in place.
      The way I solved this is having users edit their user page in the backend instead of their profile page. So basically they are on a page edit screen and not on their profile edit screen which is a different process. The drawback of this method is that the users edit their profile on a URL like .../youradminurl/access/users/edit/?id=1377. So I needed to make sure that users cannot edit other users' profiles by just switching out the id. I did this through hooks that redirect them to their own profile.
      This is far from a perfect solution. Ideally I would like to mask the page edit url to something like /myprofile but haven't found a way on how to do this, yet.
      How would you go about this?
      redirect rules in .htaccess hooking into the profile edit process? Either way I couldn't figure out how to accomplish it. Any pointers towards a solution would be very much appreciated.
    • By Thomas Diroll
      Hi guys I'm relatively new to PW and just finished developing a page for a client. I was able to include all necessary functionality using the core fieldtypes but now I it seems that I need to extend them with a custom one. What I need is a simple button, that copies the absolute url (frontend not PW-backend) of the page which is currently edited to the clipboard. As this feature is only needed inside a specific template, I tend to use a custom fieldtype which provides this feature. I've been looking inside the core modules code (eg. FieldtypeCheckbox.module) but I don't really get the structure of it and how its rendered to the admin page. I also didn't find a lot of tutorials covering custom fieldtypes.
      Maybe some of you could give me some tips on how to write a basic custom fieldtype that renders a button which copies the value of
      page->httpUrl() to the clipboard using JS. Thanks!
    • By matsn0w
      Hey all,
      I am working on a website and I want to style the login page, but I'm a bit confused. 
      I want either the existing login page styled in my own way using some CSS (I guess I prefer that) or I want to create a custom page with a form to login. (Which I could style too).
      I used the code from Ryan and Renobird posted here - which works great - but that doesn't replace the original login page. 
      Is there a way to some sort of 'disable' the original login?
      I hope my question is clear and thanks in advance,
      matsn0w
×
×
  • Create New...