mediatheque

This commit is contained in:
armansansd
2023-07-18 10:47:46 +01:00
commit 4b7c97a02e
3044 changed files with 544089 additions and 0 deletions
+170
View File
@@ -0,0 +1,170 @@
{
"root": true,
"env": {
"browser": true,
"node": true,
"es6": true
},
"parser": "@babel/eslint-parser",
"parserOptions": {
"ecmaVersion": 7,
"sourceType": "module",
"requireConfigFile": false
},
"rules": {
"accessor-pairs": 2,
"array-bracket-spacing": 0,
"block-scoped-var": 0,
"brace-style": [2, "1tbs", { "allowSingleLine": true }],
"camelcase": 0,
"comma-dangle": [2, "never"],
"comma-spacing": [2, { "before": false, "after": true }],
"comma-style": [2, "last"],
"complexity": 0,
"computed-property-spacing": 0,
"consistent-return": 0,
"consistent-this": 0,
"constructor-super": 2,
"curly": [2, "multi-line"],
"default-case": 0,
"dot-location": [2, "property"],
"dot-notation": 0,
"eol-last": 2,
"eqeqeq": [2, "allow-null"],
"func-names": 0,
"func-style": 0,
"generator-star-spacing": [2, { "before": true, "after": true }],
"guard-for-in": 0,
"handle-callback-err": [2, "^(err|error)$" ],
"indent": [2, 4, { "SwitchCase": 1 }],
"key-spacing": [2, { "beforeColon": false, "afterColon": true }],
"linebreak-style": 0,
"lines-around-comment": 0,
"max-nested-callbacks": 0,
"new-cap": [2, { "newIsCap": true, "capIsNew": false }],
"new-parens": 2,
"newline-after-var": 0,
"no-alert": 0,
"no-array-constructor": 2,
"no-caller": 2,
"no-catch-shadow": 0,
"no-cond-assign": 2,
"no-console": 0,
"no-constant-condition": 0,
"no-continue": 0,
"no-control-regex": 2,
"no-debugger": 2,
"no-delete-var": 2,
"no-div-regex": 0,
"no-dupe-args": 2,
"no-dupe-keys": 2,
"no-duplicate-case": 2,
"no-else-return": 0,
"no-empty": 0,
"no-empty-character-class": 2,
"no-eq-null": 0,
"no-eval": 2,
"no-ex-assign": 2,
"no-extend-native": 2,
"no-extra-bind": 2,
"no-extra-boolean-cast": 2,
"no-extra-parens": 0,
"no-extra-semi": 0,
"no-fallthrough": 2,
"no-floating-decimal": 2,
"no-func-assign": 2,
"no-implied-eval": 2,
"no-inline-comments": 0,
"no-inner-declarations": [2, "functions"],
"no-invalid-regexp": 2,
"no-irregular-whitespace": 2,
"no-iterator": 2,
"no-label-var": 2,
"no-labels": 2,
"no-lone-blocks": 2,
"no-lonely-if": 0,
"no-loop-func": 0,
"no-mixed-requires": 0,
"no-mixed-spaces-and-tabs": 2,
"no-multi-spaces": 2,
"no-multi-str": 2,
"no-multiple-empty-lines": [2, { "max": 1 }],
"no-native-reassign": 2,
"no-negated-in-lhs": 2,
"no-nested-ternary": 0,
"no-new": 2,
"no-new-func": 0,
"no-new-object": 2,
"no-new-require": 2,
"no-new-wrappers": 2,
"no-obj-calls": 2,
"no-octal": 2,
"no-octal-escape": 2,
"no-param-reassign": 0,
"no-path-concat": 0,
"no-process-env": 0,
"no-process-exit": 0,
"no-proto": 0,
"no-redeclare": 2,
"no-regex-spaces": 2,
"no-restricted-modules": 0,
"no-return-assign": 2,
"no-script-url": 0,
"no-self-compare": 2,
"no-sequences": 2,
"no-shadow": 0,
"no-shadow-restricted-names": 2,
"no-spaced-func": 2,
"no-sparse-arrays": 2,
"no-sync": 0,
"no-ternary": 0,
"no-this-before-super": 2,
"no-throw-literal": 2,
"no-trailing-spaces": 2,
"no-undef": 2,
"no-undef-init": 2,
"no-undefined": 0,
"no-underscore-dangle": 0,
"no-unexpected-multiline": 2,
"no-unneeded-ternary": 2,
"no-unreachable": 2,
"no-unused-expressions": 0,
"no-unused-vars": [2, { "vars": "all", "args": "none" }],
"no-use-before-define": 0,
"no-var": 0,
"no-void": 0,
"no-warning-comments": 0,
"no-with": 2,
"object-curly-spacing": 0,
"object-shorthand": 0,
"one-var": [2, { "initialized": "never" }],
"operator-assignment": 0,
"operator-linebreak": [2, "after", { "overrides": { "?": "before", ":": "before" } }],
"padded-blocks": 0,
"prefer-const": 0,
"quote-props": 0,
"quotes": [2, "single", "avoid-escape"],
"radix": 2,
"semi": [2, "always"],
"semi-spacing": 0,
"sort-vars": 0,
"keyword-spacing": [2, {"after": true, "overrides": {"throw": { "after": true}, "return": { "before": true }}}],
"space-before-blocks": [2, "always"],
"space-before-function-paren": [2, "never"],
"space-in-parens": [2, "never"],
"space-infix-ops": 2,
"space-unary-ops": [2, { "words": true, "nonwords": false }],
"spaced-comment": [2, "always", { "markers": ["global", "globals", "eslint", "eslint-disable", "*package", "!"] }],
"strict": 0,
"use-isnan": 2,
"valid-jsdoc": 0,
"valid-typeof": 2,
"vars-on-top": 0,
"wrap-iife": [2, "any"],
"wrap-regex": 0,
"yoda": [2, "never"]
}
}
+3
View File
@@ -0,0 +1,3 @@
.idea
.DS_Store
node_modules
+550
View File
@@ -0,0 +1,550 @@
# v1.3.5
## 05/09/2023
1. [](#improved)
* Various deprecation fixes for PHP 8.2+
# v1.3.4
## 02/19/2023
1. [](#improved)
* Support saving via admin in current language if not translated
# v1.3.3
## 01/04/2023
1. [](#improved)
* Save `post-save` action to session
* Set default `post-save` action to `edit` for create and edit
# v1.3.2
## 12/02/2022
1. [](#improved)
* Various translation enhancements
1. [](#bugfix)
* Fixed frontend editing objects with urlencoded ids
# v1.3.1
## 09/08/2022
1. [](#bugfix)
* Fixed `covnertUrls` action
# v1.3.0
## 06/14/2022
1. [](#new)
* Added user object to `onFlexTask.*` and `onFlexAction.*` events
* Added tasks `MediaUploadMeta` and `MediaReorder` to support remote media fields
* Added support to remove media defined in a field
2. [](#improved)
* Refactored admin controller tasks and actions
* Added image preview support for 3rd party editors
1. [](#bugfix)
* Fixed broken error responses in object media tasks
# v1.2.0
## 03/28/2022
1. [](#new)
* Require **Grav 1.7.32** and **Form 6.0.0**
2. [](#improved)
* Improved flex router event to include directory
3. [](#bugfix)
* Fixed caching issues in dynamic flex forms
* Fixed flex content in unauthorized module causing the whole page to become unauthorized
# v1.1.9
## 03/14/2022
1. [](#new)
* Added support for flex router to return a response instead of a page
# v1.1.8
## 01/28/2022
1. [](#new)
* Require **Grav 1.7.29**
3. [](#improved)
* Made path handling unicode-safe, use new `Utils::basename()` and `Utils::pathinfo()` everywhere
# v1.1.7
## 01/03/2022
1. [](#new)
* Allow intercepting object `create`, `update` and `delete` tasks by using `FlexTaskEvent` event
2. [](#improved)
* Added optional `$scope` parameter to `ObjectController::checkAuthorization()`
3. [](#bugfix)
* Fixed continue task with `PageInterface` types
# v1.1.6
## 11/29/2021
1. [](#bugfix)
* Fixed regression `Call to a member function getRoute() on null` when using CLI [#151](https://github.com/trilbymedia/grav-plugin-flex-objects/issues/151)
# v1.1.5
## 11/24/2021
1. [](#new)
* Added method `ObjectController::checkAuthorizations()` to check if one of the actions is true
2. [](#bugfix)
* Fixed regression when calling flex router with a path
# v1.1.4
## 11/16/2021
1. [](#new)
* Require **Grav 1.7.25**
1. [](#improved)
* Changed flex router not to trigger `onPageNotFound` event
* Changed flex router to be called also with empty path
* If ACL check for the object fails, display unauthorized page instead of 404
1. [](#bugfix)
* Fixed unescaped messages in JSON responses
* Fixed `Call to a member function getName() on null` when using file field [#149](https://github.com/trilbymedia/grav-plugin-flex-objects/issues/149)
# v1.1.3
## 10/26/2021
1. [](#improved)
* Updated JS dependencies to latest
* Optimized import of certain JS dependencies
* Dev: Moved away from deprecated UglifyJsPlugin in favor of TerserPlugin
* Use active form from the Form plugin to get page metadata
* Added page header `flex.access.override: true`, which allows flex to replace page `access` when user is allowed to perform action in flex
1. [](#bugfix)
* Fixed flex object page access for super users when permission was denied
# v1.1.2
## 09/14/2021
1. [](#new)
* Require **Grav 1.7.21**, optionally **Error 1.8.0**, **Login 3.5.2** and **Form 5.1.1**
* Added file upload/delete support to frontend forms
* Support proper error, login and unauthorized pages if all requirements are met
* Added page header `flex.router: [ROUTER]` which triggers `flex.router.[ROUTER]` event for child routes of the page
* Added `flex.[type].task.create.after`, `flex.[type].task.update.after` and `flex.[type].task.delete.after` events for frontend
# v1.1.1
## 09/01/2021
1. [](#bugfix)
* Fixed XSS in page admin
* Fixed check for bad folder name, prevent bad characters
# v1.1.0
## 08/31/2021
1. [](#new)
* Require **Grav 1.7.19** and **Form 5.1.0**
* Added basic frontend editing support
* Added `onBeforeFlexFormInitialize` event to help to initialize the frontend form
1. [](#bugfix)
* Fixed error in admin when field validation fails
# v1.0.16
## 07/19/2021
1. [](#new)
* Added basic new modal support for all flex types
1. [](#bugfix)
* Fixed authorization check for user configuration
# v1.0.15
## 06/16/2021
1. [](#improved)
* Better checks against missing Flex Type inside tasks
* Better authorization checks, falls back to directory level authorization checks if objects do not support authorization
1. [](#bugfix)
* Fixed missing handling of child_type in Flex Pages [getgrav/grav-plugin-admin#2087](https://github.com/getgrav/grav-plugin-admin/issues/2087)
* Added support for multiple `Exports` in a dropdown
* Admin is no longer a dependency of Flex Objects [#130](https://github.com/trilbymedia/grav-plugin-flex-objects/issues/130)
* Fixed authorization checks during page creation for users who have limited access to some pages [getgrav/grav#3382](https://github.com/getgrav/grav/issues/3382)
* Fixed permission check when moving a page [getgrav/grav#3382](https://github.com/getgrav/grav/issues/3382)
# v1.0.14
## 06/07/2021
1. [](#improved)
* Added enhanced copy modal from Pages list [getgrav/grav-plugin-admin#2139](https://github.com/getgrav/grav-plugin-admin/issues/2139)
# v1.0.13
## 06/03/2021
1. [](#bugfix)
* Fixed expert mode for Flex Pages
# v1.0.12
## 06/02/2021
1. [](#bugfix)
* Fixed logic to get form blueprints and object, prevents events from being fired twice
* Fixed breadcrumb item in Pages list not translating HTML entities [#127](https://github.com/trilbymedia/grav-plugin-flex-objects/issues/127)
# v1.0.11
## 05/24/2021
1. [](#improved)
* Allow file uploads to send data such as `data[media_order]`
# v1.0.10
## 05/19/2021
1. [](#bugfix)
* Fixed `Add Folder` not updating the page list until cache is cleared
* Fixed broken error message translations
# v1.0.9
## 04/29/2021
1. [](#bugfix)
* Fixed fatal error when copying a page in admin if no modal is being shown [getgrav/grav#3335](https://github.com/getgrav/grav/issues/3335)
# v1.0.8
## 04/23/2021
1. [](#new)
* Require **Admin 1.10.13**
* Require **Form Plugin 5.0.2**
1. [](#improved)
* Added a few missing translations
* Utilize new Admin detector to prevent Save actions that triggers unsaved notice on unload [getgrav/grav-plugin-admin#2125](https://github.com/getgrav/grav-plugin-admin/issues/2125)
* Improved copying page by adding a modal for new page title and folder name
# v1.0.7
## 04/06/2021
1. [](#new)
* Require **Grav 1.7.10**
* Added deny option support to `filepicker` field [#119](https://github.com/trilbymedia/grav-plugin-flex-objects/pull/119)
1. [](#bugfix)
* Prevent expert editing mode from anyone else than super users [grav-plugin-admin#2094](https://github.com/getgrav/grav-plugin-admin/issues/2094)
* Fixed not being able to add new folder [grav#3293](https://github.com/getgrav/grav/issues/3293)
* Fixed Flex directories defined only in theme not showing up [grav#3292](https://github.com/getgrav/grav/issues/3292)
# v1.0.6
## 03/30/2021
1. [](#bugfix)
* Fixed automatic git-sync in admin save and delete [#120](https://github.com/trilbymedia/grav-plugin-flex-objects/issues/120)
* Prevent Add Page / Add Module modals from closing if clicking on the outside overlay [grav-plugin-admin#2089](https://github.com/getgrav/grav-plugin-admin/issues/2089)
# v1.0.5
## 03/19/2021
1. [](#new)
* Require **Grav 1.7.9**
* Require **Form Plugin 5.0.1**
1. [](#improved)
* Catch JSON decoding issues in controllers
1. [](#bugfix)
* Fixed broken media upload/picker fields with `@self/path` notations [grav#3275](https://github.com/getgrav/grav/issues/3275)
* Fixed `filepicker` field not including newly uploaded and excluding newly deleted files before saving the object
* Fixed `Flex Page` CRUD ACL when creating a new page [#115](https://github.com/trilbymedia/grav-plugin-flex-objects/issues/115)
* Bumped dependencies versions [#116](https://github.com/trilbymedia/grav-plugin-flex-objects/issues/116)
* Fixed clicking `move` button on some pages resulting in endless loading spinner [grav-plugin-admin#2095](https://github.com/getgrav/grav-plugin-admin/issues/2095)
# v1.0.4
## 03/17/2021
1. [](#improved)
* Added id attributes for buttons to help on acceptance testing
1. [](#bugfix)
* Fixed fatal error in `/admin/flex-objects` [#114](https://github.com/trilbymedia/grav-plugin-flex-objects/issues/114)
* Fixed `onAdminSave` original page having empty header [grav#3259](https://github.com/getgrav/grav/issues/3259)
* Fixed flash issues on uploading files into a new page
# v1.0.3
## 02/17/2021
1. [](#improved)
* List field: added new `placement` property to decide whether to add new items at the top, bottom or based on the *position* of the clicked button [#105](https://github.com/trilbymedia/grav-plugin-flex-objects/pull/105)
* Added default styling for Flex-Objects Admin list view
1. [](#bugfix)
* Fixed fatal error if configuration is missing directories [#107](https://github.com/trilbymedia/grav-plugin-flex-objects/issues/107)
* Fixed case-sensitive `accept` in `filepicker` field
* Fixed pages admin being accessible without read/write permissions [grav-plugin-admin#2053](https://github.com/getgrav/grav-plugin-admin/issues/2053)
* Fixed missing event `onAdminCreatePageFrontmatter` when creating a new page [grav-plugin-auto-date#8](https://github.com/getgrav/grav-plugin-auto-date/issues/8)
* Fixed missing event `onAdminAfterDelMedia` when deleting a file from a page
* Fixed filepicker support for old `theme@:/` and `page@:/` notations [#109](https://github.com/trilbymedia/grav-plugin-flex-objects/issues/109)
* Fixed adding the same new page twice remembering content from the last try
* Fixed saving a new page with invalid data makes blueprint fields disappear [grav-plugin-admin#2068](https://github.com/getgrav/grav-plugin-admin/issues/2068)
# v1.0.2
## 02/01/2021
1. [](#new)
* Require **Grav 1.7.4**
1. [](#bugfix)
* Fixed saving page in expert mode [grav#3174](https://github.com/getgrav/grav/issues/3174)
# v1.0.1
## 01/20/2021
1. [](#bugfix)
* Fixed 404 when trying to edit a page with accented characters [grav-plugin-admin#2026](https://github.com/getgrav/grav-plugin-admin/issues/2026)
# v1.0.0
## 01/19/2021
1. [](#new)
* Added `$grav['flex_objects']->getAdminController()` method
1. [](#improved)
* Added support for relative paths in `getLevelListing` action
1. [](#bugfix)
* Fixed admin not working with types that do not implement `FlexAuthorizeInterface`
* Fixed bad redirect when creating new flex object and choosing to create another return to the list
* Fixed bad redirect when changing parent of new page and saving [grav-plugin-admin#2014](https://github.com/getgrav/grav-plugin-admin/issues/2014)
* Fixed page forms being empty if multi-language is enabled, but there's just one language [grav#3147](https://github.com/getgrav/grav/issues/3147)
* Fixed copying a page within a parent with no create permission [grav-plugin-admin#2002](https://github.com/getgrav/grav-plugin-admin/issues/2002)
# v1.0.0-rc.20
## 12/15/2020
1. [](#improved)
* Default cookies usage to SameSite Lax [grav-plugin-admin#1998](https://github.com/getgrav/grav-plugin-admin/issues/1998)
* Fixed typo [#89](https://github.com/trilbymedia/grav-plugin-flex-objects/issues/89)
# v1.0.0-rc.19
## 12/02/2020
1. [](#improved)
* Just keeping sync with Grav rc.19
# v1.0.0-rc.18
## 12/02/2020
1. [](#new)
* Require **PHP 7.3.6**
1. [](#improved)
* Improved frontend templates
* Improve blueprint structure
* Hooked up Duplicate and Move from within Pages list [#81](https://github.com/trilbymedia/grav-plugin-flex-objects/issues/81)
* Respect CRUD ACL actions for items shortcuts in pages list [#82](https://github.com/trilbymedia/grav-plugin-flex-objects/issues/82)
* Refresh object on controllers to make sure it is up to date
1. [](#bugfix)
* Fixed fatal error in admin if list view hasn't been defined
* Fixed fatal error in admin if directory throws exception
* Fixed attempts to add an existing page
* Fixed form loosing its form state if saving fails when using `ObjectController`
* Fixed missing context when rendering collection in frontend
* Fixed Flex Admin activating on too old Admin plugin versions
# v1.0.0-rc.17
## 10/07/2020
1. [](#bugfix)
* Fixed media uploads for objects which do not implement `FlexAuthorizeInterface`
* Fixed file picker field not recognizing `folder: @self` variants
# v1.0.0-rc.16
## 09/01/2020
1. [](#improved)
* Simplified `Flex Pages` admin not to differentiate between default language file extensions [#47](https://github.com/trilbymedia/grav-plugin-flex-objects/issues/47)
1. [](#bugfix)
* Fixed extra space in Flex admin pages
* Fixed folder creation with parent other than root [#66](https://github.com/trilbymedia/grav-plugin-flex-objects/issues/66)
* Fixed task redirects in sub-folder multi-site environments
* Fixed typo in default permissions (should have been `admin.flex-objects`) [grav#2915](https://github.com/getgrav/grav/issues/2915)
# v1.0.0-rc.15
## 07/22/2020
1. [](#new)
* Released with no changes to keep sync with Grav + Admin
# v1.0.0-rc.14
## 07/09/2020
1. [](#new)
* Released with no changes to keep sync with Grav + Admin
# v1.0.0-rc.13
## 07/01/2020
1. [](#bugfix)
* Fixed bad link in directory listing template
* Fixed admin save task displaying error message about non-existing data type
* Fixed `pagemedia` field not uploading/deleting files right away
* Fixed `Flex Pages` add, copy and move buttons appearing in edit view when no permissions
* Fixed `Flex Pages` permission issues
* Fixed some admin redirect issues
# v1.0.0-rc.12
## 06/08/2020
1. [](#new)
* Code updates to match Grav 1.7.0-rc.12
1. [](#improved)
* Changed class `admin-pages` to `admin-{{ target }}` [#59](https://github.com/trilbymedia/grav-plugin-flex-objects/issues/59)
# v1.0.0-rc.11
## 05/14/2020
1. [](#new)
* Added integration with Admin's new preset events to style the CSS
1. [](#improved)
* JS Maitenance
1. [](#bugfix)
* Fixed `Accounts` Configuration tab
# v1.0.0-rc.10
## 04/27/2020
1. [](#bugfix)
* Fixed custom actions not working
* Fixed custom folder in `mediapicker` field not working
* Fixed export title when not using CVS [#51](https://github.com/trilbymedia/grav-plugin-flex-objects/issues/51)
* Fixed preview in Page list view [admin#1845](https://github.com/getgrav/grav-plugin-admin/issues/1845)
* Fixed `404 Not Found` error after saving a new object
# v1.0.0-rc.9
## 03/20/2020
1. [](#bugfix)
* Fixed issue with touch devices and scrollbars hidden, preventing native scrolling to work [admin#1857](https://github.com/getgrav/grav-plugin-admin/issues/1857) [#1858](https://github.com/getgrav/grav-plugin-admin/issues/1858)
# v1.0.0-rc.8
## 03/19/2020
1. [](#new)
* Added a basic **Convert Data** CLI Command. Works with `Yaml` <-> `Json`
1. [](#bugfix)
* Fixed jump of the page when applying filters [grav-admin#1830](https://github.com/getgrav/grav-plugin-admin/issues/1830)
* Fixed form resetting when validation fails [grav#2764](https://github.com/getgrav/grav/issues/2764)
# v1.0.0-rc.7
## 03/05/2020
1. [](#new)
* Added option to change perPage amount of items in Flex List. 'All' also available by only at runtime.
1. [](#improved)
* Page filters now obey admin hide type settings
1. [](#bugfix)
* Fixed fatal error if there is missing blueprint [grav#2834](https://github.com/getgrav/grav/issues/2834)
* Fixed redirect when moving a page [grav#2829](https://github.com/getgrav/grav/issues/2829)
* Fixed no default access set when creating new user from admin [#31](https://github.com/trilbymedia/grav-plugin-flex-objects/issues/31)
* Flex Pages: Fixed page visibility issues when creating a new page [grav#2823](https://github.com/getgrav/grav/issues/2823)
* Flex Pages: Fixed translated page having non-translated status with `system.languages.include_default_lang_file_extension: false`
* Flex Pages: Fixed preview on home page
# v1.0.0-rc.6
## 02/11/2020
1. [](#new)
* Pass phpstan level 1 tests
* Removed legacy classes for pages, cleanup deprecated Flex types
1. [](#bugfix)
* Fixed call to `grav.flex_objects.getObject()` causing fatal error
* Minor bug fixes
# v1.0.0-rc.5
## 02/03/2020
1. [](#new)
* No changes, just keeping things in sync with Grav RC version
# v1.0.0-rc.4
## 02/03/2020
1. [](#new)
* Added support for arbitrary admin menu route for editing a flex type
* Added support for new improved ACL
* Added support for custom layouts by adding `/:layout_name` in url
* Added support for Flex Directory specific Configuration
* Added support for action aliases (`/accounts/configure` instead of `/accounts/users/:configre`)
* Added Flex type `Configuration`
* Enabled `Pages`, `Accounts` and `User Groups` by default
* Stop using deprecated `onAdminRegisterPermissions` event
* Renamed directory `grav-pages` to `pages`
* Renamed directory `grav-accounts` to `user-accounts`
* Renamed directory `grav-user-groups` to `user-groups`
1. [](#improved)
* Flex caching settings were moved into Grav core
* Flex Objects plugin now better integrates to Grav core
1. [](#bugfix)
* Fixed empty directory entries in plugin configuration
* Fixed plugin configuration displaying directories outside of the plugin
* Fixed broken blueprints if there's folder with the name of the blueprint file
* Fixed visible save button when in 404 page
* Fixed missing save location when file does not exist
* Fixed multiple ACL related issues (no access, bad links, information leaks)
* Fixed Admin Panel Page list buttons not appearing in Flex Pages
# v1.0.0-rc.3
## 01/02/2020
1. [](#new)
* Added root page support for `Flex Pages`
1. [](#bugfix)
* Fixed after save: Edit
* Fixed JS failing on initial filters setup due to no fallback implemented [#2724](https://github.com/getgrav/grav/issues/2724)
# v1.0.0-rc.2
## 12/04/2019
1. [](#new)
* Admin: Added support for editing `User Groups`
* Admin: `Flex Pages` now support **searching** and **filtering**
1. [](#bugfix)
* Hide hidden/system types (pages, accounts, user groups) from Flex Objects page type [#38](https://github.com/trilbymedia/grav-plugin-flex-objects/issues/38)
# v1.0.0-rc.1
## 11/06/2019
1. [](#new)
* Added directory configuration option for custom admin templates
* Added `Flex Accounts (Admin)` type to administer user accounts in Flex independently from Grav system setting
* Added `Flex Pages (Admin)` type to administer pages in Flex independently from Grav system setting
* Added blueprint option to hide directory from Flex Objects types page in frontend
* Deprecated all `Flex Page` classes and traits in favor of the new classes in Grav core
* Moved flex object/collection templates to `templates/flex/{TYPE}` which is easier to remember
* Admin: Added support customizable preview and export
1. [](#improved)
* Admin: Allow custom title template when editing object
* Translations: rename MODULAR to MODULE everywhere
1. [](#bugfix)
* Flex Pages: Fixed default language not being translated in both `translatedLanguages()` and `untranslatedLanguages()` results
* Flex Pages: Language interface compatibility fixes
* Flex Pages: Fixed frontend issues with plugin events [#5](https://github.com/trilbymedia/grav-plugin-flex-objects/issues/5)
* Flex Pages: Fixed `filePathClean()` and `filePathClean()` not returning file for folder
* Flex Pages: Fixed multiple multi-language related issues in admin [#10](https://github.com/trilbymedia/grav-plugin-flex-objects/issues/10)
* Flex Pages: Fixed raw edit mode
* File upload is broken for nested fields [#34](https://github.com/trilbymedia/grav-plugin-flex-objects/issues/34)
# v1.0.0-beta.10
## 10/03/2019
1. [](#bugfix)
* Flex Pages: Fixed moving visible page in admin causing ordering issues [#6](https://github.com/trilbymedia/grav-plugin-flex-objects/issues/6)
* Flex Pages List: Fixed issue where auto-hiding scrollbars in macOS would throw off the dropdown position [#20](https://github.com/trilbymedia/grav-plugin-flex-objects/issues/20)
* Flex Pages: Fixed prev/next page missing pages if pagination was turned on in page header
# v1.0.0-beta.9
## 09/26/2019
1. [](#improved)
* Show/hide dropdown menu as needed when scrolling the page columns container left and right
1. [](#bugfix)
* PHP 7.1: Fixed error when activating `Flex Pages` in Plugin parameters [#13](https://github.com/trilbymedia/grav-plugin-flex-objects/issues/13)
* Flex Pages: Fixed page template cannot be changed [#4](https://github.com/trilbymedia/grav-plugin-flex-objects/issues/4)
* Flex Pages: Fixed new pages being created with wrong template [#22](https://github.com/trilbymedia/grav-plugin-flex-objects/issues/22)
* Flex Pages: Fixed `Preview` not working [#17](https://github.com/trilbymedia/grav-plugin-flex-objects/issues/17)
* Fixed error caused by automatic path selection from cookie when destination not available [#23](https://github.com/trilbymedia/grav-plugin-flex-objects/issues/23)
* Fixed breadcrumb issue in Flex Pages List [#19](https://github.com/trilbymedia/grav-plugin-flex-objects/issues/19)
* Flex Pages: Fixed unable to change page template [#4](https://github.com/trilbymedia/grav-plugin-flex-objects/issues/4)
* Fixed `Error 404` when adding new contact [#14](https://github.com/trilbymedia/grav-plugin-flex-objects/issues/14)
* Flex Pages: Non-visible items appear in Nav menu [#24](https://github.com/trilbymedia/grav-plugin-flex-objects/issues/24)
* Disabling plugin breaks saving plugin configuration [#11](https://github.com/trilbymedia/grav-plugin-flex-objects/issues/11)
# v1.0.0-beta.8
## 09/19/2019
1. [](#new)
* Initial public release (all previous versions were in a private repo)
+21
View File
@@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2018 Trilby Media, LLC
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+289
View File
@@ -0,0 +1,289 @@
# Flex Objects Plugin
## About
The **Flex Objects** Plugin is for [Grav CMS](https://github.com/getgrav/grav). Flex objects is a powerful new plugin that allows you to build custom collections of objects, which can modified by CRUD operations via the admin plugin to easily manage large sets of data that don't make sense as simple YAML configuration files, or Grav pages. These objects are defined by blueprints written in YAML and they are rendered by a set of twig files. Additionally both objects and collections can be customized by PHP classes, which allows you to define complex behaviors and relationships between the objects.
![](assets/flex-objects-list.png)
![](assets/flex-objects-edit.png)
![](assets/flex-objects-options.png)
## System Requirements
Plugin requires **Grav** v1.7.25 or later version in order to run. Additionally you need **Form Plugin** v5.1.0 and optionally **Admin Plugin** v1.10.25 or later version.
## Installation
Typically a plugin should be installed via [GPM](http://learn.getgrav.org/advanced/grav-gpm) (Grav Package Manager):
```
$ bin/gpm install flex-objects
```
Alternatively it can be installed via the [Admin Plugin](http://learn.getgrav.org/admin-panel/plugins)
## Sample Data
Once installed you can either create entries manually, or you can copy the sample data set:
```shell
$ mkdir -p user/data/flex-objects
$ cp user/plugins/flex-objects/data/flex-objects/contacts.json user/data/flex-objects/contacts.json
```
## Configuration
This plugin works out of the box, but provides several fields that make modifying and extending this plugin easier:
```yaml
enabled: true
built_in_css: true
extra_admin_twig_path: 'theme://admin/templates'
admin_list:
per_page: 15
order:
by: updated_timestamp
dir: desc
directories:
- 'blueprints://flex-objects/contacts.yaml'
- 'blueprints://flex-objects/pages.yaml'
- 'blueprints://flex-objects/user-accounts.yaml'
- 'blueprints://flex-objects/user-groups.yaml'
```
Simply edit the **Flex Objects** plugin options in the Admin plugin, or copy the `flex-objects.yaml` default file to your `user/config/plugins/` folder and edit the values there. Read below for more help on what these fields do and how they can help you modify the plugin.
Most interesting configuration option is `directories`, which contains list or blueprint files which will define the flex types.
## Displaying
![](assets/flex-objects-site.png)
just create a page called `flex-objects.md` or set the template of your existing page to `template: flex-objects`. This will use the `flex-objects.html.twig` file provided by the plugin.
```twig
---
title: Directory
flex:
directory: contacts
---
# Directory Example
```
If you do not specify `flex.directory` name in the page header, the page will list all directories instead of displaying entries from a single directory.
![](assets/flex-objects-directory.png)
# Modifications
This plugin is configured with a sample contacts directory with a few sample fields:
* published
* first_name
* last_name
* email
* website
* tags
These are probably not the exact fields you might want, so you will probably want to change them. This is pretty simple to do with Flex Objects, you just need to change the **Blueprints** and the **Twig Templates**. This can be achieved simply enough by copying some current files and modifying them.
Let's assume you simply want to add a new "Phone Number" field to the existing Data and remove the "Tags". These are the steps you would need to perform:
1. Copy the `blueprints/flex-objects/contacts.yaml` Blueprint file to another location, let's say `user/blueprints/flex-objects/`. The file can really be stored anywhere, but if you are using admin, it is best to keep the blueprint file where admin can automatically find it.
!!! **NOTE:** If you want to put the blueprints to `user/themes/yourtheme/blueprints`, you need to use the new blueprint folder structure from Grav 1.7. See [Plugin/Theme Blueprints](https://learn.getgrav.org/17/advanced/grav-development/grav-17-upgrade-guide#plugin-theme-blueprints-blueprints-yaml).
2. Edit the `user/blueprints/flex-objects/contacts.yaml` like so:
```yaml
title: Contacts
description: Simple contact directory with tags.
type: flex-objects
config:
admin:
list:
title: name
fields:
published:
field:
type: toggle
label: Publ
width: 8
last_name:
link: edit
first_name:
link: edit
email:
phone:
data:
storage:
class: 'Grav\Framework\Flex\Storage\SimpleStorage'
options:
formatter:
class: 'Grav\Framework\File\Formatter\JsonFormatter'
folder: user-data://flex-objects/contacts.json
form:
validation: loose
fields:
published:
type: toggle
label: Published
highlight: 1
default: 1
options:
1: PLUGIN_ADMIN.YES
0: PLUGIN_ADMIN.NO
validate:
type: bool
required: true
last_name:
type: text
label: Last Name
validate:
required: true
first_name:
type: text
label: First Name
email:
type: email
label: Email Address
validate:
required: true
website:
type: url
label: Website URL
phone:
type: text
label: Phone Number
```
See how we replaced `tags:` with `phone:` in the `config.admin.list.fields` section at the top. Also, notice how we removed the `tags:` Blueprint field definition, and added a simple text field for `phone:`. If you have questions about available form fields, [check out the extensive documentation](https://learn.getgrav.org/forms/blueprints/fields-available) on the subject.
3. We need to copy the frontend Twig file and modify it to add the new "Phone" field. By default your theme already has its `templates`, so we can take advantage of it <sup>2</sup>. We'll simply copy the `user/plugins/flex-objects/templates/flex/contacts/object/default.html.twig` file to `user/themes/quark/templates/flex/contacts/object/default.html.twig`. Notice, there is no reference to `admin/` here, this is site template, not an admin one. We are also assuming you are using `Quark` theme, so you may have to change this to reference the theme you are using.
4. Edit the `default.html.twig` file you just copied so it has these modifications:
```twig
<div class="entry-details">
{% if object.website %}
<a href="{{ object.website }}"><span class="name">{{ object.last_name }}, {{ object.first_name }}</span></a>
{% else %}
<span class="name">{{ object.last_name }}, {{ object.first_name }}</span>
{% endif %}
{% if object.email %}
<p><a href="mailto:{{ object.email }}" class="email">{{ object.email }}</a></p>
{% endif %}
{% if object.phone %}
<p class="phone">{{ object.phone }}</p>
{% endif %}
</div>
```
Notice, we removed the `entry-extra` DIV, and added a new `if` block with the Twig code to display the phone number if set.
5. We also need to tweak the JavaScript initialization which provides which hooks up certain classes to the search. To do this we need to copy the `user/plugins/flex-objects/templates/flex/contacts/collection/default.html.twig` file to `user/themes/quark/templates/flex/contacts/collection/default.html.twig`. Notice this is the `collection` template this time, not the `object` template as we copied before.
Edit this file and replace the `<script></script>` tag at the bottom with this code:
```html
<script>
var options = {
valueNames: [ 'name', 'email', 'website', 'phone' ]
};
var userList = new List('flex-objects', options);
</script>
```
# File Upload
To upload files you can use the `file` form field. [The standard features apply](https://learn.getgrav.org/forms/blueprints/how-to-add-file-upload), and you can simply edit your custom blueprint with a field definition similar to:
```
item_image:
type: file
label: Item Image
random_name: true
destination: 'user/data/flex-objects/files'
multiple: true
```
> In order to fully take advantage of image uploads, you should always be using `FolderStorage`, meaning that the objects get saved to individual folders together with the images. Other storage layers may or may not support media.
# Advanced
You can radically alter the structure of the `contacts.json` data file by making major edits to the `contacts.yaml` blueprint file. However, it's best to start with an empty `contacts.json` if you are making wholesale changes or you will have data conflicts. Best to create your blueprint first. Reloading a **New Entry** until the form looks correct, then try saving, and check to make sure the stored `user/data/flex-objects/contacts.json` file looks correct.
Then you will need to make more widespread changes to the site Twig templates. You might need to adjust the number of columns and the field names. You will also need to pay attention to the JavaScript initialization in each template.
# Features
Here are the main benefits of using Flex objects:
* CRUD is automatically handled for you by Flex Objects plugin
* Objects can be stored using many different strategies, including single file, file per object or folder per object; using yaml, json etc.
* Flex types can be easily extended by custom PHP collection and object classes
* Both Flex objects and collections know how to render themselves: `echo $object->render($layout, $context)` or `{% render object layout: layout with context %}`
* You can easily create custom layouts for your objects and collections to be used in different pages
* Both Flex objects and collections support serialization and `json_encode()`
* Flex objects support Grav `Medium` objects with few lines of code
* Flex objects can have relations to other Flex objects with few lines of code defining the relation
* Flex directories support indexes which allow searching objects without loading all of them
* Efficient caching for indexes, searches, objects and rendered output
# Limitations and future improvements
Right now there are a few limitations:
* Frontend only has a basic routing for the individual pages (you need to do the advanced routing manually by yourself)
* Administration needs more features like filtering, bulk updates etc
* It would be nice to have an easy way to display Flex admin in other admin plugins (it is already possible, but not easy)
* Optional database storage layer would be nice to have
* We need general collection functions to do simple filtering, like: "display all published items" without custom PHP code
### Notes:
1. You can actually use pretty much any folder under the `user/` folder of Grav. Simply edit the **Extra Admin Twig Path** option in the `flex-objects.yaml` file. It defaults to `theme://admin/templates` which means it uses the default theme's `admin/templates/` folder if it exists.
2. You can use any path for front end Twig templates also, if you don't want to put them in your theme, you can add an entry in the **Extra Site Twig Path** option of the `flex-objects.yaml` configuration and point to another location.
# Tricks and tips
* You can enable and disable directories from **Plugins** > **Flex Objects**
* New Flex Directories can be registered by simply creating a new blueprint file in `user/blueprints/flex-objects` folder
* You can also add types from your plugins by hooking into `onFlexInit` event (see `AccountsServiceProvider` in Grav)
* To properly create your own custom types, you need at least the object blueprint and the template files for collections and objects
* Use `flex-objects.md` page to create entry point for your own directory
* In page header you can use nested `flex.directory` variable to define the directory (or do it in admin)
* In Admin you can just select the directory under the page title
# Parameters supported by Flex page type:
```
---
title: 'Flex Directories'
flex:
directories:
layout: default
list:
- accounts
- contacts
---
```
`directories.layout`: uses template file `templates/flex-objects/directories/[LAYOUT].html.twig`
`directories.list`: list of flex directories displayed in this page
@@ -0,0 +1,30 @@
{%- set user = admin.user -%}
{%- set route = controller.route -%}
{%- set type = directory.config('admin.template') ?? target -%}
{# Set action from ?preview=1 #}
{%- if key and uri.currentUri().queryParam('preview') %}
{% set action = 'preview' %}
{% endif -%}
{%- set template -%}
{%- if action == 'add' -%}
edit
{%- elseif action == 'delete' -%}
list
{%- else -%}
{{- action ?: task ?: 'types' -}}
{%- endif -%}
{%- endset -%}
{%- set separator = config.system.param_sep -%}
{%- set view_config = directory.config('admin.views.' ~ template) ?? directory.config('admin.' ~ template) ?? [] -%}
{%- include target ? [
'flex-objects/types/' ~ type ~ '/' ~ template ~ '.html.twig',
'flex-objects/types/default/' ~ template ~ '.html.twig',
'flex-objects/layouts/404.html.twig'
] : [
'flex-objects/types/default/' ~ template ~ '.html.twig',
'flex-objects/layouts/404.html.twig'
] -%}
@@ -0,0 +1 @@
{{- admin.json_response|json_encode|raw -}}
@@ -0,0 +1 @@
{{ 'PLUGIN_FLEX_OBJECTS.ERROR.LAYOUT_NOT_FOUND'|tu(template, null) }}
@@ -0,0 +1,3 @@
<a id="titlebar-button-add" class="button" href="{{ route.withGravParam('', 'add').toString(true) }}">
<i class="fa fa-plus"></i> {{ 'PLUGIN_ADMIN.ADD'|tu }}
</a>
@@ -0,0 +1,3 @@
<a id="titlebar-button-back" class="button" href="{{ back_url }}">
<i class="fa fa-reply"></i> {{ "PLUGIN_ADMIN.BACK"|tu }}
</a>
@@ -0,0 +1,7 @@
{%- set authorize = directory.config('admin.views.configure.authorize') ?? directory.config('admin.configure.authorize') ?? 'admin.super' %}
{%- if configure_url and user.authorize(authorize) %}
<a id="titlebar-button-configure" class="button" href="{{ configure_url }}">
<i class="fa fa-cog"></i> {{ 'PLUGIN_ADMIN.CONFIGURATION'|tu }}
</a>
{% endif %}
@@ -0,0 +1,3 @@
<a id="titlebar-button-delete" href="#delete" data-remodal-target="delete" data-delete-url="{{ uri.addNonce(route.withoutParams().withGravParam('task', 'delete').getUri(), 'admin-form', 'admin-nonce') }}" class="button danger">
<i class="fa fa-fw fa-trash-o"></i> {{ 'PLUGIN_ADMIN.DELETE'|tu }}
</a>
@@ -0,0 +1,3 @@
<a id="titlebar-button-export-csv" class="button" href="{{ route.withGravParam('', 'csv').toString(true) }}">
<i class="fa fa-download"></i> {{ 'PLUGIN_FLEX_OBJECTS.CSV'|tu }}
</a>
@@ -0,0 +1,21 @@
{% if export.options %}
<div id="titlebar-button-export" class="button-group">
<button id="titlebar-button-export" type="button" class="button dropdown-toggle" data-toggle="dropdown">
<i class="fa fa-download"></i> {{ 'PLUGIN_ADMIN.EXPORT'|tu }} <i class="fa fa-caret-down"></i>
</button>
<ul class="dropdown-menu lang-switcher">
{% for type,option in export.options %}
<li>
<a id="titlebar-button-export" class="button" href="{{ route.withGravParam('', 'export').withQueryParam('type', type).toString(true) }}">
{{ option.title|tu }}
</a>
</li>
{% endfor %}
</ul>
</div>
{% else %}
<a id="titlebar-button-export" class="button" href="{{ route.withGravParam('', 'export').toString(true) }}">
<i class="fa fa-download"></i> {{ export.title ?? (export.formatter.class ? 'PLUGIN_ADMIN.EXPORT'|tu : 'PLUGIN_FLEX_OBJECTS.CSV'|tu) }}
</a>
{% endif %}
@@ -0,0 +1,18 @@
<div id="titlebar-languages" class="button-group">
<button type="button" class="button disabled">
<i class="fa fa-flag-o"></i>
{{ all_languages[language] ?? language }}
</button>
{% if admin_languages|length > (language in admin_languages)|int %}
<button type="button" class="button dropdown-toggle" data-toggle="dropdown">
<i class="fa fa-caret-down"></i>
</button>
<ul class="dropdown-menu language-switcher">
{% for lang_code in admin_languages %}
{% if lang_code != language %}
<li><a href="{{ admin_route(route.getRoute(1), lang_code) }}">{{ all_languages[lang_code] ?? lang_code }}</a></li>
{% endif %}
{% endfor %}
</ul>
{% endif %}
</div>
@@ -0,0 +1,5 @@
{% if preview_url %}
<a id="titlebar-button-preview-open" href="{{ preview_url }}" class="button" target="_blank">
<i class="fa fa-external-link"></i> {{ "PLUGIN_ADMIN.OPEN_NEW_TAB"|tu }}
</a>
{% endif %}
@@ -0,0 +1,3 @@
<a id="titlebar-button-preview" href="{{ route.withGravParam('', 'preview').toString(true) }}" class="button">
<i class="fa fa-eye"></i> {{ "PLUGIN_ADMIN.PREVIEW"|tu }}
</a>
@@ -0,0 +1,4 @@
{% set task = task ?? 'save' %}
<button id="titlebar-button-save" class="button" type="submit" name="task" value="{{ task }}" form="blueprints" data-flex-safe-action>
<i class="fa fa-check"></i> {{ "PLUGIN_ADMIN.SAVE"|tu }}
</button>
@@ -0,0 +1,103 @@
{% extends 'partials/base.html.twig' %}
{% use 'flex-objects/types/default/titlebar/configure.html.twig' %}
{% set name = view_config['form'] %}
{% set form = form ?? directory.directoryForm(name) %}
{# Allowed actions #}
{% set can_save = can_save ?? user.authorize(view_config['authorize'] ?? 'admin.super') %}
{# These variables can be overridden from the main template file #}
{% set allowed = allowed ?? (directory and form and can_save) %}
{% set back_route = back_route ?? ('/' ~ route.getRoute(1)) %}
{% set title_icon = title_icon ?? view_config['icon'] ?? 'fa-cog' %}
{% set title -%}
{%- set title_config = view_config['title'] ?? null -%}
{%- if title_config.template -%}
{{- include(template_from_string(title_config.template, 'configure title template')) -}}
{%- else -%}
{{- directory.title|tu }} - {{ 'PLUGIN_ADMIN.CONFIGURATION'|tu -}}
{% endif %}
{%- endset %}
{% macro spanToggle(input, length) %}
{{ (repeat('&nbsp;&nbsp;', (length - input|length) / 2) ~ input ~ repeat('&nbsp;&nbsp;', (length - input|length) / 2))|raw }}
{% endmacro %}
{% import _self as macro %}
{% block body %}
{% set back_url = back_url ?? admin_route(back_route) %}
{{ parent() }}
{% endblock body %}
{% block content_top %}
{% if allowed and user.authorize('admin.super') %}
{% set save_location = directory.getDirectoryConfigUri(name) %}
<div class="alert notice">{{ "PLUGIN_ADMIN.SAVE_LOCATION"|tu }}: <b>{{ url(save_location, false, true)|trim('/') }}</b></div>
{% endif %}
{% endblock %}
{% block topbar %}
{% if user.authorize('admin.super') %}
<form id="admin-mode-toggle">
{% set normalText = 'PLUGIN_ADMIN.NORMAL'|tu %}
{% set expertText = 'PLUGIN_ADMIN.EXPERT'|tu %}
{% set maxLen = max([normalText|length, expertText|length]) %}
{% set normalText = macro.spanToggle(normalText, maxLen) %}
{% set expertText = macro.spanToggle(expertText, maxLen) %}
<div class="switch-toggle switch-grav">
<input type="radio" value="normal" data-leave-url="{{ route.withGravParam('mode', 'normal').toString(true) }}" id="normal" name="mode-switch" class="highlight" {% if admin.session.expert == '0' %} checked="checked"{% endif %}>
<label for="normal">{{ normalText|raw }}</label>
<input type="radio" value="expert" data-leave-url="{{ route.withGravParam('mode', 'expert').toString(true) }}" id="expert" name="mode-switch" class="highlight" {% if admin.session.expert == '1' %} checked="checked"{% endif %}>
<label for="expert">{{ expertText|raw }}</label>
<a></a>
</div>
</form>
{% endif %}
{% endblock topbar %}
{% block content %}
{{ parent() }}
{% if allowed %}
<div class="clear directory admin-flex-config">
<div class="admin-form-wrapper">
{# TODO: RAW MODE
<div id="admin-topbar">
{{ block('topbar') }}
</div>
#}
{% block edit %}
{% include 'partials/blueprints.html.twig' with { form: form, data: form.data } %}
{% endblock %}
</div>
</div>
{% include 'partials/modal-changes-detected.html.twig' %}
{% else %}
{% do page.modifyHeader('http_response_code', 404) %}
<div class="error-block">
<h1>{{ 'PLUGIN_ADMIN.ERROR'|tu }} 404</h1>
<div class="padding">
<p>
{{ 'PLUGIN_FLEX_OBJECTS.ERROR.PAGE_NOT_EXIST'|tu }}
</p>
</div>
</div>
{% endif %}
{% endblock %}
{% block bottom %}
{{ parent() }}
{# TODO: RAW MODE
<script>
$('.admin-flex-config .form-tabs .tabs-nav').css('margin-right', ($('#admin-topbar').width() + 20) + 'px');
</script>
#}
{% endblock bottom %}
@@ -0,0 +1,121 @@
{% extends 'partials/base.html.twig' %}
{% use 'flex-objects/types/default/titlebar/edit.html.twig' %}
{# Avoid defining form and object twice: object should always come from the form! #}
{% if form is not defined %}
{% set form = object.form %}
{% set object = form.object %}
{% endif %}
{# Allowed actions #}
{% set can_list = can_list ?? directory.isAuthorized('list', 'admin', user) %}
{% set can_read = can_read ?? (object.exists ? object.isAuthorized('read', 'admin', user) : directory.isAuthorized('create', 'admin', user)) %}
{% set can_create = can_create ?? object.isAuthorized('create', 'admin', user) %}
{% set can_save = can_save ?? (object.exists ? object.isAuthorized('update', 'admin', user) : directory.isAuthorized('create', 'admin', user)) %}
{% set can_delete = can_delete ?? (object.exists and object.isAuthorized('delete', 'admin', user)) %}
{% set can_translate = can_translate ?? (admin.multilang and object.hasFlexFeature('flex-translate')) %}
{% set can_preview = can_preview ?? (can_read and object.exists and (directory.config('admin.views.preview.enabled') ?? directory.config('admin.preview.enabled', false))) %}
{# Translations #}
{% if can_translate %}
{% set translate_include_default = translate_include_default ?? grav.config.get('system.languages.include_default_lang_file_extension', true) %}
{% set all_languages = grav.admin.siteLanguages %}
{% set admin_languages = admin.languages_enabled %}
{% set default_language = grav.language.default %}
{% set object_language = object.language %}
{% set language = controller.language %}
{% set has_translation = object.hasTranslation(language, false) %}
{#
{% if translate_include_default %}
{% set all_languages = all_languages|merge({'': 'Default'}) %}
{% set admin_languages = admin_languages|merge({'': ''}) %}
{% set object_languages = object.languages(true) %}
{% else %}
#}
{% set language = language ?: default_language %}
{% set object_language = object_language ?: default_language %}
{% set object_languages = object.languages(false) %}
{# endif #}
{% endif %}
{# These variables can be overridden from the main template file #}
{% set allowed = allowed ?? (directory and (object.exists and (can_read or can_save)) or (action == 'add' and can_read)) %}
{% set back_route = back_route ?? ('/' ~ (action != 'edit' and not key ? route.getRoute(1, not can_list ? -1 : null) : route.getRoute(1, not can_list ? -2 : -1))) %}
{% set title_icon = title_icon ?? view_config['icon'] ?? directory.config.admin.menu.list.icon ?? 'fa-file-text-o' %}
{% set title -%}
{%- set title_config = view_config['title'] -%}
{%- if title_config.template -%}
{{- include(template_from_string(title_config.template, 'edit title template')) -}}
{%- else -%}
{{- title ?? object.form.getValue('title') ?? object.title ?? key -}}
{% endif %}
{%- endset %}
{% block body %}
{% set back_url = back_url ?? admin_route(back_route) %}
{% set id = key %}
{% set blueprint = blueprint ?? form.blueprint %}
{{ parent() }}
{% endblock body %}
{% block content_top %}
{% if allowed and user.authorize('admin.super') %}
{% if directory and object or action == 'add' %}
{% set save_location = object.getStorageFolder() ?? directory.getStorageFolder() %}
<div class="alert notice">{{ "PLUGIN_ADMIN.SAVE_LOCATION"|tu }}: <b>{{ url(save_location, false, true)|trim('/') }} {{ not object.exists ? '[' ~ 'PLUGIN_FLEX_OBJECTS.NEW'|tu|upper ~ ']' }}</b></div>
{% endif %}
{% endif %}
{% if object.exists and form.flash.exists %}
<div class="alert secondary-accent">
<i class="fa fa-lightbulb-o"></i> {{ 'PLUGIN_FLEX_OBJECTS.STATE.EDITING_DRAFT'|tu }} <button class="button button-link" type="submit" name="task" value="reset" form="blueprints">{{ "PLUGIN_ADMIN.RESET"|tu }}</button>
</div>
{% endif %}
{% endblock %}
{% block content %}
{% if allowed %}
<div class="clear directory admin-{{ target }}">
<div class="admin-form-wrapper">
<div id="admin-topbar">
{% block topbar %}{% endblock %}
</div>
{% block edit %}
{% include 'partials/blueprints.html.twig' with { form: form, context: object, data: object } %}
{% endblock %}
</div>
</div>
{% include 'partials/modal-changes-detected.html.twig' %}
{% if can_delete %}
{% include ['flex-objects/types/' ~ target ~ '/modals/remove.html.twig', 'flex-objects/types/default/modals/remove.html.twig'] with { name: target } %}
{% endif %}
{% elseif (object.exists) %}
{% do page.modifyHeader('http_response_code', 403) %}
<div class="error-block">
<h1>{{ 'PLUGIN_ADMIN.ERROR'|tu }} 403</h1>
<div class="padding">
<p>
{{ 'PLUGIN_FLEX_OBJECTS.ERROR.PAGE_FORBIDDEN'|tu }}
</p>
</div>
</div>
{% else %}
{% do page.modifyHeader('http_response_code', 404) %}
<div class="error-block">
<h1>{{ 'PLUGIN_ADMIN.ERROR'|tu }} 404</h1>
<div class="padding">
<p>
{{ 'PLUGIN_FLEX_OBJECTS.ERROR.PAGE_NOT_EXIST'|tu }}
</p>
</div>
</div>
{% endif %}
{% endblock %}
@@ -0,0 +1,98 @@
{% extends 'partials/base.html.twig' %}
{% use 'flex-objects/types/default/titlebar/list.html.twig' %}
{# Allowed actions #}
{% set export = directory.config('admin.views.export') ?? directory.config('admin.export') ?? [] %}
{% set can_export = can_export ?? (export['enabled'] ?? export|array|count)|bool %}
{% set can_create = can_create ?? directory.isAuthorized('create', 'admin', user) %}
{% set can_translate = can_translate ?? (admin.multilang and directory.object.hasFlexFeature('flex-translate')) %}
{% set per_page = per_page ?? grav.uri.currentUri.queryParam('per_page') %}
{# Translations #}
{% if can_translate %}
{% set translate_include_default = translate_include_default ?? grav.config.get('system.languages.include_default_lang_file_extension', true) %}
{% set all_languages = grav.admin.siteLanguages %}
{% set admin_languages = admin.languages_enabled %}
{% set default_language = grav.language.default %}
{% set language = controller.language %}
{#
{% if translate_include_default %}
{% set all_languages = all_languages|merge({'': 'Default'}) %}
{% set admin_languages = admin_languages|merge({'': ''}) %}
{% else %}
#}
{% set language = language ?: default_language %}
{# endif #}
{% endif %}
{# These variables can be overridden from the main template file #}
{% set allowed = allowed ?? (directory and directory.isAuthorized('list', 'admin', user)) %}
{% set back_route = back_route ?? route.getRoute(1, -1) %}
{% set configure_path = directory.config('admin.router.actions.configure.path') %}
{% set configure_route = configure_route ?? (configure_path ? route.withRoute(admin_route(configure_path).toString(true)|trim('/')) : null) %}
{% set configure_route = configure_route ?? route.withGravParam('', 'configure').toString(true) %}
{% set title_icon = title_icon ?? view_config['icon'] ?? directory.config.admin.menu.list.icon ?? 'fa-file-text-o' %}
{% set title -%}
{%- set title_config = view_config['title'] ?? null -%}
{%- if title_config.template -%}
{{- include(template_from_string(title_config.template, 'configure title template')) -}}
{%- else -%}
{{- directory.title|tu -}}
{% endif %}
{%- endset %}
{% set schema = directory.blueprint.schema %}
{% do assets.addJs('plugin://flex-objects/js/flex-objects.js', { 'group': 'bottom', 'loading': 'defer' }) %}
{% block body %}
{% set collection = directory ? collection.isAuthorized('list', 'admin', user) %}
{% set directory_config = view_config['options'] ?? config.get('plugins.flex-objects.admin_list', { per_page: 15, order: { by: 'updated_timestamp', dir: 'desc' }}) %}
{% set per_page = max(1, per_page ?? directory_config.per_page) %}
{% set table = directory ? flex.dataTable(collection.flexDirectory(), { collection: collection, limit: per_page, sort: directory_config.order.by ~ '|' ~ directory_config.order.dir }) %}
{% set back_url = admin_route(back_route) %}
{% set configure_url = (directory.config('admin.views.configure.hidden') ?? directory.config('admin.configure.hidden', false)) is not same as(true) ? configure_route.toString(true) %}
{% set fields = table.getColumns() %}
{% set fields_count = fields ? count(fields) : 0 %}
{% set fields_width = 8 %}
{% set fields_set = 0 %}
{% set title_field = view_config['title'] %}
{% for key,options in fields %}
{% set fields_width = fields_width + options.width ?: 0 %}
{% set fields_set = fields_set + (options.width ? 1 : 0) %}
{% if not title_field and options.link == 'edit' %}
{% set title_field = key %}
{% endif %}
{% endfor %}
{{ parent() }}
{% endblock body %}
{% block content_top %}
{% if allowed and user.authorize('admin.super') %}
{% set save_location = directory.getStorageFolder() %}
<div class="alert notice">{{ "PLUGIN_ADMIN.SAVE_LOCATION"|tu }}: <b>{{ url(save_location, false, true)|trim('/') }}</b></div>
{% endif %}
{% endblock %}
{% block content %}
{% if allowed %}
{% block content_list %}
{% include ['flex-objects/types/' ~ target ~ '/list/list.html.twig', 'flex-objects/types/default/list/list.html.twig'] %}
{% endblock %}
{% else %}
{% do page.modifyHeader('http_response_code', 404) %}
<div class="error-block">
<h1>{{ 'PLUGIN_ADMIN.ERROR'|tu }} 404</h1>
<div class="padding">
<p>
{{ 'PLUGIN_FLEX_OBJECTS.ERROR.PAGE_NOT_EXIST'|tu }}
</p>
</div>
</div>
{% endif %}
{% endblock %}
@@ -0,0 +1,112 @@
{% block directory %}
<div id="directory">
{% if not fields %}
{% block no_list %}
<div class="no-entries">
<p>{{ 'PLUGIN_FLEX_OBJECTS.ERROR.BLUEPRINT_NO_LIST'|tu( target, null )|raw }}</p>
<ul>
<li>
{{ 'PLUGIN_FLEX_OBJECTS.ERROR.BLUEPRINT_NO_LIST_ADVISE'|tu }}
</li>
<li>
{{ 'PLUGIN_FLEX_OBJECTS.ERROR.BLUEPRINT_NO_LIST_TEMPLATE'|tu( target, null )|raw }}
</li>
</ul>
</div>
{% endblock %}
{% elseif not collection.count %}
{% block no_entries %}
<div class="no-entries">
{% if directory.isAuthorized('create', 'admin', user) %}
{% set createLink = admin_route(flex.adminRoute(collection, {action: 'add'})) %}
{{ 'PLUGIN_FLEX_OBJECTS.ERROR.LIST_EMPTY_ADD'|tu(createLink, null)|raw }}
{% else %}
{{ 'PLUGIN_FLEX_OBJECTS.ERROR.LIST_EMPTY'|tu }}
{% endif %}
</div>
{% endblock %}
{% else %}
{% block entries %}
{% set per_page = per_page ?? directory_config.per_page %}
{% set tableFields = [] %}
{% set searchFields = [] %}
{% for key, options in fields %}
{% set name = key %}
{% set sortField = options.sort.field ?? key %}
{% set title = (options.field.label ?? schema.get(options.alias ?? key).label)|tu %}
{% set width = options.width ?: ((100-fields_width) / ((fields_count-fields_set) ?: 1))|round(3) %}
{% set title_class = options.title_class ?: '' %}
{% set data_class = options.data_class ?: '' %}
{# Vuetable doesn't like field names with `.` in them, so we convert name and sortField to `_` #}
{% set tableFields = tableFields|merge([
{
name: name|replace({'.': '_'}),
sortField: sortField,
title: title ?? 'N/A',
width: width ~ '%',
titleClass: title_class,
dataClass: data_class
}
]) %}
{# FIXME: Search fields should be passed and individually customizable, right now defaulting to all fields selected #}
{% set searchFields = searchFields|merge([key|replace({'.': '_'})]) %}
{% endfor %}
{% set tableFields = tableFields|merge([{ name: '_actions_', title: "PLUGIN_FLEX_OBJECTS.ACTION.ACTIONS"|tu, titleClass: 'right' }]) %}
{% set list = table.jsonSerialize %}
<div
id="flex-objects-list"
data-initial-store="{{
{
data: list,
api: grav.uri.currentRoute().withExtension('json').uri()|string,
perPage: per_page,
fields: tableFields,
searchFields: searchFields,
sortOrder: [{ field: directory_config.order.by, direction: directory_config.order.dir }],
searchPlaceholder: "PLUGIN_ADMIN.RESOURCE_FILTER"|tu,
paginationInfo: "PLUGIN_FLEX_OBJECTS.LIST_INFO"|tu,
emptyResult: "PLUGIN_FLEX_OBJECTS.EMPTY_RESULT"|tu
}|json_encode|e('html_attr')
}}">
<svg viewBox="0 0 1060 {{ 31 * (min(per_page, list.data|count) + 2) }}">
{% for i in 0..((min(per_page, list.data|count) + 3) - 1) %}
<rect clip-path="url(#clip-path-{{ i }})" x="0" y="0" width="1060" height="{{ 31 * per_page }}" style="fill: url(#linear-gradient-{{ i }})"></rect>
<defs>
<clipPath id="clip-path-{{ i }}">
<rect x="13" y="{{ 31 * i + 10 }}" rx="6" ry="6" width="{{ 200 * random([0.7, 0.8, 0.9, 1]) }}" height="12"></rect>
<rect x="533" y="{{ 31 * i + 10 }}" rx="6" ry="6" width="{{ 63 * random([0.7, 0.8, 0.9, 1]) }}" height="12"></rect>
<rect x="653" y="{{ 31 * i + 10 }}" rx="6" ry="6" width="{{ 78 * random([0.7, 0.8, 0.9, 1]) }}" height="12"></rect>
<rect x="755" y="{{ 31 * i + 10 }}" rx="6" ry="6" width="{{ 117 * random([0.7, 0.8, 0.9, 1]) }}" height="12"></rect>
<rect x="938" y="{{ 31 * i + 10 }}" rx="6" ry="6" width="{{ 83 * random([0.7, 0.8, 0.9, 1]) }}" height="12"></rect>
<rect x="0" y="{{ 31 * i }}" rx="6" ry="6" width="1060" height=".3"></rect>
</clipPath>
<linearGradient id="linear-gradient-{{ i }}">
<stop offset="-0.60016" stop-color="#d9d9d9" stop-opacity="1">
<animate attributeName="offset" values="-3; 1" dur="2s" repeatCount="indefinite"></animate>
</stop>
<stop offset="0.39984" stop-color="#ecebeb" stop-opacity="1">
<animate attributeName="offset" values="-2; 2" dur="2s" repeatCount="indefinite"></animate>
</stop>
<stop offset="1.39984" stop-color="#d9d9d9" stop-opacity="1">
<animate attributeName="offset" values="-1; 3" dur="2s" repeatCount="indefinite"></animate>
</stop>
</linearGradient>
</defs>
{% endfor %}
</svg>
</div>
{% endblock %}
{% endif %}
{% block modals %}
{% include 'flex-objects/types/default/modals/remove.html.twig' with { name: target } %}
{% endblock %}
</div>
{% endblock %}
@@ -0,0 +1,39 @@
{% set object_title = title_field ? "'" ~ object[title_field]|join(' ') ~ "'" : 'Item' %}
{% set can_read = object.isAuthorized('read', 'admin', user) %}
{% set can_update = object.isAuthorized('update', 'admin', user) %}
{% set can_delete = object.isAuthorized('delete', 'admin', user) %}
{% if can_read and object.getRoute() %}
{% block action_preview %}
<a href="{{ route.withAddedPath(object.getKey()).withoutParams().withQueryParam('preview', 1).getUri() }}" title="{{ "PLUGIN_ADMIN.PREVIEW"|tu }}" class="preview-action">
<i class="fa fa-eye"></i>
</a>
{% endblock %}
{% endif %}
{% if can_update %}
{% block action_edit %}
<a href="{{ route.withAddedPath(object.getKey()).withoutParams().getUri() }}" title="Edit {{ object_title }}" class="edit-action">
<i class="fa fa-pencil"></i>
</a>
{% endblock %}
{% elseif can_read %}
{% block action_read %}
<a href="{{ route.withAddedPath(object.getKey()).withoutParams().getUri() }}" title="View {{ object_title }}" class="edit-action">
<i class="fa fa-search"></i>
</a>
{% endblock %}
{% endif %}
{% if can_delete %}
{% block action_delete %}
<a href="#delete"
class="page-delete delete-action"
title="Delete {{ object_title }}"
data-remodal-target="delete"
data-delete-url="{{ grav.uri.addNonce(route.withAddedPath(object.getKey()).withoutParams().withGravParam('task', 'delete').getUri(), 'admin-form', 'admin-nonce') }}"
>
<i class="fa fa-close"></i>
</a>
{% endblock %}
{% endif %}
@@ -0,0 +1,13 @@
<div class="remodal" data-remodal-id="delete" data-remodal-options="hashTracking: false">
<form>
{# FIXME -name|singularize- is not translatable #}
<h1>{{ 'PLUGIN_FLEX_OBJECTS.ACTION.DELETE_N'|tu }} {{ name|singularize|capitalize }}</h1>
<p class="bigger">
{{ 'PLUGIN_FLEX_OBJECTS.ACTION.REALLY_DELETE'|tu( name|singularize, null ) }}
</p>
<div class="button-bar">
<button data-remodal-action="cancel" class="button secondary remodal-cancel"><i class="fa fa-fw fa-close"></i> {{ "PLUGIN_ADMIN.CANCEL"|tu }}</button>
<a class="button disable-after-click" data-delete-action href="#"><i class="fa fa-fw fa-check"></i> {{ "PLUGIN_ADMIN.CONTINUE"|tu }}</a>
</div>
</form>
</div>
@@ -0,0 +1,62 @@
{% extends 'partials/base.html.twig' %}
{% use 'flex-objects/types/default/titlebar/preview.html.twig' %}
{# Allowed actions #}
{% set can_preview = can_preview ?? (object.exists and (view_config['enabled'] ?? false)) %}
{% set can_translate = can_translate ?? (admin.multilang and object.hasFlexFeature('flex-translate')) %}
{# These variables can be overridden from the main template file #}
{% set allowed = allowed ?? (directory and (object.exists or action == 'add')) %}
{% set back_route = back_route ?? ('/' ~ route.getRoute(1)) %}
{% set title_icon = title_icon ?? view_config['icon'] ?? directory.config.admin.menu.list.icon ?? 'fa-file-text-o' %}
{% set title -%}
{%- set title_config = view_config['title'] -%}
{%- if title_config.template -%}
{{- include(template_from_string(title_config.template, 'edit title template')) -}}
{%- else -%}
{{- title ?? object.form.getValue('title') ?? object.title ?? key -}}
{% endif %}
{%- endset %}
{% set preview_url -%}
{%- set route_config = view_config['route'] -%}
{%- if route_config.template -%}
{{- include(template_from_string(route_config.template, 'preview route template')) -}}
{%- else -%}
{{- preview_url ?? object.getRoute().uri ?: '' -}}
{%- endif -%}
{% endset -%}
{% block body %}
{% if not can_preview or not preview_url %}
{% set allowed = false %}
{% endif %}
{% set id = key %}
{% set blueprint = object.blueprint ?? directory.blueprint %}
{% set back_url = back_url ?? admin_route(back_route) %}
{{ parent() }}
{% endblock body %}
{% block content_wrapper %}
{% if can_preview and allowed and preview_url %}
<div class="content-wrapper preview-wrapper">
<div class="content-padding" width="100%" height="100%">
<iframe width="100%" height="100%" frameborder="0" src="{{ preview_url }}"></iframe>
</div>
</div>
{% else %}
{{ parent() }}
{% endif %}
{% endblock content_wrapper %}
{% block content %}
{% do page.modifyHeader('http_response_code', 404) %}
<div class="error-block">
<h1>{{ 'PLUGIN_ADMIN.ERROR'|tu }} 404</h1>
<div class="padding">
<p>
{{ 'PLUGIN_FLEX_OBJECTS.ERROR.PAGE_NOT_EXIST'|tu }}
</p>
</div>
</div>
{% endblock content %}
@@ -0,0 +1,32 @@
{% block titlebar %}
{% block titlebar_button_bar %}
<div class="button-bar">
{# BACK #}
{% block back_button %}
{% include ['flex-objects/types/' ~ target ~ '/buttons/back.html.twig', 'flex-objects/types/default/buttons/back.html.twig'] %}
{% endblock back_button %}
{% block extra_buttons %}{% endblock extra_buttons %}
{# SAVE #}
{% if can_save %}
{% block save_button %}
{% include ['flex-objects/types/' ~ target ~ '/buttons/save.html.twig', 'flex-objects/types/default/buttons/save.html.twig'] with {task: 'configure'} %}
{% endblock save_button %}
{% endif %}
</div>
{% endblock titlebar_button_bar %}
{% block titlebar_title %}
<h1>
{% if allowed %}
<i class="fa fa-fw fa-cog"></i>
{{ title }}
{% else %}
<i class="fa fa-fw fa-exclamation-triangle"></i>
{{ 'PLUGIN_ADMIN.ERROR'|tu }}
  {% endif %}
</h1>
{% endblock titlebar_title %}
{% endblock %}
@@ -0,0 +1,46 @@
{% block titlebar %}
{% block titlebar_button_bar %}
<div class="button-bar">
{# BACK #}
{% block back_button %}
{% include ['flex-objects/types/' ~ target ~ '/buttons/back.html.twig', 'flex-objects/types/default/buttons/back.html.twig'] %}
{% endblock back_button %}
{# PREVIEW #}
{% if can_preview %}
{% block preview_button %}
{% include ['flex-objects/types/' ~ target ~ '/buttons/preview.html.twig', 'flex-objects/types/default/buttons/preview.html.twig'] %}
{% endblock preview_button %}
{% endif %}
{# DELETE #}
{% if can_delete %}
{% block delete_button %}
{% include ['flex-objects/types/' ~ target ~ '/buttons/delete.html.twig', 'flex-objects/types/default/buttons/delete.html.twig'] %}
{% endblock delete_button %}
{% endif %}
{% block extra_buttons %}{% endblock extra_buttons %}
{# SAVE #}
{% if allowed and can_save %}
{% block save_button %}
{% include ['flex-objects/types/' ~ target ~ '/buttons/save.html.twig', 'flex-objects/types/default/buttons/save.html.twig'] with {task: 'save'} %}
{% endblock save_button %}
{% endif %}
</div>
{% endblock titlebar_button_bar %}
{% block titlebar_title %}
<h1>
{% if allowed %}
<i class="fa fa-fw {{ title_icon }}"></i>
{{ not object.exists ? '[' ~ 'PLUGIN_FLEX_OBJECTS.NEW'|tu|upper ~ ']' }} {{ title|tu }}
{% else %}
<i class="fa fa-fw fa-exclamation-triangle"></i>
{{ 'PLUGIN_ADMIN.ERROR'|tu }}
{% endif %}
</h1>
{% endblock titlebar_title %}
{% endblock %}
@@ -0,0 +1,48 @@
{% block titlebar %}
{% block titlebar_button_bar %}
<div class="button-bar">
{# BACK #}
{% block back_button %}
{% include ['flex-objects/types/' ~ target ~ '/buttons/back.html.twig', 'flex-objects/types/default/buttons/back.html.twig'] %}
{% endblock back_button %}
{# EXPORT #}
{% if can_export %}
{% block export_button %}
{% include ['flex-objects/types/' ~ target ~ '/buttons/export.html.twig', 'flex-objects/types/default/buttons/export.html.twig'] with {export: directory.config('admin.views.export') ?? directory.config('admin.export') ?? []} %}
{% endblock export_button %}
{% endif %}
{# CREATE #}
{% if can_create %}
{% block create_button %}
{% include ['flex-objects/types/' ~ target ~ '/buttons/add.html.twig', 'flex-objects/types/default/buttons/add.html.twig'] %}
{% endblock create_button %}
{% endif %}
{# LANGUAGES #}
{% if can_translate %}
{% block languages_button %}
{% include ['flex-objects/types/' ~ target ~ '/buttons/languages.html.twig', 'flex-objects/types/default/buttons/languages.html.twig'] %}
{% endblock languages_button %}
{% endif %}
{# CONFIGURE #}
{% block configure %}
{% include 'flex-objects/types/default/buttons/configuration.html.twig' %}
{% endblock configure %}
</div>
{% endblock titlebar_button_bar %}
{% block titlebar_title %}
<h1>
{% if allowed %}
<i class="fa fa-fw {{ directory ? title_icon : 'fa-exclamation-triangle' }}"></i>
{{ directory ? title|tu : 'Error' }}
{% else %}
<i class="fa fa-fw fa-exclamation-triangle"></i>
{{ 'PLUGIN_ADMIN.ERROR'|tu }}
{% endif %}
</h1>
{% endblock titlebar_title %}
{% endblock %}
@@ -0,0 +1,30 @@
{% block titlebar %}
{% block titlebar_button_bar %}
<div class="button-bar">
{# BACK #}
{% block back_button %}
{% include ['flex-objects/types/' ~ target ~ '/buttons/back.html.twig', 'flex-objects/types/default/buttons/back.html.twig'] %}
{% endblock back_button %}
{# PREVIEW #}
{% if can_preview %}
{% block preview_button %}
{% include ['flex-objects/types/' ~ target ~ '/buttons/preview-open.html.twig', 'flex-objects/types/default/buttons/preview-open.html.twig'] %}
{% endblock preview_button %}
{% endif %}
</div>
{% endblock titlebar_button_bar %}
{% block titlebar_title %}
<h1>
{% if allowed %}
<i class="fa fa-fw {{ title_icon }}"></i>
{{ "PLUGIN_ADMIN.PREVIEW"|tu }}: {{ not object.exists ? '[' ~ 'PLUGIN_FLEX_OBJECTS.NEW'|tu|upper ~ ']' }} <strong>{{ title }}</strong>
{% else %}
<i class="fa fa-fw fa-exclamation-triangle"></i>
{{ 'PLUGIN_ADMIN.ERROR'|tu }}
{% endif %}
</h1>
{% endblock titlebar_title %}
{% endblock %}
@@ -0,0 +1,22 @@
{% block titlebar %}
{% block titlebar_button_bar %}
<div class="button-bar">
{# BACK #}
{% block back_button %}
{% include 'flex-objects/types/default/buttons/back.html.twig' %}
{% endblock back_button %}
{# CONFIGURE #}
{% block configure %}
{% include 'flex-objects/types/default/buttons/configuration.html.twig' %}
{% endblock configure %}
</div>
{% endblock titlebar_button_bar %}
{% block titlebar_title %}
<h1>
<i class="fa fa-fw fa-list"></i>
{{ "PLUGIN_FLEX_OBJECTS.TITLE"|tu }}
</h1>
{% endblock titlebar_title %}
{% endblock %}
@@ -0,0 +1,46 @@
{% extends 'partials/base.html.twig' %}
{% use 'flex-objects/types/default/titlebar/types.html.twig' %}
{% set flex = grav['flex_objects'] %}
{# These variables can be overridden from the main template file #}
{% set back_route = back_route ?? ('/' ~ route.getRoute(1, -1)) %}
{% set configure_route = '/plugins/flex-objects' %}
{% block body %}
{% set back_url = admin_route(back_route) %}
{% set configure_url = configure_route ? admin_route(configure_route) : null %}
{{ parent() }}
{% endblock body %}
{% block content %}
<h1>{{ 'PLUGIN_FLEX_OBJECTS.TYPES_TITLE'|tu }}</h1>
<div id="types" class="card-row grid fixed-blocks pure-g">
{% for name,directory in flex.directories if directory.enabled and directory.config('admin.hidden', false) is not same as(true) and not directory.config('admin.menu') %}
{% try %}
{% set collection = directory.collection %}
{% if flex.adminRoute(collection) %}
<div class="card-item pure-u-1-3">
<h2><a href="{{ admin_route(flex.adminRoute(collection)) }}">{{ directory.title|tu }}</a> <span class="badge">{{ collection.isAuthorized('list', 'admin', user).count }}</span></h2>
<p>
{{ directory.description }}
</p>
</div>
{% endif %}
{% catch %}
<div class="card-item pure-u-1-3">
<h2>{{ 'PLUGIN_FLEX_OBJECTS.ERROR.BAD_DIRECTORY'|tu }} '{{ name }}'</h2>
<p>
{{ e.message }}
</p>
</div>
{% endcatch %}
{% endfor %}
</div>
{% endblock %}
@@ -0,0 +1,9 @@
{% extends 'flex-objects/types/default/configure.html.twig' %}
{% set back_route = back_route ?? ('/' ~ route.getRoute(1, -1)) %}
{% block content_top %}
{% include 'flex-objects/layouts/accounts/partials/top.html.twig' %}
{{ parent() }}
{% endblock %}
@@ -0,0 +1,9 @@
{% extends 'flex-objects/types/default/edit.html.twig' %}
{% if not directory.isAuthorized('list', 'admin', user) %}
{% set back_route = '/' %}
{% endif %}
{% if not object.exists %}
{% do object.onPrepareRegistration() %}
{% endif %}
@@ -0,0 +1,7 @@
{% extends 'flex-objects/types/default/list.html.twig' %}
{% block content_top %}
{% include 'flex-objects/layouts/accounts/partials/top.html.twig' %}
{{ parent() }}
{% endblock %}
@@ -0,0 +1,9 @@
{% extends 'flex-objects/types/default/configure.html.twig' %}
{% set back_route = back_route ?? ('/' ~ route.getRoute(1, -1)) %}
{% block content_top %}
{% include 'flex-objects/layouts/accounts/partials/top.html.twig' %}
{{ parent() }}
{% endblock %}
@@ -0,0 +1,7 @@
{% extends 'flex-objects/types/default/list.html.twig' %}
{% block content_top %}
{% include 'flex-objects/layouts/accounts/partials/top.html.twig' %}
{{ parent() }}
{% endblock %}
@@ -0,0 +1,69 @@
{% extends "forms/field.html.twig" %}
{% macro spanToggle(input, length) %}
{% set space = repeat('&nbsp;&nbsp;', (length - input|length) / 2) %}
{{ (space ~ input ~ space)|raw }}
{% endmacro %}
{% import _self as macro %}
{% set value = (value is null ? field.default : value) %}
{% block global_attributes %}
{{ parent() }}
data-grav-field-name="{{ (scope ~ field.name)|fieldName }}"
{% endblock %}
{% block input %}
{% set flex = grav['flex_objects'] %}
{% set all = flex.blueprints %}
{% if all|count %}
{% set legacy = flex.getLegacyBlueprintMap() %}
{% for label, directory in all %}
{% set url = directory.blueprintFile %}
{% set found = url in value %}
{% if not found and legacy[url] is defined %}
{% set found = legacy[url] in value %}
{% endif %}
<div class="form-data block size-2-3" data-grav-field="toggle" data-grav-disabled="" data-grav-default="null" data-grav-field-name="{{ (scope ~ field.name)|fieldName }}[{{ loop.index0 }}]">
<div class="switch-toggle switch-grav switch-2">
{% set maxLen = 0 %}
{% for text in ['PLUGIN_ADMIN.ENABLED', 'PLUGIN_ADMIN.DISABLED'] %}
{% set translation = grav.twig.twig.filters['tu'] is defined ? text|tu : text|t %}
{% set maxLen = max(translation|length, maxLen) %}
{% endfor %}
{% set id = "toggle_" ~ field.name ~ '_' ~ label %}
<input type="radio"
value="{{ url }}"
id="{{ id ~ '_yes' }}"
name="{{ (scope ~ field.name)|fieldName }}[{{ loop.index0 }}]"
class="highlight"
{% if found %}
checked="checked"
{% endif %}
/>
{% set text = 'PLUGIN_ADMIN.ENABLED' %}
{% set translation = (grav.twig.twig.filters['tu'] is defined ? text|tu : text|t)|trim %}
<label for="{{ id ~ '_yes' }}">{{ (macro.spanToggle(translation, maxLen)|trim)|raw }}</label>
<input type="radio"
value=""
name="{{ (scope ~ field.name)|fieldName }}[{{ loop.index0 }}]"
id="{{ id ~ '_no' }}"
{% if not found %}
checked="checked"
{% endif %}
/>
{% set text = 'PLUGIN_ADMIN.DISABLED' %}
{% set translation = (grav.twig.twig.filters['tu'] is defined ? text|tu : text|t)|trim %}
<label for="{{ id ~ '_no' }}">{{ (macro.spanToggle(translation, maxLen)|trim)|raw }}</label>
</div>
<span title="{{ directory.description|tu }}">{{ directory.title|tu }}</span>
</div>
{% endfor %}
{% else %}
<div>{{ 'PLUGIN_FLEX_OBJECTS.ERROR.NO_FLEX_DIRECTORIES'|tu }}</div>
{% endif %}
{% endblock %}
@@ -0,0 +1,36 @@
{% extends "forms/field.html.twig" %}
{% set originalValue = value %}
{% set value = (value is null ? field.default : value) %}
{% set isNew = key ? false : true %}
{% set savedOption = grav.session.post_entries_save|default('edit') %}
{% if isNew %}
{% set options = {'create-new':'PLUGIN_FLEX_OBJECTS.ACTION.CREATE_NEW', 'edit':'PLUGIN_FLEX_OBJECTS.ACTION.EDIT_ITEM', 'list':'PLUGIN_FLEX_OBJECTS.ACTION.LIST_ITEMS'} %}
{% else %}
{% set options = {'edit':'PLUGIN_FLEX_OBJECTS.ACTION.EDIT_ITEM', 'list':'PLUGIN_FLEX_OBJECTS.ACTION.LIST_ITEMS'} %}
{% endif %}
{% block input %}
{% for key, text in options %}
{% set id = field.id|default(field.name) ~ '-' ~ key %}
{% if savedOption == key %}
{% set value = savedOption %}
{% endif %}
<span class="radio">
<input type="radio"
value="{{ key|e }}"
id="{{ id|e }}"
name="{{ (field.name)|fieldName }}"
{% if key == value %}checked="checked" {% endif %}
{% if field.disabled or isDisabledToggleable %}disabled="disabled"{% endif %}
{% if field.validate.required in ['on', 'true', 1] %}required="required"{% endif %}
/>
<label style="display: inline" class="inline" for="{{ id|e }}">{{ text|tu|e }}</label>
</span>
{% endfor %}
{% endblock %}
+425
View File
@@ -0,0 +1,425 @@
import $ from 'jquery';
import Finder from '../utils/finder';
import { getInitialRoute, getStore, setInitialRoute } from './index';
// import getFilters from '../utils/get-filters';
let XHRUUID = 0;
const GRAV_CONFIG = typeof global.GravConfig !== 'undefined' ? global.GravConfig : global.GravAdmin.config;
export const Instances = {};
const isInViewport = (elem) => {
const bounding = elem.getBoundingClientRect();
const titlebar = document.querySelector('#titlebar');
const offset = titlebar ? titlebar.getBoundingClientRect().height : 0;
return (
bounding.top >= offset &&
bounding.left >= 0 &&
bounding.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
bounding.right <= (window.innerWidth || document.documentElement.clientWidth)
);
};
export class FlexPages {
constructor(container, data) {
this.container = $(container);
this.data = data;
const dataLoad = this.dataLoad;
this.finder = new Finder(
this.container,
(parent, callback) => {
return dataLoad.call(this, parent, callback);
},
{
labelKey: 'title',
defaultPath: getInitialRoute(),
itemTrigger: '[data-flexpages-expand]',
createItem: function(item) {
return FlexPages.createItem(this.config, item, this);
},
createItemContent: function(item) {
return FlexPages.createItemContent(this.config, item, this);
}
}
);
this.finder.$emitter.on('leaf-selected', (item) => {
setInitialRoute({
route: item.route.raw
});
});
this.finder.$emitter.on('interior-selected', (item) => {
setInitialRoute({
route: item.route.raw
});
});
/*
this.finder.$emitter.on('leaf-selected', (item) => {
console.log('selected', item);
this.finder.emit('create-column', () => this.createSimpleColumn(item));
});
this.finder.$emitter.on('item-selected', (selected) => {
console.log('selected', selected);
// for future use only - create column-card creation for file with details like in macOS finder
// this.finder.$emitter('create-column', () => this.createSimpleColumn(selected));
}); */
this.finder.$emitter.on('column-created', () => {
this.container[0].scrollLeft = this.container[0].scrollWidth - this.container[0].clientWidth;
});
}
static createItem(config, item, finder) {
const listItem = $('<li />');
const listItemClasses = [config.className.item];
// const href = `${GRAV_CONFIG.current_url}/${item.route.raw}`.replace('//', '/');
const link = $('<div class="fjs-item-wrapper" />');
const createItemContent = config.createItemContent || finder.createItemContent;
const fragment = createItemContent.call(this, item);
link.append(fragment)
// .attr('href', href)
.attr('tabindex', -1);
if (item.url) {
link.attr('href', item.url);
listItemClasses.push(item.className);
}
if (item[config.childKey]) {
listItemClasses.push(config.className[config.childKey]);
}
if (item.filters_hit) {
listItemClasses.push('filters-hit');
}
listItem.addClass(listItemClasses.join(' '));
listItem.append(link)
.attr('data-fjs-item', item[config.itemKey]);
listItem[0]._item = item;
return listItem;
}
static createItemContent(config, item) {
const frag = document.createDocumentFragment();
const route = `${GRAV_CONFIG.current_url}/${item.route.raw}`.replace('//', '/');
const title = $('<div class="fjs-title" />');
const link = $(`<a href="${route}" />`);
const icon = $(`<span class="fjs-icon ${item.icon} badge-${item.extras && item.extras.published ? 'published' : 'unpublished'}" />`);
if (item.extras && item.extras.lang) {
let status = '';
if (item.extras.translated) {
status = 'translated';
}
if (item.extras.lang === 'n/a') {
status = 'not-available';
}
const lang = $(`<span class="badge-lang ${status}">${item.extras.lang}</span>`);
lang.appendTo(icon);
}
if (item.extras && item.extras && (item.extras.published_date || item.extras.unpublished_date)) {
const clock = $('<span class="badge-clock" />');
clock.appendTo(icon);
}
const info = $(`<span class="fjs-info"><b title="${item.title}">${item.title}</b> <em title="${item.route.display}">${item.route.display}</em></span>`);
const actions = $('<span class="fjs-actions" />');
let dotdotdot = null;
if (item.extras) {
const LANG_URL = $('[data-lang-url]').data('langUrl');
dotdotdot = $('<div class="button-group" data-flexpages-dotx3 data-flexpages-prevent><button class="button dropdown-toggle" data-toggle="dropdown"><i class="fa fa-ellipsis-v fjs-action-toggle"></i></button></div>');
dotdotdot.on('click', (event) => {
if (!dotdotdot.find('.dropdown-menu').length) {
let tags = '';
let langs = '';
item.extras.tags.forEach((tag) => {
tags += `<span class="badge tag tag-${tag}">${tag}</span>`;
});
const translations = item.extras.langs || {};
Object.keys(translations).forEach((lang) => {
const translated = translations[lang];
langs += `<a class="lang" href="${LANG_URL.replace(/%LANG%/g, lang).replace('//', '/')}${item.route.raw}"><span class="badge lang-${lang ? lang : 'default'} lang-${translated ? 'translated' : 'non-translated'}"><i class="fa fa-fw fa-circle"></i> ${lang ? lang : 'default'}</span></a>`;
});
const canPreview = item.extras.actions.includes('preview') && (!(item.extras.tags.includes('non-routable') || item.extras.tags.includes('unpublished')));
const canEdit = item.extras.actions.includes('edit');
const canCopy = item.extras.actions.includes('copy');
const canMove = false; // item.extras.actions.includes('move');
const canDelete = item.extras.actions.includes('delete');
const ul = $(`<div class="dropdown-menu">
<div class="action-bar">
${canPreview ? `<a href="${route}/:preview" class="dropdown-item" title="Preview"><i class="fa fa-fw fa-eye"></i></a>` : ''}
${canEdit ? `<a href="${route}" class="dropdown-item" title="Edit"><i class="fa fa-fw fa-pencil"></i></a>` : ''}
${canCopy ? `<a href="${route}/task:copy/admin-nonce:${GRAV_CONFIG.admin_nonce}" class="dropdown-item" title="Duplicate" href="#modal-page-copy" data-remodal-target="modal-page-copy" data-copy-flex-page data-title="${item.title}" data-folder="${item['item-key']}"><i class="fa fa-fw fa-copy"></i></a>` : ''}
${canMove ? '<a href="#" class="dropdown-item" title="Move (coming soon)"><i class="fa fa-fw fa-arrows"></i></a>' : ''}
${canDelete ? `<a href="#delete" data-remodal-target="delete" data-delete-url="${route}/task:delete/admin-nonce:${GRAV_CONFIG.admin_nonce}" class="dropdown-item danger" title="Delete"><i class="fa fa-fw fa-trash-o"></i></a>` : ''}
</div>
<div class="divider"></div>
<div class="tags">${tags}</div>
<div class="divider"></div>
${item.extras.lang || typeof item.extras.langs !== 'undefined' ? `<div class="langs">${langs}</div><div class="divider"></div>` : ''}
<div class="details">
<div class="infos">
<table>
<tr>
<td><b>route</b></td>
<td>${item.route.display}</td>
</tr>
<tr>
<td><b>template</b></td>
<td>${item.extras.template}</td>
</tr>
${item.extras && item.extras.published_date ? `
<tr>
<td><b>publish</b></td>
<td>${item.extras.published_date}</td>
</tr>
` : ''}
${item.extras && item.extras.unpublished_date ? `
<tr>
<td><b>unpublish</b></td>
<td>${item.extras.unpublished_date}</td>
</tr>
` : ''}
<tr>
<td><b>modified</b></td>
<td>${item.modified}</td>
</tr>
</table>
</div>
</div>
</div>`);
ul.appendTo(dotdotdot);
}
return true;
});
}
if (item.child_count) {
const button = $('<button class="fjs-children" data-flexpages-expand data-flexpages-prevent />');
const count = $(`<span class="badge child-count">${typeof item.count !== 'undefined' ? `${item.count} / ` : ''}${item.child_count}</span>`);
const arrow = $('<i class="fa fa-chevron-right"></i>');
count.appendTo(button);
arrow.appendTo(button);
button.appendTo(actions);
}
icon.appendTo(title);
dotdotdot.appendTo(title);
link.appendTo(title);
info.appendTo(link);
title.appendTo(frag);
actions.appendTo(frag);
return frag;
}
static createLoadingColumn() {
return $(`
<div class="fjs-col leaf-col" style="overflow: hidden;">
<div class="leaf-row">
<div class="grav-loading"><div class="grav-loader">Loading...</div></div>
</div>
</div>
`);
}
static createErrorColumn(error) {
return $(`
<div class="fjs-col leaf-col" style="overflow: hidden;">
<div class="leaf-row error">
<i class="fa fa-fw fa-warning"></i>
<span>${error}</span>
</div>
</div>
`);
}
createSimpleColumn(item) {}
dataLoad(parent, callback, filters = getStore().filters || {}) {
/* if (!parent && Object.keys(filters).length) {
parent = { child_count: 1, route: { raw: '' } };
}*/
if (!parent) {
return callback(this.data);
}
if (!parent.child_count) {
return false;
}
const UUID = ++XHRUUID;
this.startLoader();
const withFilters = Object.keys(filters).length ? { ...filters } : {};
$.ajax({
url: `${GRAV_CONFIG.current_url}`,
method: 'post',
data: Object.assign({}, {
route: b64_encode_unicode(parent.route.raw),
action: 'listLevel'
}, withFilters),
success: (response) => {
this.stopLoader();
if (response.status === 'error') {
this.finder.$emitter.emit('create-column', FlexPages.createErrorColumn(response.message)[0]);
return false;
}
// stale request
if (UUID !== XHRUUID) {
return false;
}
if (response.data.length) {
parent.children = response.data;
}
return callback(response.data);
}
});
}
startLoader() {
if (!this.finder) {
return null;
}
this.loadingIndicator = FlexPages.createLoadingColumn();
this.finder.$emitter.emit('create-column', this.loadingIndicator[0]);
return this.loadingIndicator;
}
stopLoader() {
return this.loadingIndicator && this.loadingIndicator.remove();
}
}
export const b64_encode_unicode = (str) => {
return btoa(encodeURIComponent(str).replace(/%([0-9A-F]{2})/g,
function toSolidBytes(match, p1) {
return String.fromCharCode('0x' + p1);
}));
};
export const b64_decode_unicode = (str) => {
return decodeURIComponent(atob(str).split('').map(function(c) {
return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
}).join(''));
};
const updatePosition = (scrollingColumn, pageColumns) => {
const group = document.querySelector('#pages-columns .button-group.open');
if (group) {
const button = group.querySelector('[data-toggle="dropdown"]');
const dropdown = group.querySelector('.dropdown-menu');
const buttonInView = isInViewport(button);
if (button && dropdown) {
if (!buttonInView) {
$(dropdown).css({ display: 'none' });
} else {
$(dropdown).css({ display: 'inherit' });
const buttonClientRect = button.getBoundingClientRect();
const dropdownClientRect = dropdown.getBoundingClientRect();
const scrollTop = (window.pageYOffset || document.documentElement.scrollTop);
const scrollLeft = (window.pageXOffset || document.documentElement.scrollLeft);
const top = buttonClientRect.height + buttonClientRect.top + scrollTop;
let left = buttonClientRect.left + scrollLeft; // - dropdownClientRect.width
if (left + dropdownClientRect.width > window.innerWidth) {
left = window.innerWidth - dropdownClientRect.width - 5;
}
$(dropdown).css({ top, left });
if (scrollingColumn) {
const targetClientRect = event.target.getBoundingClientRect();
if ((top < targetClientRect.top + scrollTop) || (top > targetClientRect.top + scrollTop + targetClientRect.height)) {
$(dropdown).css({ display: 'none' });
}
}
if (pageColumns) {
const targetClientRect = event.target.getBoundingClientRect();
if ((left < targetClientRect.left + scrollLeft) || (left > targetClientRect.left + scrollLeft + targetClientRect.width)) {
$(dropdown).css({ display: 'none' });
}
}
}
}
}
};
const closeGhostDropdowns = () => {
const opened = document.querySelectorAll('#pages-columns .button-group:not(.open) .dropdown-menu') || [];
opened.forEach((item) => { item.style.display = 'none'; });
};
document.addEventListener('scroll', (event) => {
if (event.target && !event.target.classList) { return true; }
const scrollingDocument = event.target.classList.contains('gm-scroll-view') || event.target.classList.contains('content-wrapper');
const scrollingColumn = event.target.classList.contains('fjs-col');
const pageColumns = event.target.id === 'pages-columns';
if (scrollingDocument || scrollingColumn || pageColumns) {
closeGhostDropdowns();
updatePosition(scrollingColumn, pageColumns);
}
}, true);
document.addEventListener('click', (event) => {
closeGhostDropdowns();
if (event.target.dataset.toggle || event.target.closest('[data-toggle="dropdown"]')) {
const containerScroller = document.querySelectorAll('.gm-scroll-view');
((containerScroller.length ? containerScroller : document.querySelectorAll('.content-wrapper')) || []).forEach((scroll) => {
const scrollEvent = new Event('scroll');
scroll.dispatchEvent(scrollEvent);
});
}
if ((event.target.classList && event.target.classList.contains('dropdown-menu')) || (event.target.closest('.dropdown-menu'))) {
if (!$(event.target).closest('.dropdown-menu').find(event.target).length) {
event.preventDefault();
event.stopPropagation();
}
}
if (event.target.dataset.copyFlexPage || event.target.closest('[data-copy-flex-page]')) {
const target = event.target.dataset.copyFlexPage ? event.target : event.target.closest('[data-copy-flex-page]');
const modal = document.querySelector('[data-remodal-id="modal-page-copy"]');
const form = modal.querySelector('form');
const titleField = modal.querySelector('[name="data[title]"]');
const folderField = modal.querySelector('[name="data[folder]"]');
titleField.value = `${target.dataset.title} (Copy)`;
folderField.value = `${target.dataset.folder}-copy`;
form.action = target.href;
}
});
// Prevent dropdowns from closing when clicking within
$(document).on('click.bs.dropdown.data-api', '.fjs-item-wrapper .dropdown-menu', (event) => {
event.stopPropagation();
});
+102
View File
@@ -0,0 +1,102 @@
import $ from 'jquery';
import { b64_decode_unicode, b64_encode_unicode, FlexPages } from './finder';
import { isEnabled, getCookie, setCookie } from 'tiny-cookie';
import getFilters from '../utils/get-filters';
const container = document.querySelector('#pages-content-wrapper');
export const getStore = () => {
if (!isEnabled) {
return '';
}
return JSON.parse(b64_decode_unicode(getCookie('grav-admin-flexpages') || 'e30='));
};
export const setStore = (store = {}, options = { expires: '1Y', samesite: 'Lax' }) => {
if (!isEnabled) {
return '';
}
return setCookie('grav-admin-flexpages', b64_encode_unicode(JSON.stringify(store)), options);
};
export const getInitialRoute = () => {
const parsed = getStore();
return parsed.route || '';
};
export const setInitialRoute = ({ route = '', filters = getStore().filters || {}, options = { expires: '1Y' }} = {}) => {
return setStore({ route, filters }, options);
};
export let FlexPagesInstance = null;
export const ReLoad = (fresh = false) => {
const search = document.querySelector('#pages-filters [name="filters[search]"]');
const loader = container.querySelector('.grav-loading');
const content = container.querySelector('#pages-columns');
const gravConfig = typeof global.GravConfig !== 'undefined' ? global.GravConfig : global.GravAdmin.config;
if (fresh && search) {
search.focus();
}
if (loader && content) {
loader.style.display = 'block';
content.innerHTML = '';
const filters = fresh ? getStore().filters || {} : getFilters();
const withFilters = Object.keys(filters).length ? { ...filters, initial: true } : {};
const store = getStore();
store.filters = filters;
setStore(store);
let isSearchFocused = false;
if (search) {
isSearchFocused = search === document.activeElement;
}
const contentWrapper = document.querySelector('.content-wrapper .gm-scroll-view');
const scrollPosition = {
top: contentWrapper ? contentWrapper.scrollTop : 0,
left: contentWrapper ? contentWrapper.scrollLeft : 0
};
$.ajax({
url: `${gravConfig.current_url}`,
method: 'post',
data: Object.assign({}, {
route: b64_encode_unicode(getInitialRoute()),
initial: true,
action: 'listLevel'
}, withFilters),
success(response) {
loader.style.display = 'none';
if (response.status === 'error') {
content.innerHTML = response.message;
return true;
}
FlexPagesInstance = null;
FlexPagesInstance = new FlexPages(content, response.data);
if (search && isSearchFocused) {
search.focus();
}
if (contentWrapper) {
contentWrapper.scrollTo(scrollPosition);
}
return FlexPagesInstance;
}
});
}
};
if (container) {
ReLoad(true);
}
+46
View File
@@ -0,0 +1,46 @@
import '../utils/indeterminate';
import './panel';
import { ReLoad } from '../columns';
import throttle from 'lodash/throttle';
document.addEventListener('click', (event) => {
const filterType = event.target && event.target.dataset.filters;
if (filterType === 'reset') {
const filters = event.target.closest('#pages-filters');
(filters.querySelectorAll('input[type="text"]') || []).forEach((input) => {
input.value = '';
});
(filters.querySelectorAll('input[type="checkbox"]') || []).forEach((input) => {
const wrapper = input.closest('.checkboxes');
if (wrapper) {
wrapper.classList.remove('status-checked', 'status-unchecked', 'status-indeterminate');
wrapper.dataset._checkStatus = '0';
wrapper.classList.add('status-unchecked');
}
input.indeterminate = false;
input.checked = false;
input.value = '';
});
return false;
}
if (filterType === 'apply') {
ReLoad();
return false;
}
});
const throttledReload = throttle(() => {
ReLoad();
}, 350, { leading: false });
document.addEventListener('input', (event) => {
if (event.target.getAttribute && event.target.getAttribute('name') === 'filters[search]') {
throttledReload.cancel();
throttledReload();
}
});
+15
View File
@@ -0,0 +1,15 @@
const toggle = document.querySelector('.filters-bar .adv-options');
const panel = document.querySelector('.filters-advanced');
if (toggle && panel) {
document.addEventListener('click', (event) => {
if (event.target.classList.contains('adv-options') || event.target.closest('.adv-options')) {
event.preventDefault();
const isOpen = toggle.classList.contains('open');
panel.classList.toggle('hide');
toggle.classList.remove(isOpen ? 'open' : 'close');
toggle.classList.add(isOpen ? 'close' : 'open');
}
});
}
+27
View File
@@ -0,0 +1,27 @@
<template>
<div>
<flex-filter-bar :store="store" />
<flex-content-loader :store="store" v-show="loading" />
<flex-table :store="store" v-model="loading" v-show="!loading" />
</div>
</template>
<script>
import FlexTable from './components/Table.vue';
import FlexFilterBar from './components/FilterBar.vue';
import FlexContentLoader from './components/ContentLoader.vue';
export default {
props: ['initialStore'],
components: {FlexTable, FlexFilterBar, FlexContentLoader},
data: () => ({
perPage: 10,
loading: true
}),
computed: {
store() {
return JSON.parse(this.initialStore || '{}');
}
}
}
</script>
@@ -0,0 +1,29 @@
export default {
table: {
tableClass: 'table',
loadingClass: 'loading',
sortableIcon: '',
ascendingIcon: 'fa fa-fw fa-chevron-up',
descendingIcon: 'fa fa-fw fa-chevron-down',
ascendingClass: '',
descendingClass: '',
handleIcon: 'fa fa-fw fa-bars',
renderIcon: (classes, options) => `<i class="${classes.join(' ')}"></i>`
},
pagination: {
wrapperClass: 'flex-objects-pagination',
activeClass: 'button active',
disabledClass: 'button disabled',
pageClass: 'button page',
linkClass: 'button link',
icons: {
first: 'fa fa-fw fa-angle-double-left',
prev: 'fa fa-fw fa-chevron-left',
next: 'fa fa-fw fa-chevron-right',
last: 'fa fa-fw fa-angle-double-right'
}
},
paginationInfo: {
infoClass: ''
}
};
@@ -0,0 +1,48 @@
<template>
<div>
<div :style="{ height: 300, width: '100%' }"></div>
<content-loader
:height="fixedAmount * count"
:width="1060"
:speed="2"
primaryColor="#d9d9d9"
secondaryColor="#ecebeb"
>
<template v-for="index in count">
<rect x="13" :y="fixedAmount * index + offset" rx="6" ry="6" :width="200 * random()" height="12" />
<rect x="533" :y="fixedAmount * index + offset" rx="6" ry="6" :width="63 * random()" height="12" />
<rect x="653" :y="fixedAmount * index + offset" rx="6" ry="6" :width="78 * random()" height="12" />
<rect x="755" :y="fixedAmount * index + offset" rx="6" ry="6" :width="117 * random()" height="12" />
<rect x="938" :y="fixedAmount * index + offset" rx="6" ry="6" :width="83 * random()" height="12" />
<rect x="0" :y="fixedAmount * index" rx="6" ry="6" width="1060" height=".3" />
</template>
</content-loader>
</div>
</template>
<script>
import { ContentLoader } from 'vue-content-loader';
export default {
props: ['store'],
data: () => ({
fixedAmount: 31,
offset: 10,
steps: [0.7, 0.8, 0.9, 1]
}),
computed: {
count() {
return this.store.perPage;
}
},
methods: {
random() {
return this.steps[Math.floor(Math.random() * this.steps.length)];
}
},
components: {
ContentLoader
}
}
</script>
@@ -0,0 +1,66 @@
<template>
<div class="search-wrapper">
<input type="text" class="search" :placeholder="store.searchPlaceholder" v-model.trim="filterText" @input="doFilter">
<select class="filter-perPage" v-model="store.perPage" @change="changePerPage">
<option v-for="(value, title) in this.perPageOptions"
:value="value"
:selected="store.perPage === value">{{ title }}</option>
</select>
</div>
</template>
<script>
import debounce from 'lodash/debounce';
export default {
props: ['store'],
data: () => ({
filterText: '',
searchPlaceholder: 'Filter...',
selected: ''
}),
computed: {
perPageOptions() {
const options = {
'25': 25,
'50': 50,
'100': 100,
'200': 200,
'All': ''
};
if (!options[this.store.perPage]) {
options[this.store.perPage] = this.store.perPage;
}
return options;
}
},
created() {
this.doFilter = debounce(() => {
this.$events.fire('filter-set', this.filterText);
}, 250, { leading: false });
this.changePerPage = () => {
this.$events.fire('filter-perPage', this.store.perPage);
};
},
methods: {
resetFilter() {
this.filterText = '';
this.$events.fire('filter-reset');
}
}
}
</script>
<style scoped>
.search-wrapper {
display: flex;
}
.search-wrapper select {
margin-bottom: 0;
margin-left: 1rem;
}
</style>
@@ -0,0 +1,94 @@
<template>
<div>
<vuetable ref="vuetable"
:css="css.table"
:fields="store.fields || []"
:searchFields="store.searchFields || []"
:sortOrder="store.sortOrder"
:multi-sort="true"
:api-mode="true"
:api-url="store.api"
:per-page="perPage"
:append-params="extraParams"
pagination-path="links.pagination"
:show-sort-icons="true"
@vuetable:pagination-data="onPaginationData"
@vuetable:loading="onVuetableLoading"
@vuetable:load-success="onVueTableLoadSuccess"
/>
<div class="flex-list-pagination">
<vuetable-pagination-info ref="paginationInfo"
:info-template="store.paginationInfo"
:info-no-data-template="store.emptyResult"
:css="css.paginationInfo"
/>
<vuetable-pagination ref="pagination"
:css="css.pagination"
@vuetable-pagination:change-page="onChangePage"
/>
</div>
</div>
</template>
<script>
import Vue from 'vue';
import Vuetable from 'vuetable-2/src/components/Vuetable.vue';
import VuetablePagination from "vuetable-2/src/components/VuetablePagination.vue";
import VuetablePaginationInfo from 'vuetable-2/src/components/VuetablePaginationInfo.vue';
import VuetableCssConfig from "../VuetableCssConfig.js";
import set from 'lodash/set';
import unset from 'lodash/unset';
export default {
props: ['store', 'value'],
components: {Vuetable, VuetablePagination, VuetablePaginationInfo},
data: () => ({
css: VuetableCssConfig,
perPage: 10,
data: [],
extraParams: {}
}),
created() {
this.perPage = this.store.perPage;
this.data = Object.values(this.store.data);
},
mounted() {
this.$refs.vuetable.setData(this.store.data);
this.$events.$on('filter-set', event => this.onFilterSet(event));
this.$events.$on('filter-reset', event => this.onFilterReset());
this.$events.$on('filter-perPage', event => this.onFilterPerPage(event));
},
methods: {
onPaginationData(paginationData) {
this.$refs.pagination.setPaginationData(paginationData);
this.$refs.paginationInfo.setPaginationData(paginationData);
},
onFilterSet (filterText) {
set(this.extraParams, 'filter', filterText);
Vue.nextTick(() => this.$refs.vuetable.refresh());
},
onFilterReset () {
unset(this.extraParams, 'filter');
Vue.nextTick(() => this.$refs.vuetable.refresh());
},
onFilterPerPage (limit) {
// console.log('onFilterPerPage', limit, this.store.data);
this.perPage = limit || this.$refs.paginationInfo.tablePagination.total;
// this.$refs.vuetable.perPage = limit;
Vue.nextTick(() => this.$refs.vuetable.refresh());
},
onChangePage(page) {
this.$refs.vuetable.changePage(page);
},
onVuetableLoading() {
this.$emit('input', true);
},
onVueTableLoadSuccess() {
this.$emit('input', false);
}
}
}
</script>
+19
View File
@@ -0,0 +1,19 @@
import Vue from 'vue';
import VueEvents from 'vue-events';
import App from './App.vue';
Vue.use(VueEvents);
const ID = '#flex-objects-list';
const element = document.querySelector(ID);
if (element) {
const initialStore = element.dataset.initialStore;
new Vue({ // eslint-disable-line no-new
el: ID,
render: h => h(App, {
props: {initialStore}
})
});
}
+3
View File
@@ -0,0 +1,3 @@
import './list';
import './columns';
import './filters';
+393
View File
@@ -0,0 +1,393 @@
/**
* (c) Trilby Media, LLC
* Author Djamil Legato
*
* Based on Mark Matyas's Finderjs
* MIT License
*/
import $ from 'jquery';
import EventEmitter from 'eventemitter3';
export const DEFAULTS = {
labelKey: 'name',
valueKey: 'value', // new
childKey: 'children',
iconKey: 'icon', // new
itemKey: 'item-key', // new
itemTrigger: null,
pathBar: true,
className: {
container: 'fjs-container',
pathBar: 'fjs-path-bar',
col: 'fjs-col',
list: 'fjs-list',
item: 'fjs-item',
active: 'fjs-active',
children: 'fjs-has-children',
url: 'fjs-url',
itemPrepend: 'fjs-item-prepend',
itemContent: 'fjs-item-content',
itemAppend: 'fjs-item-append'
}
};
class Finder {
constructor(container, data, options) {
this.$emitter = new EventEmitter();
this.container = $(container);
this.data = data;
this.config = $.extend(true, {}, DEFAULTS, options);
this.container.off('click.finder keydown.finder');
// dom events
this.container.on('click.finder', this.clickEvent.bind(this));
this.container.on('keydown.finder', this.keydownEvent.bind(this));
// internal events
this.$emitter.on('item-selected', this.itemSelected.bind(this));
this.$emitter.on('create-column', this.addColumn.bind(this));
this.$emitter.on('navigate', this.navigate.bind(this));
this.$emitter.on('go-to', this.goTo.bind(this, this.data));
this.container.addClass(this.config.className.container).attr('tabindex', 0);
this.createColumn(this.data);
if (this.config.pathBar) {
this.pathBar = this.createPathBar();
this.pathBar.on('click.finder', '[data-breadcrumb-node]', (event) => {
event.preventDefault();
const location = $(event.currentTarget).data('breadcrumbNode');
this.goTo(this.data, location);
});
}
// '' is <Root>
if (this.config.defaultPath || this.config.defaultPath === '') {
this.goTo(this.data, this.config.defaultPath);
}
}
reload(data = this.data) {
this.createColumn(data);
// '' is <Root>
if (this.config.defaultPath || this.config.defaultPath === '') {
this.goTo(data, this.config.defaultPath);
}
}
createColumn(data, parent) {
const callback = (data) => this.createColumn(data, parent);
if (typeof data === 'function') {
data.call(this, parent, callback);
} else if (Array.isArray(data) || typeof data === 'object') {
if (typeof data === 'object') {
data = Array.from(data);
}
const list = this.config.createList || this.createList;
const div = $('<div />');
div.append(list.call(this, data)).addClass(this.config.className.col);
this.$emitter.emit('create-column', div);
return div;
} else {
throw new Error('Unknown data type');
}
}
createPathBar() {
this.container.siblings(`.${this.config.className.pathBar}`).remove();
const pathBar = $(`<div class="${this.config.className.pathBar}" />`);
pathBar.insertAfter(this.container);
return pathBar;
}
clickEvent(event) {
const target = $(event.target);
const column = target.closest(`.${this.config.className.col}`);
const item = target.closest(`.${this.config.className.item}`);
const prevent = target.is('[data-flexpages-prevent]') ? target : target.closest('[data-flexpages-prevent]');
if (prevent.data('flexpagesPrevent') === undefined) {
return true;
}
if (this.config.itemTrigger) {
if (target.is(this.config.itemTrigger) || target.closest(this.config.itemTrigger).length) {
event.stopPropagation();
event.preventDefault();
this.$emitter.emit('item-selected', {column, item});
}
return true;
}
event.stopPropagation();
event.preventDefault();
if (item.length) {
this.$emitter.emit('item-selected', { column, item });
}
}
keydownEvent(event) {
const codes = { 37: 'left', 38: 'up', 39: 'right', 40: 'down', 13: 'enter' };
if (event.keyCode in codes) {
event.stopPropagation();
event.preventDefault();
this.$emitter.emit('navigate', {
direction: codes[event.keyCode]
});
}
}
itemSelected(value) {
const element = value.item;
if (!element.length) { return false; }
const item = element[0]._item;
const column = value.column;
const data = item[this.config.childKey] || this.data; // TODO: this.data for constant refresh
const active = $(column).find(`.${this.config.className.active}`);
if (active.length) {
active.removeClass(this.config.className.active);
}
element.addClass(this.config.className.active);
column.nextAll().remove(); // ?!?!?
this.container[0].focus();
window.scrollTo(window.pageXOffset, window.pageYOffset);
this.updatePathBar();
let newColumn;
if (data) {
newColumn = this.createColumn(data, item);
this.$emitter.emit('interior-selected', item);
} else {
this.$emitter.emit('leaf-selected', item);
}
return newColumn;
}
addColumn(column) {
this.container.append(column);
this.$emitter.emit('column-created', column);
}
navigate(value) {
const active = this.findLastActive();
const direction = value.direction;
let column;
let item;
let target;
if (active) {
item = active.item;
column = active.column;
if (direction === 'up' && item.prev().length) {
target = item.prev();
} else if (direction === 'down' && item.next().length) {
target = item.next();
} else if (direction === 'right' && column.next().length) {
column = column.next();
target = column.find(`.${this.config.className.item}`).first();
} else if (direction === 'left' && column.prev().length) {
column = column.prev();
target = column.find(`.${this.config.className.active}`).first() || column.find(`.${this.config.className.item}`);
}
} else {
column = this.container.find(`.${this.config.className.col}`).first();
target = column.find(`.${this.config.className.item}`).first();
}
if (active && direction === 'enter') {
const href = active.item.find('a').prop('href');
if (href) {
window.location = href;
}
}
if (target) {
this.$emitter.emit('item-selected', {
column,
item: target
});
if (!this.isInView(target, column, true)) {
this.scrollToView(target[0], column[0]);
}
}
}
goTo(data, path) {
path = Array.isArray(path) ? path : path.split('/').map(bit => bit.trim()).filter(Boolean);
if (path.length) {
this.container.children().remove();
}
if (typeof data === 'function') {
data.call(this, null, (data) => this.selectPath(path, data));
} else {
this.selectPath(path, data);
}
}
selectPath(path, data, column) {
column = column || (path.length ? this.createColumn(data) : this.container.find(`> .${this.config.className.col}`));
const current = path[0] || '';
const children = data.find((item) => item[this.config.itemKey] === current);
const item = column.find(`[data-fjs-item="${current}"]`).first();
const newColumn = this.itemSelected({
column,
item
});
if (!this.isInView(item, column, true)) {
this.scrollToView(item[0], column[0]);
}
path.shift();
if (path.length && children) {
this.selectPath(path, children[this.config.childKey], newColumn);
}
}
findLastActive() {
const active = this.container.find(`.${this.config.className.active}`);
if (!active.length) {
return null;
}
const item = active.last();
const column = item.closest(`.${this.config.className.col}`);
return { item, column };
}
createList(data) {
const list = $('<ul />');
const createItem = this.config.createItem || this.createItem;
const items = data.map((item) => createItem.call(this, item));
const fragments = items.reduce((fragment, current) => {
fragment.appendChild(current[0] || current);
return fragment;
}, document.createDocumentFragment());
list.append(fragments).addClass(this.config.className.list);
return list;
}
createItem(item) {
const listItem = $('<li />');
const listItemClasses = [this.config.className.item];
const link = $(`<a href="${item.href || ''}" />`);
const createItemContent = this.config.createItemContent || this.createItemContent;
const fragment = createItemContent.call(this, item);
link.append(fragment)
.attr('href', '')
.attr('tabindex', -1);
if (item.url) {
link.attr('href', item.url);
listItemClasses.push(item.className);
}
if (item[this.config.childKey]) {
listItemClasses.push(this.config.className[this.config.childKey]);
}
listItem.addClass(listItemClasses.join(' '));
listItem.append(link)
.attr('data-fjs-item', item[this.config.itemKey]);
listItem[0]._item = item;
return listItem;
}
updatePathBar() {
if (!this.config.pathBar) { return false; }
const activeItems = this.container.find(`.${this.config.className.active}`);
let itemKeys = '';
this.pathBar.empty();
activeItems.each((index, activeItem) => {
const item = activeItem._item;
const isLast = (index + 1) === activeItems.length;
itemKeys += `/${item[this.config.itemKey]}`;
this.pathBar.append(`
<span class="breadcrumb-node ${item.icon}" ${item.type === 'dir' || item.child_count > 0 ? `data-breadcrumb-node="${itemKeys}"` : ''}>
<i class="${item.icon}"></i>
<span class="breadcrumb-node-name">${$('<div />').html(item[this.config.labelKey]).html()}</span>
${!isLast ? '<i class="fa fa-fw fa-chevron-right"></i>' : ''}
</span>
`);
});
}
getIcon(type) {
switch (type) {
case 'root':
return 'fa-sitemap';
case 'file':
return 'fa-file-o';
case 'dir':
default:
return 'fa-folder';
}
}
isInView(element, container, partial) {
if (!element.length || !container.length) {
return true;
}
const containerHeight = container.height();
const elementTop = $(element).offset().top - container.offset().top;
const elementBottom = elementTop + $(element).height();
const isTotal = (elementTop >= 0 && elementBottom <= containerHeight);
const isPartial = ((elementTop < 0 && elementBottom > 0) || (elementTop > 0 && elementTop <= container.height())) && partial;
return isTotal || isPartial;
}
scrollToView(element, container) {
const top = parseInt(container.getBoundingClientRect().top, 10);
const bot = parseInt(container.getBoundingClientRect().bottom, 10);
const now_top = parseInt(element.getBoundingClientRect().top, 10);
const now_bot = parseInt(element.getBoundingClientRect().bottom, 10);
let scroll_by = 0;
if (now_top < top) {
scroll_by = -(top - now_top);
} else if (now_bot > bot) {
scroll_by = now_bot - bot;
}
if (scroll_by !== 0) {
container.scrollTop += scroll_by;
}
}
}
export default Finder;
@@ -0,0 +1,34 @@
export default () => {
const inputs = document.querySelectorAll('#pages-filters input[name]');
const filters = {};
const trackMulti = [];
inputs.forEach((filter) => {
if (filter.type === 'checkbox') {
if (filter.indeterminate || filter.checked) {
if (filter.name.match(/\[]$/)) {
const name = filter.name.replace(/\[]$/, '');
if (!filters[name]) {
filters[name] = [];
}
if (!trackMulti.includes(name)) {
trackMulti.push(name);
}
filters[name].push(filter.value);
} else {
filters[filter.name] = filter.value;
}
}
} else if (filter.value) {
filters[filter.name] = filter.value;
}
});
trackMulti.forEach((multi) => {
filters[multi] = filters[multi].join(',');
});
return filters;
};
@@ -0,0 +1,44 @@
document.addEventListener('click', (event) => {
const wrapper = event.target.closest('.checkboxes.indeterminate');
if (wrapper) {
event.preventDefault();
const checkbox = wrapper.querySelector('input[type="checkbox"]:not([disabled])');
const checkStatus = wrapper.dataset._checkStatus;
wrapper.classList.remove('status-checked', 'status-unchecked', 'status-indeterminate');
switch (checkStatus) {
// checked, going indeterminate
case '1':
wrapper.dataset._checkStatus = '2';
checkbox.indeterminate = true;
checkbox.checked = false;
checkbox.value = 0;
wrapper.classList.add('status-indeterminate');
break;
// indeterminate, going unchecked
case '2':
wrapper.dataset._checkStatus = '0';
checkbox.indeterminate = false;
checkbox.checked = false;
checkbox.value = '';
wrapper.classList.add('status-unchecked');
break;
// unchecked, going checked
case '0':
default:
wrapper.dataset._checkStatus = '1';
checkbox.indeterminate = false;
checkbox.checked = true;
checkbox.value = 1;
wrapper.classList.add('status-checked');
break;
}
const input = new CustomEvent('input', { detail: { target: checkbox }});
document.dispatchEvent(input);
}
});
(document.querySelectorAll('input[type="checkbox"][indeterminate="true"]') || []).forEach((input) => { input.indeterminate = true; });
Binary file not shown.

After

Width:  |  Height:  |  Size: 135 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 156 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 234 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 250 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 248 KiB

+55
View File
@@ -0,0 +1,55 @@
name: Flex Objects
slug: flex-objects
type: plugin
version: 1.3.5
description: Flex Objects plugin allows you to manage Flex Objects in Grav Admin.
icon: list-alt
author:
name: Trilby Media
email: hello@trilby.media
homepage: https://github.com/trilbymedia/grav-plugin-flex-objects
keywords: grav, plugin, crud, directory
bugs: https://github.com/trilbymedia/grav-plugin-flex-objects/issues
docs: https://github.com/trilbymedia/grav-plugin-flex-objects/blob/develop/README.md
license: MIT
dependencies:
- { name: grav, version: '>=1.7.32' }
- { name: form, version: '>=6.0.0' }
form:
validation: loose
fields:
enabled:
type: toggle
label: PLUGIN_ADMIN.PLUGIN_STATUS
highlight: 1
default: 0
options:
1: PLUGIN_ADMIN.ENABLED
0: PLUGIN_ADMIN.DISABLED
validate:
type: bool
built_in_css:
type: toggle
label: PLUGIN_FLEX_OBJECTS.USE_BUILT_IN_CSS
highlight: 1
default: 1
options:
1: PLUGIN_ADMIN.ENABLED
0: PLUGIN_ADMIN.DISABLED
validate:
type: bool
extra_admin_twig_path:
type: text
label: PLUGIN_FLEX_OBJECTS.EXTRA_ADMIN_TWIG_PATH
validate:
required: true
directories:
type: flex-objects
label: PLUGIN_FLEX_OBJECTS.DIRECTORIES
array: true
ignore_empty: true
validate:
type: array
@@ -0,0 +1,16 @@
type: flex-objects
form:
fields:
# Don't mess with this stuff below unless you know what you are doing
tools_section:
type: section
field_classes: overlay bottom
fields:
_post_entries_save:
label: PLUGIN_FLEX_OBJECTS.AFTER_SAVE
type: save-redirect
default: create-new
@@ -0,0 +1,158 @@
title: Contacts
description: Simple contact directory with tags.
type: flex-objects
# Flex Configuration
config:
# Administration Configuration
admin:
# Admin router (optional)
router:
path: '/contacts'
# Admin menu (optional)
menu:
list:
route: '/contacts'
title: Contacts
icon: fa-address-card-o
# Authorization to collection admin
authorize: ['admin.contacts.list', 'admin.super']
# Priority -10 .. 10 (highest goes up)
priority: 2
# Admin template type / folder
template: default
# Permissions
permissions:
# Primary permissions
admin.contacts:
type: crudpl
label: Contacts
# List view
list:
title: name
fields:
published:
field:
type: toggle
label: Publ
width: 8
last_name:
link: edit
first_name:
link: edit
email:
website:
tags:
# Edit View
edit:
title:
template: '{{ object.last_name ?? ''Last'' }}, {{ object.first_name ?? ''First Name'' }}'
# Preview View
preview:
enabled: false
route:
#template: '/plugins/flex-objects/directory:contacts'
# Data Export
export:
enabled: true
method: 'jsonSerialize'
formatter:
class: 'Grav\Framework\File\Formatter\YamlFormatter'
filename: 'contacts'
# Site Configuration
site:
templates:
collection:
# Lookup for the template layout files for collections of objects
paths:
- 'flex/{TYPE}/collection/{LAYOUT}{EXT}'
object:
# Lookup for the template layout files for objects
paths:
- 'flex/{TYPE}/object/{LAYOUT}{EXT}'
defaults:
# Default template {TYPE}; overridden by filename of this blueprint if template folder exists
type: contacts
# Default template {LAYOUT}; can be overridden in render calls (usually Twig in templates)
layout: default
# Data Configuration
data:
# Object class to be used, allowing custom methods for the object
object: 'Grav\Common\Flex\Types\Generic\GenericObject'
# Collection class to be used, allowing custom methods for the collections
collection: 'Grav\Common\Flex\Types\Generic\GenericCollection'
# Index class to be used, works as a quick database-like lookup index
index: 'Grav\Common\Flex\Types\Generic\GenericIndex'
storage:
# Storage class, use single file storage (does not support images and assets)
class: 'Grav\Framework\Flex\Storage\SimpleStorage'
options:
formatter:
# File formatter class, in this case the file is stored in markdown
class: 'Grav\Framework\File\Formatter\JsonFormatter'
# JSON file where all the objects will be stored
folder: user-data://flex-objects/contacts.json
search:
# Search options
options:
contains: 1
# Fields to be searched
fields:
- last_name
- first_name
- email
form:
validation: loose
fields:
published:
type: toggle
label: Published
highlight: 1
default: 1
options:
1: PLUGIN_ADMIN.YES
0: PLUGIN_ADMIN.NO
validate:
type: bool
required: true
last_name:
type: text
label: Last Name
validate:
required: true
first_name:
type: text
label: First Name
validate:
required: true
email:
type: email
label: Email Address
validate:
required: true
website:
type: url
label: Website URL
tags:
type: selectize
size: large
label: Tags
classes: fancy
validate:
type: commalist
@@ -0,0 +1,22 @@
title: Pages (Admin)
description: Manage your Grav Pages in the new Flex Pages admin. This allows you to use Flex Pages only in Admin plugin, while still using the old pages in the frontend.
type: flex-objects
extends@:
type: pages
context: blueprints://flex
form:
fields:
lang:
type: hidden
value: ''
tabs:
fields:
security:
type: tab
title: PLUGIN_ADMIN.SECURITY
import@:
type: partials/security
context: blueprints://pages
@@ -0,0 +1,7 @@
title: User Accounts (Admin)
description: Manage your User Accounts in the new Flex Accounts admin. This allows you to use Flex Accounts only in Admin plugin, while still using the old users in the frontend.
type: flex-objects
extends@:
type: user-accounts
context: blueprints://flex
@@ -0,0 +1,7 @@
title: User Groups (Admin)
description: Manage your User Groups in the new Flex admin.
type: flex-objects
extends@:
type: user-groups
context: blueprints://flex
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,361 @@
<?php
declare(strict_types=1);
namespace Grav\Plugin\FlexObjects\Controllers;
use Grav\Common\Config\Config;
use Grav\Common\Grav;
use Grav\Common\Inflector;
use Grav\Common\Language\Language;
use Grav\Common\Session;
use Grav\Common\Uri;
use Grav\Common\User\Interfaces\UserInterface;
use Grav\Common\Utils;
use Grav\Framework\Controller\Traits\ControllerResponseTrait;
use Grav\Framework\Flex\FlexDirectory;
use Grav\Framework\Flex\FlexForm;
use Grav\Framework\Flex\FlexFormFlash;
use Grav\Framework\Flex\Interfaces\FlexFormInterface;
use Grav\Framework\Flex\Interfaces\FlexObjectInterface;
use Grav\Framework\Psr7\Response;
use Grav\Framework\RequestHandler\Exception\NotFoundException;
use Grav\Framework\RequestHandler\Exception\PageExpiredException;
use Grav\Framework\Route\Route;
use Grav\Plugin\FlexObjects\Flex;
use Grav\Plugin\Form\Forms;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;
use RocketTheme\Toolbox\Event\Event;
use RocketTheme\Toolbox\Session\Message;
use function in_array;
use function is_callable;
/**
* Class AbstractController
* @package Grav\Plugin\FlexObjects\Controllers
*/
abstract class AbstractController implements RequestHandlerInterface
{
use ControllerResponseTrait;
/** @var string */
protected $nonce_action = 'flex-object';
/** @var string */
protected $nonce_name = 'nonce';
/** @var ServerRequestInterface */
protected $request;
/** @var Grav */
protected $grav;
/** @var UserInterface|null */
protected $user;
/** @var string */
protected $type;
/** @var string */
protected $key;
/** @var FlexDirectory */
protected $directory;
/** @var FlexObjectInterface */
protected $object;
/**
* Handle request.
*
* Fires event: flex.[directory].[task|action].[command]
*
* @param ServerRequestInterface $request
* @return Response
*/
public function handle(ServerRequestInterface $request): ResponseInterface
{
$attributes = $request->getAttributes();
$this->request = $request;
$this->grav = $attributes['grav'] ?? Grav::instance();
$this->type = $attributes['type'] ?? null;
$this->key = $attributes['key'] ?? null;
if ($this->type) {
$this->directory = $this->getFlex()->getDirectory($this->type);
$this->object = $attributes['object'] ?? null;
if (!$this->object && $this->key && $this->directory) {
$this->object = $this->directory->getObject($this->key) ?? $this->directory->createObject([], $this->key ?? '');
if (is_callable([$this->object, 'refresh'])) {
$this->object->refresh();
}
}
}
/** @var Route $route */
$route = $attributes['route'];
$post = $this->getPost();
if ($this->isFormSubmit()) {
$form = $this->getForm();
$this->nonce_name = $attributes['nonce_name'] ?? $form->getNonceName();
$this->nonce_action = $attributes['nonce_action'] ?? $form->getNonceAction();
}
try {
$task = $request->getAttribute('task') ?? $post['task'] ?? $route->getParam('task');
if ($task) {
if (empty($attributes['forwarded'])) {
$this->checkNonce($task);
}
$type = 'task';
$command = $task;
} else {
$type = 'action';
$command = $request->getAttribute('action') ?? $post['action'] ?? $route->getParam('action') ?? 'display';
}
$command = strtolower($command);
$event = new Event(
[
'controller' => $this,
'response' => null
]
);
$this->grav->fireEvent("flex.{$this->type}.{$type}.{$command}", $event);
$response = $event['response'];
if (!$response) {
/** @var Inflector $inflector */
$inflector = $this->grav['inflector'];
$method = $type . $inflector::camelize($command);
if ($method && method_exists($this, $method)) {
$response = $this->{$method}($request);
} else {
throw new NotFoundException($request);
}
}
} catch (\Exception $e) {
$response = $this->createErrorResponse($e);
}
if ($response instanceof Response) {
return $response;
}
return $this->createJsonResponse($response);
}
/**
* @return ServerRequestInterface
*/
public function getRequest(): ServerRequestInterface
{
return $this->request;
}
/**
* @param string|null $name
* @param mixed $default
* @return mixed
*/
public function getPost(string $name = null, $default = null)
{
$body = $this->request->getParsedBody();
if ($name) {
return $body[$name] ?? $default;
}
return $body;
}
/**
* @return bool
*/
public function isFormSubmit(): bool
{
return (bool)$this->getPost('__form-name__');
}
/**
* @param string|null $type
* @return FlexForm
*/
public function getForm(string $type = null): FlexFormInterface
{
$object = $this->getObject();
if (!$object) {
throw new \RuntimeException('Not Found', 404);
}
$formName = $this->getPost('__form-name__');
if ($formName) {
/** @var Forms $forms */
$forms = $this->getGrav()['forms'];
$form = $forms->getActiveForm();
if ($form instanceof FlexForm && $form->getName() === $formName && $form->getObject()->getFlexKey() === $object->getFlexKey()) {
return $form;
}
}
return $object->getForm($type ?? 'edit');
}
/**
* @param FlexObjectInterface $object
* @param string $type
* @return FlexFormFlash
*/
protected function getFormFlash(FlexObjectInterface $object, string $type = '')
{
/** @var Uri $uri */
$uri = $this->grav['uri'];
$url = $uri->url;
$formName = $this->getPost('__form-name__');
if (!$formName) {
$form = $object->getForm($type);
$formName = $form->getName();
$uniqueId = $form->getUniqueId();
} else {
$uniqueId = $this->getPost('__unique_form_id__') ?: $formName ?: sha1($url);
}
/** @var Session $session */
$session = $this->grav['session'];
$config = [
'session_id' => $session->getId(),
'unique_id' => $uniqueId,
'form_name' => $formName,
];
$flash = new FlexFormFlash($config);
if (!$flash->exists()) {
$flash->setUrl($url)->setUser($this->grav['user']);
}
return $flash;
}
/**
* @return Grav
*/
public function getGrav(): Grav
{
return $this->grav;
}
/**
* @return Session
*/
public function getSession(): Session
{
return $this->grav['session'];
}
/**
* @return Flex
*/
public function getFlex(): Flex
{
return $this->grav['flex_objects'];
}
/**
* @return string
*/
public function getDirectoryType(): string
{
return $this->type;
}
/**
* @return string
*/
public function getObjectKey(): string
{
return $this->key;
}
/**
* @return FlexDirectory|null
*/
public function getDirectory(): ?FlexDirectory
{
return $this->directory;
}
/**
* @return FlexObjectInterface|null
*/
public function getObject(): ?FlexObjectInterface
{
return $this->object;
}
/**
* @param string $string
* @param array $args
* @return string
*/
public function translate(string $string, ...$args): string
{
/** @var Language $language */
$language = $this->grav['language'];
array_unshift($args, $string);
return $language->translate($args);
}
/**
* @param string $message
* @param string $type
* @return $this
*/
public function setMessage(string $message, string $type = 'info'): self
{
/** @var Message $messages */
$messages = $this->grav['messages'];
$messages->add($message, $type);
return $this;
}
/**
* @param UserInterface $user
* @return void
*/
public function setUser(UserInterface $user): void
{
$this->user = $user;
}
/**
* @return Config
*/
protected function getConfig(): Config
{
return $this->grav['config'];
}
/**
* @param string $task
* @return void
* @throws PageExpiredException
*/
protected function checkNonce(string $task): void
{
$nonce = null;
if (in_array(strtoupper($this->request->getMethod()), ['POST', 'PUT', 'PATCH', 'DELETE'])) {
$nonce = $this->getPost($this->nonce_name);
}
if (!$nonce) {
$nonce = $this->grav['uri']->param($this->nonce_name);
}
if (!$nonce) {
$nonce = $this->grav['uri']->query($this->nonce_name);
}
if (!$nonce || !Utils::verifyNonce($nonce, $this->nonce_action)) {
throw new PageExpiredException($this->request);
}
}
}
@@ -0,0 +1,675 @@
<?php
declare(strict_types=1);
namespace Grav\Plugin\FlexObjects\Controllers;
use Exception;
use Grav\Common\Debugger;
use Grav\Common\Page\Interfaces\PageInterface;
use Grav\Common\Page\Medium\Medium;
use Grav\Common\Page\Medium\MediumFactory;
use Grav\Common\Utils;
use Grav\Framework\Flex\FlexObject;
use Grav\Framework\Flex\Interfaces\FlexAuthorizeInterface;
use Grav\Framework\Flex\Interfaces\FlexObjectInterface;
use Grav\Framework\Media\Interfaces\MediaInterface;
use LogicException;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\UploadedFileInterface;
use RocketTheme\Toolbox\Event\Event;
use RuntimeException;
use function is_array;
use function is_string;
/**
* Class MediaController
* @package Grav\Plugin\FlexObjects\Controllers
*/
class MediaController extends AbstractController
{
/**
* @return ResponseInterface
*/
public function taskMediaUpload(): ResponseInterface
{
$this->checkAuthorization('media.create');
$object = $this->getObject();
if (null === $object) {
throw new RuntimeException('Not Found', 404);
}
if (!method_exists($object, 'checkUploadedMediaFile')) {
throw new RuntimeException('Not Found', 404);
}
// Get updated object from Form Flash.
$flash = $this->getFormFlash($object);
if ($flash->exists()) {
$object = $flash->getObject() ?? $object;
$object->update([], $flash->getFilesByFields());
}
// Get field for the uploaded media.
$field = $this->getPost('name', 'undefined');
if ($field === 'undefined') {
$field = null;
}
$request = $this->getRequest();
$files = $request->getUploadedFiles();
if ($field && isset($files['data'])) {
$files = $files['data'];
$parts = explode('.', $field);
$last = array_pop($parts);
foreach ($parts as $name) {
if (!is_array($files[$name])) {
throw new RuntimeException($this->translate('PLUGIN_ADMIN.INVALID_PARAMETERS'), 400);
}
$files = $files[$name];
}
$file = $files[$last] ?? null;
} else {
// Legacy call with name being the filename instead of field name.
$file = $files['file'] ?? null;
$field = null;
}
/** @var UploadedFileInterface $file */
if (is_array($file)) {
$file = reset($file);
}
if (!$file instanceof UploadedFileInterface) {
throw new RuntimeException($this->translate('PLUGIN_ADMIN.INVALID_PARAMETERS'), 400);
}
$filename = $file->getClientFilename();
$object->checkUploadedMediaFile($file, $filename, $field);
try {
// TODO: This only merges main level data, but is good for ordering (for now).
$data = $flash->getData() ?? [];
$data = array_replace($data, (array)$this->getPost('data'));
$crop = $this->getPost('crop');
if (is_string($crop)) {
$crop = json_decode($crop, true, 512, JSON_THROW_ON_ERROR);
}
$flash->setData($data);
$flash->addUploadedFile($file, $field, $crop);
$flash->save();
} catch (Exception $e) {
throw new RuntimeException($e->getMessage(), $e->getCode(), $e);
}
// Include exif metadata into the response if configured to do so
$metadata = [];
$include_metadata = $this->grav['config']->get('system.media.auto_metadata_exif', false);
if ($include_metadata) {
$medium = MediumFactory::fromUploadedFile($file);
$media = $object->getMedia();
$media->add($filename, $medium);
$basename = str_replace(['@3x', '@2x'], '', Utils::pathinfo($filename, PATHINFO_BASENAME));
if (isset($media[$basename])) {
$metadata = $media[$basename]->metadata() ?: [];
}
}
$response = [
'code' => 200,
'status' => 'success',
'message' => $this->translate('PLUGIN_ADMIN.FILE_UPLOADED_SUCCESSFULLY'),
'filename' => htmlspecialchars($filename, ENT_QUOTES | ENT_HTML5, 'UTF-8'),
'metadata' => $metadata
];
return $this->createJsonResponse($response);
}
/**
* @return ResponseInterface
*/
public function taskMediaUploadMeta(): ResponseInterface
{
try {
$this->checkAuthorization('media.create');
$object = $this->getObject();
if (null === $object) {
throw new RuntimeException('Not Found', 404);
}
if (!method_exists($object, 'getMediaField')) {
throw new RuntimeException('Not Found', 404);
}
$object->refresh();
// Get updated object from Form Flash.
$flash = $this->getFormFlash($object);
if ($flash->exists()) {
$object = $flash->getObject() ?? $object;
$object->update([], $flash->getFilesByFields());
}
// Get field and data for the uploaded media.
$field = (string)$this->getPost('field');
$media = $object->getMediaField($field);
if (!$media) {
throw new RuntimeException('Media field not found: ' . $field, 404);
}
$data = $this->getPost('data');
if (is_string($data)) {
$data = json_decode($data, true);
}
$filename = Utils::basename($data['name'] ?? '');
// Update field.
$files = $object->getNestedProperty($field, []);
// FIXME: Do we want to save something into the field as well?
$files[$filename] = [];
$object->setNestedProperty($field, $files);
$info = [
'modified' => $data['modified'] ?? null,
'size' => $data['size'] ?? null,
'mime' => $data['mime'] ?? null,
'width' => $data['width'] ?? null,
'height' => $data['height'] ?? null,
'duration' => $data['duration'] ?? null,
'orientation' => $data['orientation'] ?? null,
'meta' => array_filter($data, static function ($val) { return $val !== null; })
];
$info = array_filter($info, static function ($val) { return $val !== null; });
// As the file may not be saved locally, we need to update the index.
$media->updateIndex([$filename => $info]);
$object->save();
$flash->save();
$response = [
'code' => 200,
'status' => 'success',
'message' => $this->translate('PLUGIN_ADMIN.FILE_UPLOADED_SUCCESSFULLY'),
'field' => $field,
'filename' => $filename,
'metadata' => $data
];
} catch (\Exception $e) {
/** @var Debugger $debugger */
$debugger = $this->grav['debugger'];
$debugger->addException($e);
return $this->createJsonErrorResponse($e);
}
return $this->createJsonResponse($response);
}
/**
* @return ResponseInterface
*/
public function taskMediaReorder(): ResponseInterface
{
try {
$this->checkAuthorization('media.update');
$object = $this->getObject();
if (null === $object) {
throw new RuntimeException('Not Found', 404);
}
if (!method_exists($object, 'getMediaField')) {
throw new RuntimeException('Not Found', 404);
}
$object->refresh();
// Get updated object from Form Flash.
$flash = $this->getFormFlash($object);
if ($flash->exists()) {
$object = $flash->getObject() ?? $object;
$object->update([], $flash->getFilesByFields());
}
// Get field and data for the uploaded media.
$field = (string)$this->getPost('field');
$media = $object->getMediaField($field);
if (!$media) {
throw new RuntimeException('Media field not found: ' . $field, 404);
}
// Create id => filename map from all files in the media.
$map = [];
foreach ($media as $name => $medium) {
$id = $medium->get('meta.id');
if ($id) {
$map[$id] = $name;
}
}
// Get reorder list and reorder the map.
$data = $this->getPost('data');
if (is_string($data)) {
$data = json_decode($data, true);
}
$data = array_fill_keys($data, null);
$map = array_filter(array_merge($data, $map), static function($val) { return $val !== null; });
// Reorder the files.
$files = $object->getNestedProperty($field, []);
$map = array_fill_keys($map, null);
$files = array_filter(array_merge($map, $files), static function($val) { return $val !== null; });
// Update field.
$object->setNestedProperty($field, $files);
$object->save();
$flash->save();
$response = [
'code' => 200,
'status' => 'success',
'message' => $this->translate('PLUGIN_ADMIN.FIELD_REORDER_SUCCESSFUL'),
'field' => $field,
'ordering' => array_keys($files)
];
} catch (\Exception $e) {
/** @var Debugger $debugger */
$debugger = $this->grav['debugger'];
$debugger->addException($e);
$ex = new RuntimeException($this->translate('PLUGIN_ADMIN.FIELD_REORDER_FAILED', $field), $e->getCode(), $e);
return $this->createJsonErrorResponse($ex);
}
return $this->createJsonResponse($response);
}
/**
* @return ResponseInterface
*/
public function taskMediaDelete(): ResponseInterface
{
$this->checkAuthorization('media.delete');
/** @var FlexObjectInterface|null $object */
$object = $this->getObject();
if (!$object) {
throw new RuntimeException('Not Found', 404);
}
$filename = $this->getPost('filename');
// Handle bad filenames.
if (!Utils::checkFilename($filename)) {
throw new RuntimeException($this->translate('PLUGIN_ADMIN.NO_FILE_FOUND'), 400);
}
try {
$field = $this->getPost('name');
$flash = $this->getFormFlash($object);
$flash->removeFile($filename, $field);
$flash->save();
} catch (Exception $e) {
throw new RuntimeException($e->getMessage(), $e->getCode(), $e);
}
$response = [
'code' => 200,
'status' => 'success',
'message' => $this->translate('PLUGIN_ADMIN.FILE_DELETED') . ': ' . htmlspecialchars($filename, ENT_QUOTES | ENT_HTML5, 'UTF-8')
];
return $this->createJsonResponse($response);
}
/**
* Used in pagemedia field.
*
* @return ResponseInterface
*/
public function taskMediaCopy(): ResponseInterface
{
$this->checkAuthorization('media.create');
/** @var FlexObjectInterface|null $object */
$object = $this->getObject();
if (!$object) {
throw new RuntimeException('Not Found', 404);
}
if (!method_exists($object, 'uploadMediaFile')) {
throw new RuntimeException('Not Found', 404);
}
$request = $this->getRequest();
$files = $request->getUploadedFiles();
$file = $files['file'] ?? null;
if (!$file instanceof UploadedFileInterface) {
throw new RuntimeException($this->translate('PLUGIN_ADMIN.INVALID_PARAMETERS'), 400);
}
$post = $request->getParsedBody();
$filename = $post['name'] ?? $file->getClientFilename();
// Upload media right away.
$object->uploadMediaFile($file, $filename);
// Include exif metadata into the response if configured to do so
$metadata = [];
$include_metadata = $this->grav['config']->get('system.media.auto_metadata_exif', false);
if ($include_metadata) {
$basename = str_replace(['@3x', '@2x'], '', Utils::pathinfo($filename, PATHINFO_BASENAME));
$media = $object->getMedia();
if (isset($media[$basename])) {
$metadata = $media[$basename]->metadata() ?: [];
}
}
if ($object instanceof PageInterface) {
// Backwards compatibility to existing plugins.
// DEPRECATED: page
$this->grav->fireEvent('onAdminAfterAddMedia', new Event(['object' => $object, 'page' => $object]));
}
$response = [
'code' => 200,
'status' => 'success',
'message' => $this->translate('PLUGIN_ADMIN.FILE_UPLOADED_SUCCESSFULLY'),
'filename' => htmlspecialchars($filename, ENT_QUOTES | ENT_HTML5, 'UTF-8'),
'metadata' => $metadata
];
return $this->createJsonResponse($response);
}
/**
* Used in pagemedia field.
*
* @return ResponseInterface
*/
public function taskMediaRemove(): ResponseInterface
{
$this->checkAuthorization('media.delete');
/** @var FlexObjectInterface|null $object */
$object = $this->getObject();
if (!$object) {
throw new RuntimeException('Not Found', 404);
}
if (!method_exists($object, 'deleteMediaFile')) {
throw new RuntimeException('Not Found', 404);
}
$field = $this->getPost('field');
$filename = $this->getPost('filename');
// Handle bad filenames.
if (!Utils::checkFilename($filename)) {
throw new RuntimeException($this->translate('PLUGIN_ADMIN.NO_FILE_FOUND'), 400);
}
$object->deleteMediaFile($filename, $field);
if ($field) {
$order = $object->getNestedProperty($field);
unset($order[$filename]);
$object->setNestedProperty($field, $order);
$object->save();
}
if ($object instanceof PageInterface) {
// Backwards compatibility to existing plugins.
// DEPRECATED: page
$this->grav->fireEvent('onAdminAfterDelMedia', new Event(['object' => $object, 'page' => $object, 'media' => $object->getMedia(), 'filename' => $filename]));
}
$response = [
'code' => 200,
'status' => 'success',
'message' => $this->translate('PLUGIN_ADMIN.FILE_DELETED') . ': ' . htmlspecialchars($filename, ENT_QUOTES | ENT_HTML5, 'UTF-8')
];
return $this->createJsonResponse($response);
}
/**
* @return ResponseInterface
*/
public function actionMediaList(): ResponseInterface
{
$this->checkAuthorization('media.list');
/** @var MediaInterface|FlexObjectInterface $object */
$object = $this->getObject();
if (!$object) {
throw new RuntimeException('Not Found', 404);
}
// Get updated object from Form Flash.
$flash = $this->getFormFlash($object);
if ($flash->exists()) {
$object = $flash->getObject() ?? $object;
$object->update([], $flash->getFilesByFields());
}
$media = $object->getMedia();
$media_list = [];
/**
* @var string $name
* @var Medium $medium
*/
foreach ($media->all() as $name => $medium) {
$media_list[$name] = [
'url' => $medium->display($medium->get('extension') === 'svg' ? 'source' : 'thumbnail')->cropZoom(400, 300)->url(),
'size' => $medium->get('size'),
'metadata' => $medium->metadata() ?: [],
'original' => $medium->higherQualityAlternative()->get('filename')
];
}
$response = [
'code' => 200,
'status' => 'success',
'results' => $media_list
];
return $this->createJsonResponse($response);
}
/**
* Used by the filepicker field to get a list of files in a folder.
*
* @return ResponseInterface
*/
protected function actionMediaPicker(): ResponseInterface
{
$this->checkAuthorization('media.list');
/** @var FlexObject $object */
$object = $this->getObject();
if (!$object || !\is_callable([$object, 'getFieldSettings'])) {
throw new RuntimeException('Not Found', 404);
}
// Get updated object from Form Flash.
$flash = $this->getFormFlash($object);
if ($flash->exists()) {
$object = $flash->getObject() ?? $object;
$object->update([], $flash->getFilesByFields());
}
$name = $this->getPost('name');
$settings = $name ? $object->getFieldSettings($name) : null;
if (empty($settings['media_picker_field'])) {
throw new RuntimeException('Not Found', 404);
}
$media = $object->getMediaField($name);
$available_files = [];
$metadata = [];
$thumbs = [];
/**
* @var string $name
* @var Medium $medium
*/
foreach ($media->all() as $name => $medium) {
$available_files[] = $name;
if (isset($settings['include_metadata'])) {
$img_metadata = $medium->metadata();
if ($img_metadata) {
$metadata[$name] = $img_metadata;
}
}
}
// Peak in the flashObject for optimistic filepicker updates
$pending_files = [];
$sessionField = base64_encode($this->grav['uri']->url());
$flash = $this->getSession()->getFlashObject('files-upload');
$folder = $media->getPath() ?: null;
if ($flash && isset($flash[$sessionField])) {
foreach ($flash[$sessionField] as $field => $data) {
foreach ($data as $file) {
$test = \dirname($file['path']);
if ($test === $folder) {
$pending_files[] = $file['name'];
}
}
}
}
$this->getSession()->setFlashObject('files-upload', $flash);
// Handle Accepted file types
// Accept can only be file extensions (.pdf|.jpg)
if (isset($settings['accept'])) {
$available_files = array_filter($available_files, function ($file) use ($settings) {
return $this->filterAcceptedFiles($file, $settings);
});
$pending_files = array_filter($pending_files, function ($file) use ($settings) {
return $this->filterAcceptedFiles($file, $settings);
});
}
if (isset($settings['deny'])) {
$available_files = array_filter($available_files, function ($file) use ($settings) {
return $this->filterDeniedFiles($file, $settings);
});
$pending_files = array_filter($pending_files, function ($file) use ($settings) {
return $this->filterDeniedFiles($file, $settings);
});
}
// Generate thumbs if needed
if (isset($settings['preview_images']) && $settings['preview_images'] === true) {
foreach ($available_files as $filename) {
$thumbs[$filename] = $media[$filename]->zoomCrop(100,100)->url();
}
}
$response = [
'code' => 200,
'status' => 'success',
'files' => array_values($available_files),
'pending' => array_values($pending_files),
'folder' => $folder,
'metadata' => $metadata,
'thumbs' => $thumbs
];
return $this->createJsonResponse($response);
}
/**
* @param string $file
* @param array $settings
* @return false|int
*/
protected function filterAcceptedFiles(string $file, array $settings)
{
$valid = false;
foreach ((array)$settings['accept'] as $type) {
$find = str_replace('*', '.*', $type);
$valid |= preg_match('#' . $find . '$#i', $file);
}
return $valid;
}
/**
* @param string $file
* @param array $settings
* @return false|int
*/
protected function filterDeniedFiles(string $file, array $settings)
{
$valid = true;
foreach ((array)$settings['deny'] as $type) {
$find = str_replace('*', '.*', $type);
$valid = !preg_match('#' . $find . '$#i', $file);
}
return $valid;
}
/**
* @param string $action
* @return void
* @throws LogicException
* @throws RuntimeException
*/
protected function checkAuthorization(string $action): void
{
$object = $this->getObject();
if (!$object) {
throw new RuntimeException('Not Found', 404);
}
// If object does not have ACL support ignore ACL checks.
if (!$object instanceof FlexAuthorizeInterface) {
return;
}
switch ($action) {
case 'media.list':
$action = 'read';
break;
case 'media.create':
case 'media.update':
case 'media.delete':
$action = $object->exists() ? 'update' : 'create';
break;
default:
throw new LogicException(sprintf('Unsupported authorize action %s', $action), 500);
}
if (!$object->isAuthorized($action, null, $this->user)) {
throw new RuntimeException('Forbidden', 403);
}
}
}
@@ -0,0 +1,543 @@
<?php
declare(strict_types=1);
namespace Grav\Plugin\FlexObjects\Controllers;
use Grav\Common\Grav;
use Grav\Framework\Flex\FlexForm;
use Grav\Framework\Flex\FlexObject;
use Grav\Framework\Flex\Interfaces\FlexAuthorizeInterface;
use Grav\Framework\Route\Route;
use Grav\Plugin\FlexObjects\Events\FlexTaskEvent;
use Nyholm\Psr7\ServerRequest;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use RocketTheme\Toolbox\Event\Event;
use RuntimeException;
/**
* Object controller is for the frontend.
*
* Currently following tasks are supported:
*
* - save (create or update)
* - create
* - update
* - delete
* - reset
* - preview
*/
class ObjectController extends AbstractController
{
/**
* Save object.
*
* Forwards call to either create or update task.
*
* @param ServerRequestInterface $request
* @return ResponseInterface
*/
public function taskSave(ServerRequestInterface $request): ResponseInterface
{
$form = $this->getForm();
$object = $form->getObject();
return $object->exists() ? $this->taskUpdate($request) : $this->taskCreate($request);
}
/**
* Create object.
*
* Task fails if object exists.
*
* @param ServerRequestInterface $request
* @return ResponseInterface
*/
public function taskCreate(ServerRequestInterface $request): ResponseInterface
{
$this->checkAuthorization('create');
$form = $this->getForm();
$callable = function (array $data, array $files, FlexObject $object) {
if (method_exists($object, 'storeOriginal')) {
$object->storeOriginal();
}
$object->update($data, $files);
if (\is_callable([$object, 'check'])) {
$object->check($this->user);
}
$event = new FlexTaskEvent($this, $object, 'create');
$this->grav->dispatchEvent($event);
$object->save();
};
$form->setSubmitMethod($callable);
$form->handleRequest($request);
if (!$form->isValid()) {
$error = $form->getError();
if ($error) {
$this->setMessage($error, 'error');
}
$errors = $form->getErrors();
foreach ($errors as $field) {
foreach ($field as $error) {
$this->setMessage($error, 'error');
}
}
$data = $form->getData();
if (null !== $data) {
$object = $form->getObject();
$flash = $form->getFlash();
$flash->setObject($object);
$flash->setData($data->toArray());
$flash->save();
}
return $this->createDisplayResponse();
}
// FIXME: make it conditional
$grav = $this->grav;
$grav->fireEvent('gitsync');
$this->object = $form->getObject();
$event = new Event(
[
'task' => 'create',
'controller' => $this,
'object' => $this->object,
'response' => null,
'message' => null,
]
);
$this->grav->fireEvent("flex.{$this->type}.task.create.after", $event);
$this->setMessage($event['message'] ?? $this->translate('PLUGIN_FLEX_OBJECTS.STATE.CREATED_SUCCESSFULLY'), 'info');
if ($event['response']) {
return $event['response'];
}
$redirect = $request->getAttribute('redirect', (string)$request->getUri());
return $this->createRedirectResponse($redirect, 303);
}
/**
* Update object.
*
* Task fails if object does not exist.
*
* @param ServerRequestInterface $request
* @return ResponseInterface
*/
public function taskUpdate(ServerRequestInterface $request): ResponseInterface
{
$this->checkAuthorization('update');
$form = $this->getForm();
$callable = function (array $data, array $files, FlexObject $object) {
if (method_exists($object, 'storeOriginal')) {
$object->storeOriginal();
}
$object->update($data, $files);
if (\is_callable([$object, 'check'])) {
$object->check($this->user);
}
$event = new FlexTaskEvent($this, $object, 'update');
$this->grav->dispatchEvent($event);
$object->save();
};
$form->setSubmitMethod($callable);
$form->handleRequest($request);
if (!$form->isValid()) {
$error = $form->getError();
if ($error) {
$this->setMessage($error, 'error');
}
$errors = $form->getErrors();
foreach ($errors as $field) {
foreach ($field as $error) {
$this->setMessage($error, 'error');
}
}
$data = $form->getData();
if (null !== $data) {
$object = $form->getObject();
$flash = $form->getFlash();
$flash->setObject($object);
$flash->setData($data->toArray());
$flash->save();
}
return $this->createDisplayResponse();
}
// FIXME: make it conditional
$grav = $this->grav;
$grav->fireEvent('gitsync');
$this->object = $form->getObject();
$event = new Event(
[
'task' => 'update',
'controller' => $this,
'object' => $this->object,
'response' => null,
'message' => null,
]
);
$this->grav->fireEvent("flex.{$this->type}.task.update.after", $event);
$this->setMessage($event['message'] ?? $this->translate('PLUGIN_FLEX_OBJECTS.STATE.UPDATED_SUCCESSFULLY'), 'info');
if ($event['response']) {
return $event['response'];
}
$redirect = $request->getAttribute('redirect', (string)$request->getUri()->getPath());
return $this->createRedirectResponse($redirect, 303);
}
/**
* Delete object.
*
* @param ServerRequestInterface $request
* @return ResponseInterface
*/
public function taskDelete(ServerRequestInterface $request): ResponseInterface
{
$this->checkAuthorization('delete');
$object = $this->getObject();
if (!$object) {
throw new RuntimeException('Not Found', 404);
}
$event = new FlexTaskEvent($this, $object, 'delete');
$this->grav->dispatchEvent($event);
$object->delete();
// FIXME: make it conditional
$grav = $this->grav;
$grav->fireEvent('gitsync');
$event = new Event(
[
'task' => 'delete',
'controller' => $this,
'object' => $object,
'response' => null,
'message' => null,
]
);
$this->grav->fireEvent("flex.{$this->type}.task.delete.after", $event);
$this->setMessage($this->translate($event['message'] ?? 'PLUGIN_FLEX_OBJECTS.STATE.DELETED_SUCCESSFULLY'), 'info');
if ($event['response']) {
return $event['response'];
}
$redirect = $request->getAttribute('redirect', (string)$request->getUri()->getPath());
return $this->createRedirectResponse($redirect, 303);
}
/**
* Reset form to original values.
*
* @param ServerRequestInterface $request
* @return ResponseInterface
*/
public function taskReset(ServerRequestInterface $request): ResponseInterface
{
$this->checkAuthorization('save');
$flash = $this->getForm()->getFlash();
$flash->delete();
$redirect = $request->getAttribute('redirect', (string)$request->getUri()->getPath());
return $this->createRedirectResponse($redirect, 303);
}
/**
* Preview object.
*
* Takes a form input and converts it to visible presentation of the object.
*
* @param ServerRequestInterface $request
* @return ResponseInterface
*/
public function taskPreview(ServerRequestInterface $request): ResponseInterface
{
$this->checkAuthorization('save');
/** @var FlexForm $form */
$form = $this->getForm('edit');
$form->setRequest($request);
if (!$form->validate()) {
$error = $form->getError();
if ($error) {
$this->setMessage($error, 'error');
}
$errors = $form->getErrors();
foreach ($errors as $field) {
foreach ($field as $error) {
$this->setMessage($error, 'error');
}
}
return $this->createRedirectResponse((string)$request->getUri(), 303);
}
$this->object = $form->updateObject();
return $this->actionDisplayPreview();
}
/**
* @param ServerRequestInterface $request
* @return ResponseInterface
*/
public function taskMediaList(ServerRequestInterface $request): ResponseInterface
{
$directory = $this->getDirectory();
if (!$directory) {
throw new RuntimeException('Not Found', 404);
}
return $this->forwardMediaTask('action', 'media.list');
}
/**
* @param ServerRequestInterface $request
* @return ResponseInterface
*/
public function taskMediaUpload(ServerRequestInterface $request): ResponseInterface
{
$directory = $this->getDirectory();
if (!$directory) {
throw new RuntimeException('Not Found', 404);
}
return $this->forwardMediaTask('task', 'media.upload');
}
/**
* @param ServerRequestInterface $request
* @return ResponseInterface
*/
public function taskMediaUploadMeta(ServerRequestInterface $request): ResponseInterface
{
$directory = $this->getDirectory();
if (!$directory) {
throw new RuntimeException('Not Found', 404);
}
return $this->forwardMediaTask('task', 'media.upload.meta');
}
/**
* @param ServerRequestInterface $request
* @return ResponseInterface
*/
public function taskMediaReorder(ServerRequestInterface $request): ResponseInterface
{
$directory = $this->getDirectory();
if (!$directory) {
throw new RuntimeException('Not Found', 404);
}
return $this->forwardMediaTask('task', 'media.reorder');
}
/**
* @param ServerRequestInterface $request
* @return ResponseInterface
*/
public function taskMediaDelete(ServerRequestInterface $request): ResponseInterface
{
$directory = $this->getDirectory();
if (!$directory) {
throw new RuntimeException('Not Found', 404);
}
return $this->forwardMediaTask('task', 'media.delete');
}
/**
* @param ServerRequestInterface $request
* @return ResponseInterface
*/
public function taskGetFilesInFolder(ServerRequestInterface $request): ResponseInterface
{
$directory = $this->getDirectory();
if (!$directory) {
throw new RuntimeException('Not Found', 404);
}
return $this->forwardMediaTask('action', 'media.picker');
}
/**
* @param ServerRequestInterface $request
* @return ResponseInterface
* @deprecated Do not use
*/
public function taskFilesUpload(ServerRequestInterface $request): ResponseInterface
{
/** @var Route $route */
$route = $this->grav['route'];
if ($route->getParam('task') === 'media.upload') {
return $this->taskMediaUpload($request);
}
throw new RuntimeException('Task filesUpload should not be called, please update form plugin!', 400);
}
/**
* @param ServerRequestInterface $request
* @return ResponseInterface
* @deprecated Do not use
*/
public function taskRemoveMedia(ServerRequestInterface $request): ResponseInterface
{
/** @var Route $route */
$route = $this->grav['route'];
if ($route->getParam('task') === 'media.delete') {
return $this->taskMediaDelete($request);
}
throw new RuntimeException('Task removeMedia should not be called, please update form plugin!', 400);
}
/**
* Display object preview.
*
* @return ResponseInterface
*/
public function actionDisplayPreview(): ResponseInterface
{
$this->checkAuthorization('save');
$this->checkAuthorization('read');
$object = $this->getObject();
if (!$object) {
throw new RuntimeException('No object found!', 404);
}
$grav = Grav::instance();
$grav['twig']->init();
$grav['theme'];
$content = [
'code' => 200,
'id' => $object->getKey(),
'exists' => $object->exists(),
'html' => (string)$object->render('preview', ['nocache' => []])
];
$accept = $this->getAccept(['application/json', 'text/html']);
if ($accept === 'text/html') {
return $this->createHtmlResponse($content['html']);
}
if ($accept === 'application/json') {
return $this->createJsonResponse($content);
}
throw new RuntimeException('Not found', 404);
}
/**
* @param string $action
* @param string|null $scope
* @return void
* @throws RuntimeException
*/
public function checkAuthorization(string $action, string $scope = null): void
{
$object = $this->getObject();
if (!$object) {
throw new RuntimeException('Not Found', 404);
}
if ($object instanceof FlexAuthorizeInterface) {
if (!$object->isAuthorized($action, $scope, $this->user)) {
throw new RuntimeException('Forbidden', 403);
}
}
}
/**
* @param string[] $actions
* @return void
* @throws RuntimeException
*/
public function checkAuthorizations(array $actions): void
{
$object = $this->getObject();
if (!$object) {
throw new RuntimeException('Not Found', 404);
}
if ($object instanceof FlexAuthorizeInterface) {
$test = false;
foreach ($actions as $action) {
$test |= $object->isAuthorized($action, null, $this->user);
}
if (!$test) {
throw new RuntimeException('Forbidden', 403);
}
}
}
/**
* @param string $type
* @param string $name
* @return ResponseInterface
*/
protected function forwardMediaTask(string $type, string $name): ResponseInterface
{
/** @var Route $route */
$route = $this->grav['route']->withGravParam('task', null)->withGravParam($type, $name);
$object = $this->getObject();
/** @var ServerRequest $request */
$request = $this->grav['request'];
$request = $request
->withAttribute($type, $name)
->withAttribute('type', $this->type)
->withAttribute('key', $this->key)
->withAttribute('storage_key', $object && $object->exists() ? $object->getStorageKey() : null)
->withAttribute('route', $route)
->withAttribute('forwarded', true)
->withAttribute('object', $object);
$controller = new MediaController();
if ($this->user) {
$controller->setUser($this->user);
}
return $controller->handle($request);
}
}
@@ -0,0 +1,84 @@
<?php
namespace Grav\Plugin\FlexObjects\Events;
use Grav\Framework\Flex\Interfaces\FlexCollectionInterface;
use Grav\Framework\Flex\Interfaces\FlexDirectoryInterface;
use Grav\Framework\Flex\Interfaces\FlexObjectInterface;
use Grav\Framework\Object\Interfaces\ObjectInterface;
use Grav\Plugin\FlexObjects\Controllers\AbstractController;
/**
* @template T as FlexObjectInterface
* @template C as FlexCollectionInterface
*/
class FlexTaskEvent
{
/** @var string */
public $task;
/** @var string */
public $type;
/** @var string */
public $key;
/** @var ObjectInterface */
private $object;
/** @var AbstractController */
private $controller;
/**
* @param AbstractController $controller
* @param string $task
*/
public function __construct(AbstractController $controller, ObjectInterface $object, string $task)
{
$this->task = $task;
$this->type = $controller->getDirectoryType();
$this->key = $controller->getObjectKey();
$this->object = $object;
$this->controller = $controller;
}
/**
* @return AbstractController
*/
public function getController(): AbstractController
{
return $this->controller;
}
/**
* @return FlexDirectoryInterface
*/
public function getDirectory(): FlexDirectoryInterface
{
return $this->getController()->getDirectory();
}
/**
* @return FlexObjectInterface
* @phpstan-return T
*/
public function getModifiedObject(): FlexObjectInterface
{
return $this->object;
}
/**
* @return FlexObjectInterface
* @phpstan-return T
*/
public function getOriginalObject(): FlexObjectInterface
{
return $this->controller->getObject();
}
/**
* @return FlexCollectionInterface
* @phpstan-return C
*/
public function getCollection(): FlexCollectionInterface
{
return $this->getController()->getDirectory()->getCollection();
}
}
+444
View File
@@ -0,0 +1,444 @@
<?php
declare(strict_types=1);
namespace Grav\Plugin\FlexObjects;
use Grav\Common\Config\Config;
use Grav\Common\Filesystem\Folder;
use Grav\Common\Grav;
use Grav\Common\Page\Interfaces\PageInterface;
use Grav\Common\Utils;
use Grav\Framework\Flex\FlexDirectory;
use Grav\Framework\Flex\FlexObject;
use Grav\Framework\Flex\Interfaces\FlexCollectionInterface;
use Grav\Framework\Flex\Interfaces\FlexCommonInterface;
use Grav\Framework\Flex\Interfaces\FlexDirectoryInterface;
use Grav\Framework\Flex\Interfaces\FlexInterface;
use Grav\Framework\Flex\Interfaces\FlexObjectInterface;
use Grav\Plugin\FlexObjects\Admin\AdminController;
use Grav\Plugin\FlexObjects\Table\DataTable;
/**
* Class Flex
* @package Grav\Plugin\FlexObjects
*/
class Flex implements FlexInterface
{
/** @var FlexInterface */
protected $flex;
/** @var array */
protected $adminRoutes;
/** @var array */
protected $adminMenu;
/** @var array */
protected $managed;
/**
* @param bool $newToOld
* @return array
* @internal
*/
public static function getLegacyBlueprintMap(bool $newToOld = true): array
{
$map = [
'blueprints://flex-objects/pages.yaml' => 'blueprints://flex-objects/grav-pages.yaml',
'blueprints://flex-objects/user-accounts.yaml' => 'blueprints://flex-objects/grav-accounts.yaml',
'blueprints://flex-objects/user-groups.yaml' => 'blueprints://flex-objects/grav-user-groups.yaml'
];
return $newToOld ? $map : array_flip($map);
}
/**
* Flex constructor.
* @param FlexInterface $flex
* @param array $types
*/
public function __construct(FlexInterface $flex, array $types)
{
$this->flex = $flex;
$this->managed = [];
$legacy = static::getLegacyBlueprintMap(false);
foreach ($types as $blueprint) {
// Backwards compatibility to v1.0.0-rc.3
$blueprint = $legacy[$blueprint] ?? $blueprint;
$type = Utils::basename((string)$blueprint, '.yaml');
if ($type) {
$this->managed[] = $type;
}
}
}
/**
* @param string $type
* @param string $blueprint
* @param array $config
* @return $this
*/
public function addDirectoryType(string $type, string $blueprint, array $config = [])
{
$this->flex->addDirectoryType($type, $blueprint, $config);
return $this;
}
/**
* @param FlexDirectory $directory
* @return $this
*/
public function addDirectory(FlexDirectory $directory)
{
$this->flex->addDirectory($directory);
return $this;
}
/**
* @param string $type
* @return bool
*/
public function hasDirectory(string $type): bool
{
return $this->flex->hasDirectory($type);
}
/**
* @param string[]|null $types
* @param bool $keepMissing
* @return array<FlexDirectoryInterface|null>
*/
public function getDirectories(array $types = null, bool $keepMissing = false): array
{
return $this->flex->getDirectories($types, $keepMissing);
}
/**
* Get directories which are not hidden in the site.
*
* @return array
*/
public function getDefaultDirectories(): array
{
$list = $this->getDirectories();
foreach ($list as $type => $directory) {
if ($directory->getConfig('site.hidden', false)) {
unset($list[$type]);
}
}
return $list;
}
/**
* @param string $type
* @return FlexDirectory|null
*/
public function getDirectory(string $type): ?FlexDirectory
{
return $this->flex->getDirectory($type);
}
/**
* @param string $type
* @param array|null $keys
* @param string|null $keyField
* @return FlexCollectionInterface|null
*/
public function getCollection(string $type, array $keys = null, string $keyField = null): ?FlexCollectionInterface
{
return $this->flex->getCollection($type, $keys, $keyField);
}
/**
* @param array $keys
* @param array $options In addition to the options in getObjects(), following options can be passed:
* collection_class: Class to be used to create the collection. Defaults to ObjectCollection.
* @return FlexCollectionInterface
* @throws \RuntimeException
*/
public function getMixedCollection(array $keys, array $options = []): FlexCollectionInterface
{
return $this->flex->getMixedCollection($keys, $options);
}
/**
* @param array $keys
* @param array $options Following optional options can be passed:
* types: List of allowed types.
* type: Allowed type if types isn't defined, otherwise acts as default_type.
* default_type: Set default type for objects given without type (only used if key_field isn't set).
* keep_missing: Set to true if you want to return missing objects as null.
* key_field: Key field which is used to match the objects.
* @return array
*/
public function getObjects(array $keys, array $options = []): array
{
return $this->flex->getObjects($keys, $options);
}
/**
* @param string $key
* @param string|null $type
* @param string|null $keyField
* @return FlexObjectInterface|null
*/
public function getObject(string $key, string $type = null, string $keyField = null): ?FlexObjectInterface
{
return $this->flex->getObject($key, $type, $keyField);
}
/**
* @return int
*/
public function count(): int
{
return $this->flex->count();
}
public function isManaged(string $type): bool
{
return \in_array($type, $this->managed, true);
}
/**
* @return array
*/
public function getAll(): array
{
$directories = $this->getDirectories($this->managed);
$all = $this->getBlueprints();
/** @var FlexDirectory $directory */
foreach ($all as $type => $directory) {
if (!isset($directories[$type])) {
$directories[$type] = $directory;
}
}
ksort($directories);
return $directories;
}
/**
* @return array
*/
public function getBlueprints(): array
{
$params = [
'pattern' => '|\.yaml|',
'value' => 'Url',
'recursive' => false,
'folders' => false
];
$directories = [];
$all = Folder::all('blueprints://flex-objects', $params);
foreach ($all as $url) {
$type = Utils::basename($url, '.yaml');
$directory = new FlexDirectory($type, $url);
if ($directory->getConfig('hidden') !== true) {
$directories[$type] = $directory;
}
}
// Order blueprints by title.
usort($directories, static function (FlexDirectory $a, FlexDirectory $b) {
return $a->getTitle() <=> $b->getTitle();
});
return $directories;
}
/**
* @param string|FlexDirectory $type
* @param array $options
* @return DataTable
*/
public function getDataTable($type, array $options = []): DataTable
{
$directory = $type instanceof FlexDirectory ? $type : $this->getDirectory($type);
if (!$directory) {
throw new \RuntimeException('Not Found', 404);
}
$collection = $options['collection'] ?? $directory->getCollection();
if (isset($options['filters']) && is_array($options['filters'])) {
$collection = $collection->filterBy($options['filters']);
}
$table = new DataTable($options);
$table->setCollection($collection);
return $table;
}
/**
* @param string|object|null $type
* @param array $params
* @param string $extension
* @return string
*/
public function adminRoute($type = null, array $params = [], string $extension = ''): string
{
if (\is_object($type)) {
$object = $type;
if ($object instanceof FlexCommonInterface || $object instanceof FlexDirectory) {
$type = $type->getFlexType();
} else {
return '';
}
} else {
$object = null;
}
$routes = $this->getAdminRoutes();
$grav = Grav::instance();
/** @var Config $config */
$config = $grav['config'];
if (!Utils::isAdminPlugin()) {
$parts = [
trim($grav['base_url'], '/'),
trim($config->get('plugins.admin.route'), '/')
];
}
if ($type && isset($routes[$type])) {
if (!$routes[$type]) {
// Directory has empty route.
return '';
}
// Directory has it's own menu item.
$parts[] = trim($routes[$type], '/');
} else {
if (empty($routes[''])) {
// Default route has been disabled.
return '';
}
// Use default route.
$parts[] = trim($routes[''], '/');
if ($type) {
$parts[] = $type;
}
}
// Append object key if available.
if ($object instanceof FlexObject) {
if ($object->exists()) {
$parts[] = trim($object->getKey(), '/');
} else {
if ($object->hasKey()) {
$parts[] = trim($object->getKey(), '/');
}
$params = ['' => 'add'] + $params;
}
}
$p = [];
$separator = $config->get('system.param_sep');
foreach ($params as $key => $val) {
$p[] = $key . $separator . $val;
}
$parts = array_filter($parts, static function ($val) { return $val !== ''; });
$route = '/' . implode('/', $parts);
$extension = $extension ? '.' . $extension : '';
return $route . $extension . ($p ? '/' . implode('/', $p) : '');
}
public function getAdminController(): ?AdminController
{
$grav = Grav::instance();
if (!isset($grav['admin'])) {
return null;
}
/** @var PageInterface $page */
$page = $grav['page'];
$header = $page->header();
$callable = $header->controller['controller']['instance'] ?? null;
if (null !== $callable && \is_callable($callable)) {
return $callable();
}
return null;
}
/**
* @return array
*/
public function getAdminRoutes(): array
{
if (null === $this->adminRoutes) {
$routes = [];
/** @var FlexDirectory $directory */
foreach ($this->getDirectories() as $directory) {
$config = $directory->getConfig('admin');
if (!$directory->isEnabled() || !empty($config['disabled'])) {
continue;
}
// Resolve route.
$route = $config['router']['path']
?? $config['menu']['list']['route']
?? "/flex-objects/{$directory->getFlexType()}";
$routes[$directory->getFlexType()] = $route;
}
$this->adminRoutes = $routes;
}
return $this->adminRoutes;
}
/**
* @return array
*/
public function getAdminMenuItems(): array
{
if (null === $this->adminMenu) {
$routes = [];
$count = 0;
$directories = $this->getDirectories();
/** @var FlexDirectory $directory */
foreach ($directories as $directory) {
$config = $directory->getConfig('admin');
if (!$directory->isEnabled() || !empty($config['disabled'])) {
continue;
}
$type = $directory->getFlexType();
$items = $directory->getConfig('admin.menu') ?? [];
if ($items) {
foreach ($items as $view => $item) {
$item += [
'route' => '/' . $type,
'title' => $directory->getTitle(),
'icon' => 'fa fa-file',
'directory' => $type
];
$routes[$type] = $item;
}
} else {
$count++;
}
}
if ($count && !isset($routes[''])) {
$routes[''] = ['route' => '/flex-objects'];
}
$this->adminMenu = $routes;
}
return $this->adminMenu;
}
}
@@ -0,0 +1,80 @@
<?php
declare(strict_types=1);
namespace Grav\Plugin\FlexObjects;
use Grav\Common\Grav;
use Grav\Common\Page\Interfaces\PageInterface;
use Grav\Common\Page\Page;
use Grav\Framework\Form\Interfaces\FormFactoryInterface;
use Grav\Framework\Form\Interfaces\FormInterface;
use RocketTheme\Toolbox\Event\Event;
use function is_callable;
use function is_string;
/**
* Class FlexFormFactory
* @package Grav\Plugin\FlexObjects
*/
class FlexFormFactory implements FormFactoryInterface
{
/**
* @param Page $page
* @param string $name
* @param array $form
* @return FormInterface|null
*/
public function createPageForm(Page $page, string $name, array $form): ?FormInterface
{
return $this->createFormForPage($page, $name, $form);
}
/**
* @param PageInterface $page
* @param string $name
* @param array $form
* @return FormInterface|null
*/
public function createFormForPage(PageInterface $page, string $name, array $form): ?FormInterface
{
// Fire event
$grav = Grav::instance();
$grav->fireEvent('onBeforeFlexFormInitialize', new Event(['page' => $page, 'name' => $name, 'form' => &$form]));
$page->addForms([$form], true);
$formFlex = $form['flex'] ?? [];
$type = $formFlex['type'] ?? null;
$key = $formFlex['key'] ?? null;
if (null !== $key && !is_string($key)) {
$key = (string)$key;
}
$layout = $formFlex['layout'] ?? $name;
/** @var Flex $flex */
$flex = Grav::instance()['flex_objects'];
if (is_string($type)) {
$directory = $flex->getDirectory($type);
if (!$directory) {
return null;
}
$create = $form['actions']['create'] ?? true;
$edit = $form['actions']['edit'] ?? true;
$object = $edit && null !== $key ? $directory->getObject($key) : null;
if ($object) {
if (is_callable([$object, 'refresh'])) {
$object->refresh();
}
} elseif ($create) {
$object = $directory->createObject([], $key ?? '');
}
} else {
$object = $flex->getObject($key);
}
return $object ? $object->getForm($layout, ['form' => $form]) : null;
}
}
@@ -0,0 +1,64 @@
<?php
namespace Grav\Plugin\FlexObjects;
use Grav\Framework\Route\Route;
use Grav\Plugin\FlexObjects\Controllers\MediaController;
use Grav\Plugin\FlexObjects\Controllers\ObjectController;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
/**
* Class FlexRouter
* @package Grav\Plugin\FlexObjects
*/
class FlexRouter implements MiddlewareInterface
{
/**
* @param ServerRequestInterface $request
* @param RequestHandlerInterface $handler
* @return ResponseInterface
*/
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
$context = $request->getAttributes();
/** @var Route $route */
$route = $context['route'];
$post = $request->getParsedBody();
$task = $post['task'] ?? $route->getParam('task');
if (\in_array($task, ['cropupload', 'filesupload'])) {
$task = 'media.upload';
}
switch ($task) {
case 'media.upload':
case 'media.delete':
case 'media.copy':
case 'media.remove':
case 'media.list':
case 'media.add':
case 'listmedia':
case 'addmedia':
case 'delmedia':
return (new MediaController())->handle($request);
case 'save':
case 'create':
case 'update':
case 'delete':
case 'reset':
case 'preview':
case 'move':
return (new ObjectController())->handle($request);
}
// No handler found.
return $handler->handle($request);
}
}
@@ -0,0 +1,410 @@
<?php
declare(strict_types=1);
namespace Grav\Plugin\FlexObjects\Table;
use Grav\Common\Debugger;
use Grav\Common\Grav;
use Grav\Framework\Collection\CollectionInterface;
use Grav\Framework\Flex\Interfaces\FlexAuthorizeInterface;
use Grav\Framework\Flex\Interfaces\FlexCollectionInterface;
use Grav\Framework\Flex\Interfaces\FlexObjectInterface;
use JsonSerializable;
use Throwable;
use Twig\Environment;
use Twig\Error\LoaderError;
use Twig\Error\RuntimeError;
use Twig\Error\SyntaxError;
use function is_array;
use function is_string;
/**
* Class DataTable
* @package Grav\Plugin\Gitea
*
* https://github.com/ratiw/vuetable-2/wiki/Data-Format-(JSON)
* https://github.com/ratiw/vuetable-2/wiki/Sorting
*/
class DataTable implements JsonSerializable
{
/** @var string */
private $url;
/** @var int */
private $limit;
/** @var int */
private $page;
/** @var array */
private $sort;
/** @var string */
private $search;
/** @var FlexCollectionInterface */
private $collection;
/** @var FlexCollectionInterface */
private $filteredCollection;
/** @var array */
private $columns;
/** @var Environment */
private $twig;
/** @var array */
private $twig_context;
/**
* DataTable constructor.
* @param array $params
*/
public function __construct(array $params)
{
$this->setUrl($params['url'] ?? '');
$this->setLimit((int)($params['limit'] ?? 10));
$this->setPage((int)($params['page'] ?? 1));
$this->setSort($params['sort'] ?? ['id' => 'asc']);
$this->setSearch($params['search'] ?? '');
}
/**
* @param string $url
* @return void
*/
public function setUrl(string $url): void
{
$this->url = $url;
}
/**
* @param int $limit
* @return void
*/
public function setLimit(int $limit): void
{
$this->limit = max(1, $limit);
}
/**
* @param int $page
* @return void
*/
public function setPage(int $page): void
{
$this->page = max(1, $page);
}
/**
* @param string|string[] $sort
* @return void
*/
public function setSort($sort): void
{
if (is_string($sort)) {
$sort = $this->decodeSort($sort);
} elseif (!is_array($sort)) {
$sort = [];
}
$this->sort = $sort;
}
/**
* @param string $search
* @return void
*/
public function setSearch(string $search): void
{
$this->search = $search;
}
/**
* @param CollectionInterface $collection
* @return void
*/
public function setCollection(CollectionInterface $collection): void
{
$this->collection = $collection;
$this->filteredCollection = null;
}
/**
* @return int
*/
public function getLimit(): int
{
return $this->limit;
}
/**
* @return int
*/
public function getPage(): int
{
return $this->page;
}
/**
* @return int
*/
public function getLastPage(): int
{
return 1 + (int)floor(max(0, $this->getTotal()-1) / $this->getLimit());
}
/**
* @return int
*/
public function getTotal(): int
{
$collection = $this->filteredCollection ?? $this->getCollection();
return $collection ? $collection->count() : 0;
}
/**
* @return array
*/
public function getSort(): array
{
return $this->sort;
}
/**
* @return FlexCollectionInterface|null
*/
public function getCollection(): ?FlexCollectionInterface
{
return $this->collection;
}
/**
* @param int $page
* @return string|null
*/
public function getUrl(int $page): ?string
{
if ($page < 1 || $page > $this->getLastPage()) {
return null;
}
return "{$this->url}.json?page={$page}&per_page={$this->getLimit()}&sort={$this->encodeSort()}";
}
/**
* @return array
*/
public function getColumns(): array
{
if (null === $this->columns) {
$collection = $this->getCollection();
if (!$collection) {
return [];
}
$blueprint = $collection->getFlexDirectory()->getBlueprint();
$schema = $blueprint->schema();
$columns = $blueprint->get('config/admin/views/list/fields') ?? $blueprint->get('config/admin/list/fields', []);
$list = [];
foreach ($columns as $key => $options) {
if (!isset($options['field'])) {
$options['field'] = $schema->get($options['alias'] ?? $key);
}
if (!$options['field'] || !empty($options['field']['ignore'])) {
continue;
}
$list[$key] = $options;
}
$this->columns = $list;
}
return $this->columns;
}
/**
* @return array
*/
public function getData(): array
{
$grav = Grav::instance();
/** @var Debugger $debugger */
$debugger = $grav['debugger'];
$debugger->startTimer('datatable', 'Data Table');
$collection = $this->getCollection();
if (!$collection) {
return [];
}
if ($this->search !== '') {
$collection = $collection->search($this->search);
}
$columns = $this->getColumns();
$collection = $collection->sort($this->getSort());
$this->filteredCollection = $collection;
$limit = $this->getLimit();
$page = $this->getPage();
$to = $page * $limit;
$from = $to - $limit + 1;
if ($from < 1 || $from > $this->getTotal()) {
$debugger->stopTimer('datatable');
return [];
}
$array = $collection->slice($from-1, $limit);
$twig = $grav['twig'];
$grav->fireEvent('onTwigSiteVariables');
$this->twig = $twig->twig;
$this->twig_context = $twig->twig_vars;
$list = [];
/** @var FlexObjectInterface $object */
foreach ($array as $object) {
$item = [
'id' => $object->getKey(),
'timestamp' => $object->getTimestamp()
];
foreach ($columns as $name => $column) {
$item[str_replace('.', '_', $name)] = $this->renderColumn($name, $column, $object);
}
$item['_actions_'] = $this->renderActions($object);
$list[] = $item;
}
$debugger->stopTimer('datatable');
return $list;
}
/**
* @return array
*/
public function jsonSerialize(): array
{
$data = $this->getData();
$total = $this->getTotal();
$limit = $this->getLimit();
$page = $this->getPage();
$to = $page * $limit;
$from = $to - $limit + 1;
$empty = empty($data);
return [
'links' => [
'pagination' => [
'total' => $total,
'per_page' => $limit,
'current_page' => $page,
'last_page' => $this->getLastPage(),
'next_page_url' => $this->getUrl($page+1),
'prev_page_url' => $this->getUrl($page-1),
'from' => $empty ? null : $from,
'to' => $empty ? null : min($to, $total),
]
],
'data' => $data
];
}
/**
* @param string $name
* @param array $column
* @param FlexObjectInterface $object
* @return false|string
* @throws Throwable
* @throws LoaderError
* @throws RuntimeError
* @throws SyntaxError
*/
protected function renderColumn(string $name, array $column, FlexObjectInterface $object)
{
$grav = Grav::instance();
$flex = $grav['flex_objects'];
$value = $object->getFormValue($name) ?? $object->getNestedProperty($name, $column['field']['default'] ?? null);
$type = $column['field']['type'] ?? 'text';
$hasLink = $column['link'] ?? null;
$link = null;
$authorized = $object instanceof FlexAuthorizeInterface
? ($object->isAuthorized('read') || $object->isAuthorized('update')) : true;
if ($hasLink && $authorized) {
$route = $grav['route']->withExtension('');
$link = $route->withAddedPath($object->getKey())->withoutParams()->getUri();
}
$template = $this->twig->resolveTemplate(["forms/fields/{$type}/edit_list.html.twig", 'forms/fields/text/edit_list.html.twig']);
return $this->twig->load($template)->render([
'value' => $value,
'link' => $link,
'field' => $column['field'],
'object' => $object,
'flex' => $flex,
'route' => $grav['route']->withExtension('')
] + $this->twig_context);
}
/**
* @param FlexObjectInterface $object
* @return false|string
* @throws Throwable
* @throws LoaderError
* @throws RuntimeError
* @throws SyntaxError
*/
protected function renderActions(FlexObjectInterface $object)
{
$grav = Grav::instance();
$type = $object->getFlexType();
$template = $this->twig->resolveTemplate(["flex-objects/types/{$type}/list/list_actions.html.twig", 'flex-objects/types/default/list/list_actions.html.twig']);
return $this->twig->load($template)->render([
'object' => $object,
'flex' => $grav['flex_objects'],
'route' => $grav['route']->withExtension('')
] + $this->twig_context);
}
/**
* @param string $sort
* @param string $fieldSeparator
* @param string $orderSeparator
* @return array
*/
protected function decodeSort(string $sort, string $fieldSeparator = ',', string $orderSeparator = '|'): array
{
$strings = explode($fieldSeparator, $sort);
$list = [];
foreach ($strings as $string) {
$item = explode($orderSeparator, $string, 2);
$key = array_shift($item);
$order = array_shift($item) === 'desc' ? 'desc' : 'asc';
$list[$key] = $order;
}
return $list;
}
/**
* @param string $fieldSeparator
* @param string $orderSeparator
* @return string
*/
protected function encodeSort(string $fieldSeparator = ',', string $orderSeparator = '|'): string
{
$list = [];
foreach ($this->getSort() as $key => $order) {
$list[] = $key . $orderSeparator . ($order ?: 'asc');
}
return implode($fieldSeparator, $list);
}
}
@@ -0,0 +1,141 @@
<?php
namespace Grav\Plugin\Console;
use Exception;
use Grav\Common\Utils;
use Grav\Common\Yaml;
use Grav\Console\ConsoleCommand;
use Symfony\Component\Console\Input\InputOption;
use function count;
/**
* Class FlushQueueCommand
* @package Grav\Console\Cli\
*/
class FlexConvertDataCommand extends ConsoleCommand
{
/** @var array */
protected $options = [];
/**
* @return void
*/
protected function configure(): void
{
$this
->setName('convert-data')
->setAliases(['convertdata'])
->addOption(
'in',
'i',
InputOption::VALUE_REQUIRED,
'path to file to convert from (valid types: [json|yaml])'
)
->addOption(
'out',
'o',
InputOption::VALUE_REQUIRED,
'format of file to convert to [json|yaml]'
)
->setDescription('Converts data from one format to another')
->setHelp('The <info>clear-queue-failures</info> command clears any queue failures that have accumulated');
}
/**
* @return int
*/
protected function serve(): int
{
$input = $this->getInput();
$io = $this->getIO();
$out_raw = null;
$in = $input->getOption('in');
$in_parts = Utils::pathinfo($in);
$in_extension = $in_parts['extension'];
$out_extension = $input->getOption('out');
$io->title('Flex Convert Data');
if (!file_exists($in)) {
$io->error('cannot find the file: ' . realpath($in));
return 1;
}
if (!$in_extension) {
$io->error($in . ' has no file extension defined');
return 1;
}
if (!$out_extension) {
$io->error($out_extension . ' is not a valid extension');
return 1;
}
$in_raw = file_get_contents($in);
// Get the input data
if ($in_extension === 'yaml' || $in_extension === 'yml') {
$in_data = Yaml::parse($in_raw);
} elseif ($in_extension === 'json' ) {
$in_data = json_decode($in_raw, true, 512, JSON_THROW_ON_ERROR);
} else {
$io->error('input files with extension ' . $in_extension . ', is not supported');
return 1;
}
// Simple progress bar
$progress = $io->createProgressBar(count($in_data));
$progress->setFormat('verbose');
$progress->start();
// add Unique Id if needed
$index = 0;
$out_data = [];
foreach ($in_data as $key => $entry) {
if ($key === $index++) {
$out_data[$this->generateKey()] = $entry;
} else {
$out_data[$key] = $entry;
}
$progress->advance();
}
// render progress
$progress->finish();
$io->newLine(2);
// Convert to output format
if ($out_extension === 'yaml' || $out_extension === 'yml') {
$out_raw = Yaml::dump($out_data);
} elseif ($out_extension === 'json' ) {
$out_raw = json_encode($out_data, JSON_PRETTY_PRINT);
} else {
$io->error('input files with extension ' . $out_extension . ', is not supported');
return 1;
}
// Write the file:
$out_filename = $in_parts['dirname'] . '/' . $in_parts['filename'] . '.' . $out_extension;
file_put_contents($out_filename, $out_raw);
$io->success('successfully converted the file and saved as: ' . $out_filename);
return 0;
}
/**
* @return string|false
* @throws Exception
*/
protected function generateKey()
{
return substr(hash('sha256', random_bytes(32)), 0, 32);
}
}
+32
View File
@@ -0,0 +1,32 @@
{
"name": "getgrav/grav-plugin-flex-objects",
"type": "grav-plugin",
"description": "Flex Objects plugin for Grav CMS",
"keywords": ["flex-objects"],
"homepage": "https://github.com/trilbymedia/grav-plugin-flex-objects",
"license": "MIT",
"authors": [
{
"name": "Team Grav",
"email": "devs@getgrav.org",
"homepage": "http://getgrav.org",
"role": "Developer"
}
],
"require": {
"php": "^7.3.6 || ^8.0",
"ext-json": "*",
"ext-mbstring": "*"
},
"autoload": {
"psr-4": {
"Grav\\Plugin\\FlexObjects\\": "classes/"
},
"classmap": ["flex-objects.php"]
},
"config": {
"platform": {
"php": "7.3.6"
}
}
}
+25
View File
@@ -0,0 +1,25 @@
{
"_readme": [
"This file locks the dependencies of your project to a known state",
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "8cd40668f2f51e9d1c7bceed7be5d27d",
"packages": [],
"packages-dev": [],
"aliases": [],
"minimum-stability": "stable",
"stability-flags": [],
"prefer-stable": false,
"prefer-lowest": false,
"platform": {
"php": "^7.3.6 || ^8.0",
"ext-json": "*",
"ext-mbstring": "*"
},
"platform-dev": [],
"platform-overrides": {
"php": "7.3.6"
},
"plugin-api-version": "2.2.0"
}
File diff suppressed because one or more lines are too long
+11
View File
@@ -0,0 +1,11 @@
{
"version": 3,
"file": "admin.css",
"sources": [
"../scss/admin.scss",
"../hdr0",
"../scss/plugin/_admin.scss"
],
"names": [],
"mappings": "AEAA,+BAA+B;AAE/B,AAAA,MAAM,CAAC;EACH,OAAO,EAAE,IAAI,GAUhB;EAXD,AAGI,MAHE,CAGF,UAAU,CAAC;IACP,SAAS,EAAE,IAAI,GAKlB;IATL,AAMQ,MANF,CAGF,UAAU,CAGN,CAAC,CAAC;MACE,MAAM,EAAE,QAAQ,GACnB;;AAKT,AAEI,UAFM,CAEN,WAAW,CAAC;EACR,OAAO,EAAE,IAAI,GAChB;;AAJL,AAMI,UANM,CAMN,eAAe,CAAC;EACZ,MAAM,EAAE,UAAU,GACrB;;AARL,AAUI,UAVM,CAUN,MAAM,CAAC,kBAAkB,CAAC;EACtB,cAAc,EAAE,MAAM;EACtB,OAAO,EAAE,YAAY,GACxB;;AAbL,AAeI,UAfM,CAeN,mBAAmB,AAAA,SAAS,CAAC;EACzB,OAAO,EAAE,IAAI,GAChB;;AAjBL,AAmBI,UAnBM,CAmBN,YAAY,CAAC;EACT,KAAK,EAAE,IAAI,GACd;;AArBL,AAuBI,UAvBM,CAuBN,cAAc,CAAC;EACX,KAAK,EAAE,KAAK,GACf;;AAzBL,AA2BI,UA3BM,CA2BN,KAAK,CAAC;EACF,OAAO,EAAE,KAAK,GACjB;;AA7BL,AA+BI,UA/BM,CA+BN,KAAK,CAAC;EACF,OAAO,EAAE,kBAAkB,GAC9B;;AAjCL,AAmCI,UAnCM,CAmCN,KAAK,CAAC;EACF,OAAO,EAAE,eAAe,GAC3B;;AArCL,AAuCI,UAvCM,CAuCN,EAAE,CAAC;EACC,OAAO,EAAE,SAAS;EAClB,IAAI,EAAE,IAAI,GACb;;AA1CL,AA4CI,UA5CM,CA4CN,EAAE,EA5CN,UAAU,CA4CF,EAAE,CAAC;EACH,OAAO,EAAE,UAAU;EACnB,IAAI,EAAE,IAAI,GAIb;EAlDL,AA+CQ,UA/CE,CA4CN,EAAE,AAGG,MAAM,EA/Cf,UAAU,CA4CF,EAAE,AAGD,MAAM,CAAC;IACJ,OAAO,EAAE,YAAY,GACxB;;AAjDT,AAoDI,UApDM,CAoDN,EAAE,CAAC;EACC,WAAW,EAAE,MAAM;EACnB,QAAQ,EAAE,MAAM;EAChB,aAAa,EAAE,QAAQ,GAC1B;;AAxDL,AA0DI,UA1DM,CA0DN,EAAE,AAAA,WAAW,CAAC;EACV,UAAU,EAAE,KAAK;EACjB,WAAW,EAAE,MAAM,GAMtB;EAlEL,AA6DQ,UA7DE,CA0DN,EAAE,AAAA,WAAW,CAGT,CAAC,CAAC;IACE,KAAK,EAAE,IAAI;IACX,OAAO,EAAE,YAAY;IACrB,YAAY,EAAE,GAAG,GACpB;;AAjET,AAoEI,UApEM,CAoEN,YAAY,CAAC,SAAS,CAAC;EACnB,OAAO,EAAE,IAAI,GAChB;;AAtEL,AAwEI,UAxEM,CAwEN,MAAM,CAAC;EACH,MAAM,EAAE,MAAM;EACd,UAAU,EAAE,MAAM,GAkBrB;EA5FL,AA4EQ,UA5EE,CAwEN,MAAM,CAIF,YAAY,CAAC;IACT,OAAO,EAAE,MAAM,GAClB;EA9ET,AAgFQ,UAhFE,CAwEN,MAAM,CAQF,OAAO,CAAC;IACJ,MAAM,EAAE,CAAC,GACZ;EAlFT,AAoFQ,UApFE,CAwEN,MAAM,CAYF,KAAK,EApFb,UAAU,CAwEN,MAAM,CAYK,MAAM,CAAC;IACV,OAAO,EAAE,YAAY;IACrB,KAAK,EAAE,IAAI,GACd;EAvFT,AAyFQ,UAzFE,CAwEN,MAAM,CAiBF,MAAM,CAAC;IACH,cAAc,EAAE,MAAM,GACzB;;AA3FT,AA8FI,UA9FM,CA8FN,qBAAqB,CAAC;EAClB,MAAM,EAAE,YAAY;EACpB,OAAO,EAAE,UAAU;EACnB,OAAO,EAAE,IAAI;EACb,WAAW,EAAE,MAAM,GAwBtB;EA1HL,AAoGQ,UApGE,CA8FN,qBAAqB,CAMjB,yBAAyB,EApGjC,UAAU,CA8FN,qBAAqB,CAMU,wBAAwB,CAAC;IAChD,IAAI,EAAE,CAAC,GACV;EAtGT,AAwGQ,UAxGE,CA8FN,qBAAqB,CAUjB,wBAAwB,CAAC;IACrB,UAAU,EAAE,KAAK;IACjB,mBAAmB,EAAE,IAAI;IACzB,gBAAgB,EAAE,IAAI;IACtB,kBAAkB,EAAE,IAAI;IACxB,eAAe,EAAE,IAAI,GAYxB;IAzHT,AA+GY,UA/GF,CA8FN,qBAAqB,CAUjB,wBAAwB,CAOpB,OAAO,CAAC;MACJ,aAAa,EAAE,GAAG;MAClB,UAAU,EAAE,WAAW,GAO1B;MAxHb,AAoHoB,UApHV,CA8FN,qBAAqB,CAUjB,wBAAwB,CAOpB,OAAO,AAIF,SAAS,AACL,OAAO,CAAC;QACL,MAAM,EAAE,CAAC,GACZ;;AAOrB,aAAa;AAEb,AACI,cADU,CACV,WAAW,CAAC;EACR,qBAAqB,EAAE,IAAI;EAAE,gBAAgB;EAC7C,mBAAmB,EAAE,IAAI;EAAE,YAAY;EACvC,kBAAkB,EAAE,IAAI;EAAE,oBAAoB;EAC9C,gBAAgB,EAAE,IAAI;EAAE,aAAa;EACrC,eAAe,EAAE,IAAI;EAAE,4BAA4B;EACnD,WAAW,EAAE,IAAI,GAWpB;EAlBL,AAUY,cAVE,CACV,WAAW,AAQN,qBAAqB,CAClB,KAAK,CAAA,AAAA,IAAC,CAAK,UAAU,AAAf,IAAmB,KAAK,AAAA,OAAO,CAAC;IAClC,OAAO,EAAE,OAAO;IAChB,WAAW,EAAE,yBAAyB;IACtC,SAAS,EAAE,MAAM;IACjB,WAAW,EAAE,CAAC;IACd,UAAU,EAAE,MAAM,GACrB;;AAKb,gBAAgB;AAChB,kBAAkB;AAClB,AAAA,UAAU,CAAC,eAAe,CAAC,KAAK,EAAE,UAAU,CAAC,eAAe,CAAC,MAAM,CAAC;EAClE,KAAK,EAAE,OAAO;EACd,MAAM,EAAE,iBAAiB;EACzB,gBAAgB,EAAE,OAAO,GAC1B;;AACD,AAAA,UAAU,CAAC,eAAe,CAAC,KAAK,AAAA,2BAA2B,EAAE,UAAU,CAAC,eAAe,CAAC,MAAM,AAAA,2BAA2B,CAAC;EACxH,KAAK,EAAE,OAAO,GACf;;AACD,AAAA,UAAU,CAAC,eAAe,CAAC,KAAK,AAAA,kBAAkB,EAAE,UAAU,CAAC,eAAe,CAAC,MAAM,AAAA,kBAAkB,CAAC;EACtG,KAAK,EAAE,OAAO,GACf;;AACD,AAAA,UAAU,CAAC,eAAe,CAAC,KAAK,AAAA,iBAAiB,EAAE,UAAU,CAAC,eAAe,CAAC,MAAM,AAAA,iBAAiB,CAAC;EACpG,KAAK,EAAE,OAAO,GACf;;AACD,AAAA,UAAU,CAAC,eAAe,CAAC,KAAK,AAAA,sBAAsB,EAAE,UAAU,CAAC,eAAe,CAAC,MAAM,AAAA,sBAAsB,CAAC;EAC9G,KAAK,EAAE,OAAO,GACf;;AACD,AAAA,UAAU,CAAC,YAAY,CAAC;EACtB,KAAK,EAAE,kBAAkB,GAC1B;;AACD,AAAA,UAAU,CAAC,cAAc,CAAC;EACxB,KAAK,EAAE,kBAAkB,GAC1B;;AACD,AAAA,UAAU,CAAC,KAAK,CAAC,EAAE,CAAC;EAClB,UAAU,EAAE,OAAO,GACpB;;AACD,AAAA,UAAU,CAAC,KAAK,CAAC,EAAE,AAAA,UAAW,CAAA,IAAI,EAAE;EAClC,UAAU,EAAE,KAAK,GAClB;;AACD,AAAA,UAAU,CAAC,CAAC,AAAA,UAAU,CAAC;EACrB,KAAK,EAAE,OAAO,GACf;;AACD,AAAA,UAAU,CAAC,CAAC,AAAA,YAAY,CAAC;EACvB,KAAK,EAAE,OAAO,GACf;;AACD,AAAA,UAAU,CAAC,qBAAqB,CAAC;EAC/B,UAAU,EAAE,OAAO;EACnB,UAAU,EAAE,iBAAiB,GAC9B;;AACD,AAAA,UAAU,CAAC,qBAAqB,CAAC,wBAAwB,CAAC,OAAO,CAAC;EAChE,KAAK,EAAE,OAAO,GACf;;AACD,AAAA,UAAU,CAAC,qBAAqB,CAAC,wBAAwB,CAAC,OAAO,AAAA,OAAO,CAAC;EACvE,KAAK,EAAE,OAAO;EACd,UAAU,EAAE,OAAO,GACpB;;AACD,AAAA,UAAU,CAAC,qBAAqB,CAAC,wBAAwB,CAAC,OAAO,AAAA,SAAS,CAAC;EACzE,KAAK,EAAE,OAAO,GACf"
}
+1
View File
@@ -0,0 +1 @@
#types{padding:3rem}#types .card-item{max-width:100%}#types .card-item p{margin:1rem 0 0}#directory .no-entries{padding:3rem}#directory .search-wrapper{margin:.5rem 1rem}#directory #pager .selectize-control{display:inline-block;vertical-align:middle}#directory .tablesorter-filter.disabled{display:none}#directory .edit-action{float:left}#directory .delete-action{float:right}#directory table{display:table}#directory thead{display:table-header-group}#directory tbody{display:table-row-group}#directory tr{display:table-row;-ms-flex:none;flex:none}#directory td,#directory th{display:table-cell;-ms-flex:none;flex:none}#directory td:focus,#directory th:focus{outline:0!important}#directory td{overflow:hidden;white-space:nowrap;text-overflow:ellipsis}#directory td:last-child{text-align:right;white-space:nowrap}#directory td:last-child a{display:inline-block;float:none;padding-left:5px}#directory .tablesorter .filtered{display:none}#directory #pager{margin:50px 0;text-align:center}#directory #pager .pagedisplay{padding:0 10px}#directory #pager .button{border:0}#directory #pager input,#directory #pager select{display:inline-block;width:auto}#directory #pager select{vertical-align:middle}#directory .flex-list-pagination{display:-ms-flexbox;display:flex;margin:1rem 0 -1rem;padding:.5rem 1rem;-ms-flex-align:center;align-items:center}#directory .flex-list-pagination .flex-objects-pagination,#directory .flex-list-pagination .vuetable-pagination-info{-ms-flex:1;flex:1}#directory .flex-list-pagination .flex-objects-pagination{-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;text-align:right;-khtml-user-select:none}#directory .flex-list-pagination .flex-objects-pagination .button{border-radius:4px;background:0 0}#directory .flex-list-pagination .flex-objects-pagination .button.disabled:active{margin:0}#pages-filters .checkboxes{-webkit-user-select:none;-ms-user-select:none;user-select:none;-webkit-touch-callout:none}#pages-filters .checkboxes.status-indeterminate input[type=checkbox]+label:before{font-family:FontAwesome,sans-serif;font-size:1.2rem;line-height:1;content:'\f00d';text-align:center}#directory .search-wrapper input,#directory .search-wrapper select{color:#646e7c;border:1px solid #e6e6e6;background-color:#fcfcfc}#directory .search-wrapper input::-webkit-input-placeholder,#directory .search-wrapper select::-webkit-input-placeholder{color:#a7afb8}#directory .search-wrapper input::-moz-placeholder,#directory .search-wrapper select::-moz-placeholder{color:#a7afb8}#directory .search-wrapper input:-moz-placeholder,#directory .search-wrapper select:-moz-placeholder{color:#a7afb8}#directory .search-wrapper input:-ms-input-placeholder,#directory .search-wrapper select:-ms-input-placeholder{color:#a7afb8}#directory .edit-action{color:#0090d9!important}#directory .delete-action{color:#f45857!important}#directory thead tr{background:#fafafa}#directory tbody tr:nth-child(even){background:#fff}#directory i.published{color:#27ae60}#directory i.unpublished{color:#e74c3c}#directory .flex-list-pagination{border-top:1px solid #f2f2f2;background:#fcfcfc}#directory .flex-list-pagination .flex-objects-pagination .button{color:#414147}#directory .flex-list-pagination .flex-objects-pagination .button.active{color:#fff;background:#0090d9}#directory .flex-list-pagination .flex-objects-pagination .button.disabled{color:#99a2ad}
+27
View File
@@ -0,0 +1,27 @@
#flex-objects .text-center { text-align: center; }
#flex-objects .search { display: inline-block; width: 50%; padding: 0.7rem; }
#flex-objects button.sort { vertical-align: top; }
#flex-objects button.sort:after { font-family: FontAwesome; }
#flex-objects button.sort.asc:after { content: "\2191"; }
#flex-objects button.sort.desc:after { content: "\2193"; }
#flex-objects ul { margin: 3rem 0; }
#flex-objects ul li { display: inline-block; width: 33%; margin: 1rem 0; }
#flex-objects ul li .entry-details .name { font-weight: bold; font-size: 120%; }
#flex-objects ul li .entry-details p { margin: 0; }
#flex-objects ul li .entry-extra span { background: #eee; border-radius: 4px; padding: 3px 5px; font-size: 85%; }
@media only screen and (max-width: 800px) { #flex-objects ul li { width: 50%; } }
@media only screen and (max-width: 500px) { #flex-objects ul li { width: 100%; } }
/*# sourceMappingURL=data:application/json;charset=utf8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic2l0ZS5jc3MiLCJzb3VyY2VzIjpbInNpdGUuc2NzcyIsInBsdWdpbi9fc2l0ZS5zY3NzIl0sInNvdXJjZXNDb250ZW50IjpbIi8vIExvYWQgTWFpbiBTdHlsZXNcbkBpbXBvcnQgXCJwbHVnaW4vc2l0ZVwiOyIsIiNmbGV4LW9iamVjdHMge1xuXG4gIC50ZXh0LWNlbnRlciB7XG4gICAgdGV4dC1hbGlnbjogY2VudGVyO1xuICB9XG5cbiAgLnNlYXJjaCB7XG4gICAgZGlzcGxheTogaW5saW5lLWJsb2NrO1xuICAgIHdpZHRoOiA1MCU7XG4gICAgcGFkZGluZzogMC43cmVtO1xuICB9XG5cbiAgYnV0dG9uLnNvcnQge1xuICAgIHZlcnRpY2FsLWFsaWduOiB0b3A7XG5cbiAgICAmOmFmdGVyIHtcbiAgICAgIGZvbnQtZmFtaWx5OiBGb250QXdlc29tZTtcbiAgICB9XG4gICAgJi5hc2M6YWZ0ZXIge1xuICAgICAgY29udGVudDogXCJcXDIxOTFcIjtcbiAgICB9XG4gICAgJi5kZXNjOmFmdGVyIHtcbiAgICAgIGNvbnRlbnQ6IFwiXFwyMTkzXCI7XG4gICAgfVxuICB9XG5cbiAgdWwge1xuICAgIG1hcmdpbjogM3JlbSAwO1xuXG4gICAgbGkge1xuICAgICAgZGlzcGxheTogaW5saW5lLWJsb2NrO1xuICAgICAgd2lkdGg6IDMzJTtcbiAgICAgIG1hcmdpbjogMXJlbSAwO1xuXG4gICAgICAuZW50cnktZGV0YWlscyB7XG5cbiAgICAgICAgLm5hbWUge1xuICAgICAgICAgIGZvbnQtd2VpZ2h0OiBib2xkO1xuICAgICAgICAgIGZvbnQtc2l6ZTogMTIwJTtcbiAgICAgICAgfVxuICAgICAgICBwIHtcbiAgICAgICAgICBtYXJnaW46IDA7XG4gICAgICAgIH1cbiAgICAgIH1cblxuICAgICAgLmVudHJ5LWV4dHJhIHtcbiAgICAgICAgc3BhbiB7XG4gICAgICAgICAgYmFja2dyb3VuZDogI2VlZTtcbiAgICAgICAgICBib3JkZXItcmFkaXVzOiA0cHg7XG4gICAgICAgICAgcGFkZGluZzogM3B4IDVweDtcbiAgICAgICAgICBmb250LXNpemU6IDg1JTtcbiAgICAgICAgfVxuICAgICAgfVxuICAgIH1cbiAgfVxufVxuXG5AbWVkaWEgb25seSBzY3JlZW4gYW5kIChtYXgtd2lkdGg6IDgwMHB4KSB7XG4gICNmbGV4LW9iamVjdHMgdWwgbGkge1xuICAgIHdpZHRoOiA1MCU7XG4gIH1cbn1cblxuQG1lZGlhIG9ubHkgc2NyZWVuIGFuZCAobWF4LXdpZHRoOiA1MDBweCkge1xuICAjZmxleC1vYmplY3RzIHVsIGxpIHtcbiAgICB3aWR0aDogMTAwJTtcbiAgfVxufSJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUNBQSxBQUVFLGFBRlcsQ0FFWCxZQUFZLENBQUMsRUFDWCxVQUFVLEVBQUUsTUFBTSxHQUNuQjs7QUFKSCxBQU1FLGFBTlcsQ0FNWCxPQUFPLENBQUMsRUFDTixPQUFPLEVBQUUsWUFBWSxFQUNyQixLQUFLLEVBQUUsR0FBRyxFQUNWLE9BQU8sRUFBRSxNQUFNLEdBQ2hCOztBQVZILEFBWUUsYUFaVyxDQVlYLE1BQU0sQUFBQSxLQUFLLENBQUMsRUFDVixjQUFjLEVBQUUsR0FBRyxHQVdwQjs7QUF4QkgsQUFlSSxhQWZTLENBWVgsTUFBTSxBQUFBLEtBQUssQUFHUixNQUFNLENBQUMsRUFDTixXQUFXLEVBQUUsV0FBVyxHQUN6Qjs7QUFqQkwsQUFrQkksYUFsQlMsQ0FZWCxNQUFNLEFBQUEsS0FBSyxBQU1SLElBQUksQUFBQSxNQUFNLENBQUMsRUFDVixPQUFPLEVBQUUsT0FBTyxHQUNqQjs7QUFwQkwsQUFxQkksYUFyQlMsQ0FZWCxNQUFNLEFBQUEsS0FBSyxBQVNSLEtBQUssQUFBQSxNQUFNLENBQUMsRUFDWCxPQUFPLEVBQUUsT0FBTyxHQUNqQjs7QUF2QkwsQUEwQkUsYUExQlcsQ0EwQlgsRUFBRSxDQUFDLEVBQ0QsTUFBTSxFQUFFLE1BQU0sR0EyQmY7O0FBdERILEFBNkJJLGFBN0JTLENBMEJYLEVBQUUsQ0FHQSxFQUFFLENBQUMsRUFDRCxPQUFPLEVBQUUsWUFBWSxFQUNyQixLQUFLLEVBQUUsR0FBRyxFQUNWLE1BQU0sRUFBRSxNQUFNLEdBcUJmOztBQXJETCxBQW9DUSxhQXBDSyxDQTBCWCxFQUFFLENBR0EsRUFBRSxDQUtBLGNBQWMsQ0FFWixLQUFLLENBQUMsRUFDSixXQUFXLEVBQUUsSUFBSSxFQUNqQixTQUFTLEVBQUUsSUFBSSxHQUNoQjs7QUF2Q1QsQUF3Q1EsYUF4Q0ssQ0EwQlgsRUFBRSxDQUdBLEVBQUUsQ0FLQSxjQUFjLENBTVosQ0FBQyxDQUFDLEVBQ0EsTUFBTSxFQUFFLENBQUMsR0FDVjs7QUExQ1QsQUE4Q1EsYUE5Q0ssQ0EwQlgsRUFBRSxDQUdBLEVBQUUsQ0FnQkEsWUFBWSxDQUNWLElBQUksQ0FBQyxFQUNILFVBQVUsRUFBRSxJQUFJLEVBQ2hCLGFBQWEsRUFBRSxHQUFHLEVBQ2xCLE9BQU8sRUFBRSxPQUFPLEVBQ2hCLFNBQVMsRUFBRSxHQUFHLEdBQ2Y7O0FBTVQsTUFBTSxNQUFNLE1BQU0sTUFBTSxTQUFTLEVBQUUsS0FBSyxJQUN0QyxBQUFBLGFBQWEsQ0FBQyxFQUFFLENBQUMsRUFBRSxDQUFDLEVBQ2xCLEtBQUssRUFBRSxHQUFHLEdBQ1g7O0FBR0gsTUFBTSxNQUFNLE1BQU0sTUFBTSxTQUFTLEVBQUUsS0FBSyxJQUN0QyxBQUFBLGFBQWEsQ0FBQyxFQUFFLENBQUMsRUFBRSxDQUFDLEVBQ2xCLEtBQUssRUFBRSxJQUFJLEdBQ1oifQ== */
+11
View File
@@ -0,0 +1,11 @@
{
"version": 3,
"file": "site.css",
"sources": [
"../scss/site.scss",
"../hdr0",
"../scss/plugin/_site.scss"
],
"names": [],
"mappings": "AEAA,AAEE,aAFW,CAEX,YAAY,CAAC;EACX,UAAU,EAAE,MAAM,GACnB;;AAJH,AAME,aANW,CAMX,OAAO,CAAC;EACN,OAAO,EAAE,YAAY;EACrB,KAAK,EAAE,GAAG;EACV,OAAO,EAAE,MAAM,GAChB;;AAVH,AAYE,aAZW,CAYX,MAAM,AAAA,KAAK,CAAC;EACV,cAAc,EAAE,GAAG,GAWpB;EAxBH,AAeI,aAfS,CAYX,MAAM,AAAA,KAAK,AAGR,MAAM,CAAC;IACN,WAAW,EAAE,WAAW,GACzB;EAjBL,AAkBI,aAlBS,CAYX,MAAM,AAAA,KAAK,AAMR,IAAI,AAAA,MAAM,CAAC;IACV,OAAO,EAAE,OAAO,GACjB;EApBL,AAqBI,aArBS,CAYX,MAAM,AAAA,KAAK,AASR,KAAK,AAAA,MAAM,CAAC;IACX,OAAO,EAAE,OAAO,GACjB;;AAvBL,AA0BE,aA1BW,CA0BX,EAAE,CAAC;EACD,MAAM,EAAE,MAAM,GA2Bf;EAtDH,AA6BI,aA7BS,CA0BX,EAAE,CAGA,EAAE,CAAC;IACD,OAAO,EAAE,YAAY;IACrB,KAAK,EAAE,GAAG;IACV,MAAM,EAAE,MAAM,GAqBf;IArDL,AAoCQ,aApCK,CA0BX,EAAE,CAGA,EAAE,CAKA,cAAc,CAEZ,KAAK,CAAC;MACJ,WAAW,EAAE,IAAI;MACjB,SAAS,EAAE,IAAI,GAChB;IAvCT,AAwCQ,aAxCK,CA0BX,EAAE,CAGA,EAAE,CAKA,cAAc,CAMZ,CAAC,CAAC;MACA,MAAM,EAAE,CAAC,GACV;IA1CT,AA8CQ,aA9CK,CA0BX,EAAE,CAGA,EAAE,CAgBA,YAAY,CACV,IAAI,CAAC;MACH,UAAU,EAAE,IAAI;MAChB,aAAa,EAAE,GAAG;MAClB,OAAO,EAAE,OAAO;MAChB,SAAS,EAAE,GAAG,GACf;;AAMT,MAAM,MAAM,MAAM,MAAM,SAAS,EAAE,KAAK;EACtC,AAAA,aAAa,CAAC,EAAE,CAAC,EAAE,CAAC;IAClB,KAAK,EAAE,GAAG,GACX;;AAGH,MAAM,MAAM,MAAM,MAAM,SAAS,EAAE,KAAK;EACtC,AAAA,aAAa,CAAC,EAAE,CAAC,EAAE,CAAC;IAClB,KAAK,EAAE,IAAI,GACZ"
}
+1
View File
@@ -0,0 +1 @@
#flex-objects .text-center{text-align:center}#flex-objects .search{display:inline-block;width:50%;padding:.7rem}#flex-objects button.sort{vertical-align:top}#flex-objects button.sort:after{font-family:FontAwesome}#flex-objects button.sort.asc:after{content:'\2191'}#flex-objects button.sort.desc:after{content:'\2193'}#flex-objects ul{margin:3rem 0}#flex-objects ul li{display:inline-block;width:33%;margin:1rem 0}#flex-objects ul li .entry-details .name{font-size:120%;font-weight:700}#flex-objects ul li .entry-details p{margin:0}#flex-objects ul li .entry-extra span{font-size:85%;padding:3px 5px;border-radius:4px;background:#eee}@media only screen and (max-width:800px){#flex-objects ul li{width:50%}}@media only screen and (max-width:500px){#flex-objects ul li{width:100%}}
+805
View File
@@ -0,0 +1,805 @@
<?php
namespace Grav\Plugin;
use Composer\Autoload\ClassLoader;
use Grav\Common\Debugger;
use Grav\Common\Grav;
use Grav\Common\Page\Interfaces\PageInterface;
use Grav\Common\Page\Pages;
use Grav\Common\Page\Types;
use Grav\Common\Plugin;
use Grav\Common\User\Interfaces\UserInterface;
use Grav\Common\Utils;
use Grav\Events\FlexRegisterEvent;
use Grav\Events\PermissionsRegisterEvent;
use Grav\Events\PluginsLoadedEvent;
use Grav\Framework\Acl\PermissionsReader;
use Grav\Framework\Flex\FlexDirectory;
use Grav\Framework\Flex\FlexForm;
use Grav\Framework\Flex\Interfaces\FlexAuthorizeInterface;
use Grav\Framework\Flex\Interfaces\FlexInterface;
use Grav\Framework\Form\Interfaces\FormInterface;
use Grav\Framework\Route\Route;
use Grav\Plugin\Admin\Admin;
use Grav\Plugin\FlexObjects\Controllers\ObjectController;
use Grav\Plugin\FlexObjects\FlexFormFactory;
use Grav\Plugin\Form\Forms;
use Grav\Plugin\FlexObjects\Admin\AdminController;
use Grav\Plugin\FlexObjects\Flex;
use Psr\Http\Message\ServerRequestInterface;
use RocketTheme\Toolbox\Event\Event;
use function is_array;
use function is_callable;
/**
* Class FlexObjectsPlugin
* @package Grav\Plugin
*/
class FlexObjectsPlugin extends Plugin
{
/** @var string */
protected const MIN_GRAV_VERSION = '1.7.0';
/** @var int[] */
public $features = [
'blueprints' => 1000,
];
/** @var AdminController */
protected $controller;
/**
* @return bool
*/
public static function checkRequirements(): bool
{
return version_compare(GRAV_VERSION, static::MIN_GRAV_VERSION, '>=');
}
/**
* @return array
*
* The getSubscribedEvents() gives the core a list of events
* that the plugin wants to listen to. The key of each
* array section is the event that the plugin listens to
* and the value (in the form of an array) contains the
* callable (or function) as well as the priority. The
* higher the number the higher the priority.
*/
public static function getSubscribedEvents(): array
{
if (!static::checkRequirements()) {
return [];
}
return [
PluginsLoadedEvent::class => [
['initializeFlex', 10]
],
PermissionsRegisterEvent::class => [
['onRegisterPermissions', 100]
],
FlexRegisterEvent::class => [
['onRegisterFlex', 100]
],
'onCliInitialize' => [
['autoload', 100000],
['initializeFlex', 10]
],
'onPluginsInitialized' => [
['onPluginsInitialized', 0],
],
'onFormRegisterTypes' => [
['onFormRegisterTypes', 0]
]
];
}
/**
* Get list of form field types specified in this plugin. Only special types needs to be listed.
*
* @return array
*/
public function getFormFieldTypes()
{
return [
'list' => [
'array' => true
],
'pagemedia' => [
'array' => true,
'media_field' => true,
'validate' => [
'type' => 'ignore'
]
],
'filepicker' => [
'media_picker_field' => true
],
];
}
/**
* @return ClassLoader
*/
public function autoload(): ClassLoader
{
return require __DIR__ . '/vendor/autoload.php';
}
/**
* [PluginsLoadedEvent:10]: Initialize Flex
*
* @return void
*/
public function initializeFlex(): void
{
$config = $this->config->get('plugins.flex-objects.directories') ?? [];
// Add to DI container
$this->grav['flex_objects'] = static function (Grav $grav) use ($config) {
/** @var FlexInterface $flex */
$flex = $grav['flex'];
$flexObjects = new Flex($flex, $config);
// This event is for backwards compatibility only, do not use it!
$grav->fireEvent('onFlexInit', new Event(['flex' => $flexObjects]));
return $flexObjects;
};
}
/**
* Initialize the plugin
*
* @return void
*/
public function onPluginsInitialized(): void
{
if ($this->isAdmin()) {
/** @var UserInterface|null $user */
$user = $this->grav['user'] ?? null;
if (null === $user || !$user->authorize('login', 'admin')) {
return;
}
$this->enable([
'onAdminTwigTemplatePaths' => [
['onAdminTwigTemplatePaths', 10]
],
'onAdminMenu' => [
['onAdminMenu', 0]
],
'onAdminPage' => [
['onAdminPage', 0]
],
'onAdminCompilePresetSCSS' => [
['onAdminCompilePresetSCSS', 0]
],
'onDataTypeExcludeFromDataManagerPluginHook' => [
['onDataTypeExcludeFromDataManagerPluginHook', 0]
],
'onAdminControllerInit' => [
['onAdminControllerInit', 0]
],
'onThemeInitialized' => [
['onThemeInitialized', 0]
],
'onPageInitialized' => [
['onAdminPageInitialized', 0]
],
'onTwigSiteVariables' => [
['onTwigAdminVariables', 0]
],
'onGetPageTemplates' =>
['onGetPageTemplates', 0]
]);
} else {
$this->enable([
'onTwigTemplatePaths' => [
['onTwigTemplatePaths', 0]
],
'onPagesInitialized' => [
['onPagesInitialized', -10000]
],
'onPageInitialized' => [
['authorizePage', 10000]
],
'onBeforeFlexFormInitialize' => [
['onBeforeFlexFormInitialize', -10]
],
'onPageTask' => [
['onPageTask', -10]
],
]);
}
}
/**
* @param FlexRegisterEvent $event
* @return void
*/
public function onRegisterFlex(FlexRegisterEvent $event): void
{
/** @var \Grav\Framework\Flex\Flex $flex */
$flex = $event->flex;
$types = (array)$this->config->get('plugins.flex-objects.directories', []);
$this->registerDirectories($flex, $types);
}
/**
* @return void
*/
public function onThemeInitialized(): void
{
// Register directories defined in the theme.
/** @var \Grav\Framework\Flex\Flex $flex */
$flex = $this->grav['flex'];
$types = (array)$this->config->get('plugins.flex-objects.directories', []);
$this->registerDirectories($flex, $types, true);
$this->controller = new AdminController();
/** @var Debugger $debugger */
$debugger = Grav::instance()['debugger'];
$names = implode(', ', array_keys($flex->getDirectories()));
$debugger->addMessage(sprintf('Registered flex types: %s', $names), 'debug');
}
/**
* @param Event $event
*/
public function onBeforeFlexFormInitialize(Event $event): void
{
/** @var array $form */
$form = $event['form'];
$edit = $form['actions']['edit'] ?? false;
if (!isset($form['flex']['key']) && $edit === true) {
/** @var Route $route */
$route = $this->grav['route'];
$id = rawurldecode($route->getGravParam('id'));
if (null !== $id) {
$form['flex']['key'] = $id;
$event['form'] = $form;
}
}
}
/**
* [onPagesInitialized:-10000] Default router for flex pages.
*
* @param Event $event
*/
public function onPagesInitialized(Event $event): void
{
/** @var Route|null $route */
$route = $event['route'] ?? null;
if (null === $route) {
// Stop if in CLI.
return;
}
/** @var PageInterface|null $page */
$page = $this->grav['page'] ?? null;
$base = '';
$path = [];
if (!$page->routable() || $page->template() === 'notfound') {
/** @var Pages $pages */
$pages = $this->grav['pages'];
// Find first existing and routable parent page.
$parts = explode('/', $route->getRoute());
array_shift($parts);
$page = null;
while (!$page && $parts) {
$path[] = array_pop($parts);
$base = '/' . implode('/', $parts);
$page = $pages->find($base);
if ($page && !$page->routable()) {
$page = null;
}
}
}
// If page is found, check if it contains flex directory router.
if ($page) {
$flex = $this->grav['flex'];
$options = $page->header()->flex ?? null;
$router = $options['router'] ?? null;
$type = $options['directory'] ?? null;
$directory = $type ? $flex->getDirectory($type) : null;
if (\is_string($router)) {
$path = implode('/', array_reverse($path));
$response = null;
$flexEvent = new Event([
'flex' => $flex,
'directory' => $directory,
'parent' => $page,
'page' => $page,
'base' => $base,
'path' => $path,
'route' => $route,
'options' => $options,
'request' => $event['request'],
'response' => &$response,
]);
$flexEvent = $this->grav->fireEvent("flex.router.{$router}", $flexEvent);
if ($response) {
$this->grav->close($response);
}
/** @var PageInterface|null $routedPage */
$routedPage = $flexEvent['page'];
if ($routedPage) {
/** @var Debugger $debugger */
$debugger = Grav::instance()['debugger'];
$debugger->addMessage(sprintf('Flex uses page %s', $routedPage->route()));
unset($this->grav['page']);
$this->grav['page'] = $routedPage;
$event->stopPropagation();
}
}
}
}
/**
* [onPageInitialized:10000] Authorize Flex Objects Page
*
* @param Event $event
*/
public function authorizePage(Event $event): void
{
/** @var PageInterface|null $page */
$page = $event['page'];
if (!$page instanceof PageInterface) {
return;
}
$header = $page->header();
$forms = $page->getForms();
// Update dynamic flex forms from the page.
$form = null;
foreach ($forms as $name => $test) {
$type = $form['type'] ?? null;
if ($type === 'flex') {
$form = $test;
// Update the form and add it back to the page.
$this->grav->fireEvent('onBeforeFlexFormInitialize', new Event(['page' => $page, 'name' => $name, 'form' => &$form]));
$page->addForms([$form], true);
}
}
// Make sure the page contains flex.
$config = $header->flex ?? null;
if (!is_array($config) && !$form) {
return;
}
/** @var Route $route */
$route = $this->grav['route'];
$type = $form['flex']['type'] ?? $config['directory'] ?? $route->getGravParam('directory') ?? null;
$key = $form['flex']['key'] ?? $config['id'] ?? $route->getGravParam('id') ?? '';
if (\is_string($type)) {
/** @var Flex $flex */
$flex = $this->grav['flex_objects'];
$directory = $flex->getDirectory($type);
} else {
$directory = null;
}
if (!$directory) {
return;
}
$create = (bool)($form['actions']['create'] ?? false);
$edit = (bool)($form['actions']['edit'] ?? false);
$scope = $config['access']['scope'] ?? null;
$object = $key !== '' ? $directory->getObject($key) : null;
$hasAccess = null;
$action = $config['access']['action'] ?? null;
if (null === $action) {
if (!$form) {
$action = $key !== '' ? 'read' : 'list';
if (null === $scope) {
$hasAccess = true;
}
} elseif ($object) {
if ($edit) {
$scope = $scope ?? 'admin';
$action = 'update';
} else {
$hasAccess = false;
}
} elseif ($create) {
$object = $directory->createObject([], $key);
$scope = $scope ?? 'admin';
$action = 'create';
} else {
$hasAccess = false;
}
}
if ($action && $hasAccess === null) {
if ($object instanceof FlexAuthorizeInterface) {
$hasAccess = $object->isAuthorized($action, $scope);
} else {
$hasAccess = $directory->isAuthorized($action, $scope);
}
}
if (!$hasAccess) {
// Hide the page (404).
$page->routable(false);
$page->visible(false);
// If page is not a module, replace the current page with unauthorized page.
if (!$page->isModule()) {
$login = $this->grav['login'] ?? null;
$unauthorized = $login ? $login->addPage('unauthorized') : null;
if ($unauthorized) {
unset($this->grav['page']);
$this->grav['page'] = $unauthorized;
}
}
} elseif ($config['access']['override'] ?? false) {
// Override page access settings (allow).
$page->modifyHeader('access', []);
}
}
/**
* @param Event $event
*/
public function onPageTask(Event $event): void
{
/** @var FormInterface|null $form */
$form = $event['form'] ?? null;
if (!$form instanceof FlexForm) {
return;
}
$object = $form->getObject();
/** @var ServerRequestInterface $request */
$request = $event['request'];
$request = $request
->withAttribute('type', $object->getFlexType())
->withAttribute('key', $object->getKey())
->withAttribute('object', $object)
->withAttribute('form', $form);
$controller = new ObjectController();
$response = $controller->handle($request);
if ($response->getStatusCode() !== 418) {
$this->grav->close($response);
}
}
/**
* @param \Grav\Framework\Flex\Flex $flex
* @param array $types
* @param bool $report
*/
protected function registerDirectories(\Grav\Framework\Flex\Flex $flex, array $types, bool $report = false): void
{
$map = Flex::getLegacyBlueprintMap(false);
foreach ($types as $blueprint) {
// Backwards compatibility to v1.0.0-rc.3
$blueprint = $map[$blueprint] ?? $blueprint;
$type = Utils::basename((string)$blueprint, '.yaml');
if (!$type) {
continue;
}
if (!file_exists($blueprint)) {
if ($report) {
/** @var Debugger $debugger */
$debugger = Grav::instance()['debugger'];
$debugger->addMessage(sprintf('Flex: blueprint for flex type %s is missing', $type), 'error');
}
continue;
}
$directory = $flex->getDirectory($type);
if (!$directory || !$directory->isEnabled()) {
$flex->addDirectoryType($type, $blueprint);
}
}
}
/**
* Initial stab at registering permissions (WIP)
*
* @param PermissionsRegisterEvent $event
* @return void
*/
public function onRegisterPermissions(PermissionsRegisterEvent $event): void
{
/** @var Flex $flex */
$flex = $this->grav['flex_objects'];
$directories = $flex->getDirectories();
$permissions = $event->permissions;
$actions = [];
foreach ($directories as $directory) {
$data = $directory->getConfig('admin.permissions', []);
$actions[] = PermissionsReader::fromArray($data, $permissions->getTypes());
}
$actions[] = PermissionsReader::fromYaml("plugin://{$this->name}/permissions.yaml");
$permissions->addActions(array_replace(...$actions));
}
/**
* @param Event $event
* @return void
*/
public function onFormRegisterTypes(Event $event): void
{
/** @var Forms $forms */
$forms = $event['forms'];
$forms->registerType('flex', new FlexFormFactory());
}
/**
* @param Event $event
* @return void
*/
public function onAdminPage(Event $event): void
{
if ($this->controller->isActive()) {
$event->stopPropagation();
/** @var PageInterface $page */
$page = $event['page'];
$page->init(new \SplFileInfo(__DIR__ . '/admin/pages/flex-objects.md'));
$page->slug($this->controller->getLocation());
$header = $page->header();
$header->access = ['admin.login'];
$header->controller = $this->controller->getInfo();
}
}
/**
* [onPageInitialized:0]: Run controller
*
* @return void
*/
public function onAdminPageInitialized(): void
{
if ($this->controller->isActive()) {
$this->controller->execute();
$this->controller->redirect();
}
}
/**
* @param Event $event
* @return void
*/
public function onAdminControllerInit(Event $event): void
{
$eventController = $event['controller'];
// Blacklist all admin routes, including aliases and redirects.
$eventController->blacklist_views[] = 'flex-objects';
foreach ($this->controller->getAdminRoutes() as $route => $info) {
$eventController->blacklist_views[] = trim($route, '/');
}
}
/**
* Add Flex-Object's preset.scss to the Admin Preset SCSS compile process
*
* @param Event $event
* @return void
*/
public function onAdminCompilePresetSCSS(Event $event): void
{
$event['scss']->add($this->grav['locator']->findResource('plugins://flex-objects/scss/_preset.scss'));
}
/**
* @param Event $event
* @return void
*/
public function onGetPageTemplates(Event $event): void
{
/** @var Types $types */
$types = $event->types;
$types->register('flex-objects', 'plugins://flex-objects/blueprints/pages/flex-objects.yaml');
}
/**
* Form select options listing all enabled directories.
*
* @return array
*/
public static function directoryOptions(): array
{
/** @var Flex $flex */
$flex = Grav::instance()['flex_objects'];
$directories = $flex->getDirectories();
$list = [];
/**
* @var string $type
* @var FlexDirectory $directory
*/
foreach ($directories as $type => $directory) {
if (!$directory->getConfig('site.hidden')) {
$list[$type] = $directory->getTitle();
}
}
return $list;
}
/**
* @return array
*/
public function getAdminMenu(): array
{
/** @var Flex $flex */
$flex = $this->grav['flex_objects'];
$list = [];
foreach ($flex->getAdminMenuItems() as $name => $item) {
$route = trim($item['route'] ?? $name, '/');
$list[$route] = $item;
}
return $list;
}
/**
* Add Flex Directory to admin menu
*
* @return void
*/
public function onAdminMenu(): void
{
/** @var Flex $flex */
$flex = $this->grav['flex_objects'];
/** @var Admin $admin */
$admin = $this->grav['admin'];
foreach ($this->getAdminMenu() as $route => $item) {
$directory = null;
if (isset($item['directory'])) {
$directory = $flex->getDirectory($item['directory']);
if (!$directory || !$directory->isEnabled()) {
continue;
}
}
$title = $item['title'] ?? 'PLUGIN_FLEX_OBJECTS.TITLE';
$index = $item['index'] ?? 0;
if (($this->grav['twig']->plugins_hooked_nav[$title]['index'] ?? 1000) <= $index) {
continue;
}
$location = $item['location'] ?? $route;
$hidden = $item['hidden'] ?? false;
$icon = $item['icon'] ?? 'fa-list';
$authorize = $item['authorize'] ?? ($directory ? null : ['admin.flex-objects', 'admin.super']);
if ($hidden || (null === $authorize && $directory->isAuthorized('list', 'admin', $admin->user))) {
continue;
}
$cache = $directory ? $directory->getCache('index') : null;
$count = $cache ? $cache->get('admin-count-' . md5($admin->user->username)) : false;
if (null === $count) {
try {
$collection = $directory->getCollection();
if (is_callable([$collection, 'isAuthorized'])) {
$count = $collection->isAuthorized('list', 'admin', $admin->user)->count();
} else {
$count = $collection->count();
}
$cache->set('admin-count-' . md5($admin->user->username), $count);
} catch (\InvalidArgumentException $e) {
continue;
}
}
$badge = $directory ? ['badge' => ['count' => $count]] : [];
$priority = $item['priority'] ?? 0;
$this->grav['twig']->plugins_hooked_nav[$title] = [
'location' => $location,
'route' => $route,
'index' => $index,
'icon' => $icon,
'authorize' => $authorize,
'priority' => $priority
] + $badge;
}
}
/**
* Exclude Flex Directory data from the Data Manager plugin
*
* @return void
*/
public function onDataTypeExcludeFromDataManagerPluginHook(): void
{
$this->grav['admin']->dataTypesExcludedFromDataManagerPlugin[] = 'flex-objects';
}
/**
* Add current directory to twig lookup paths.
*
* @return void
*/
public function onTwigTemplatePaths(): void
{
$extra_site_twig_path = $this->config->get('plugins.flex-objects.extra_site_twig_path');
$extra_path = $extra_site_twig_path ? $this->grav['locator']->findResource($extra_site_twig_path) : null;
if ($extra_path) {
$this->grav['twig']->twig_paths[] = $extra_path;
}
$this->grav['twig']->twig_paths[] = __DIR__ . '/templates';
}
/**
* Add plugin templates path
*
* @param Event $event
* @return void
*/
public function onAdminTwigTemplatePaths(Event $event): void
{
$extra_admin_twig_path = $this->config->get('plugins.flex-objects.extra_admin_twig_path');
$extra_path = $extra_admin_twig_path ? $this->grav['locator']->findResource($extra_admin_twig_path) : null;
$paths = $event['paths'];
if ($extra_path) {
$paths[] = $extra_path;
}
$paths[] = __DIR__ . '/admin/templates';
$event['paths'] = $paths;
}
/**
* Set needed variables to display directory.
*
* @return void
*/
public function onTwigAdminVariables(): void
{
if ($this->controller->isActive()) {
// Twig shortcuts
$this->grav['twig']->twig_vars['controller'] = $this->controller;
$this->grav['twig']->twig_vars['action'] = $this->controller->getAction();
$this->grav['twig']->twig_vars['task'] = $this->controller->getTask();
$this->grav['twig']->twig_vars['target'] = $this->controller->getTarget();
$this->grav['twig']->twig_vars['key'] = $this->controller->getId();
$this->grav['twig']->twig_vars['flex'] = $this->grav['flex_objects'];
$this->grav['twig']->twig_vars['directory'] = $this->controller->getDirectory();
$this->grav['twig']->twig_vars['collection'] = $this->controller->getCollection();
$this->grav['twig']->twig_vars['object'] = $this->controller->getObject();
// CSS / JS Assets
$this->grav['assets']->addCss('plugin://flex-objects/css/admin.css');
$this->grav['assets']->addCss('plugin://admin/themes/grav/css/codemirror/codemirror.css');
}
}
}
+14
View File
@@ -0,0 +1,14 @@
enabled: true
built_in_css: true
extra_admin_twig_path: 'theme://admin/templates'
admin_list:
per_page: 15
order:
by: updated_timestamp
dir: desc
directories:
- 'blueprints://flex-objects/pages.yaml'
- 'blueprints://flex-objects/user-accounts.yaml'
- 'blueprints://flex-objects/user-groups.yaml'
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+73
View File
@@ -0,0 +1,73 @@
PLUGIN_FLEX_OBJECTS:
PLUGIN_NAME: "Flex Objects"
PLUGIN_DESCRIPTION: "Das Flex Objects Plugin ermöglicht die Verwaltung von Flex Objekten im Grav Admin."
TITLE: Flex Objects
TYPES_TITLE: Verzeichnisse
AFTER_SAVE: Nach dem Speichern…
LIST_INFO: '{from} bis {to} von {total} Einträgen angezeigt'
EMPTY_RESULT: Diese Abfrage liefert keine Ergebnisse
USE_BUILT_IN_CSS: "Nutze das integrierte Plugin CSS"
EXTRA_ADMIN_TWIG_PATH: "Extra Admin Twig Pfad"
DIRECTORIES: "Verzeichnisse"
CSV: "CSV"
PARENTS: "Eltern"
NEW: "Neu"
CONTROLLER:
TASK_DELETE_SUCCESS: 'Eintrag erfolgreich gelöscht'
TASK_DELETE_FAILURE: 'Löschen des Eintrags fehlgeschlagen: %s'
TASK_NEW_FOLDER_SUCCESS: 'Ordner erfolgreich angelegt'
TASK_COPY_SUCCESS: 'Kopie erfolgreich angelegt'
TASK_COPY_FAILURE: 'Kopieren des Eintrags fehlgeschlagen: %s'
TASK_SAVE_SUCCESS: 'Eintrag erfolgreich gespeichert'
TASK_SAVE_FAILURE: 'Speichern des Eintrags fehlgeschlagen: %s'
TASK_CONFIGURE_SUCCESS: 'Konfiguration erfolgreich gespeichert'
TASK_CONFIGURE_FAILURE: 'Speichern der Konfiguration fehlgeschlagen: %s'
ACTION:
CREATE_NEW: Neuen Eintrag anlegen
EDIT_ITEM: Eintrag bearbeiten
LIST_ITEMS: Einträge auflisten
DELETE_N: "Löschen von"
REALLY_DELETE: "Willst du wirklich diese %s löschen?"
SEARCH_PLACEHOLDER: "Suchen…"
ADVANCED_OPTIONS: "Erweiterte Optionen"
APPLY_FILTERS: "Filter anwenden"
RESET_FILTERS: "Filter zurücksetzen"
ACTIONS: "Aktionen"
FILTER:
PAGE_ATTRIBUTES: "Seitenattribute"
PAGE_TYPES: "Seitentypen"
MODULAR_TYPES: "Modultypen"
LANGUAGE:
USING_DEFAULT: "Benutze <b>Standard</b> Sprachdatei."
UNUSED_DEFAULT: "<b>Standard</b> Sprachdatei (ohne Spracherweiterung) existiert, wird aber nicht genutzt."
USING_OVERRIDE: "Sprachdatei <b>%s</b> in Nutzung."
NOT_TRANSLATED_YET: "Diese Seite ist noch nicht in <i class=\"fa fa-flag-o\"></i> <b>%s</b> übersetzt worden!"
NO_FALLBACK_FOUND: "Keine Übersetzung vorhanden, auf die zurückgegriffen werden kann."
FALLING_BACK: "Falle zurück auf <b>%s</b>."
STATE:
LOADING: "Lädt…"
CREATED_SUCCESSFULLY: "Erfolgreich angelegt"
UPDATED_SUCCESSFULLY: "Erfolgreich aktualisiert"
DELETED_SUCCESSFULLY: "Erfolgreich gelöscht"
EDITING_DRAFT: "Du bearbeitest einen gespeicherten Entwurf."
NOT_CREATED_YET: "Diese Seite existiert nicht, bis du sie speicherst."
ERROR:
BAD_DIRECTORY: "Fehlerhaftes Verzeichnis"
PAGE_NOT_FOUND: "Seite nicht gefunden"
PAGE_NOT_EXIST: "Uups! Diese Seite existiert nicht."
PAGE_FORBIDDEN: "Uups! Du hast keine Berechtigung um auf diese Seite zuzugreifen."
LAYOUT_NOT_FOUND: "Objekt-Layout '%s' nicht gefunden."
BLUEPRINT_NO_LIST: "Der Blueprint von <i>%s</i> oder dessen Override enthält keine Informationen zur Listendarstellung."
BLUEPRINT_NO_LIST_ADVISE: "Bitte ergänze den Blueprint um Feldliste."
BLUEPRINT_NO_LIST_TEMPLATE: "Bitte leg für diesen Typ ein Template an <b>flex-objects/types/%s/list.html.twig</b>"
LIST_EMPTY: "Keine Einträge vorhanden."
LIST_EMPTY_ADD: "Keine Einträge vorhanden. Klicke den <a href=\"%s\">Hinzufügen</a> Button um einen Eintrag anzulegen."
NO_FLEX_DIRECTORIES: "Keine Flex Verzeichnisse erkannt"
+73
View File
@@ -0,0 +1,73 @@
PLUGIN_FLEX_OBJECTS:
PLUGIN_NAME: "Flex Objects"
PLUGIN_DESCRIPTION: "Flex Objects plugin allows you to manage Flex Objects in Grav Admin."
TITLE: Flex Objects
TYPES_TITLE: Directories
AFTER_SAVE: After Save…
LIST_INFO: "Displaying {from} to {to} out of {total} records"
EMPTY_RESULT: The requested query returns no result
USE_BUILT_IN_CSS: "Use built in CSS"
EXTRA_ADMIN_TWIG_PATH: "Extra Admin Twig Path"
DIRECTORIES: "Directories"
CSV: "CSV"
PARENTS: "Parents"
NEW: "New"
CONTROLLER:
TASK_DELETE_SUCCESS: "Entry deleted successfully"
TASK_DELETE_FAILURE: "Failed to delete entry: %s"
TASK_NEW_FOLDER_SUCCESS: "Folder created successfully"
TASK_COPY_SUCCESS: "Copy created successfully"
TASK_COPY_FAILURE: "Failed to create copy: %s"
TASK_SAVE_SUCCESS: "Entry saved successfully"
TASK_SAVE_FAILURE: "Failed to save entry: %s"
TASK_CONFIGURE_SUCCESS: "Configuration saved successfully"
TASK_CONFIGURE_FAILURE: "Failed to save configuration: %s"
ACTION:
CREATE_NEW: Create New Item
EDIT_ITEM: Edit Item
LIST_ITEMS: "List Items"
DELETE_N: "Delete" # In some languages 'delete OBJECT' may need a special declination
REALLY_DELETE: "Are you sure you want to permanently delete the %s?"
SEARCH_PLACEHOLDER: "Search…"
ADVANCED_OPTIONS: "Advanced Options"
APPLY_FILTERS: "Apply Filters"
RESET_FILTERS: "Reset to Defaults"
ACTIONS: "Actions"
FILTER:
PAGE_ATTRIBUTES: "Page Attributes"
PAGE_TYPES: "Page Types"
MODULAR_TYPES: "Module Types"
LANGUAGE:
USING_DEFAULT: "Using <b>Default</b> language file."
UNUSED_DEFAULT: "Unused <b>Default</b> language file also exists."
USING_OVERRIDE: "Using <b>%s</b> language override."
NOT_TRANSLATED_YET: "This page has not been translated to <i class=\"fa fa-flag-o\"></i> <b>%s</b> language yet!"
NO_FALLBACK_FOUND: "No fallback translations found."
FALLING_BACK: "Falling back to <b>%s</b> language."
STATE:
LOADING: "Loading…"
CREATED_SUCCESSFULLY: "Successfully created"
UPDATED_SUCCESSFULLY: "Successfully updated"
DELETED_SUCCESSFULLY: "Successfully deleted"
EDITING_DRAFT: "You are editing a saved draft."
NOT_CREATED_YET: "This page will not exist until it is saved."
ERROR:
BAD_DIRECTORY: "Bad Directory"
PAGE_NOT_FOUND: "Page Not Found"
PAGE_NOT_EXIST: "Woops! Looks like this page doesn't exist."
PAGE_FORBIDDEN: "Woops! Looks like you do not have permissions to access this page."
LAYOUT_NOT_FOUND: "Object layout '%s' not found."
BLUEPRINT_NO_LIST: "Blueprint for <i>%s</i> does not define displayed fields or list page override."
BLUEPRINT_NO_LIST_ADVISE: "Please add list of fields to blueprints file."
BLUEPRINT_NO_LIST_TEMPLATE: "Please create template file for the type in <b>flex-objects/types/%s/list.html.twig</b>"
LIST_EMPTY: "There are currently no entries."
LIST_EMPTY_ADD: "There are currently no entries. Click the <a href=\"%s\">Add</a> button to create a new one…"
NO_FLEX_DIRECTORIES: "No Flex Directories detected"
+73
View File
@@ -0,0 +1,73 @@
PLUGIN_FLEX_OBJECTS:
PLUGIN_NAME: "Flex Objects"
PLUGIN_DESCRIPTION: "El plugin Flex Objects permite manejar Objetos Flex en Grav Admin."
TITLE: Flex Objects
TYPES_TITLE: Directorios
AFTER_SAVE: "Después de guardar…"
LIST_INFO: "Mostrando de {from} a {to} de {total} registros"
EMPTY_RESULT: The requested query returns no result
USE_BUILT_IN_CSS: "Usar CSS incorporado"
EXTRA_ADMIN_TWIG_PATH: "Ruta extra de twig para Admin"
DIRECTORIES: "Directorios"
CSV: "CSV"
PARENTS: "Madres"
NEW: Nuevo
CONTROLLER:
TASK_DELETE_SUCCESS: "Entrada eliminada exitosamente"
TASK_DELETE_FAILURE: "Falla al eliminar la entrada: %s"
TASK_NEW_FOLDER_SUCCESS: "Carpeta creada exitosamente"
TASK_COPY_SUCCESS: "Copia creada exitosamente"
TASK_COPY_FAILURE: "Falla al crear la copia: %s"
TASK_SAVE_SUCCESS: "Entrada guardada exitosamente"
TASK_SAVE_FAILURE: "Falla al guardar la entrada: %s"
TASK_CONFIGURE_SUCCESS: "Configuración gardada exitosamente"
TASK_CONFIGURE_FAILURE: "Falla al guardar la configuración: %s"
ACTION:
CREATE_NEW: "Crear nuevo ítem"
EDIT_ITEM: "Editar ítem"
LIST_ITEMS: "Listar ítems"
DELETE_N: "Eliminar"
REALLY_DELETE: "¿Realmente quieres eliminar %s permanentemente?"
SEARCH_PLACEHOLDER: "Buscar…"
ADVANCED_OPTIONS: "Opciones avazadas"
APPLY_FILTERS: "Aplicar filtros"
RESET_FILTERS: "Restablecer filtros"
ACTIONS: "Acciones"
FILTER:
PAGE_ATTRIBUTES: "Atributos de página"
PAGE_TYPES: "Tipos de página"
MODULAR_TYPES: "Tipos de módulo"
LANGUAGE:
USING_DEFAULT: "Usando archivo de idioma <b>Predeterminado</b>."
UNUSED_DEFAULT: "También existe un archvivo de idioma <b>Predeterminado</b>."
USING_OVERRIDE: "Usando el idioma de remplazo <b>%s</b>."
NOT_TRANSLATED_YET: "¡Esta página aún no ha sido traducida a <i class=\"fa fa-flag-o\"></i> <b>%s</b>!"
NO_FALLBACK_FOUND: "No se han encontrado traducciones de apoyo."
FALLING_BACK: "Retrocediendo a <b>%s</b>."
STATE:
LOADING: "Cargando…"
CREATED_SUCCESSFULLY: "Creado exitosamente"
UPDATED_SUCCESSFULLY: "Actualizado exitosamente"
DELETED_SUCCESSFULLY: "Eliminado exitosamente"
EDITING_DRAFT: "Estás editando un borrador."
NOT_CREATED_YET: "Esta página no existe hasta que se guarde."
ERROR:
BAD_DIRECTORY: "Directorio errado"
PAGE_NOT_FOUND: "Página no encontrada"
PAGE_NOT_EXIST: "¡Uy! Parece que esta página no existe."
PAGE_FORBIDDEN: "¡Uy! Parece que no tienes permiso para ver esta página."
LAYOUT_NOT_FOUND: "Maquetación de objeto '%s' no encontrada."
BLUEPRINT_NO_LIST: "Plano para <i>%s</i> no define campos mostrados ni remplazo de página de lista."
BLUEPRINT_NO_LIST_ADVISE: "Por favor agrega una lista de campos para el archivo de planos."
BLUEPRINT_NO_LIST_TEMPLATE: "Por favor crea un archivo de plantilla para el tipo en <b>flex-objects/types/%s/list.html.twig</b>"
LIST_EMPTY: "No hay entradas por el momento."
LIST_EMPTY_ADD: "No hay entradas por el momento. Da clic en el botón <a href=\"%s\">Agregar</a> para crear una nueva…"
NO_FLEX_DIRECTORIES: "No se detectan Directorios Flex"
+73
View File
@@ -0,0 +1,73 @@
PLUGIN_FLEX_OBJECTS:
PLUGIN_NAME: "Flex Objects"
PLUGIN_DESCRIPTION: "Flex Objects plugin allows you to manage Flex Objects in Grav Admin."
TITLE: Flex Objects
TYPES_TITLE: ディレクトリー
AFTER_SAVE: 保存した後…
LIST_INFO: "全{total}件中、{from} 件目から {to} 件目を表示中"
EMPTY_RESULT: 指定の検索条件を満たすものは、見つかりませんでした
USE_BUILT_IN_CSS: "内蔵の CSS を利用"
EXTRA_ADMIN_TWIG_PATH: "追加の Admin Twig パス"
DIRECTORIES: "ディレクトリー"
CSV: "CSV"
PARENTS: "親"
NEW: "新規"
CONTROLLER:
TASK_DELETE_SUCCESS: "データを削除しました"
TASK_DELETE_FAILURE: "データの削除に失敗: %s"
TASK_NEW_FOLDER_SUCCESS: "フォルダーを作成しました"
TASK_COPY_SUCCESS: "コピーを作成しました"
TASK_COPY_FAILURE: "コピーの作成に失敗: %s"
TASK_SAVE_SUCCESS: "データを保存しました"
TASK_SAVE_FAILURE: "データの保存に失敗: %s"
TASK_CONFIGURE_SUCCESS: "設定を保存しました"
TASK_CONFIGURE_FAILURE: "設定の保存に失敗: %s"
ACTION:
CREATE_NEW: 新しいデータを作成
EDIT_ITEM: "データの編集"
LIST_ITEMS: "データの一覧表示"
DELETE_N: "削除" # In some languages 'delete OBJECT' may need a special declination
REALLY_DELETE: "%s を削除します。復元はできませんがよろしいですか?"
SEARCH_PLACEHOLDER: "検索…"
ADVANCED_OPTIONS: "詳細設定"
APPLY_FILTERS: "フィルターを適用"
RESET_FILTERS: "標準にリセットする"
ACTIONS: "アクション"
FILTER:
PAGE_ATTRIBUTES: "ページ属性"
PAGE_TYPES: "ページ タイプ"
MODULAR_TYPES: "モジュラー タイプ"
LANGUAGE:
USING_DEFAULT: "<b>デフォルト</b> の言語ファイルを使用しています。"
UNUSED_DEFAULT: "未使用の <b>デフォルト</b> の言語ファイルもあります。"
USING_OVERRIDE: "<b>%s</b> 言語ファイルでオーバーライドします。"
NOT_TRANSLATED_YET: "このページは、まだ <i class=\"fa fa-flag-o\"></i> <b>%s</b> 言語へ翻訳されていません!"
NO_FALLBACK_FOUND: "フォールバック先の翻訳が見つかりません。"
FALLING_BACK: "<b>%s</b> 言語へフォールバックしています。"
STATE:
LOADING: "読み込んでいます…"
CREATED_SUCCESSFULLY: "作成しました"
UPDATED_SUCCESSFULLY: "更新しました"
DELETED_SUCCESSFULLY: "削除しました"
EDITING_DRAFT: "保存済みの下書きを編集中です"
NOT_CREATED_YET: "このページは保存するまで、存在しません。"
ERROR:
BAD_DIRECTORY: "ディレクトリーの指定が正しくありません"
PAGE_NOT_FOUND: "ページが見つかりません"
PAGE_NOT_EXIST: "残念! このページは存在しないようです。"
PAGE_FORBIDDEN: "残念! あなたはこのページへのアクセス権をもっていないようです。"
LAYOUT_NOT_FOUND: "Object layout '%s' が見つかりません。"
BLUEPRINT_NO_LIST: "<i>%s</i> の Blueprint では、表示用の項目やリストページのオーバーライドが未定義です。"
BLUEPRINT_NO_LIST_ADVISE: "Blueprint ファイルにリストの項目を追加してください。"
BLUEPRINT_NO_LIST_TEMPLATE: "表示のためのテンプレートファイルを以下のファイル名で作成してください。<b>flex-objects/types/%s/list.html.twig</b>"
LIST_EMPTY: "現在、データがありません。"
LIST_EMPTY_ADD: "現在データがありません。<a href=\"%s\">追加</a> ボタンをクリックして新規作成してください…"
NO_FLEX_DIRECTORIES: "Flex ディレクトリーが見つかりません。"
+71
View File
@@ -0,0 +1,71 @@
PLUGIN_FLEX_OBJECTS:
PLUGIN_NAME: "Flex Objects"
PLUGIN_DESCRIPTION: "De Flex Objects plugin maakt het mogelijk om flexibele objecten te maken en te beheren."
TITLE: Flex Objects
TYPES_TITLE: Mappen
AFTER_SAVE: Na opslaan…
LIST_INFO: '{from} tot {to} van {total} items weergegeven'
EMPTY_RESULT: Deze zoekopdracht geeft geen resultaat
USE_BUILT_IN_CSS: "Gebruik ingebouwde CSS"
EXTRA_ADMIN_TWIG_PATH: "Extra Admin Twig pad"
DIRECTORIES: "Mappen"
CSV: "CSV"
PARENTS: "Ouders"
CONTROLLER:
TASK_DELETE_SUCCESS: 'Item succesvol verwijderd'
TASK_DELETE_FAILURE: 'Item verwijderen mislukt: %s'
TASK_NEW_FOLDER_SUCCESS: 'Map succesvol aangemaakt'
TASK_COPY_SUCCESS: 'Kopie succesvol aangemaakt'
TASK_COPY_FAILURE: 'Kopie aanmaken mislukt: %s'
TASK_SAVE_SUCCESS: 'Item succesvol opgeslagen'
TASK_SAVE_FAILURE: 'Item opslaan mislukt: %s'
TASK_CONFIGURE_SUCCESS: 'Configuratie succesvol opgeslagen'
TASK_CONFIGURE_FAILURE: 'Configuratie opslaan mislukt: %s'
ACTION:
CREATE_NEW: Nieuw item aanmaken
EDIT_ITEM: Item bewerken
LIST_ITEMS: Items weergeven
DELETE_N: "Verwijderen"
REALLY_DELETE: "Weet je zeker dat je %s permanent wilt verwijderen?"
SEARCH_PLACEHOLDER: "Zoeken…"
ADVANCED_OPTIONS: "Geavanceerde opties"
APPLY_FILTERS: "Filters toepassen"
RESET_FILTERS: "Filters resetten"
FILTER:
PAGE_ATTRIBUTES: "Pagina attributen"
PAGE_TYPES: "Pagina types"
MODULAR_TYPES: "Modulaire types"
LANGUAGE:
USING_DEFAULT: "<b>Standaard</b> taalbestand in gebruik."
UNUSED_DEFAULT: "Ongebruikt <b>standaard</b> taalbestand aanwezig."
USING_OVERRIDE: "<b>%s</b> taalbestand in gebruik."
NOT_TRANSLATED_YET: "Deze pagina is nog niet vertaald naar <i class=\"fa fa-flag-o\"></i> <b>%s</b>!"
NO_FALLBACK_FOUND: "Geen fallback taalbestand gevonden."
FALLING_BACK: "Terugvallen op <b>%s</b> taalbestand."
STATE:
LOADING: "Laden…"
CREATED_SUCCESSFULLY: "Succesvol aangemaakt"
UPDATED_SUCCESSFULLY: "Succesvol bijgewerkt"
DELETED_SUCCESSFULLY: "Succesvol verwijderd"
EDITING_DRAFT: "Je bewerkt een concept."
NOT_CREATED_YET: "Deze pagina bestaat niet, totdat je hem opslaat."
ERROR:
BAD_DIRECTORY: "Niet bestaande map"
PAGE_NOT_FOUND: "Pagina niet gevonden"
PAGE_NOT_EXIST: "Oeps! Deze pagina bestaat niet."
PAGE_FORBIDDEN: "Oeps! Je hebt geen toegang tot deze pagina."
LAYOUT_NOT_FOUND: "Object layout '%s' niet gevonden."
BLUEPRINT_NO_LIST: "De blueprint van <i>%s</i> bevat geen velddata of lijst pagina overschrijving."
BLUEPRINT_NO_LIST_ADVISE: "Voeg een <i>list</i> sectie toe aan de blueprint of overschrijf de <i>list</i> pagina in de configuratie."
BLUEPRINT_NO_LIST_TEMPLATE: "Maak een template bestand voor dit type aan in <b>flex-objects/types/%s/list.html.twig</b>"
LIST_EMPTY: "Er zijn geen items gevonden."
LIST_EMPTY_ADD: "Er zijn geen items op dit moment. Klik op de knop <a href=\"%s\">Toevoegen</a> om een nieuw item aan te maken."
NO_FLEX_DIRECTORIES: "Geen Flex Object mappen gedefinieerd."
+12
View File
@@ -0,0 +1,12 @@
PLUGIN_FLEX_OBJECTS:
PLUGIN_NAME: "Flex Objects"
PLUGIN_DESCRIPTION: "Плагин Flex Objects позволяет управлять объектами Flex через плагин админки."
USE_BUILT_IN_CSS: "Использовать встроенный CSS"
EXTRA_ADMIN_TWIG_PATH: "Дополнительный путь к шаблонам Twig"
DIRECTORIES: "Каталоги"
STATE:
CREATED_SUCCESSFULLY: "Успешно создано"
UPDATED_SUCCESSFULLY: "Успешно обновлено"
DELETED_SUCCESSFULLY: "Успешно удалено"
+51
View File
@@ -0,0 +1,51 @@
{
"name": "flex-objects",
"description": "Flexible CRUD Architecture for Grav",
"author": "Trilby Media <hello@trilby.media>",
"private": true,
"scripts": {
"watch": "webpack --mode development --watch --progress --color --mode development --config webpack.conf.js",
"dev": "webpack --mode development --progress --color --config webpack.conf.js",
"prod": "webpack --mode production --config webpack.conf.js"
},
"dependencies": {
"axios": "^0.21.4",
"eventemitter3": "^4.0.7",
"lodash": "^4.17.21",
"tiny-cookie": "^2.3.2",
"vue": "^2.6.14",
"vue-content-loader": "0.2.3",
"vue-events": "^3.1.0",
"vue-template-compiler": "^2.6.14",
"vuetable-2": "^1.7.5"
},
"devDependencies": {
"@babel/core": "^7.15.5",
"@babel/eslint-parser": "^7.15.7",
"@babel/plugin-proposal-object-rest-spread": "^7.15.6",
"@babel/preset-env": "^7.15.6",
"babel-loader": "^8.2.2",
"cross-env": "^7.0.3",
"css-loader": "^6.3.0",
"eslint": "^7.32.0",
"eslint-webpack-plugin": "^3.0.1",
"exports-loader": "^3.0.0",
"gulp-sourcemaps": "^3.0.0",
"imports-loader": "^3.0.0",
"terser-webpack-plugin": "^5.2.4",
"vue-loader": "^15.9.8",
"webpack": "^5.53.0",
"webpack-cli": "^4.8.0"
},
"resolutions": {
"vuetable-2/axios": "^0.21.4"
},
"browserslist": [
"last 4 Chrome versions",
"Edge >= 12",
"Firefox ESR",
"last 4 Safari versions",
"last 4 Opera versions",
"Explorer >= 10"
]
}
+45
View File
@@ -0,0 +1,45 @@
actions:
admin.flex-objects:
type: crudl
label: Flex Objects
types:
crudpl:
type: crud
use:
- crudp
- crudl
crudp:
type: crud
letters:
p:
action: publish
label: PLUGIN_ADMIN.PUBLISH
crudl:
type: crud
letters:
l:
action: list
label: PLUGIN_ADMIN.LIST
crud:
type: compact
letters:
c:
action: create
label: PLUGIN_ADMIN.CREATE
r:
action: read
label: PLUGIN_ADMIN.READ
u:
action: update
label: PLUGIN_ADMIN.UPDATE
d:
action: delete
label: PLUGIN_ADMIN.DELETE
default:
type: access
+69
View File
@@ -0,0 +1,69 @@
// Flex-Objects
/* Flex Objects Admin Styles */
#directory {
.search-wrapper {
input, select {
color: $form-field-text;
border: 1px solid $form-border;
background-color: $form-field-bg;
@include placeholder {
@if (lightness($form-field-bg) < 50) {
color: darken($form-field-text, 25%);
} @else {
color: lighten($form-field-text, 25%);
}
}
}
}
.edit-action {
color: $button-bg !important;
}
.delete-action {
color: $critical-bg !important;
}
thead {
tr {
background: darken($content-bg,2%);
}
}
tbody {
tr:nth-child(even) {
background: lighten($content-bg,1%);
}
}
i.published {
color: $flat-green-4;
}
i.unpublished {
color: $flat-red-1;
}
.flex-list-pagination {
background: darken($content-bg,1%);
border-top: 1px solid darken($content-bg,5%);
.flex-objects-pagination {
.button {
color: $content-header;
&.active {
color: $button-text;
background: $button-bg;
}
&.disabled {
color: lighten($content-text, 15%);
}
}
}
}
}
+3
View File
@@ -0,0 +1,3 @@
// Load Main Styles
@import "plugin/admin";
@@ -0,0 +1,215 @@
/* Flex-Objects Admin Styles */
#types {
padding: 3rem;
.card-item {
max-width: 100%;
p {
margin: 1rem 0 0;
}
}
}
#directory {
.no-entries {
padding: 3rem;
}
.search-wrapper {
margin: .5rem 1rem;
}
#pager .selectize-control {
vertical-align: middle;
display: inline-block;
}
.tablesorter-filter.disabled {
display: none;
}
.edit-action {
float: left;
}
.delete-action {
float: right;
}
table {
display: table;
}
thead {
display: table-header-group;
}
tbody {
display: table-row-group;
}
tr {
display: table-row;
flex: none;
}
td, th {
display: table-cell;
flex: none;
&:focus {
outline: 0 !important;
}
}
td {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
td:last-child {
text-align: right;
white-space: nowrap;
a {
float: none;
display: inline-block;
padding-left: 5px;
}
}
.tablesorter .filtered {
display: none;
}
#pager {
margin: 50px 0;
text-align: center;
.pagedisplay {
padding: 0 10px;
}
.button {
border: 0;
}
input, select {
display: inline-block;
width: auto;
}
select {
vertical-align: middle;
}
}
.flex-list-pagination {
margin: 1rem 0 -1rem;
padding: .5rem 1rem;
display: flex;
align-items: center;
.vuetable-pagination-info, .flex-objects-pagination {
flex: 1;
}
.flex-objects-pagination {
text-align: right;
-webkit-user-select: none;
-moz-user-select: none;
-khtml-user-select: none;
-ms-user-select: none;
.button {
border-radius: 4px;
background: transparent;
&.disabled {
&:active {
margin: 0;
}
}
}
}
}
}
/* FILTERS */
// form .checkboxes input[type=checkbox]:checked+label:before
#pages-filters {
.checkboxes {
-webkit-touch-callout: none; /* iOS Safari */
-webkit-user-select: none; /* Safari */
-khtml-user-select: none; /* Konqueror HTML */
-moz-user-select: none; /* Firefox */
-ms-user-select: none; /* Internet Explorer/Edge */
user-select: none;
&.status-indeterminate {
input[type="checkbox"] + label:before {
content: "\f00d";
font-family: "FontAwesome", sans-serif;
font-size: 1.2rem;
line-height: 1;
text-align: center;
}
}
}
}
/** Defaults **/
/* Admin Styles */
#directory .search-wrapper input, #directory .search-wrapper select {
color: #646e7c;
border: 1px solid #e6e6e6;
background-color: #fcfcfc;
}
#directory .search-wrapper input::-webkit-input-placeholder, #directory .search-wrapper select::-webkit-input-placeholder {
color: #a7afb8;
}
#directory .search-wrapper input::-moz-placeholder, #directory .search-wrapper select::-moz-placeholder {
color: #a7afb8;
}
#directory .search-wrapper input:-moz-placeholder, #directory .search-wrapper select:-moz-placeholder {
color: #a7afb8;
}
#directory .search-wrapper input:-ms-input-placeholder, #directory .search-wrapper select:-ms-input-placeholder {
color: #a7afb8;
}
#directory .edit-action {
color: #0090D9 !important;
}
#directory .delete-action {
color: #F45857 !important;
}
#directory thead tr {
background: #fafafa;
}
#directory tbody tr:nth-child(even) {
background: white;
}
#directory i.published {
color: #27AE60;
}
#directory i.unpublished {
color: #E74C3C;
}
#directory .flex-list-pagination {
background: #fcfcfc;
border-top: 1px solid #f2f2f2;
}
#directory .flex-list-pagination .flex-objects-pagination .button {
color: #414147;
}
#directory .flex-list-pagination .flex-objects-pagination .button.active {
color: #ffffff;
background: #0090D9;
}
#directory .flex-list-pagination .flex-objects-pagination .button.disabled {
color: #99a2ad;
}
@@ -0,0 +1,68 @@
#flex-objects {
.text-center {
text-align: center;
}
.search {
display: inline-block;
width: 50%;
padding: 0.7rem;
}
button.sort {
vertical-align: top;
&:after {
font-family: FontAwesome;
}
&.asc:after {
content: "\2191";
}
&.desc:after {
content: "\2193";
}
}
ul {
margin: 3rem 0;
li {
display: inline-block;
width: 33%;
margin: 1rem 0;
.entry-details {
.name {
font-weight: bold;
font-size: 120%;
}
p {
margin: 0;
}
}
.entry-extra {
span {
background: #eee;
border-radius: 4px;
padding: 3px 5px;
font-size: 85%;
}
}
}
}
}
@media only screen and (max-width: 800px) {
#flex-objects ul li {
width: 50%;
}
}
@media only screen and (max-width: 500px) {
#flex-objects ul li {
width: 100%;
}
}
+2
View File
@@ -0,0 +1,2 @@
// Load Main Styles
@import "plugin/site";
@@ -0,0 +1,42 @@
{% extends "partials/base.html.twig" %}
{% set object = form.object %}
{% if object is null %}
{% throw 404 'Not Found' %}
{% endif %}
{% if form.flash.exists() %}
{% set draft = true %}
{% try %}
{% set object = form.updateObject() %}
{% catch %}
{% endcatch %}
{% endif %}
{% if header.title %}
{% do page.title('Edit ' ~ object.title) %}
{% endif %}
{% block content %}
{% include 'partials/messages.html.twig' ignore missing %}
<section id="default-page" class="basic-row">
<div class="container grid-sm form">
{{ page.content()|raw }}
{#
<div class="unsaved-changes d-hide">You have unsaved changes</div>
#}
{% include "forms/form.html.twig" %}
</div>
</section>
{% block modals %}
{#
{% if form.buttons.preview %}
{% include 'modals/flex-preview.html.twig' %}
{% endif %}
#}
{% endblock %}
{% endblock %}
@@ -0,0 +1,21 @@
{% extends 'partials/base.html.twig' %}
{% block content %}
{% block page_content %}
{{ page.content|raw }}
{% endblock %}
{% block flex_content %}
{% set options = page.header.flex %}
{% set page_assets = page.header.assets %}
{% set type = options.directory ?? page.header.directory %}
{% if type %}
{% set list = type|array %}
{% else %}
{% set type = uri.param('directory') %}
{% endif %}
{% set id = options.id ?? uri.param('id') %}
{% include 'flex-objects/page.html.twig' %}
{% endblock %}
{% endblock %}

Some files were not shown because too many files have changed in this diff Show More