Leaderboard
Popular Content
Showing content with the highest reputation on 07/10/2024 in all areas
-
Logs JSON Viewer Formats JSON data in ProcessLogger for improved readability. Because log files can only contain strings, it's a common practice to use json_encode() to convert an array to a string when you want to save the data to a log file. But the resulting JSON is not as readable as it could be when viewing the log in ProcessLogger. The Logs JSON Viewer module uses the json-viewer library to improve the readability of JSON data in ProcessLogger and add some useful features. Before: After: Configuration You can set the config options for json-viewer in a textarea field. See the json-viewer readme for information about the options. There is also an option to set the width of the column that contains the JSON data. This setting exists because otherwise the column jumps around in an inconsistent and distracting way when changing from log to log or between paginations. Features You can switch the view of the JSON data between formatted and unformatted using the toggle button at the bottom of the data. The viewer has a number of useful features such as: Progressively expand or collapse levels in the data. View the count of child items and the data type of each item. Search for a string within the data. Copy all or part of the data to the clipboard (requires the HTTPS protocol). https://github.com/Toutouwai/LogsJsonViewer https://processwire.com/modules/logs-json-viewer/10 points
-
There have been a few useful updates to the PW and Tracy logs panels. 1) Display json as interactive "dumped" array. 2) Support for @Robin S awesome new CustomLogs module. 3) New options to exclude certain logs from being displayed. By default "modules", "sessions", and "file-compiler" are excluded. I think this is a major improvement because often these logs overwhelm the more important alert/warning/error type logs.5 points
-
4 points
-
Thanks, and I just noticed the HelloWorld panel too. Very helpful ? That's a good solution, thanks. I missed that there is now a JSON viewing feature. That's what my upcoming module does too. Still worth releasing I think because it has some extra features and config options.3 points
-
Overview The TextformatterTokens module allows other modules to register tokens and replace them with actual values. Installation Enable the TextformatterTokens module. Enable the SiteTokens module. Enable the TextformatterTokens text formatter for a field. Usage Use tokens provided by the SiteTokens module inside a field that has the TextformatterTokens text formatter enabled. Save and display your page. The tokens should be replaced with the actual content. Example Tokens [site:name] [page id=123] where id is the ID of the page you want to embed its body field. Debugging You can view logs under your ProcessWire admin dashboard at Setup > Logs > tokens. Writing own tokens To create custom tokens, refer to the SiteTokens.module file for an example implementation.2 points
-
@Robin S - I've committed that new version to support CustomLogs in the PW Logs panel. It was your Logs JSON Viewer module (I found it a couple of weeks ago via Github) that got me thinking about supporting it in the PW and Tracy Logs panels.2 points
-
Custom Logs When you use the core $log->save() method you can only save a single string of text. When you view the log in the core ProcessLogger the columns and their header labels are predetermined. The Custom Logs module is different in that it lets you write and view log files with the number of columns and the column header labels you specify in the module configuration. Configuration In the "Custom logs" textarea field, enter custom logs, one per line, in the format... name: column label, column label, column label ...with as many comma-separated column labels as needed. The log name must be a word consisting of only [-._a-z0-9] and no extension. If you prefix a URL column label with {url} then the value in the column will be rendered as a link in the log viewer. The date/time will automatically be added as the first column so you do not need to specify it here. Writing to a custom log Use the CustomLogs::save($name, $data, $options) method to save data to a custom log file. $cl = $modules->get('CustomLogs'); $cl->save('my-log', $my_data); Arguments $name Name of log to save to (word consisting of only [-._a-z0-9] and no extension). $data An array of strings to save to the log. The number and order of items in the array should match the columns that are configured for the log. $options (optional) Options for FileLog::save(). Normally you won't need to use this argument. Example of use Custom log definition in the module configuration: visits: {url}URL, IP Address, User Agent, {url}Referrer Saving data to the log: $cl = $modules->get('CustomLogs'); $data = [ $_SERVER['REQUEST_URI'] ?? '', $_SERVER['REMOTE_ADDR'] ?? '', $_SERVER['HTTP_USER_AGENT'] ?? '', $_SERVER['HTTP_REFERER'] ?? '', ]; $cl->save('visits', $data); Viewing the resulting log in Setup > Logs > visits: https://github.com/Toutouwai/CustomLogs https://processwire.com/modules/custom-logs/1 point
-
Page classes are an outstanding feature of ProcessWire and probably ranks among my favorites overall (and that's really saying something with ProcessWire). I don't remember how I lived without them, well I do, but I don't like to think about it. I wrote a response to a question asking how ProcessWire can help transition from procedural code to OOP and in the process (pardon the pun) of answering, I realized how much I've come to use, if not outright rely on, page classes in every project. I wanted to compile a few more thoughts and examples here because there may be devs who are finding this feature for the first time, and besides, why not go down the rabbit hole? Page classes have been around since ProcessWire 3.0.152 and if you're not familiar this will all make a lot more sense if you do a quick read of the feature announcement by Ryan here because this post assumes familiarity. We're also going to pick up the pace between the examples below compared to my comment in the link above. Here are a few goals that I have that page classes put solutions for within reach. You may have some of your own and I'd love to hear about those as well. Keep templates clean by restricting logic to flow operations- if statements and loops. Work with data in context, accessing data and fields closer to their source. Create "universal" methods available in every template, but also have scoped methods per-template Increase the scalability of a project, growth with limited increases in complexity. Stay DRY Embrace and extend the power of OOP in ProcessWire's DNA This seems like quite a list for one feature to handle, but rest assured, this is an example of how much power comes with using page classes. I'll continue and build on the blog example from my linked response above, and you can "follow along" with the Blog module since the templates mentioned are present out of the box. Some of the examples below are taken from production code, but please excuse any errors that I may have introduced by accident and I'm happy to update this post with corrections. Some of these things can be done other ways but they're just to illustrate, replace with your ideas and think of times that this would be useful. First up, what are some features and behaviors that everyone needs on every project? What are some universal methods that would be great to have everywhere? Let's start out by creating a "base" page class called DefaultPage. Going forward, page classes will extend this class instead of Page and benefit from having access to universal methods and properties. EDIT: I initially wrote this using BasePage rather than DefaultPage for this class name as described by the custom page class writeup by Ryan in the article above. I've changed this to now use the correct DefaultPage class name. <?php namespace ProcessWire; // /site/classes/DefaultPage.php class DefaultPage extends Page { /** * Returns all top level pages for the main navigation. */ public function navigationPages(): PageArray { $pages = wire('pages'); $selector = 'parent=1'; $excludedIds = $pages->get('template=website_settings') ->nav_main_excluded_pages ->explode('id'); // Check for excluded pages from nav defined in the "Website Settings" page count($excludedIds) && $selector .= ',id!=' . implode('|', $excludedIds); $homePage = $pages->get('id=1'); $topLevelPages = $pages->find($selector); $topLevelPages->prepend($homePage); return $topLevelPages; } } All your base page are belong to us. Right off the bat we've managed to pull complex logic usually in templates and kept our markup clean. This isn't for DRY, it's to store logic out of templates. This simple example only illustrates working with a top level page nav. We can start to appreciate the simplicity when considering how much more the navigation may call for in the future. A navigationChildren method that also accounts for excluded pages is a prime example. In our markup: <!-- /site/templates/components/site_nav.php --> <nav> <ul> <?php foreach ($page->navigationPages() as $navPage): ?> <li> <a href="<?= $navPage->url; ?>"><?= $navPage->title; ?></a> </li> <?php endforeach ?> </ul> </nav> Next up, our settings page has newsletter signup fields where users can copy/paste form embed code from Mailchimp. Our website has a "settings" page where global values and fields can be edited and maintained There are multiple textarea fields on the settings page that can each contain a different mailchimp embed code There is an embed select field that can be added to templates. The values are textarea field names so an embed can be chosen by the user. An embed select field has been added to the settings page which allows for choosing a default embed code if one isn't selected when editing a page Embedded forms should be available anywhere on any template <?php namespace ProcessWire; // /site/classes/DefaultPage.php class DefaultPage extends Page { // ...Other DefaultPage methods /** * Renders either a selected form embed, fallback to default selected form */ public function renderEmailSignup(): ?string { $settingsPage = wire('pages')->get('website_settings'); $embedField = $this->signup_embed_select ?: $settingsPage->default_email_embed; return $settingsPage->$embedField; } } Excellent. We've got some solid logic that otherwise wouldn't have a great home to live in without page classes. We can also modify this one method if there are additional options or complexity added to the admin pages in the future. Wherever we use renderEmailSignup, the return value will be either the selected or default form embed code. Let's create a folder called "components" in our templates directory that will hold standalone reusable markup we can use wherever needed, here's our newsletter signup component: <!-- /site/templates/components/newsletter_signup.php --> <div class="newsletter-signup"> <?= $page->renderEmailSignup(); ?> </div> Great! We've kept logic out of our code. We can render the field, and we can account for an empty value with a fallback. Unfortunately this is pretty limited in that it only handles a specific field, and it's implementation isn't as flexible as a component should be. We can fix that, and here are some new requirements to boot: Some templates may have multiple embed select fields which may or may not have a value Each embed select field is now paired up with a text field to add some content that appears with each form embed, think "Sign up for our newsletter today!" We want to render our mailchimp form embeds using the same component file, but handle different fields Sounds like a tall order, but it's a challenge easily overcome with page classes. We're going to introduce a new method called renderComponent that will be global, reusable, and very flexible. We're also going to make use of another very great ProcessWire feature to do it. Back to our DefaultPage class: <?php namespace ProcessWire; // /site/classes/DefaultPage.php class DefaultPage extends Page { // ...Other DefaultPage methods /** * Renders a file located in /site/templates/components/ with optional variables */ final public function renderComponent(string $file, ...$variables): string { $componentsPath = wire('config')->paths->templates . 'components/'; return wire('files')->render($file, $variables, ['defaultPath' => $componentsPath]); } /** * Renders either a selected form embed, fallback to default selected form */ public function renderEmailSignup(?string $embedSelectField = null): ?string { $settingsPage = wire('pages')->get('website_settings'); $embedField = $this->$embedSelectField ?: $settingsPage->default_email_embed; return $settingsPage->$embedField; } } We've done a couple of things here. We've updated renderEmailSignup to accept a field name, so now we're flexible on exactly which field select we'd like to check for a value on before falling back to default. We've also created a renderComponent method that is going to be super useful throughout the rest of our ProcessWire application. Our renderComponent receives a file name located in the components directory and any number of named parameters. You could change the $variables parameter to an array if you'd like, but I'm a big fan of the great features we have now in PHP 8+. Here's our refactored component file: <!-- /site/templates/components/newsletter_signup.php --> <?php if ($embedField): ?> <div class="newsletter-signup"> <?php if ($text): ?> <h2><?= $text; ?></h2> <?php endif ?> <?= $page->renderEmailSignup($embedField); ?> </div> <?php endif ?> And let's hop over to our (abbreviated) home page template: <body> <!-- ...Sections full of great content --> <section class="signup-call-to-action"> <?= $page->renderComponent('newsletter_signup.php', embedField: $page->embed_select, text: $page->text_1); ?> </section> <!-- ...Your awesome template design --> <section class="page-end-call-to-action"> <?= $page->renderComponent('newsletter_signup.php', embedField: $page->embed_select_2, text: $page->text_2); ?> </section> <!-- Your footer here --> </body> I don't know about you, but this is looking really good to me. The number of things we've accomplished while having written so little code is remarkable: Because we used wire('files')->render(), the entire ProcessWire API is available within the component, so now our renderEmailSignup method is too. The variadic function parameters (or array if preferred) let us pass an arbitrary number of variables to any component file, unrestricted future flexibility Variables are scoped to each component! There's no reference to template fields in our component that could break if changes are made No more PHP includes, we don't have to juggle paths or constantly repeat them in our code, nor rely on declaring variables before including a file. ProcessWire will throw an exception if we try to render a file that does not exist which makes locating issues very easy We'll also see an exception if we try to reference a variable in our component that wasn't passed which can also help troubleshooting. Notice that the renderComponent is final. We want that behavior to remain consistent everywhere we use it and not overwritten either intentionally or by accident on our inheriting page classes. We want to eliminate any confusion between templates by knowing it will always do the same thing the same way. We can explore other uses too, perhaps a renderPartial for files in /site/templates/partials where we store files like site_header.php. As mentioned above however, if a variable is expected in the rendered file but not included in our render method, we'll see an exception. Let's use site_header.php as an example because we're sure to run into situations where variables may or may not exist: <?php namespace ProcessWire; // /site/templates/partials/site_header.php ?> <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title><?= $page->title; ?></title> <meta name="description" content="<?= $metaDescription ?? null; ?>"> <?php if ($includeAnalytics ?? true): ?> <script> // All that Google Analytics jazz </script> <?php endif ?> </head> <body class="<?= $bodyClasses ?? null; ?>"> <header> <?= $page->renderComponent('site_nav.php', includeEmailButton: true); ?> </header> Problem: solved. By using nullsafe ?? operators, we can call $page->renderPartial('site_header', description: 'The ultimate Spice Girls fan page.'); and never get errors for variables that may not be included when calling renderComponent, such as includeAnalytics, which now also has a default value of 'true'. Nice. We haven't even gotten to our actual page classes yet... Our templates are about to receive superpowers. Let's take our blog to the next level. In my comment on the other thread, I created a specific example of adding a readTime method to our blog posts, let's go one level higher to our blog.php template. We'll populate some methods up front and then talk about what we've done: <?php namespace ProcessWire; // /site/classes/BlogPage.php class BlogPage extends DefaultPage { /** * Get latest blog posts, optionally with/without pinned post, optionally in blog category */ public function latestPosts( int $limit = 3, bool $includePinnedPost = true, ?int $categoryId = null ): PageArray { $selector = 'template=blog-post'; if ($categoryId) { $selector .= ",blog_category={$categoryId}"; } $posts = wire('pages')->get($selector); if ($includePinnedPost) { $pinnedPost = $this->getPinnedPost(); if ($pinnedPost) { $posts->remove($pinnedPost); $posts->prepend($pinnedPost); } } return $posts->slice(0, $limit); } /** * Gets an optional pinned post if set/chosen * pin_blog_post A checkbox to indicate whether a post should be pinned * pinned_post A InputfieldPage field to choose a blog post */ public function getPinnedPost(): ?Page { if (!$this->pin_blog_post) { return null; } return wire('pages')->get($this->pinned_post); } } On our main blog page a user has the ability to choose whether a blog post is "pinned". A pinned post will always remain the first post anywhere a list of posts is needed, something like a big company announcement that the client wants to keep visible. These two methods alone have given us awesome abilities. For our main blog page, when someone visits our blog page, the most recent or pinned post is presented at the top, followed by the next two most recent posts, followed by two rows of 3 posts, for a total of 9. Let's assume that we've already created the BlogPostPage.php with the readTime method from my previous example. Here's our blog.php template <?php namespace ProcessWire; echo $page->renderPartial( 'site_header.php', bodyClasses: 'blog-page', metaDescription: $page->blog_description ); $posts = $page->latestPosts(9); $firstPost = $blogPosts->first(); ?> <section class="main-post"> <article> <img src="<?= $firstPost->blog_image->url; ?>" alt="<?= $firstPost->blog_image->description; ?>"> <h1><?= $firstPost->title; ?></h1> <?= $firstPost->summary; ?> <span><?= $firstPost->readTime(); ?></span> <a href="<?= $firstPost->url; ?>">Read More</a> </article> </section> <section class="recent-Posts"> <?= $page->renderComponent('blog_preview_card.php', blogPost: $posts->get(1)); ?> <?= $page->renderComponent('blog_preview_card.php', blogPost: $posts->get(2)); ?> </section> <section class="past-posts"> <?php foreach ($posts->slice(3, 6) as $post): ?> <?= $page->renderComponent('blog-preview_card.php', blogPost: $post); ?> <?php endforeach ?> </section> <?= $page->renderPartial('site_footer.php'); ?> So first off, we've really started to use our renderComponents method and component files. We also implemented a renderPartial as speculated upon above. Each does a similar thing, but having separate methods makes everything clear, handles paths, but still has a similar interface when calling them. A big thing to notice here is that at no point have we added any markup to our page classes, and no business logic to our templates. If we need to find anything, we know where to look just by glancing at the template. Ultimate maintainability. Here's our blog_preview_card.php component: <?php namespace ProcessWire; ?> <!-- /site/templates/components/blog_preview_card.php --> <article class="blog-preview-card card"> <img src="<?= $blogPost->blog_image->url; ?>" alt="<?= $blogPost->blog_image->description; ?>"> <h2><?= $blogPost->title; ?></h2> <div class="blog-summary"> <?= $blogPost->blog_summary; ?> </div> <span class="blog-read-time"><?= $blogPost->readTime(); ?></span> <a href="<?= $blogPost->url; ?>">Read More</a> </article> I am liking how well this is working out! Page classes have done a ton of heavy lifting here: We're using our renderComponent method to it's maximum potential and it's payed off in spades Our template couldn't be cleaner or more easily maintainable BlogPostPage.php has taken care of all of our needs as far as delivering the PageArray of posts and all our template does is output the data as needed Our "card" component will render the same thing everywhere and we can update how that looks globally with changes to one file If you don't think this can get more awesome, or think this post is already too long, I have bad news for you and you should stop reading now. Still here? Let's create a BlogPostPage class and add a method: <?php namespace ProcessWire; // /site/classes/BlogPostPage.php class BlogPostPage extends DefaultPage { // ... Other page methods, like readTime() public function relatedPosts(int $limit = 3): PageArray { return wire('pages')->get('template=blog')->latestPosts( limit: $limit, includePinnedPost: false, categoryId: $this->blog_category?->id ); } } The BlogPage::latestPosts method is really flexing it's muscle here. We've used it in two different places for different purposes but requesting similar data, blog posts. If you noticed, we're also specifying a category for this blog since we have a page select field that references a blog category. That was a parameter that we included back in our BlogPage::latestPosts() method. So, blog posts have a "You might also be interested in..." section with posts in the same category as the that one that a visitor has just read. With page classes this couldn't be easier to add, so let's use the relatedPosts method we just created in BlogPostPage.php in our blog-post.php template: <?php namespace ProcessWire; // /site/templates/blog-post.php echo $page->renderPartial( 'site_header.php', bodyClasses: 'blog-post', metaDescription: $sanitizer->truncate($page->blog_summary, 160) ); ?> <!-- The page hero, blog content, stuff, etc. --> <section class="related-posts"> <?php foreach ($page->relatedPosts() as $post): ?> <?= $page->renderComponent('blog_preview_card.php', blogPost: $post); ?> <?php endforeach ?> </section> <?= $page->renderPartial('site_footer.php'); ?> We've just added a related posts feature in *checks stopwatch* seconds. This project is going so fast that you can already hear the crack of the cold beer after a job well done. One more example, and I'll let you decide if this is too much, or feels just right. We want a blog feed on our home page. So before we go to the home.php template, let's do what we do (surprise, it's a page class): <?php namespace ProcessWire; // /site/classes/HomePage.php class HomePage extends DefaultPage { // Other HomePage methods... public function blogFeed(int $limit = 3): PageArray { return wire('pages')->get('template=blog')->latestPosts(limit: $limit); } } At first glance, this might seem like a bit much. Why create a method in our page class that is so short and simple? We could just call $pages->get('template=blog')->latestPosts(3) inside our template and get away with it just fine. Here's why I think it's worth creating a dedicated page class method: This creates yet another example of predictability between templates It promotes thie philosophy I like of page classes talking to page classes It's likely that we'll have other more complex methods in the HomePage class, keeping them together feels right and helps with uniformity- we always know where to look when seeing custom methods If we need to make a change to the BlogPage::latestPosts() method that affect how other page classes call that method, we don't have to root around in our templates to make any changes. It's pretty nice to see that thus far we have only ever referenced the $page object in our templates! There aren't any calls to $pages because our template is really about doing one thing- rendering the current page without caring about other pages. That's not to say that we can't, or shouldn't, use other ProcessWire objects in our templates, but it's still impressive how much we've been able to scope the data that we're working with in each template. I truly love the page classes feature in ProcessWire and even though this is a deeper dive than my other post, this really is still just scratching the surface because we can imagine having more complex behavior and other benefits. Here are some from my experience: Handling AJAX calls to the same page. Choosing how and what type of content is returned when the page is loaded is really nice. This can be done with hooks, but I really like page classes handling things like this in-context. Working with custom sessions between one or more pages. Creating a trait that shares session behavior between page classes is a great way to extend functionality while keeping it scoped. Adding external API calls and whatever complexity that may entail Provides the ability to add significant complexity to our templates in the admin yet little to no additional complexity in template files. The basic "pinned post" and form embed features are just the start As you can imagine, this makes replicating code you use often between projects trivial Page classes provide a home for logic that otherwise would be stuck in a template, or relegated to a messy functions.php file. No more functions.php ever. We met every single goal I listed here at the beginning Every single template thus far only uses if statements and loops DefaultPage could have a "bootApplication" method if needed that does things for every page and is called in ready.php. This is also a great way to create a "bootable" method in all of your page classes where bootApplication could call a bootPage method in your page classes if it exists. Thanks for coming to my TED talk. Hope you find this useful! If you have any questions, corrections, use cases, or things to add, the comments below are open and I'd love to hear them!1 point
-
@BrendonKoz Thank you. Yes, your comment is really helpful to me. I thought the brand new TinyMCE ships with some basic pre-built citations option. From an editor perspective it would nice to add blockquote caption within the main body text when you work with large amounts of text. However, I could not find a reliable TineMCE plugin. I do prefer future-proofing too. Hanna Code is a quick workaround, but Combo field is exactly what I was thinking, too. ?1 point
-
1 point
-
Is there a specific reason you need to use this page-tree structure? It seems to me that one particular floor can only ever belong to one particular house. You could better enforce this constraint by having the floors be children of their houses or by having a single-page-reference field in the floors referencing their house. I would strongly prefer the first option, but it would change your URLs. Reasons you should change your structure to floors being children of their houses: It correctly models the 1:n relationship. Vitally, it doesn’t allow one floor to belong to multiple houses! You may not even need the custom floor number field because you can just use “sort” (sort starts at 0, so +1 accordingly). Adding pages from a separate parent through a Page Reference Field is weird and dangerous for your use case. For example, if you enter “Ground Floor” and a page called “Ground Floor” already exists (seems likely), it will silently use the existing page and not create a new one. Your URLs will be nicer in every way. Your current setup will have to create unique names for every ground floor, so you’ll eventually have /floors/ground-floor-27/ as opposed to /city-hall/ground-floor/. If you really like the /floor/ground-floor/ URLs, you can use path hook trickery to fake them. This is much less of a hassle than your approach. Here’s a hook you could use to generate numbers for pages added through Page Reference Field, but I’m telling you right now: don’t do this! $this->addHookAfter("InputfieldPage::processInputAddPages", function(HookEvent $event) { $inputfield = $event->object; $floors = $inputfield->value; $c = 0; foreach ($floors as $floor) { $c++; if ($floor->_added === true || $floor->floor_number <= 0) $floor->setAndSave('floor_number', $c, [ 'quiet' => true ]); } }); These are just my 2 cents without knowing all considerations of your project, so I may be completely off. There may also be better and more robust ways to achieve what you want than this little hook1 point
-
See https://github.com/MetaTunes/GoCardlessConfig I built this little module to hold some API keys for GoCardless as I wanted them to be accessible by superuser but without having access to underlying files and code. I'd be interested in views regarding the security of this approach. I realise that there are more secure ways of holding API keys, but a balance has to be struck between usability and security. To avoid inadvertent disclosure or amendment, it requires the superuser to re-enter their password to access the keys. It could easily be amended to hold other types of keys, if that is useful to anyone. Any improvements are also welcome!1 point
-
Just to clarify a few things... is this the overall structure you have/want to accomplish? ├── Homepage ├── Houses (template: houses) │ ├── Small House (template: house) │ ├── Medium House (template: house) │ ├── Big Mamas House (template: house) │ │ └── ref_floors: (page reference field: "parent=floors, template=floor") │ │ ├── Basement │ │ ├── 1st Floor │ │ ├── 2nd Floor │ │ └── 3rd Floor │ ├── ... │ └── Super Large House (template: house) ├── Floors (template: floors) │ ├── Cellar (template: floor) │ ├── Basement (template: floor) │ ├── 1st Floor (template: floor) │ ├── 2nd Floor (template: floor) │ ├── ... │ └── Attic (template: floor) └── ... If so... could you mark and add more details where you need some kind of logic/automation, please. And those additional fields where you want to add custom content if and where needed.1 point
-
Hi @Robin S - yes, it is easy to add custom panels to Tracy from your module, eg: https://github.com/wireframe-framework/Wireframe/tree/master/TracyPanels There isn't a way to hook into the Logs panel though at the moment, however, I think I have reasonable solution based on the new JSON viewing functionality I added recently in the latest version. Here is how that looks when viewing the logs panel with a custom visits log. Obviously not as nice as the PW logs viewer with the way you modified the table, but I think this works well enough and allows easy integration with the PW logs panel and its icon colour change to indicate new entries, combining with the latest from all logs, etc. Any thoughts before I commit?1 point
-
Thanks. Good call, I didn't think about that panel. You don't want to be making changes and additions to Tracy Debugger for the sake of other modules. The appeal of CustomLogs might be quite niche so I'll wait for a bit, but I could perhaps look at including a panel for Tracy within the CustomLogs module. I think I remember reading somewhere that you've allowed for third-party panels - can you refresh my memory on this? And could there be a way to hook into the ProcessWire Logs panel rendering (if there isn't already)? I have another log-related module to release and I could potentially add a feature so that it applies to the Logs panel too.1 point
-
Not entirely helpful, but I use a Combo field to hold my blockquotes. I also wanted to make sure that there could be an optional cite available with the blockquote. Although there are plugins and adjustments that could be made to a WYSIWYG editor, they don't always transition easily from version-to-version, or from editor-to-editor. For scenarios like that, I prefer future-proofing it and going with a potentially less user-friendly interface, but one that likely won't break. An alternative that you could use, if you didn't want to find a TinyMCE plugin for this, could be to use Hanna Code. I'm guessing you don't want anything that looks like code of any sort, but shy of a button, it would definitely provide you the ability to keep things within the TinyMCE field and still render a blockquote (with optional cite).1 point
-
@MarkE If it's a Process module (i.e. extends the Process class) then that's a module designed to be an application in the admin, and that module is only executed when clicked on in the navigation (assuming the user has permission for it). It sounds like you also need a module that has the "autoload" enabled, meaning that it either loads every time PW boots, or under some autoload condition that you define. Process modules aren't meant to be "autoload" modules. Process modules are interactive applications, creating and processing forms in the admin. Autoload modules hook into ProcessWire and adjust runtime behavior. These are very different responsibilities, so you want 2 modules: one Process module and one autoload module. For instance, there's FormBuilder and ProcessFormBuilder, UserActivity and ProcessUserActivity, ProCache and ProcessProCache, etc. If you absolutely had to have it all in a Process module, you could, but you'd have to do your own permission check in the execute() method(s), and your module would appear in the admin navigation even for people that didn't have access to it. It's cleaner to do it with two modules, and one can install/uninstall the other just by using the "installs" property in your module info for one of them, and "requires" for the other.1 point
-
To simply hide the Add button of the PageListSelectMultiple field for non-superusers or for all, you can use some custom JS. Example: JS in site/templates/scripts/admin-custom.js // custom admin script console.log("admin-custom.js loaded"); /** * Hides the "Add" button for the Inputfield Page List Select Multiple field with given fieldName. * This function is intended to restrict access to adding existing items for non-superuser roles. */ function hideAddButtonForInputfieldPagelistSelectMultiple(fieldName) { // Uncomment the following line to also hide for superusers // config object is globally available in the admin and contains user objecyt with roles array // if (config.user.roles.includes('superuser')) return; const wrapper = document.getElementById('wrap_Inputfield_' + fieldName); if (!wrapper) return; const start = wrapper.querySelector('a.PageListSelectActionToggleStart'); if (start) { start.style.display = 'none'; } } window.onload = function () { hideAddButtonForInputfieldPagelistSelectMultiple('my_pagelist_select_multiple'); // pass in your field name here }; To load that script in site/templates/admin.php ... // load site/templates/admin-custom.js $config->scripts->add($config->urls->templates . 'scripts/admin-custom.js'); /** @var Config $config */ require($config->paths->core . "admin.php"); You could do this with custom CSS. But there you don't have the user role available. EDIT: or you do it with CSS and load it depending on user role in admin.php ? site/templates/admin.php // load site/templates/admin-custom.css for non superusers if(!$user->isSuperuser()) $config->styles->add($config->urls->templates . 'styles/admin-custom.css'); ... site/templates/styles/admin-custom.css /* Hides the "Add" button for the Inputfield Page List Select Multiple field with name "my_pagelist_select_multiple" */ #wrap_Inputfield_my_pagelist_select_multiple a.PageListSelectActionToggleStart { display: none; } Hope this helps.1 point
-
This is incredibly useful and could not come at a better time!1 point
-
@dan222 coming to the htaccess file you just have to use pw one and uncommenting the lines you need about those www (section 13 of the file) an https (section 9) things, evyting thing is very well commented in the file it self have a nice day1 point
-
Lots more Adminer updates in the last couple of weeks, but the key things are: 1) Shift-click on the DB icon links will now open Adminer in full mode rather than with the Tracy panel 2) Page, template, field, etc ID within Adminer table views are now linked - note the page title (and path) and the link to edit that page - this comes from hovering on any of the id, parent_id, templates_id, created_users_id, modified_users_id, etc. These sorts of links are present throughout including linking to modules from the "modules" table, hanna_code, pages_meta, etc. It also works for all Page Reference field's page IDs in Profields Table "field_table" tables, Profields Combo, repeater, RM fields, etc. Hopefully you'll all find this as useful as I am.1 point
-
Ok, sorry, clickbait ? Hooks are great! But sometimes, there are even better solutions: I'm cleaning up RockForms to finally release it ? I have some pages that are only for storing data (like form entries and such), so I don't want them to be editable, not even for superusers, as I control them solely via code in my module. -- Solution 1 -- With a regular hook that would look like this: <?php // site/ready.php $wire->addHookAfter("Page::editable", function($event) { $page = $event->object; if($page instanceof \RockForms\Root) $event->return = false; }); That's quite nice, but this approach has some drawbacks: First, sooner or later you might end up with hook-hell in ready.php; That's not ideal and really hard to debug on more complex projects. Second, as we are defining the hook with a callback in a non-OOP style these hooks get a LOT harder to debug! Have a look at tracy's debug panel: The second highlighted hook is the one coming from ready.php and it does not show any helpful information whereas the first one does show clearly that the hook is attached in RockForms\Root in the method "hookUneditEntries" (it should be hookUneditRoot, but I made a mistake when copy-pasting, sorry ? ). -- Solution 2 -- So the next best solution IMHO is using custom page classes! Then you get OOP style and a lot better structure for your project with really very little effort! Just create a file in /site/classes and that's it. Now to attach hooks directly in custom page classes you have to do one additional step. You can watch my video about this if you are interested. If not, head over to solution number 3 which is even simpler ? This solution might look something like this: <?php namespace RockForms; use ProcessWire\HookEvent; use ProcessWire\Page; use RockMigrations\MagicPage; use function ProcessWire\wire; class Root extends Page { use MagicPage; public function init() { wire()->addHookAfter("Page::editable", $this, "hookUneditRoot"); } protected function hookUneditRoot(HookEvent $event): void { $page = $event->object; if (!$page instanceof self) return; $event->return = false; } } This might look like a lot more code, but it's a lot better in the long run in my opinion as things that are related solely to the root page are inside the Root.php file of my module/project. -- Solution 3 -- But then I remembered: As our "Root"-page is a custom page class and PW checks if the page is editable or not by calling $page->editable() we can simply override this method like so: <?php namespace RockForms; use ProcessWire\Page; class Root extends Page { public function editable() { return false; } } You don't even need to make it a "MagicPage" because you don't need an init() method to attach any hooks. Now it's only very little additional code compared to a hook in ready.php but with a lot cleaner setup ? It's not a new invention, but I thought I'd share it nevertheless. Maybe it's helpful for some and maybe it's a good reminder for others, that even hooks are sometimes "overkill" ?1 point