Jump to content

Leaderboard

Popular Content

Showing content with the highest reputation on 07/26/2025 in Posts

  1. ProcessWire 3.0.251 has several updates to the AdminThemeUikit default theme by Konkat, a page-finding selector bug fix, and more. This version should fix the majority of reported issues with the new default theme in AdminThemeUikit, as well as cover the scope of Uikit features much more broadly. As we get closer to our next main/master version, we appreciate your help testing it. This version converts to “—pw” namespaced CSS variables. Previously variables were named like "—main-color" (no namespace prefix) and now they all have a "—pw" prefix, i.e. "—pw-main-color". Make note of that if you are using any custom CSS with the default theme by Konkat, as you may need to update your CSS variable names. This version also adds 3 new toggles (available in the AdminThemeUikit module settings). These toggles enable you to customize specific parts of the theme to be more similar to or consistent with the Original theme. They are intended to answer common feature requests for the theme. If you think any of these should be enabled by default, please let us know. Currently they require you to enable them in the module settings. These toggles include: Use bold headers for repeaters, files, images, etc. - This uses the selected “main color” as the background color of repeatable and sortable item headers, which results a treatment that’s heavier and more similar to the Original theme. Use buttons for page list actions - These makes the theme use page list action buttons that resemble those in the Original theme. It also slightly modifies the appearance of pagination links. Highlight focused inputs - This makes the color of an input change (to white or black) when it is the focused input. It makes select and text inputs have the same color presentation. And it makes TinyMCE have a white (or black) background when focused, rather than a muted background. In addition to the above, today’s version also adds version query strings to the CSS/JS files used by the Konkat default theme. Previously it didn’t, which due to browser caching could have caused some to see the incorrect output of the theme. ProcessWire 3.0.251 also fixes a bug with word matching operators that query the database, like those you might use in a $pages->find() selector. (Word matching operators are those with a “~” in them). If you attempted to match a word that was more than 80 characters long, it would cause the word to get filtered out of the query completely, rather than force a non-match. So if your selector was “a=b, c=d, e~=[81 character word]”, then it would behave the same as a “a=b, c=d” selector, with the “e” part no longer contributing to the result. The correct result here is to match 0 pages, but it would instead match public pages that matched selector “a=b, c=d”. It was corrected by truncating the long word to 80 characters, rather than removing it from the query. If you are using full-word matching operators in your site search engines, it’s worth throwing some 81+ character words at them just to see if it causes any issues with your results, perhaps matching incorrect, irrelevant or too many pages. If so, then you may want to upgrade to PW 3.0.251, or truncate your search text to 80 characters before putting it into the selector. i.e. $q = substr($q, 0, 80); Thanks to @adrian for finding and reporting the issue. Lastly, ProcessWire 3.0.251 contains a few other updates, such as the ability for Process modules to use icons in their headlines, a ProcessPageLister fix, and more. ProcessWire Weekly #584 covers a couple of them in more detail. That’s all for this week, thanks for reading and have a great weekend!
    7 points
  2. @ryan @diogo @jploch are you planning to upgrade also the font awesome icons? They haven't been upgraded for over a decade... Few days ago the v7 was released. Last year I've created a module that upgrades backend with the latest Font Awesome free icons (solid + brands) but I think a core upgrade would be much better integration.
    7 points
  3. I can’t believe ProcessWire Weekly is still going strong! Its 10 year anniversary was actually in May last year.
    3 points
  4. @ryan, can you pretty please look at this issue that has to do with a sub-selector bug that occurs when there are about 1500+ pages? https://github.com/processwire/processwire-issues/issues/2084 It would be nice to have that fixed before the next master version.
    2 points
  5. If you were doing this you could just make user pages available whereever you want: https://processwire.com/blog/posts/processwire-core-updates-2.5.14/#multiple-templates-or-parents-for-users Otherwise you could use UrlSegments or Path Hooks to load the requested user, such as: //requesting /teachers/paul $teacher = users()->get('roles=teacher, name=' . input()->urlSegment1); //urlsegments are already sanitized if (!$teacher->id) throw new Wire404Exception(); echo $teacher->firstname . ' ' . $teacher->lastname;
    2 points
  6. @Jan Romero wow thanks for your work and for sharing it! Interesting to see what is needed to accommodate something like this. Re yet more on the selector DSL- yes. It fascinates me that it's possible to do so much with simple text strings and not trip up. I would love to see unit tests in PW covering these cases - this would mean the format could be developed with more confidence that the new work hadn't broken the existing logic.
    1 point
  7. I’ve also longed for this feature and took this occasion to spend the day figuring out what it might take to support it in the core… Unfortunately, the when|published syntax is already supported as a shorthand for sort=when, sort=published, so we can’t use it without causing major breakage. Putting parens around it throws an exception somewhere deep in the codebase because it’s used for OR-groups. Since that doesn’t make sense in a sort selector, the syntax could probably be used, but I didn’t dare venture that deep, so I invented my own delimiter just for the POC. It’s /, i.e. you’d go pages()->find('template=person, sort=sortname/lastname|givenname'); and the full query would end up as: SELECT pages.id,pages.parent_id,pages.templates_id FROM `pages` LEFT JOIN field_givenname AS _sort_givenname ON _sort_givenname.pages_id=pages.id LEFT JOIN field_sortname AS _sort_sortname ON _sort_sortname.pages_id=pages.id LEFT JOIN field_lastname AS _sort_lastname ON _sort_lastname.pages_id=pages.id WHERE pages.templates_id = 69 AND pages.status < 1024 GROUP BY pages.id ORDER BY coalesce(_sort_sortname.data, _sort_lastname.data),_sort_givenname.data Which I think is a somewhat relatable real-world example (think: surname = “van Aaken”, sortname = “Aaken, van”). Unfortunately, the mod requires refactoring many lines into a method, so it’s a bit long to post here. But here it is regardless. Go to /wire/core/PageFinder.php and replace the entire function getQuerySortSelector() with this to check it out: protected function getQuerySortSelector(DatabaseQuerySelect $query, Selector $selector) { // $field = is_array($selector->field) ? reset($selector->field) : $selector->field; $values = is_array($selector->value) ? $selector->value : array($selector->value); $fields = $this->fields; $pages = $this->pages; $database = $this->database; $user = $this->wire()->user; $language = $this->languages && $user && $user->language ? $user->language : null; // support `sort=a|b|c` in correct order (because orderby prepend used below) if(count($values) > 1) $values = array_reverse($values); foreach($values as $value) { $fc = substr($value, 0, 1); $lc = substr($value, -1); $descending = $fc == '-' || $lc == '-'; $value = trim($value, "-+"); // $terValue = ''; // not currently used, here for future use if($this->lastOptions['reverseSort']) $descending = !$descending; if(!strpos($value, '/')) { $value = $this->processSortValue($query, $pages, $database, $fields, $descending, $language, $value); } else { $coalesce = []; foreach (explode('/', $value) as $value) { $value = $this->processSortValue($query, $pages, $database, $fields, $descending, $language, $value); if(is_string($value) && strlen($value)) $coalesce[] = $value; } if (!count($coalesce)) continue; $value = 'coalesce(' . implode(', ', $coalesce) . ')'; } if(is_string($value) && strlen($value)) { if($descending) { $query->orderby("$value DESC", true); } else { $query->orderby("$value", true); } } } } private function processSortValue($query, $pages, $database, $fields, $descending, $language, $value) { $subValue = ''; if(strpos($value, ".")) { list($value, $subValue) = explode(".", $value, 2); // i.e. some_field.title if(strpos($subValue, ".")) { list($subValue, $terValue) = explode(".", $subValue, 2); $terValue = $this->sanitizer->fieldName($terValue); if(strpos($terValue, ".")) $this->syntaxError("$value.$subValue.$terValue not supported"); } $subValue = $this->sanitizer->fieldName($subValue); } $value = $this->sanitizer->fieldName($value); if($value == 'parent' && $subValue == 'path') $subValue = 'name'; // path not supported, substitute name if($value == 'random') { $value = 'RAND()'; } else if($value == 'num_children' || $value == 'numChildren' || ($value == 'children' && $subValue == 'count')) { // sort by quantity of children $value = $this->getQueryNumChildren($query, $this->wire(new SelectorGreaterThan('num_children', "-1"))); } else if($value == 'parent' && ($subValue == 'num_children' || $subValue == 'numChildren' || $subValue == 'children')) { throw new WireException("Sort by parent.num_children is not currently supported"); } else if($value == 'parent' && (empty($subValue) || $pages->loader()->isNativeColumn($subValue))) { // sort by parent native field only if(empty($subValue)) $subValue = 'name'; $subValue = $database->escapeCol($subValue); $tableAlias = "_sort_parent_$subValue"; $query->join("pages AS $tableAlias ON $tableAlias.id=pages.parent_id"); $value = "$tableAlias.$subValue"; } else if($value == 'template') { // sort by template $tableAlias = $database->escapeTable("_sort_templates" . ($subValue ? "_$subValue" : '')); $query->join("templates AS $tableAlias ON $tableAlias.id=pages.templates_id"); $value = "$tableAlias." . ($subValue ? $database->escapeCol($subValue) : "name"); } else if($fields->isNative($value) && !$subValue && $pages->loader()->isNativeColumn($value)) { // sort by a native field (with no subfield) if($value == 'name' && $language && !$language->isDefault() && $this->supportsLanguagePageNames()) { // substitute language-specific name field when LanguageSupportPageNames is active and language is not default $value = "if(pages.name$language!='', pages.name$language, pages.name)"; } else { $value = "pages." . $database->escapeCol($value); } } else { // sort by custom field, or parent w/custom field if($value == 'parent') { $useParent = true; $value = $subValue ? $subValue : 'title'; // needs a custom field, not "name" $subValue = 'data'; $idColumn = 'parent_id'; } else { $useParent = false; $idColumn = 'id'; } $field = $fields->get($value); if(!$field) { // unknown field return null; } $fieldName = $database->escapeCol($field->name); $subValue = $database->escapeCol($subValue); $tableAlias = $useParent ? "_sort_parent_$fieldName" : "_sort_$fieldName"; if($subValue) $tableAlias .= "_$subValue"; $table = $database->escapeTable($field->table); if($field->type instanceof FieldtypePage) { $blankValue = new PageArray(); } else { $blankValue = $field->type->getBlankValue($this->pages->newNullPage(), $field); } $query->leftjoin("$table AS $tableAlias ON $tableAlias.pages_id=pages.$idColumn"); $customValue = $field->type->getMatchQuerySort($field, $query, $tableAlias, $subValue, $descending); if(!empty($customValue)) { // Fieldtype handled it: boolean true (handled by Fieldtype) or string to add to orderby if(is_string($customValue)) $query->orderby($customValue, true); $value = false; } else if($subValue === 'count') { if($this->isRepeaterFieldtype($field->type)) { // repeaters have a native count column that can be used for sorting $value = "$tableAlias.count"; } else { // sort by quantity of items $value = "COUNT($tableAlias.data)"; } } else if(is_object($blankValue) && ($blankValue instanceof PageArray || $blankValue instanceof Page)) { // If it's a FieldtypePage, then data isn't worth sorting on because it just contains an ID to the page // so we also join the page and sort on it's name instead of the field's "data" field. if(!$subValue) $subValue = 'name'; $tableAlias2 = "_sort_" . ($useParent ? 'parent' : 'page') . "_$fieldName" . ($subValue ? "_$subValue" : ''); if($this->fields->isNative($subValue) && $pages->loader()->isNativeColumn($subValue)) { $query->leftjoin("pages AS $tableAlias2 ON $tableAlias.data=$tableAlias2.$idColumn"); $value = "$tableAlias2.$subValue"; if($subValue == 'name' && $language && !$language->isDefault() && $this->supportsLanguagePageNames()) { // append language ID to 'name' when performing sorts within another language and LanguageSupportPageNames in place $value = "if($value$language!='', $value$language, $value)"; } } else if($subValue == 'parent') { $query->leftjoin("pages AS $tableAlias2 ON $tableAlias.data=$tableAlias2.$idColumn"); $value = "$tableAlias2.name"; } else { $subValueField = $this->fields->get($subValue); if($subValueField) { $subValueTable = $database->escapeTable($subValueField->getTable()); $query->leftjoin("$subValueTable AS $tableAlias2 ON $tableAlias.data=$tableAlias2.pages_id"); $value = "$tableAlias2.data"; if($language && !$language->isDefault() && $subValueField->type instanceof FieldtypeLanguageInterface) { // append language id to data, i.e. "data1234" $value .= $language; } } else { // error: unknown field } } } else if(!$subValue && $language && !$language->isDefault() && $field->type instanceof FieldtypeLanguageInterface) { // multi-language field, sort by the language version $value = "if($tableAlias.data$language != '', $tableAlias.data$language, $tableAlias.data)"; } else { // regular field, just sort by data column $value = "$tableAlias." . ($subValue ? $subValue : "data"); ; } } return $value; } Almost all of this is original PW code. The only changes are: The method processSortValue() was inside getQuerySortSelector() before. I think I only added the return at the end and changed a continue to a return in the middle. Of course, the bit where it goes if(!strpos($value, '/')) is new. The method had to be factored out because it assumes a single “value”, but since we’re injecting multiple values into a single value by our fabricated ”/“ delimiter, we want to call it in our own loop. It figures out what joins to add to the query and transforms the original value from the selector into the corresponding SQL expression (at one point it transforms “random” into “RAND()”, to name a simple example that doesn’t mutate anything outside the method). I have probably missed all kinds of things and introducing yet another arcane operator to selectors may not be a great idea, but my site seems to be running fine with this hack, and it seems to work.
    1 point
×
×
  • Create New...