Jump to content

Problems with image uploads when including ProcessEditPage form on another admin page


jordanlev
 Share

Recommended Posts

Hi,

I'm using this snippet from soma: https://gist.github.com/somatonic/5011926 to render and process a page's edit form on my own admin page (via a process module).

It all seems to work very well, except there are some problems with some of the more advanced input types, like the image field's drag-and-drop file upload does not work, and using the PageTable field kinda-sorta works mostly but has problems rendering the table sometimes or it adds a new page but doesn't "connect" it to the parent properly.

My guess is that these issues are related to missing javascript, but the css and js files for all of the inputfields are being outputted to the page. I've also made a call to wire('modules')->get('ProcessPageEdit')  just to give that module a chance to output its own css/js as well (and when I look at the page source I see that its css/js files are in fact there in the header). Note that I am in the admin section here... this is not for "Front-end editing" or anything like that.

Or perhaps there are some element classes or ids in the "ProcessPageEdit" display itself that the javascript expects to find? But how could this be the case since other people make admin themes so they can't assume that the overall page markup is going to be the same?

Anyone have any ideas on how to get this to work?

Thanks!

EDIT: Also the "image" button on the CKEditor toolbar doesn't work (it pops up a dialog with a PHP error in it about 'pages' being null)

Link to comment
Share on other sites

I'm slightly confused because soma's example does not use a Process-module to provide any functionality - it's meant to be used in the frontend of your application - and you mentioned Process-modules and that you are trying to do this on the default PW admin-site? Could you provide us some code so we could get a feeling on what you're trying to do? Also a short description on why you are trying to do this could be helpful as well :)

Edit: I'll already mention this afterall. A good way to implement your own page editing logic on the admin site would be creating a new Process-module, that just extends ProcessPageEdit and overrides the necessary methods. Then you would assign your Edit-controller (page) the template 'admin' and select your own ProcessPageEdit-module (you could even change the default Process-module for page-editing under /Admin/Pages/Edit page). Sometimes it's simpler to just hook into ProcessPageEdit though. But this all depends a lot on what you're trying to do.

Edited by sforsman
Link to comment
Share on other sites

Thanks for the response. I will give your suggestion to extend ProcessPageEdit a shot, although I will try to explain exactly what I'm wanting to do here.

The situation is that I have a somewhat complex data model. Products are being sold on specific dates in specific locations, and at each location there are different product variations (or "product options"... like different sizes, each with different prices). Kind of like a company that goes around to farmers markets or fairs and sells things, but depending on the season and what they have in stock, they might have certain products available one week but not the next, and also they might need to charge different prices at different weeks depending on how hard it was to get something. (Also also, they might have different product variations/options available from week to week, because they might run out of one option or size but then get more in stock a few weeks later, etc.).

It seems that the general consensus amongst the PW developer here is that I can use the built-in page tree as my editing interface for this data. It's true that I can do that, but it does not meet my standards for providing a "guided interface" to my clients who need to manage this data. Here are some examples of the things that I don't like about using the built-in pages interface for managing this data:

  1. There will be generic content pages on this site ("home", "about us", "terms & conditions", etc.), AND there will be pages that are populated by the product/location/dates-of-sale/variations/etc data. To the client, these are different things because the generic content pages are more free-form and might look very different from week to week, but the "data" stuff is much more structured and ties into their actual business (not just the marketing of the business). So I think it would be confusing to have to use the same "page tree" interface to manage both the pages of the site and the "data" of the site. Instead I'd prefer to have a separate section in the admin interface that is specific to the management of the "data", and then they just use the page tree for the generic content pages. This I am able to achieve very nicely via a ProcessModule (as I explained in my other recent thread here: https://processwire.com/talk/topic/8043-how-to-provide-admin-interface-for-editors/?p=77757 ).
  2. Using the built-in "page edit" admin pages burdens the client with too many options. The only tab I want them to see is "content", not "settings" or "delete".
  3. I don't want users to have to see the "page name" field, because these are pages that don't correspond to pages on the site front-end... so why should the user have to care about the url slug? I think there are ways around this one though with various settings.
  4. I can use the PageTable field to manage the "sub-records" (for example, the product variations that are applicable to a certain week-of-sale), but it does not give me the ability to enforce certain constraints. For example, in a particular week they will add the various locations that they are travelling to. The list of possible destinations exists as another template... so there is a "page" field that allows them to choose which location they are adding a sub-record for. But each time they add another sub-record (via the PageTable field), it shows them the entire list of cities again. This means they could possibly add 2 sub-records of the same city, which will cause problems. It just doesn't make sense in the data model that the same location would have 2 sub-records in a particular week.
  5. Another problem with the PageTable interface is it opens the sub-page in a modal dialog and it too has the tabs at the top. I *only* want the "content" tab to appear. And I can actually make this happen by going into the database for the template and adding "noSettings: 1" to the json data for the fields. But it still shows the "content" tab, even though it's the only tab. It takes up a lot of vertical real estate and is one of those things that isn't a huge deal but it's still less than perfect.
  6. In general, I think the "edit"/"move"/"view" buttons in the page tree are not very nice. Please don't hate me :) I totally understand that this is a matter of opinion, and most people seem to like them, so that's fine. But I think it would be better to have actual buttons next to each item in the list.

So really what it comes down to is I want to leverage the individual "field" interfaces (inputfields), but not be forced to use them in the built-in ProcessPageEdit page or the Page Tree. I was hoping that because ProcessWire is so modular and well-thought-out, that this would not be a difficult thing to achieve. But perhaps this is not the case, which would be a bummer. Because then it seems my options are to give up and deliver a not-very-polished editing interface to my client, or re-build all of PW's functionality so I have the control to make things just the way I want (and at that point, I might as well not even use a CMS and just build a from-scratch app... which I *really* was hoping to avoid).

Thanks for your time and assistance, it is greatly appreciated. I know I'm being very particular here, but the complete markup flexibility is what attracted me to PW in the first place, so I hope it's something the community can appreciate :)

Link to comment
Share on other sites

I just thought of a more succinct way to ask my question (for those who don't want to read through my long rambles):

Does anyone ever use the built-in "image" inputfield on pages other than ProcessPageEdit? What about the CKEditor? If so, how exactly do they make those work outside the confines of the specific markup on the ProcessPageEdit admin page? Do I need to surround them with specific elements/classes/id's? Or do I need to make sure certain JS files are included (other than the ones that the inputfield module outputs itself automatically)? Surely I'm not the first person who wants to allow image uploads outside the context of a normal page edit operation?

Link to comment
Share on other sites

Well, after a ton of debugging and stepping through code, I've begun to understand what the problems I was having are. Unfortunately these require very specific fixes to the way that specific inputfields work (there's not some global setting), so I would imagine that there are other field types I haven't gotten to yet that will cause problems as well. But for what it's worth, here is how to fix the issue with the image uploader widget and the CKEditor image button:

IMAGE FIELD (2 things required):

* The image uploader needs to make an ajax request to the ProcessPageEdit page (/processwire/page/edit), but instead of using the actual url to that page it retrieves the form action. Since I've changed the form action to post back to my own page, this was resulting in the ajax call not getting what I needed. To fix this, I added this at the very beginning of my "executeEditMyPage()" function:

if ($this->config->ajax) {
	return $this->modules->get('ProcessPageEdit')->execute();
}
* The image uploader needs to know what page it is working with, so add this somewhere on the page (it can live anywhere on the page, but I'm adding it as an "InputfieldMarkup" field to the form so everything is self-contained there):
$form->add($modules->get("InputfieldMarkup")->set('name', 'PageIDIndicator')->set('value', "<p id='PageIDIndicator'>{$page->id}</p>"));
(note that the image uploader only needs that p tag, but I am also setting a "name" on the inputfield so that the generated markup gets an id I can then hide via css, like so: #Inputfield_PageIDIndicator { display: none; })

CKEDITOR IMAGE BUTTON (1 thing required):

* The ckeditor image plugin needs to know what page it is working with, but it looks for a different element than the image uploader widget does. So add this to the form as well (this actually needs to be a form field):

$form->add($modules->get("InputfieldHidden")->set('name', 'id')->set('value', $page->id));
VOILA!

Wish me luck when it comes time to figure out Repeater and PageTable :)

Link to comment
Share on other sites

  • 3 weeks later...

Below is some full working code for a process module that manages pages of a specific template. To install it, create a new folder in your site's "site/modules" directory called "ProcessCustomPageAdmin", then inside that new directory create a file called "ProcessCustomPageAdmin.module" and paste the code below into that file. Then install from your site's admin back-end (you'll need to "refresh" the directory to get PW to recognize it, just like with all new modules you try to install manually).

A few things to note:

  • The "create" and "delete" actions respond to GET requests, which is possibly dangerous. The "create" action is probably ok because this is behind your admin backend so you don't have to worry about webcrawlers accidentally hitting the pages. But depending on how important the data in the pages is, you might want to make it respond to a POST request only and put up some kind of confirmation. I was trying to keep things as simple and focused on your question as possible so I didn't include any of that stuff.
  • There is no functionality to sort the pages. You need to output your own markup for the list view if you want to achieve this and then wire up your own "sort" action and set up JQueryUI sortable on the list display... way outside the scope of this forum topic here.
  • You'll want to output this snippet of CSS on that page to hide the id that gets outputted for the CKEditor to function. Create a "ProcessCustomPageAdmin.css" file in your module directory and PW will automatically load it for you.
    • ​#Inputfield_PageIDIndicator { display: none; }
  • Last but not least, you need to change the $template_name and $parent_page_selector variables in the module class to suit your needs.
Best of luck!
 
 
<?php

class ProcessCustomPageAdmin extends Process {

	public static function getModuleInfo() {
		return array(
			'title' => 'Custom Page Admin',
			'summary' => 'Dashboard interface for managing pages',
			'version' => '100',
			'author' => 'Your Name Here',
			'permission' => 'page-edit',
			'page' => array(
				'name' => 'custom-page-admin',
				'title' => 'Custom Page Admin',
			),
		);
	}
	
	private $template_name = 'your-template-name';
	private $parent_page_selector = 'template=parent-page-template'; //determines where new pages are added under (can be any page selector)
	
	public function execute() {
		$table = $this->modules->get('MarkupAdminDataTable');
		$table->setEncodeEntities(false);
		$table->setSortable(false);
		
		$pages = $this->pages->find("template={$this->template_name}, sort=sort");
		foreach ($pages as $page) {
			$table->row(array(
				htmlspecialchars($page->title),
				"[<a href=\"{$this->page->url}/edit?id={$page->id}\">Edit</a>]",
				"[<a href=\"{$this->page->url}/delete?id={$page->id}\">Delete</a>]",
			));
		}
		
		$table->action(array('Create New Page' => "{$this->page->url}/create"));

		return $table->render();
	}

	public function executeCreate() {
		$page = new Page();
		$page->template = $this->template_name;
		$page->parent = $this->pages->get($this->parent_page_selector);
		$page->title = 'Untitled Page';
		//note that we don't need to set a name because it is autogenerated (as per the parent template settings)
		$page->save();

		$this->session->redirect($this->page->url . '/edit?id=' . $page->id);
	}

	public function executeEdit() {
		if ($this->config->ajax) {
			return $this->modules->get('ProcessPageEdit')->execute(); //ajax-y form widgets (e.g. image uploader) need to be handled by the real ProcessPageEdit module (but for some reason they look to the form action for their ajax url instead of always going to /[admin]/page/edit)
		}
		
		$page = $this->getPageById($this->input->get->id);

		$action = "{$this->page->url}/edit?id={$page->id}";
		$form = $this->buildPageEditForm($action, $page);
		if ($this->processPageEditForm($form, $page)) {
			$this->setPageNameFromTitle($page);
			$this->session->message('Page saved.');
			$this->session->redirect($this->page->url);
		}

		Wire::setFuel('processHeadline', $page->title);

		return $form->render();
	}

	public function executeDelete() {
		$page = $this->getPageById($this->input->get->id);
		$page->delete();
		$this->session->message('Page deleted');
		$this->session->redirect($this->page->url);
	}

/*** HELPER FUNCTIONS ***/

	private function getPageById($id) {
		$id = (int)$id;
		if (empty($id)) {
			throw new Wire404Exception('Missing or invalid page id');
		}

		$page = $this->pages->get($id);
		if (!$page || ($page->template != $this->template_name)) {
			throw new Wire404Exception('Unknown page (it may have been recently deleted or had its template changed)');
		}

		return $page;
	}

	//pass in empty string for cancel_button_url to not include one
	private function buildPageEditForm($form_action, $page, $include_save_button = true, $cancel_button_url = '') {
	//code inspired by: https://gist.github.com/somatonic/5011926

		$modules = wire('modules');

		$form = $modules->get("InputfieldForm");
		$form->method = 'post';
		$form->action = $form_action;

		foreach($page->getInputfields() as $input_field) {
			$form->add($input_field);
		}

		if ($include_save_button) {
			$form->add($modules->get("InputfieldSubmit")
				->set('name', 'submit')
				->set('value', 'Save')
			);
		}

		if (!empty($cancel_button_url)) {
			$form->add($modules->get("InputfieldButton")
				->set('name', 'cancel')
				->set('value', 'Cancel')
				->set('href', $cancel_button_url)
				->set('icon', 'times-circle')
				->set('class', 'ui-button ui-widget ui-corner-all ui-priority-secondary') //gives it a faded look
			);
		}

		//IMAGE UPLOADER WIDGET NEEDS THIS TO FUNCTION PROPERLY (note that the "name" is only so we can hide this via CSS... but the only thing the image uploader needs is the p tag):
		$form->add($modules->get("InputfieldMarkup")->set('name', 'PageIDIndicator')->set('value', "<p id='PageIDIndicator'>{$page->id}</p>"));

		//CKEDITOR IMAGE BUTTON NEEDS THIS TO FUNCTION PROPERLY:
		$form->add($modules->get("InputfieldHidden")->set('name', 'id')->set('value', $page->id));

		return $form;
	}

	private function processPageEditForm(&$form, $page) {
	//code inspired by: https://gist.github.com/somatonic/5011926

		$input = wire('input');

		if (!$input->post->submit) {
			return false;
		}

		$form->processInput($input->post);

		if ($form->getErrors()) { //pass true to clear the errors
			return false;
		}

		$page->setOutputFormatting(false);
		foreach($form as $field) {
			$page->set($field->name, $field->value);
		}
		$page->save();

		return true;
	}

	private function setPageNameFromTitle($page, $append_number = null) {
		$name = wire('sanitizer')->pageName($page->title);
		if (!is_null($append_number)) {
			$name .= '-' . (int)$append_number; //yes, we want to explicitly cast to int and then implicitly cast back to string (via concatenation)
		}
		
		if ($name == $page->name) {
			return; //avoid the page finding itself as a dup in the query below
		} else if (wire('pages')->count("parent=$page->parent, name=$name")) {
			$append_number = (int)$append_number + 1;
			$this->setPageNameFromTitle($page, $append_number);
		} else {
			$page->set('name', $name)->save();
		}
	}

}
  • Like 2
Link to comment
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
 Share

  • Recently Browsing   0 members

    • No registered users viewing this page.
×
×
  • Create New...