Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[RTM] Extended picker #755

Merged
merged 27 commits into from
Apr 21, 2017
Merged

[RTM] Extended picker #755

merged 27 commits into from
Apr 21, 2017

Conversation

leofeyer
Copy link
Member

@leofeyer leofeyer commented Apr 7, 2017

This PR is the basis for an extended picker that allows to pick news, events etc. (see #7656).

It uses the KnpMenuBundle to generate the menu and dispatches the contao.build_picker_menu event so you can extend it (see http://symfony.com/doc/current/bundles/KnpMenuBundle/events.html).

TODO

  • Unit tests
  • Make the parent and list views pickable
  • Add a picker for news, events and maybe more (articles, FAQs)
  • Add a "check all" checkbox

@leofeyer leofeyer added this to the 4.4.0 milestone Apr 7, 2017
@leofeyer leofeyer self-assigned this Apr 7, 2017
Copy link
Member

@Toflar Toflar left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good at first sight! Nice work! I think there are quite some adjustments needed, though 😄
General thoughts:

  • Lots of repetition, I think even though the DC_* classes are shit, we should not continue that pattern.
  • What about the old page.php and file.php picker entry points? Can we map them BC to the new picker? Or is this only for filePicker and pagePicker?
  • Did you think about maybe unifying that whole picker stuff? I mean right now we have pageTree, filePicker and now some picker wizard callbacks. What if this just becomes the inputType picker and all you need is configured through widget config? We could just map the old pageTree and fileTree configuration to the new picker and drop the old widgets maybe?

['uri' => $this->route('contao_backend', 'files', $request)]
);

$item->setLinkAttribute('class', 'filemounts');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this should be handled by the renderer. That's what the interface method setCurrent() is for.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suppose this comment relates to line 177, doesn't it? Because setCurrent() will not set the link attribute.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, this comment must have been moved.

@@ -1579,7 +1579,11 @@ public function tableImportWizard()
*/
public function pagePicker(DataContainer $dc)
{
return ' <a href="' . (($dc->value == '' || strpos($dc->value, '{{link_url::') !== false) ? 'contao/page' : 'contao/file') . '?do=' . Input::get('do') . '&amp;table=' . $dc->table . '&amp;field=' . $dc->field . '&amp;value=' . rawurlencode(str_replace(array('{{link_url::', '}}'), '', $dc->value)) . '&amp;switch=1' . '" title="' . StringUtil::specialchars($GLOBALS['TL_LANG']['MSC']['pagepicker']) . '" onclick="Backend.getScrollOffset();Backend.openModalSelector({\'width\':768,\'title\':\'' . StringUtil::specialchars(str_replace("'", "\\'", $GLOBALS['TL_DCA'][$dc->table]['fields'][$dc->field]['label'][0])) . '\',\'url\':this.href,\'id\':\'' . $dc->field . '\',\'tag\':\'ctrl_'. $dc->field . ((Input::get('act') == 'editAll') ? '_' . $dc->id : '') . '\',\'self\':this});return false">' . Image::getHtml('pickpage.svg', $GLOBALS['TL_LANG']['MSC']['pagepicker']) . '</a>';
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't like that I would need to write this for every extension. I always hated that. Is there maybe a better solution?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Still asking here. This seems pretty universal now to me. Why not add an 'eval' => ['picker' => true'] setting here to ease it for devs?

@@ -178,6 +178,26 @@ public function __construct($strTable)
$this->arrValidFileTypes = \StringUtil::trimsplit(',', strtolower($GLOBALS['TL_DCA'][$this->strTable]['config']['validFileTypes']));
}

// Configure the picker
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What of this is specific to DC_Folder only? Looks like something that should be part of DataContainer? I think DataContainer should handle this in a general way and the drivers extending it should override what they want to do differently.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ACK.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have checked it and it does not really make sense to move any code into the parent class. Especially as we would have to add another abstract method setPickerValue(), which will break compatibility with all existing DCs.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Obviously I would not make it abstract. I'd just add the "normal" case to DataContainer and override the stuff that differs in the child tables :-)

$strClass = ' picker unselectable';
$strPicker .= ' id="tl_select"';

if (isset($GLOBALS['TL_DCA'][$this->strTable]['config']['picker']['insertTag']))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't like that it's limited to insert tags. I agree that most of the times you would want to have some placeholder that is dynamically replaced by a value but it could be anything, it doesn't have to be an insert tag.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is not limited to insert tags; the picker will return any given value.

But the JavaScript additionally supports the data-inserttag and data-callback attributes; the first one will wrap the given value in an insert tag and the second one will trigger an Ajax callback. Both are optional.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok.

@@ -2692,12 +2740,28 @@ protected function generateTree($path, $intMargin, $mount=false, $blnProtected=t
{
$return .= ' <a href="'.$this->addToUrl('&amp;act=move&amp;mode=2&amp;pid='.$currentEncoded).'" title="'.\StringUtil::specialchars(sprintf($GLOBALS['TL_LANG']['tl_files']['uploadFF'], $currentEncoded)).'">'.\Image::getHtml('new.svg', $GLOBALS['TL_LANG'][$this->strTable]['move'][0]).'</a>';
}

if ($this->strPickerField)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know, a lot of code is already duplicated and it's shit anyway but does that mean we have to add even more duplicate lines? :)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I checked this, too, but there are too many small differences to extract the code into a parent method.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm, let me check again once everything is implemented. There might be some optimization potential after all.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have moved most of the code to the parent class now.

$this->arrPickerValue = $varValue;

// TinyMCE will pass the path instead of the ID
if (strpos($varValue[0], \Config::get('uploadPath') . '/') === 0)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This smells. If something needs a special handling this is always an indicator that it is even wrong or third party bundles might need special handling to = needs extension point.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok. Still ;)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know what you mean. :)

*
* @throws \RuntimeException
*/
public function createMenu(BackendUser $user)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think passing the user is not needed at all. The ones that want to limit the menu to some permissions should check for themselves, meaning the event listeners should listen to the event and inject the security.token_storage and check for permissions there.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also they need to set the label as stated above.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ACK.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed in ab0ebb3.

*
* @param string $pagePickerLabel
*/
public function setPagePickerLabel($pagePickerLabel)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This introduces state and has nothing to do with the picker menu builder. The file picker and page picker should both listen to the event and set their own label.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I fully agree that the service must not be stateful.

However, I think it is ok if the service adds the two core navigation items. Do you really think we need a separate event listener for that?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I think the core should not behave differently than all the other listeners. It will also prevent from stuff like "the core can do this, you cannot in the listener unfortunately".
It's separation of concerns, easier to test, easier to remove etc. pp. I mean, create an AbstractDcaListener or something that gets the url, label and css class from its child class. That way it's a breeze implementing FAQ and other regular DCA related listeners.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed in ab0ebb3.

$strPicker .= ' data-inserttag="' . $GLOBALS['TL_DCA'][$this->strTable]['config']['picker']['insertTag'] . '"';
}

if (isset($GLOBALS['TL_DCA'][$this->strTable]['config']['picker']['callback']))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is data-callback? Is this maybe missing in this PR? Also, I suppose it's a JS callback, can we maybe name it as such? :)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's the data-callback attribute we are using already (since Contao 4).

https://github.com/contao/core-bundle/blob/master/src/Resources/public/core.js#L952

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, didn't know that, cool stuff.

@jankout
Copy link

jankout commented Apr 7, 2017

Thank you very much. It is great that we have the compatibility between the Contao's extensions.

@leofeyer leofeyer changed the title [RFC] Extended picker [WIP] Extended picker Apr 11, 2017
@leofeyer leofeyer force-pushed the feature/dca-picker branch 3 times, most recently from aa472d6 to a759b51 Compare April 11, 2017 09:52
@leofeyer
Copy link
Member Author

leofeyer commented Apr 11, 2017

Ok, I have completely reworked the implementation according to what @Toflar and I have discussed. It now uses tagged services that provide the picker information and a custom route that redirects depending on the picker value. Works great. 😄

Here's how to add a provider in your custom bundle: contao/news-bundle#10

@leofeyer leofeyer changed the title [WIP] Extended picker [RFC] Extended picker Apr 11, 2017
*
* @return Response
*
* @Route("/picker", name="contao_backend_picker")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To me, this is definitely a rather internal route. I'd prefer to have something like _contao/picker like we do for the cron.

@@ -1579,7 +1579,11 @@ public function tableImportWizard()
*/
public function pagePicker(DataContainer $dc)
{
return ' <a href="' . (($dc->value == '' || strpos($dc->value, '{{link_url::') !== false) ? 'contao/page' : 'contao/file') . '?do=' . Input::get('do') . '&amp;table=' . $dc->table . '&amp;field=' . $dc->field . '&amp;value=' . rawurlencode(str_replace(array('{{link_url::', '}}'), '', $dc->value)) . '&amp;switch=1' . '" title="' . StringUtil::specialchars($GLOBALS['TL_LANG']['MSC']['pagepicker']) . '" onclick="Backend.getScrollOffset();Backend.openModalSelector({\'width\':768,\'title\':\'' . StringUtil::specialchars(str_replace("'", "\\'", $GLOBALS['TL_DCA'][$dc->table]['fields'][$dc->field]['label'][0])) . '\',\'url\':this.href,\'id\':\'' . $dc->field . '\',\'tag\':\'ctrl_'. $dc->field . ((Input::get('act') == 'editAll') ? '_' . $dc->id : '') . '\',\'self\':this});return false">' . Image::getHtml('pickpage.svg', $GLOBALS['TL_LANG']['MSC']['pagepicker']) . '</a>';
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Still asking here. This seems pretty universal now to me. Why not add an 'eval' => ['picker' => true'] setting here to ease it for devs?

$strPicker = '';

// Add the picker attributes
if ($this->strPickerField)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Still a lot of repetition here. Can't we put this in its own method? :)

@@ -270,7 +270,7 @@
'exclude' => true,
'search' => true,
'inputType' => 'text',
'eval' => array('rgxp'=>'url', 'decodeEntities'=>true, 'maxlength'=>255, 'dcaPicker'=>true, 'fieldType'=>'radio', 'filesOnly'=>true, 'tl_class'=>'w50 wizard'),
'eval' => array('rgxp'=>'url', 'decodeEntities'=>true, 'maxlength'=>255, 'dcaPicker'=>array(), 'fieldType'=>'radio', 'filesOnly'=>true, 'tl_class'=>'w50 wizard'),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it should remain 'dcaPicker'=>true but optionally accepting an array.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

@leofeyer
Copy link
Member Author

leofeyer commented Apr 18, 2017

@ausi We could add the following code here:

$this->setPickerValue();

$strDriver = 'DC_' . $GLOBALS['TL_DCA'][$this->strPickerTable]['config']['dataContainer'];
$objDca = new $strDriver($this->strPickerTable);
$objDca->field = $this->strPickerField;

// Set the active record
if (isset($_GET['id']) && $this->Database->tableExists($this->strPickerTable))
{
	/** @var Model $strModel */
	$strModel = \Model::getClassFromTable($this->strPickerTable);

	if (class_exists($strModel))
	{
		$objModel = $strModel::findByPk(\Input::get('id'));

		if ($objModel !== null)
		{
			$objDca->activeRecord = $objModel;
		}
	}
}

// Call the load_callback
if (is_array($GLOBALS['TL_DCA'][$this->strPickerTable]['fields'][$this->strPickerField]['load_callback']))
{
	foreach ($GLOBALS['TL_DCA'][$this->strPickerTable]['fields'][$this->strPickerField]['load_callback'] as $callback)
	{
		if (is_array($callback))
		{
			$this->import($callback[0]);
			$this->arrPickerValue = $this->{$callback[0]}->{$callback[1]}($this->arrPickerValue, $objDca);
		}
		elseif (is_callable($callback))
		{
			$this->arrPickerValue = $callback($this->arrPickerValue, $objDca);
		}
	}
}

Would this solve your problem?

@ausi
Copy link
Member

ausi commented Apr 18, 2017

@leofeyer I think this is not enough because the ID of the databse record is missing. I created the PR #763, this should be a working solution.

@leofeyer
Copy link
Member Author

Nope, the ID is not missing. It is added by the FileTree and PageTree classes, therefore I have added the isset($_GET['id']) check.

    <p><a href="' . ampersand(\System::getContainer()->get('router')->generate('contao_backend_picker', array('do'=>'page', 'target'=>$this->strTable.'.'.$this->strField, 'value'=>implode(',', $arrSet), 'id'=>$this->activeRecord->id, 'popup'=>1))) . '" class="tl_submit" id="pt_' . $this->strField . '">'.$GLOBALS['TL_LANG']['MSC']['changeSelection'].'</a></p>

(The changes have not been pushed yet.)

@ausi
Copy link
Member

ausi commented Apr 18, 2017

But wouldn’t the $_GET['id'] conflict with the data container that shows the list/tree view (like in a news archive)? That’s why I added the ID to the target parameter in my PR.

@leofeyer
Copy link
Member Author

Yes, you are right – as always. 😄

@leofeyer
Copy link
Member Author

Well, you were only half way right. It id parameter was only used in the PageTree and FileTree widgets, which are both not switchable, which is why the problem did not occur.

However, your changes in line 484 now always add the ID, which is a different behavior than before. I don't think it will break anything, but we should review it carefully.

if (!isset($GLOBALS['TL_DCA'][$this->strPickerTable]['fields'][$this->strPickerField]))
{
throw new InternalServerErrorException('Target field "' . $this->strPickerTable . '.' . $this->strPickerField . '" does not exist.');
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This if needs to be moved to the end of the method, because the DCA onload_callback may add the field dynamically.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed in d86a642.

@@ -941,20 +941,24 @@ var Backend =
alert('Could not find the SimpleModal frame');
return;
}
ul = frm.document.getElementById(opt.id);
ul = frm.document.getElementById('tl_select');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change broke an existing URL picker (similar to the old one from tl_content) in my test. Without this change the picker works as expected, but with it the {{link_url::*}} insert tag around the ID is missing.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm, the core is not passing an id attribute anymore, because you are supposed to pass a callback function instead. Therefore, tl_select is kind of a convention.

Is it crucial that we support opt.id here, too?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it’s very likely that somebody copied the tl_content::pagePicker() method to use it for an extension or in a project, this would include \'id\':\'' . $dc->field . '\', too. Not supporting opt.id is be a BC break I think.

How about using tl_select as a fallback?
frm.document.getElementById(opt.id || 'tl_select')

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's possible of course, but it does not really help. Do we want to support opt.id in Contao 5 as well? Then we should encourage people to provide it. Or do we want to use tl_select in Contao 5? Then we should add a deprecation notice if someone uses opt.id.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we want to support opt.id in Contao 5 as well?

I think it’s OK to deprecate it.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed in 2b43a70.

@leofeyer leofeyer merged commit 81da4a0 into develop Apr 21, 2017
leofeyer added a commit that referenced this pull request Apr 21, 2017
@leofeyer leofeyer deleted the feature/dca-picker branch April 21, 2017 19:32
leofeyer added a commit to contao/calendar-bundle that referenced this pull request Apr 28, 2017
leofeyer added a commit to contao/faq-bundle that referenced this pull request Apr 28, 2017
leofeyer pushed a commit that referenced this pull request Sep 25, 2019
Description
-----------

As discussed in contao/contao#745 (comment)

If no image size is selected or configured the image should not get modified by Contao and the original image should be used instead.

An exception to this rule is if the image has an EXIF orientation tag, in this case the image gets rotated.

Commits
-------

f33ebafb Enable skipIfDimensionsMatch for empty configs
3903b17e CS fixes
@leofeyer leofeyer mentioned this pull request Jun 11, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants