Compare commits

...

129 Commits

Author SHA1 Message Date
wotnak 15a27800af
Merge c82787d432 into fb6fd0d0bc 2024-02-19 20:02:20 -07:00
Michael Stenta fb6fd0d0bc Use $this->entityTypeManager() instead of Drupal::entityTypeManager() in entity classes #796 2024-02-09 07:19:10 -05:00
Michael Stenta 6abe3e73b0 farmOS 3.1.1 2024-02-08 05:18:39 -05:00
Michael Stenta 92c558dde3 Use plain strings in submitForm() calls in functional tests.
This fixes failing tests after upgrading to Drupal 10.2.3.
See https://www.drupal.org/project/drupal/issues/3399413
2024-02-08 05:05:39 -05:00
Michael Stenta cec7ed04d0 Update Drupal core to 10.2.3. 2024-02-07 20:30:55 -05:00
Paul Weidner a9a6d8008f Correct gin issue #3342164 2024-02-07 20:13:01 -05:00
Paul Weidner 38271bf84f Patch Gin to fix Issue #3419904: Help text is cutoff at xsmall breakpoint #793 2024-02-07 20:12:34 -05:00
Michael Stenta dadf79c8f0 Update Migrate Tools to 6.0.3 for SA-CONTRIB-2024-008. 2024-02-07 14:47:06 -05:00
Paul Weidner 462c82d6ca Grant config permissions create quick_form permission #791
This permission is required on the quick form add_page and add_form routes.
2024-02-05 05:59:32 -05:00
Michael Stenta 96bf17bc69 farmOS 3.1.0 2024-02-02 20:00:34 -05:00
Michael Stenta 1e2c7c2e47 Update drupal/gin to 3.0-rc9. 2024-02-02 19:56:18 -05:00
Michael Stenta ee71b28ae6 Provide a plan_record entity type for plan record relationships with metadata #781 2024-02-02 19:55:41 -05:00
Michael Stenta 98130dec1c Add CHANGELOG.md line for #781. 2024-02-02 19:55:15 -05:00
Paul Weidner 06b930886a Add PlanRecordInterface::getPlan() 2024-02-02 19:55:15 -05:00
Michael Stenta bd091b7d39 Expose a plan_record edit form at /plan/record/%id/edit. 2024-02-02 19:55:15 -05:00
Michael Stenta a79a2989a8 Add a simple access handler for plan_record entities that checks plan entity access. 2024-02-02 19:55:14 -05:00
Michael Stenta 8bf87f00fe Merge plan_record module into plan module. 2024-02-02 19:55:14 -05:00
Michael Stenta 36fc154c47 Patch drupal/entity_reference_integrity to fix Issue #3418000: Delete action only overridden on first entity type 2024-02-02 19:55:14 -05:00
Michael Stenta 8d25b32b01 Enforce entity reference integrity on plan reference fields. 2024-02-02 19:55:14 -05:00
Michael Stenta 4f4f666c28 Add a required single-value plan entity reference base field. 2024-02-02 19:55:14 -05:00
Michael Stenta 79704c3c7c Add plan record type bundle plugin support via the farm_entity module. 2024-02-02 19:55:14 -05:00
Michael Stenta 9d31aa5ce7 Add a simple Plan record relationship entity type (with bundles). 2024-02-02 19:55:14 -05:00
Michael Stenta 9c6d597404 Initial Plan Record module. 2024-02-02 19:54:20 -05:00
Michael Stenta 9058e39f7c Show map on /locations #779 2024-02-02 19:53:31 -05:00
Michael Stenta 70eb37cbf9 Remove "All" from "Items per page" options in Views #776 2024-02-02 19:52:09 -05:00
Michael Stenta 471e2cda6a Add CHANGELOG.md line for #779. 2024-02-02 19:52:09 -05:00
Michael Stenta dc00956117 Issue #2551091: Show map on /locations 2024-02-02 19:52:09 -05:00
Michael Stenta 1b2ce0b075 Add a locations map behavior and refactor dashboard map logic to use it. 2024-02-02 19:52:09 -05:00
Michael Stenta 433747d0ff Add a MapRenderEvent::getMapBehaviors() method. 2024-02-02 19:52:09 -05:00
Michael Stenta f7e6318026 Make map behavior library optional. 2024-02-02 19:52:09 -05:00
Michael Stenta cce51328c0 Fix MapBlock::build() map #behaviors.
$this->configuration['map_behaviors'] will already be an
array of values, not an associative array of behavior machine
names and their labels. The necessary form value conversion
using array_keys() happens in MapBlock::blockSubmit().
2024-02-02 19:52:09 -05:00
Michael Stenta efc6797899 Remove hard dependency on farm_ui_dashboard from farm_ui_map.
We implement hook_farm_dashboard_panes() but that will only
run if farm_ui_dashboard is enabled. It doesn't depend on it.
Removing this hard dependency allows farm_ui_dashboard to be
disabled without disabling farm_ui_map.
2024-02-02 19:52:09 -05:00
Michael Stenta 3e891a880d Improve asset and log CSV exports #783 2024-02-02 19:51:14 -05:00
Michael Stenta c0cc0a92b5 Add CHANGELOG.md line for #783. 2024-02-02 19:49:55 -05:00
Paul Weidner 65c0159e79 Implement BaseFormIdInterface 2024-02-02 19:49:55 -05:00
Paul Weidner 28b2d3b83d Use strict type checking for context variables that should be TRUE 2024-02-02 19:49:55 -05:00
Paul Weidner 604ce5f88e Change export warning message 2024-02-02 19:49:55 -05:00
Paul Weidner 0bb5facc66 Simplify and include count in CSV export question 2024-02-02 19:49:55 -05:00
Michael Stenta 4d05465f7c Update export docs to describe the difference between asset, log, quantity exports. 2024-02-02 19:49:55 -05:00
Michael Stenta 8b8ac5bb20 Remove CSV REST export displays from log, asset, plan Views. 2024-02-02 19:49:55 -05:00
Michael Stenta 22024a8f86 Install the farm_export_csv module on sites that have farm_ui_views installed. 2024-02-02 19:49:54 -05:00
Michael Stenta ee34ca660a Configure CSV export normalizers to flatten values. 2024-02-02 19:49:54 -05:00
Michael Stenta 0a3a5084c1 Provide a TimestampItemNormalizer with an option to return RFC3339 formatted date. 2024-02-02 19:49:54 -05:00
Michael Stenta 6ccf2cda0d Provide a TextLongFieldItemNormalizer with an option to return processed text. 2024-02-02 19:49:54 -05:00
Michael Stenta 9995efd994 Provide an EntityReferenceFieldItemNormalizer with options to return content entity labels and config entity IDs. 2024-02-02 19:49:54 -05:00
Michael Stenta 59802748e8 Add logic for restricting columns to supported base and bundle field types. 2024-02-02 19:49:54 -05:00
Michael Stenta 410da5d812 Provide a ContentEntityNormalizer with an option to explicity include columns. 2024-02-02 19:49:54 -05:00
Michael Stenta 5fcb89a16f Initial farmOS CSV module. 2024-02-02 19:49:54 -05:00
Michael Stenta 21d022bfad Add farm_export_csv to optional modules. 2024-02-02 19:49:54 -05:00
Michael Stenta 3cc0c515ef Issue #3328886: Make it clear that CSV exports are limited 2024-02-02 19:49:54 -05:00
Michael Stenta 4e22c5fd83 Move CSV export action logic to a confirmation form. 2024-02-02 19:49:54 -05:00
Michael Stenta 60d2357fbc Provide CSV export actions for asset and log entities via new farm_export_csv module. 2024-02-02 19:49:54 -05:00
Michael Stenta 07882e177a Move KML export actions to new farm_export_kml module. 2024-02-02 19:49:54 -05:00
Michael Stenta 146719f363 Initial farmOS Export module. 2024-02-02 19:49:54 -05:00
Michael Stenta e869407d45 Edit form UI improvements #770 2024-02-02 19:49:11 -05:00
Michael Stenta 73c9eef921 Add CHANGELOG.md line for #770. 2024-02-02 19:46:57 -05:00
Michael Stenta 09677800a2 Put equipment field into asset field group of logs. 2024-02-02 19:46:57 -05:00
Michael Stenta c8b9fad4b6 Add a "Group membership" field group for group and is_group_assignment log fields. 2024-02-02 19:46:57 -05:00
Michael Stenta 067ea5fa05 Allow modules to add entity form field groups. 2024-02-02 19:46:57 -05:00
Paul Weidner bacf11f295 Preserve delete and other action buttons in sidebar 2024-02-02 19:46:57 -05:00
Paul Weidner fa5724e035 Remove the sidebar if the display is not using field groups. 2024-02-02 19:46:57 -05:00
Paul Weidner 0e0258b66c Increase weight of notes field in forms 2024-02-02 19:46:57 -05:00
Paul Weidner 0525fd5a49 Improve styling in content form 2024-02-02 19:46:57 -05:00
Paul Weidner 2f8b3e05e8 Add additional revision information 2024-02-02 19:46:57 -05:00
Paul Weidner 268412bd23 Add tabs specific to each entity 2024-02-02 19:46:57 -05:00
Paul Weidner c4d3f174d5 Render fields in field groups 2024-02-02 19:46:57 -05:00
Paul Weidner 55e999af27 Provide GinContentFormBase class for entity forms 2024-02-02 19:46:56 -05:00
Paul Weidner 6dc23e7054 Implement farm_ui_theme_field_group_items for core module base fields 2024-02-02 19:46:56 -05:00
Paul Weidner abd6ee9646 Add hook_farm_ui_theme_field_group_items to collect field group mappings 2024-02-02 19:46:56 -05:00
Paul Weidner 69d9ef713b Patch drupal/gin for issue 334216 2024-02-02 19:46:56 -05:00
Paul Weidner 71206d03d2 Implement gin content form for farm entities 2024-02-02 19:46:56 -05:00
Michael Stenta 1410fa5ea7 Add UI for creating instances of quick forms #785 2024-02-02 17:19:55 -05:00
Michael Stenta 359ee1e79c Add CHANGELOG.md line for #785. 2024-02-02 17:19:00 -05:00
Paul Weidner 52f968ef6c Implement save function to add message and redirect to collection page 2024-02-02 17:19:00 -05:00
Paul Weidner 1cee2cb2e1 Hide actions for disabled quick forms from views 2024-02-02 17:19:00 -05:00
Michael Stenta d67e4492d1 Sanitize quick form labels, descriptions, and help text with Html::escape(). 2024-02-02 17:19:00 -05:00
Paul Weidner 4d9bd96234 Update tests to use edit form 2024-02-02 17:19:00 -05:00
Paul Weidner 3972ea268f Add cache tags to quick form help text render array 2024-02-02 17:19:00 -05:00
Paul Weidner e23e5923dc Grant administer quick_form to config permissions 2024-02-02 17:19:00 -05:00
Paul Weidner d8943488ab Remove disabled quick form routes and menu items 2024-02-02 17:19:00 -05:00
Paul Weidner 681de9f9ea Remove quick form configure tab 2024-02-02 17:19:00 -05:00
Paul Weidner 2c4953feff Add message when no quick form plugins exist 2024-02-02 17:19:00 -05:00
Paul Weidner 78a78b3aaa Add quick form add form 2024-02-02 17:19:00 -05:00
Paul Weidner ad3965208f Add quick form edit form 2024-02-02 17:19:00 -05:00
Paul Weidner 860790f9d5 Add quick form delete form 2024-02-02 17:19:00 -05:00
Paul Weidner e5894bdce1 Add quick form setup page 2024-02-02 17:19:00 -05:00
Paul Weidner 7033224a0d Load disabled quick forms 2024-02-02 17:19:00 -05:00
Paul Weidner a53f2b76e2 Add status and label entity keys 2024-02-02 17:19:00 -05:00
Michael Stenta 104885274c Inventory quick form #766 2024-02-02 17:18:17 -05:00
Michael Stenta a09ef42add Add CHANGELOG.md line for #766. 2024-02-02 16:34:26 -05:00
Michael Stenta 6879ff591f Save unit term label to configuration and automatically generate term on form load. 2024-02-02 16:34:26 -05:00
Michael Stenta ce8173f152 Add a dependency on farm_quick. 2024-02-02 16:34:26 -05:00
Michael Stenta f5c7139a63 Check inventory quick form access based on configured log type and asset. 2024-02-02 16:34:26 -05:00
Michael Stenta a4544a7ecb Move log type and custom name fields to Advanced section (hidden by default). 2024-02-02 16:34:26 -05:00
Michael Stenta 1cb57c2178 Make inventory quick form configurable. 2024-02-02 16:34:26 -05:00
Michael Stenta 0467595a0a Fix error when creating new unit terms.
The entity_autocomplete widget's values are different depending
on whether the selected term already exists. If it exists, the
value is the term ID. If it is being created, the value is an
array with an 'entity' key containing the new TermInterface
object.
2024-02-02 16:34:26 -05:00
Michael Stenta ca5298245a Allow the log name to be customized. 2024-02-02 16:34:26 -05:00
Michael Stenta b074a635c9 Validate that the selected log type can be created. 2024-02-02 16:34:26 -05:00
Michael Stenta 5cbd753905 Allow log type to be selected. 2024-02-02 16:34:26 -05:00
Michael Stenta 74f3ee4b5d Initial inventory quick form with tests. 2024-02-02 16:34:26 -05:00
Michael Stenta a37820e514 Add a Product asset type and Product type taxonomy #787 2024-02-02 16:33:49 -05:00
Michael Stenta 31cd1aa177 Recommend running composer update twice #653 2024-02-02 13:22:31 -05:00
Michael Stenta 220c99c510 Whitespace fix. 2024-01-31 06:38:30 -05:00
Paul Weidner 5a49dce4f5 Remove decreased page top margin #788 2024-01-31 06:22:22 -05:00
Michael Stenta 84421be197 Add enforced module dependency to core.entity_view_mode.asset.map_popup.yml. 2024-01-31 05:12:46 -05:00
Michael Stenta 74ffac59ec Document Account Admin role in docs/guide/people. 2024-01-26 14:56:47 -05:00
Michael Stenta 1c404b5152 Announce new releases on farmOS.discourse.group #780 2024-01-22 10:42:42 -05:00
Michael Stenta 755e07bfdf
Only Docker is necessary in dev environment docs.
Docker Compose is included with Docker now.
2024-01-19 21:42:50 -05:00
Michael Stenta 6c03557b3c farmOS 3.0.1 2024-01-18 16:23:31 -05:00
Michael Stenta 7329866866 Update drupal/entity_reference_revisions to 1.11. 2024-01-18 16:23:31 -05:00
Michael Stenta 11b256b33a Remove farm.settings.farm_info menu link.
This is currently the only sub-menu link provided for
settings pages, which is weird. This is a quick fix which
now forces users to click "Settings" to see all the sub-tabs,
instead of thinking there is only one by looking at the
toolbar menu. In a follow-up we may consider providing a
deriver that automatically generates menu links for each
sub-tab so they all show in the toolbar menu. It would be
burdensome to require every module that provides a settings
tab to also add a menu link.
2024-01-18 16:23:31 -05:00
Michael Stenta 004ecd8e9e Allow users with asset view access to see asset/[id]/locations. 2024-01-18 15:09:17 -05:00
Michael Stenta 2a992f5a7c Filter out assets that the user can't view. 2024-01-18 15:08:35 -05:00
Michael Stenta f1c6a47846 Prevent users without asset edit access from moving them in location hierarchy. 2024-01-18 14:41:21 -05:00
Michael Stenta 31c5280662 Fix duplicated revision tab on entities #773 2024-01-18 06:25:14 -05:00
Michael Stenta 8091b51561 Update satellite map layer instructions in installation guide.
We no longer include Google Maps layers in farmOS core.
Simplify the docs by only describing how to enable Mapbox
layers.
2024-01-18 06:22:31 -05:00
Michael Stenta ca3bc10c7d Add Mapbox module to /setup/modules. 2024-01-18 06:22:31 -05:00
Michael Stenta 83b15287ba Update Drupal core to 10.2.2. 2024-01-17 15:41:04 -05:00
Michael Stenta 8f73c593a1 Fix Error: Call to a member function label() on null in farm_ui_views_views_pre_render().
These errors generally only happen for admins editing Views
through the UI. This makes the code more defensive.
2024-01-16 17:28:59 -05:00
Paul Weidner 17b792aca7 Patch drupal/core to fix Issue #3414883: datetime_timestamp widget does not use default field value #771
The datetime_timestamp widget does not use default field value.
This means that the timestamp field in the log form does not have a
default value and requires the user to input a value before submitting the form
2024-01-16 14:24:14 -05:00
Michael Stenta 68ae993c41 Add min/max options to integer fields in farm_field.factory #768 2024-01-12 05:51:45 -05:00
Michael Stenta 5f09c940c1 Use paratest to speedup tests in Github actions workflow #762 2024-01-11 13:02:52 -05:00
wotnak 8c1fd8fe11 Install pg_trgm PostgreSQL extension before starting tests to avoid race conditions when trying to automatically install it in concurrently run tests 2024-01-11 13:02:27 -05:00
wotnak 8ce641adc5 Revert "Use phpunit instead of paratest."
This reverts commit 6a9c872e9b.
2024-01-11 13:00:57 -05:00
wotnak c82787d432
Remove unnecessary 'Checkout the repository' step from 'Run PHP Codesniffer and PHPStan' job 2023-12-31 11:44:33 +01:00
wotnak 5a5bcb3b0b
Persist phpstan result cache between workflow runs 2023-12-31 11:44:27 +01:00
wotnak a970b5b56a
Use phpcs results cache and persist it between workflow runs 2023-12-31 09:25:12 +01:00
wotnak 76aeedc3b1
Combine PHP compatibility checks of contrib modules and themes into single phpcs run 2023-12-28 19:27:15 +01:00
wotnak dfc25b5865
Run phpcs checks in parallel 2023-12-28 19:26:41 +01:00
126 changed files with 3732 additions and 586 deletions

View File

@ -62,8 +62,6 @@ jobs:
runs-on: ubuntu-latest
needs: build
steps:
- name: Checkout the repository
uses: actions/checkout@v3
- name: Restore farmOS dev Docker image from cache
uses: actions/cache@v3
with:
@ -71,14 +69,44 @@ jobs:
key: farmos-dev-${{ github.run_id }}
- name: Load farmos/farmos:3.x-dev image
run: docker load < /tmp/farmos-dev.tar
- name: Create cache directory
run: mkdir -p ./cache
- name: Load phpcs and phpstan cache
uses: actions/cache/restore@v3
with:
path: |
./cache/.phpcs.cache.farmos
./cache/.phpcs.cache.contrib
./cache/resultCache.php
key: phpcs-phpstan-${{ hashFiles('./cache/.phpcs.cache.farmos', './cache/.phpcs.cache.contrib', './cache/resultCache.php') }}
restore-keys: phpcs-phpstan-
- name: Run PHP CodeSniffer
run: docker run farmos/farmos:3.x-dev phpcs /opt/drupal/web/profiles/farm
run: |
docker run \
-v ./cache:/tmp/phpcs \
farmos/farmos:3.x-dev \
phpcs --cache=/tmp/phpcs/.phpcs.cache.farmos --parallel=$(nproc) /opt/drupal/web/profiles/farm
- name: Run PHPStan
run: docker run farmos/farmos:3.x-dev phpstan analyze --memory-limit 1G /opt/drupal/web/profiles/farm
run: |
docker run \
-v ./cache:/tmp/phpstan \
farmos/farmos:3.x-dev \
phpstan -vvv analyze --memory-limit 1G /opt/drupal/web/profiles/farm
- name: Check PHP compatibility of contrib modules and themes (ignore warnings).
run: |
docker run farmos/farmos:3.x-dev phpcs --standard=PHPCompatibility --runtime-set testVersion 8.2- --warning-severity=0 /opt/drupal/web/modules
docker run farmos/farmos:3.x-dev phpcs --standard=PHPCompatibility --runtime-set testVersion 8.2- --warning-severity=0 /opt/drupal/web/themes
docker run \
-v ./cache:/tmp/phpcs \
farmos/farmos:3.x-dev \
phpcs --cache=/tmp/phpcs/.phpcs.cache.contrib --parallel=$(nproc) --standard=PHPCompatibility --runtime-set testVersion 8.2- --warning-severity=0 /opt/drupal/web/modules /opt/drupal/web/themes
- name: Save phpcs and phpstan cache
uses: actions/cache/save@v3
with:
path: |
./cache/.phpcs.cache.farmos
./cache/.phpcs.cache.contrib
./cache/resultCache.php
key: phpcs-phpstan-${{ hashFiles('./cache/.phpcs.cache.farmos', './cache/.phpcs.cache.contrib', './cache/resultCache.php') }}
test:
name: Run PHPUnit tests
runs-on: ubuntu-latest
@ -92,10 +120,13 @@ jobs:
include:
- dbms: pgsql
DB_URL: pgsql://farm:farm@db/farm
processes: auto
- dbms: mariadb
DB_URL: mysql://farm:farm@db/farm
processes: auto
- dbms: sqlite
DB_URL: sqlite://localhost/sites/default/files/db.sqlite
processes: 1
steps:
- name: Print test matrix variables
run: echo "matrix.dbms=${{ matrix.dbms }}, matrix.DB_URL=${{ matrix.DB_URL }}"
@ -122,8 +153,12 @@ jobs:
# The www-container-fs-ready file is only created once we expect the containers to be online
# so waiting for that lets us know it is safe to start the tests
run: until [ -f ./www/www-container-fs-ready ]; do sleep 0.1; done
- name: Install pg_trgm PostgreSQL extension
# This avoids race conditions when trying to automatically install it in concurrently run tests.
if: matrix.dbms == 'pgsql'
run: docker compose exec -T db psql -U farm -c 'CREATE EXTENSION IF NOT EXISTS pg_trgm;'
- name: Run PHPUnit tests
run: docker compose exec -u www-data -T www phpunit --verbose /opt/drupal/web/profiles/farm
run: docker compose exec -u www-data -T www paratest -vv --processes=${{ matrix.processes }} /opt/drupal/web/profiles/farm
- name: Test Drush site install with all modules
run: docker compose exec -u www-data -T www drush site-install --db-url=${{ matrix.DB_URL }} farm farm.modules='all'
release:
@ -215,8 +250,8 @@ jobs:
fi
outputs:
announce: ${{ env.ANNOUNCE_RELEASE }}
announce:
name: Announce new release
announce-microblog:
name: Announce new release on farmOS-microblog
if: needs.publish.outputs.announce
needs:
- build
@ -227,3 +262,25 @@ jobs:
message: '#farmOS ${{ needs.build.outputs.farmos_version }} has been released! https://github.com/farmOS/farmOS/releases/${{ needs.build.outputs.farmos_version }}'
secrets:
MICROBLOG_DEPLOY_KEY: ${{ secrets.MICROBLOG_DEPLOY_KEY }}
announce-discourse:
name: Announce new release on farmOS.discourse.group
if: needs.publish.outputs.announce
runs-on: ubuntu-latest
needs:
- build
- release
- publish
steps:
- name: Discourse API request
env:
DISCOURSE_API_KEY: ${{ secrets.DISCOURSE_API_KEY }}
run: |
curl --fail-with-body -X POST "https://farmos.discourse.group/posts/" \
-H "Content-Type: application/json" \
-H "Api-Key: ${DISCOURSE_API_KEY}" \
-H "Api-Username: mstenta" \
-d '{
"title": "farmOS ${{ needs.build.outputs.farmos_version }} has been released",
"raw": "farmOS [${{ needs.build.outputs.farmos_version }}](https://github.com/farmOS/farmOS/releases/${{ needs.build.outputs.farmos_version }}) has been released.\n\nFor the full release notes, see [CHANGELOG.md](https://github.com/farmOS/farmOS/blob/${{ needs.build.outputs.farmos_version }}/CHANGELOG.md).",
"category": 7
}'

View File

@ -7,6 +7,51 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
## [3.1.1] 2024-02-07
### Fixed
- [Grant config permissions create quick_form permission #791](https://github.com/farmOS/farmOS/pull/791)
- Patch Gin to fix [Issue #3419904: Help text is cutoff at xsmall breakpoint](https://www.drupal.org/node/3419904)
### Security
- Update Migrate Tools to 6.0.3 for [SA-CONTRIB-2024-008](https://www.drupal.org/sa-contrib-2024-008)
## [3.1.0] 2024-02-02
### Added
- [Announce new releases on farmOS.discourse.group #780](https://github.com/farmOS/farmOS/pull/780)
- [Add a Product asset type and Product type taxonomy #787](https://github.com/farmOS/farmOS/pull/787)
- [Inventory quick form #766](https://github.com/farmOS/farmOS/pull/766)
- [Add UI for creating instances of quick forms #785](https://github.com/farmOS/farmOS/pull/785)
- [Show map on /locations #779](https://github.com/farmOS/farmOS/pull/779)
- [Provide a plan_record entity type for plan record relationships with metadata #781](https://github.com/farmOS/farmOS/pull/781)
### Changed
- [Recommend running composer update twice #653](https://github.com/farmOS/farmOS/pull/786)
- [Edit form UI improvements #770](https://github.com/farmOS/farmOS/pull/770)
- [Improve asset and log CSV exports #783](https://github.com/farmOS/farmOS/pull/783)
- [Remove "All" from "Items per page" options in Views #776](https://github.com/farmOS/farmOS/pull/776)
## [3.0.1] 2024-01-18
### Added
- [Add min/max options to integer fields in farm_field.factory #768](https://github.com/farmOS/farmOS/pull/768)
### Changed
- Allow users with asset view access to see /asset/%id/locations.
### Fixed
- [Patch drupal/core to fix Issue #3414883: datetime_timestamp widget does not use default field value #771](https://github.com/farmOS/farmOS/pull/771)
- [Fix duplicated revision tab on entities #773](https://github.com/farmOS/farmOS/pull/773)
- Improve access checking on location hierarchy forms.
## [3.0.0] 2024-01-05
This is the first "stable" release of farmOS v3. See the release notes for
@ -662,7 +707,10 @@ moving forward.
Drupal 7, which required a complete refactor of the codebase. By comparison,
updating from Drupal 9 to 10 will simply involve updating deprecated code.
[Unreleased]: https://github.com/farmOS/farmOS/compare/3.0.0...HEAD
[Unreleased]: https://github.com/farmOS/farmOS/compare/3.1.1...HEAD
[3.1.0]: https://github.com/farmOS/farmOS/releases/tag/3.1.1
[3.1.0]: https://github.com/farmOS/farmOS/releases/tag/3.1.0
[3.0.1]: https://github.com/farmOS/farmOS/releases/tag/3.0.1
[3.0.0]: https://github.com/farmOS/farmOS/releases/tag/3.0.0
[3.0.0-beta3]: https://github.com/farmOS/farmOS/releases/tag/3.0.0-beta3
[3.0.0-beta2]: https://github.com/farmOS/farmOS/releases/tag/3.0.0-beta2

View File

@ -18,20 +18,20 @@
"require": {
"cweagans/composer-patches": "^1.6",
"drupal/admin_toolbar": "^3.3",
"drupal/core": "10.2.1",
"drupal/core": "10.2.3",
"drupal/config_update": "^2.0@alpha",
"drupal/consumers": "^1.15",
"drupal/csv_serialization": "^4.0",
"drupal/date_popup": "^1.3",
"drupal/entity": "1.4",
"drupal/entity_browser": "^2.10",
"drupal/entity_reference_integrity": "^1.1",
"drupal/entity_reference_revisions": "1.10",
"drupal/entity_reference_integrity": "1.2",
"drupal/entity_reference_revisions": "1.11",
"drupal/entity_reference_validators": "^1.0@beta",
"drupal/exif_orientation": "^1.2",
"drupal/fraction": "^2.3.1",
"drupal/geofield": "^1.40",
"drupal/gin": "^3.0@rc",
"drupal/gin": "3.0-rc9",
"drupal/inline_entity_form": "^1.0@RC",
"drupal/inspire_tree": "^1.0",
"drupal/jsonapi_extras": "^3.22",
@ -40,7 +40,7 @@
"drupal/migrate_plus": "^6.0",
"drupal/migrate_source_csv": "^3.5",
"drupal/migrate_source_ui": "^1.0",
"drupal/migrate_tools": "^6.0.2",
"drupal/migrate_tools": "^6.0.3",
"drupal/role_delegation": "^1.2",
"drupal/simple_oauth": "6.0.0-beta5",
"drupal/simple_oauth_password_grant": "^1.0@RC",
@ -61,14 +61,22 @@
"Issue #2429699: Add Views EntityReference filter to be available for all entity reference fields.": "https://www.drupal.org/files/issues/2021-12-02/2429699-453-9.3.x.patch",
"Issue #2339235: Remove taxonomy hard dependency on node module": "https://www.drupal.org/files/issues/2024-01-03/2339235-10.2.x-82.patch",
"Issue #1874838: Allow exposed blocks to use 'Link display' settings": "https://www.drupal.org/files/issues/2021-11-11/1874838-26.patch",
"Issue #2909128: Autocomplete not working on Chrome Android": "https://www.drupal.org/files/issues/2023-07-11/2909128-92.patch"
"Issue #2909128: Autocomplete not working on Chrome Android": "https://www.drupal.org/files/issues/2023-07-11/2909128-92.patch",
"Issue #3414883: Datetime_timestamp widget does not use default field value": "https://www.drupal.org/files/issues/2024-01-15/3414883-11.patch"
},
"drupal/entity": {
"Issue #3206703: Provide reverse relationships for bundle plugin entity_reference fields.": "https://www.drupal.org/files/issues/2022-05-11/3206703-10.patch"
},
"drupal/entity_reference_integrity": {
"Issue #3418000: Delete action only overridden on first entity type": "https://www.drupal.org/files/issues/2024-01-30/3418000-3.patch"
},
"drupal/entity_reference_revisions": {
"Issue #3267304: Infer target_revision_id to be Latest Revision when Only a target_id is Provided": "https://www.drupal.org/files/issues/2022-05-13/3267304-9.patch"
},
"drupal/gin": {
"Issue #3342164: Remove implicit dependency on node module for gin content form.": "https://www.drupal.org/files/issues/2024-01-15/3342164-6.patch",
"Issue #3419904: Help text is cutoff at xsmall breakpoint.": "https://www.drupal.org/files/issues/2024-02-07/3419904-4.patch"
},
"drupal/jsonapi_schema": {
"Issue #3256795: Float fields have a null schema": "https://www.drupal.org/files/issues/2022-01-03/3256795-4.patch",
"Issue #3246251: Change format utc-millisec to date-time": "https://www.drupal.org/files/issues/2021-10-27/3246251-2.patch",

View File

@ -1,12 +1,13 @@
{
"require": {
"cweagans/composer-patches": "^1.7",
"drupal/core-composer-scaffold": "10.2.1"
"drupal/core-composer-scaffold": "10.2.3"
},
"require-dev": {
"behat/mink": "^1.10",
"behat/mink-browserkit-driver": "^2.1",
"behat/mink-selenium2-driver": "^1.6",
"brianium/paratest": "^6",
"drupal/coder": "^8.3",
"mglaman/phpstan-drupal": "^1.1",
"mikey179/vfsstream": "^1.6",

View File

@ -2,8 +2,7 @@
Follow these instructions to set up a local farmOS development environment.
The only requirements are [Docker](https://www.docker.com) and
[Docker Compose](https://docs.docker.com/compose).
The only requirement is [Docker](https://www.docker.com).
## 1. Set up Docker containers

View File

@ -142,6 +142,8 @@ Both methods expect an array of field definition options. These include:
- `integer` - Integer number. Additional options:
- `size` (optional) - The integer database column size (`tiny`,
`small`, `medium`, `normal`, or `big`). Defaults to `normal`.
- `min` (optional) - The minimum value.
- `max` (optional) - The maximum value.
- `list_string` - Select list with allowed values. Additional options:
- `allowed_values` - An associative array of allowed values.
- `allowed_values_function` - The name of a function that returns an

View File

@ -2,10 +2,24 @@
## CSV
All [Asset](/guide/assets), [Log](/guide/logs), and
[Quantity](/guide/quantities) lists in farmOS include an "Export CSV" link at
the bottom that will generate and download a CSV file. Any sorts or filters
that are applied to the list will be represented in the CSV output.
All [Asset](/guide/assets) and [Log](/guide/logs) lists in farmOS provide an
"Export CSV" action that will generate a CSV of selected records. These include
most of the record's information, including columns that are not visible in the
list pages themselves.
[Quantity](/guide/quantities) lists provide an "Export CSV" link at the bottom of the page
that serve a similar purpose. These exports include all of the columns that are
visible on the Quantity list page, including information about the Quantity
itself, as well as some information about the Log records that the Quantity
is attached to.
Any sorts or filters that are applied to the list will be represented in the
CSV output.
**Warning: CSV exports do not include all data.**
The [farmOS API](/development/api) is the best way to get access to all raw data
in a farmOS instance.
## KML

View File

@ -8,7 +8,7 @@ Roles can be "managed" or "unmanaged." The permissions of managed roles are
controlled by modules and cannot be modified through the UI. Unmanaged roles
can be added/edited through the UI.
Three managed roles are provided with farmOS:
Three default managed roles are provided with farmOS:
- **Manager** - Has access to everything in farmOS. They can create, edit, and
delete records, and they can change configuration settings.
@ -22,6 +22,12 @@ Three managed roles are provided with farmOS:
These roles can be disabled by uninstalling the "Default Roles" module.
The "farmOS Account Admin Role" module provides another optional managed role
called **Account Admin**, which has permission to add/edit/remove other users.
This is useful in situations where an instance administrator wants to give
someone the ability to set up other accounts, without giving them full admin
access.
Permissions for managed roles cannot be modified through the UI. This is not
generally an issue since the provided roles have been carefully tailored to
work for most applications. In some cases, you may want to further customize

View File

@ -171,6 +171,11 @@ farmOS instance via the web UI or by running `drush en mymodule`.
Composer provides a simple way to update project dependencies:
composer update --no-dev
composer update --no-dev
**Note: It is necessary to run this command twice to ensure all dependencies
are properly updated.** We have an issue open to figure out a better solution:
[Composer merge plugin dependencies are not correctly updated #653](https://github.com/farmOS/farmOS/issues/653).
This will check for newer versions of all your project's dependencies (based
on the version constraints in your `composer.json` file), install them, and

View File

@ -66,16 +66,14 @@ These resources may be helpful:
Nginx reverse proxy with self-signed certificates for local farmOS
development with HTTPS.
### API Keys
### Satellite map layers
Optional modules are available for adding satellite imagery layers to maps (eg:
Mapbox, Google Maps, etc). However, because these layers are hosted by
third-party providers, API keys are required to use them. Instructions for
obtaining API keys are available via the links below. API keys can be entered
into farmOS by going to Settings > Map.
- [Mapbox](https://docs.mapbox.com/help/how-mapbox-works/access-tokens)
- [Google Maps](https://developers.google.com/maps/documentation/javascript/get-api-key)
farmOS includes an optional [Mapbox](https://www.mapbox.com) module that can be
enabled to add satellite imagery layers to the map. A Mapbox API key is
required. For more information, see Mapbox's official documentation:
[Access tokens](https://docs.mapbox.com/help/how-mapbox-works/access-tokens).
Enable the Mapbox module at Setup > Modules, and then add the API key at
Setup > Settings > Map > Mapbox.
## farmOS Codebase

View File

@ -25,6 +25,7 @@ included with farmOS define the following Asset types:
- Sensor
- Water
- Material
- Product
- Group&ast;
&ast;Group Assets are unique in that they can "contain" other Assets as "group
@ -272,6 +273,12 @@ Plant Assets have the following additional relationships:
- Plant type (references a Term in the "Plant type" vocabulary)
- Season (references a Term in the "Season" vocabulary)
#### Product Assets
Product Assets have the following additional relationships:
- Product type (references a Term in the "Product type" vocabulary)
#### Sensor Assets
Sensor Assets have an additional "Data streams" relationship, which is used to

View File

@ -20,6 +20,7 @@ is enabled. The modules included with farmOS define the following vocabularies:
- Log category
- Material type
- Plant type
- Product type
- Season
- Unit

View File

@ -48,6 +48,7 @@ function farm_modules() {
'farm_inventory' => t('Inventory management'),
'farm_material' => t('Material assets'),
'farm_seed' => t('Seed assets'),
'farm_product' => t('Product assets'),
'farm_sensor' => t('Sensor assets'),
'farm_compost' => t('Compost assets'),
'farm_group' => t('Group assets'),
@ -55,9 +56,11 @@ function farm_modules() {
'farm_lab_test' => t('Lab test logs'),
'farm_birth' => t('Birth logs'),
'farm_medical' => t('Medical logs'),
'farm_export_csv' => t('CSV exporter'),
'farm_import_csv' => t('CSV importer'),
'farm_kml' => t('KML export features'),
'farm_export_kml' => t('KML exporter'),
'farm_import_kml' => t('KML asset importer'),
'farm_map_mapbox' => t('Mapbox map layers: Satellite, Outdoors'),
'farm_api_default_consumer' => t('Default API Consumer'),
'farm_fieldkit' => t('Field Kit integration'),
'farm_l10n' => t('Translation/localization features'),

View File

@ -32,3 +32,15 @@ function farm_equipment_farm_entity_bundle_field_info(EntityTypeInterface $entit
return $fields;
}
/**
* Implements hook_farm_ui_theme_field_group_items().
*/
function farm_equipment_farm_ui_theme_field_group_items(string $entity_type, string $bundle) {
if ($entity_type == 'log') {
return [
'equipment' => 'asset',
];
}
return [];
}

View File

@ -6,13 +6,10 @@ dependencies:
- image.style.thumbnail
module:
- asset
- csv_serialization
- farm_group
- farm_location
- image
- options
- rest
- serialization
- state_machine
- user
id: farm_group_members
@ -621,7 +618,7 @@ display:
items_per_page: true
items_per_page_label: 'Items per page'
items_per_page_options: '25, 50, 100, 250, 500'
items_per_page_options_all: true
items_per_page_options_all: false
items_per_page_options_all_label: '- All -'
offset: false
offset_label: Offset
@ -1052,17 +1049,6 @@ display:
plugin_id: result
empty: false
content: 'Displaying @start - @end of @total'
display_link:
id: display_link
table: views
field: display_link
relationship: none
group_type: group
admin_label: ''
plugin_id: display_link
label: 'Export CSV'
empty: false
display_id: csv
display_extenders: { }
cache_metadata:
max-age: 0
@ -1073,61 +1059,6 @@ display:
- url.query_args
- user.permissions
tags: { }
csv:
id: csv
display_title: 'CSV export (rest)'
display_plugin: rest_export
position: 2
display_options:
pager:
type: none
options:
offset: 0
style:
type: serializer
options:
uses_fields: false
formats:
csv: csv
row:
type: data_field
options:
field_options:
asset_bulk_form:
alias: ''
raw_output: false
image_target_id:
alias: ''
raw_output: false
id:
alias: ''
raw_output: false
name:
alias: ''
raw_output: false
type:
alias: ''
raw_output: false
flag_value:
alias: ''
raw_output: false
status:
alias: ''
raw_output: false
display_description: ''
display_extenders: { }
path: assets.csv
auth:
- cookie
cache_metadata:
max-age: 0
contexts:
- 'languages:language_content'
- 'languages:language_interface'
- request_format
- url
- user.permissions
tags: { }
page:
id: page
display_title: 'Group members (page)'

View File

@ -71,3 +71,34 @@ function farm_group_farm_ui_theme_region_items(string $entity_type) {
}
return $region_items;
}
/**
* Implements hook_farm_ui_theme_field_groups().
*/
function farm_group_farm_ui_theme_field_groups(string $entity_type, string $bundle) {
// Add a field group for group membership fields on logs.
if ($entity_type == 'log') {
return [
'group' => [
'location' => 'main',
'title' => t('Group'),
'weight' => 60,
],
];
}
return [];
}
/**
* Implements hook_farm_ui_theme_field_group_items().
*/
function farm_group_farm_ui_theme_field_group_items(string $entity_type, string $bundle) {
if ($entity_type == 'log') {
return [
'group' => 'group',
'is_group_assignment' => 'group',
];
}
return [];
}

View File

@ -60,11 +60,9 @@ class MapRenderEventSubscriber implements EventSubscriberInterface {
*/
public function onMapRender(MapRenderEvent $event) {
// Get the map ID.
$map_id = $event->getmapType()->id();
// Add land type layers to dashboard map.
if ($map_id == 'dashboard') {
// If the "locations" behavior is added to the map, add layers for each
// land type.
if (in_array('locations', $event->getMapBehaviors())) {
$layers = [];
// Define the parent group.

View File

@ -0,0 +1,11 @@
langcode: en
status: true
dependencies:
enforced:
module:
- farm_product
id: product
label: Product
description: ''
workflow: asset_default
new_revision: true

View File

@ -0,0 +1,11 @@
langcode: en
status: true
dependencies:
enforced:
module:
- farm_product
id: asset_product
color: blue
conditions:
asset_type:
- product

View File

@ -0,0 +1,9 @@
name: Product asset
description: Adds an Product asset type.
type: module
package: farmOS Assets
core_version_requirement: ^10
dependencies:
- farm:asset
- farm:farm_entity
- farm:farm_product_type

View File

@ -0,0 +1,43 @@
<?php
namespace Drupal\farm_product\Plugin\Asset\AssetType;
use Drupal\farm_entity\Plugin\Asset\AssetType\FarmAssetType;
/**
* Provides the product asset type.
*
* @AssetType(
* id = "product",
* label = @Translation("Product"),
* )
*/
class Product extends FarmAssetType {
/**
* {@inheritdoc}
*/
public function buildFieldDefinitions() {
$fields = parent::buildFieldDefinitions();
$field_info = [
'product_type' => [
'type' => 'entity_reference',
'label' => $this->t('Product type'),
'description' => $this->t("Enter the type of product."),
'target_type' => 'taxonomy_term',
'target_bundle' => 'product_type',
'auto_create' => TRUE,
'required' => TRUE,
'weight' => [
'form' => -90,
'view' => -50,
],
],
];
foreach ($field_info as $name => $info) {
$fields[$name] = $this->farmFieldFactory->bundleFieldDefinition($info);
}
return $fields;
}
}

View File

@ -60,11 +60,9 @@ class MapRenderEventSubscriber implements EventSubscriberInterface {
*/
public function onMapRender(MapRenderEvent $event) {
// Get the map ID.
$map_id = $event->getmapType()->id();
// Add structure type layers to dashboard map.
if ($map_id == 'dashboard') {
// If the "locations" behavior is added to the map, add layers for each
// structure type.
if (in_array('locations', $event->getMapBehaviors())) {
$layers = [];
// Define the parent group.

View File

@ -148,7 +148,7 @@ class Asset extends RevisionableContentEntityBase implements AssetInterface {
*/
public function getBundleLabel() {
/** @var \Drupal\asset\Entity\AssetTypeInterface $type */
$type = \Drupal::entityTypeManager()
$type = $this->entityTypeManager()
->getStorage('asset_type')
->load($this->bundle());
return $type->label();

View File

@ -113,7 +113,7 @@ class AssetType extends ConfigEntityBundleBase implements AssetTypeInterface {
// If the asset type id changed, update all existing assets of that type.
if ($update && $this->getOriginalId() != $this->id()) {
$update_count = \Drupal::entityTypeManager()->getStorage('asset')->updateType($this->getOriginalId(), $this->id());
$update_count = $this->entityTypeManager()->getStorage('asset')->updateType($this->getOriginalId(), $this->id());
if ($update_count) {
\Drupal::messenger()->addMessage(\Drupal::translation()->formatPlural($update_count,
'Changed the asset type of 1 post from %old-type to %type.',
@ -127,7 +127,7 @@ class AssetType extends ConfigEntityBundleBase implements AssetTypeInterface {
if ($update) {
// Clear the cached field definitions as some settings affect the field
// definitions.
\Drupal::entityTypeManager()->clearCachedDefinitions();
$this->entityTypeManager()->clearCachedDefinitions();
\Drupal::service('entity_field.manager')->clearCachedFieldDefinitions();
}
}

View File

@ -40,7 +40,7 @@ class AssetCRUDTest extends AssetTestBase {
];
$this->drupalGet('asset/add/default');
$this->submitForm($edit, $this->t('Save'));
$this->submitForm($edit, 'Save');
$result = \Drupal::entityTypeManager()
->getStorage('asset')
@ -85,7 +85,7 @@ class AssetCRUDTest extends AssetTestBase {
'name[0][value]' => $this->randomMachineName(),
];
$this->drupalGet($asset->toUrl('edit-form'));
$this->submitForm($edit, $this->t('Save'));
$this->submitForm($edit, 'Save');
$this->assertSession()->pageTextContains($edit['name[0][value]']);
}
@ -100,7 +100,7 @@ class AssetCRUDTest extends AssetTestBase {
$asset_id = $asset->id();
$this->drupalGet($asset->toUrl('delete-form'));
$this->submitForm([], $this->t('Delete'));
$this->submitForm([], 'Delete');
$this->assertSession()->responseContains($this->t('The @entity-type %label has been deleted.', [
'@entity-type' => $asset->getEntityType()->getSingularLabel(),
'%label' => $label,

View File

@ -0,0 +1,5 @@
name: farmOS CSV
description: Provides CSV features for farmOS.
type: module
package: farmOS
core_version_requirement: ^10

View File

@ -0,0 +1,19 @@
services:
farm_csv.normalizer.content_entity_normalizer:
class: Drupal\farm_csv\Normalizer\ContentEntityNormalizer
tags:
- { name: normalizer, priority: 10 }
arguments: ['@entity_type.manager', '@entity_type.repository', '@entity_field.manager']
farm_csv.normalizer.text_long_field_item:
class: Drupal\farm_csv\Normalizer\TextLongFieldItemNormalizer
tags:
- { name: normalizer, priority: 10 }
farm_csv.normalizer.entity_reference_field_item:
class: Drupal\farm_csv\Normalizer\EntityReferenceFieldItemNormalizer
tags:
- { name: normalizer, priority: 10 }
arguments: ['@entity.repository']
farm_csv.normalizer.timestamp_item:
class: Drupal\farm_csv\Normalizer\TimestampItemNormalizer
tags:
- { name: normalizer, priority: 10 }

View File

@ -0,0 +1,43 @@
<?php
namespace Drupal\farm_csv\Normalizer;
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\serialization\Normalizer\ContentEntityNormalizer as CoreContentEntityNormalizer;
/**
* Normalizes farmOS content entities for CSV exports.
*/
class ContentEntityNormalizer extends CoreContentEntityNormalizer {
/**
* The supported format.
*/
const FORMAT = 'csv';
/**
* {@inheritdoc}
*/
public function normalize($entity, $format = NULL, array $context = []): array|string|int|float|bool|\ArrayObject|NULL {
$data = parent::normalize($entity, $format, $context);
// If columns were explicitly included, remove others.
if (!empty($context['include_columns'])) {
foreach (array_keys($data) as $key) {
if (!in_array($key, $context['include_columns'])) {
unset($data[$key]);
}
}
}
return $data;
}
/**
* {@inheritdoc}
*/
public function supportsNormalization($data, string $format = NULL, array $context = []): bool {
return $data instanceof ContentEntityInterface && $format == static::FORMAT;
}
}

View File

@ -0,0 +1,51 @@
<?php
namespace Drupal\farm_csv\Normalizer;
use Drupal\Core\Config\Entity\ConfigEntityInterface;
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItemInterface;
use Drupal\serialization\Normalizer\EntityReferenceFieldItemNormalizer as CoreEntityReferenceFieldItemNormalizer;
/**
* Normalizes entity reference fields for farmOS CSV exports.
*/
class EntityReferenceFieldItemNormalizer extends CoreEntityReferenceFieldItemNormalizer {
/**
* The supported format.
*/
const FORMAT = 'csv';
/**
* {@inheritdoc}
*/
public function normalize($field_item, $format = NULL, array $context = []): array|string|int|float|bool|\ArrayObject|NULL {
// Attempt to load the referenced entity.
/** @var \Drupal\Core\Entity\EntityInterface $entity */
if ($entity = $field_item->get('entity')->getValue()) {
// Return content entity labels, if desired.
if ($entity instanceof ContentEntityInterface && isset($context['content_entity_labels']) && $context['content_entity_labels'] === TRUE) {
return $entity->label();
}
// Return config entity IDs, if desired.
if ($entity instanceof ConfigEntityInterface && isset($context['config_entity_ids']) && $context['config_entity_ids'] === TRUE) {
return $entity->id();
}
}
// Otherwise, delegate to the parent method.
return parent::normalize($field_item, $format, $context);
}
/**
* {@inheritdoc}
*/
public function supportsNormalization($data, string $format = NULL, array $context = []): bool {
return $data instanceof EntityReferenceItemInterface && $format == static::FORMAT;
}
}

View File

@ -0,0 +1,40 @@
<?php
namespace Drupal\farm_csv\Normalizer;
use Drupal\serialization\Normalizer\FieldItemNormalizer;
use Drupal\text\Plugin\Field\FieldType\TextLongItem;
/**
* Normalizes long text fields for farmOS CSV exports.
*/
class TextLongFieldItemNormalizer extends FieldItemNormalizer {
/**
* The supported format.
*/
const FORMAT = 'csv';
/**
* {@inheritdoc}
*/
public function normalize($field_item, $format = NULL, array $context = []): array|string|int|float|bool|\ArrayObject|NULL {
/** @var \Drupal\text\Plugin\Field\FieldType\TextLongItem $field_item */
// Return processed text, if desired.
if (isset($context['processed_text']) && $context['processed_text'] === TRUE) {
return $field_item->get('processed')->getValue();
}
// Delegate to the parent method.
return parent::normalize($field_item, $format, $context);
}
/**
* {@inheritdoc}
*/
public function supportsNormalization($data, string $format = NULL, array $context = []): bool {
return $data instanceof TextLongItem && $format == static::FORMAT;
}
}

View File

@ -0,0 +1,31 @@
<?php
namespace Drupal\farm_csv\Normalizer;
use Drupal\serialization\Normalizer\TimestampItemNormalizer as CoreTimestampItemNormalizer;
/**
* Normalizes timestamp fields for farmOS CSV exports.
*/
class TimestampItemNormalizer extends CoreTimestampItemNormalizer {
/**
* The supported format.
*/
const FORMAT = 'csv';
/**
* {@inheritdoc}
*/
public function normalize($object, $format = NULL, array $context = []): array|string|int|float|bool|\ArrayObject|NULL {
$data = parent::normalize($object, $format, $context);
// Return the RFC3339 formatted date, if desired.
if (isset($context['rfc3339_dates']) && $context['rfc3339_dates'] === TRUE) {
return $data['value'];
}
return $data;
}
}

View File

@ -145,7 +145,7 @@ display:
items_per_page: true
items_per_page_label: Limit
items_per_page_options: '1000,500,100,10,1'
items_per_page_options_all: true
items_per_page_options_all: false
items_per_page_options_all_label: '- All -'
offset: false
offset_label: Offset

View File

@ -134,7 +134,7 @@ class DataStream extends ContentEntityBase implements DataStreamInterface {
*/
public function getBundleLabel() {
/** @var \Drupal\data_stream\Entity\DataStreamTypeInterface $type */
$type = \Drupal::entityTypeManager()
$type = $this->entityTypeManager()
->getStorage('data_stream_type')
->load($this->bundle());
return $type->label();

View File

@ -16,6 +16,7 @@ function farm_entity_install() {
'data_stream',
'file',
'log',
'plan',
'quantity',
'taxonomy_term',
'user',

View File

@ -46,7 +46,7 @@ function farm_entity_entity_type_build(array &$entity_types) {
}
// Enable the use of bundle plugins on specific entity types.
foreach (['asset', 'log', 'plan', 'quantity'] as $entity_type) {
foreach (['asset', 'log', 'plan', 'plan_record', 'quantity'] as $entity_type) {
if (!empty($entity_types[$entity_type])) {
$entity_types[$entity_type]->set('bundle_plugin_type', $entity_type . '_type');
$entity_types[$entity_type]->setHandlerClass('bundle_plugin', FarmEntityBundlePluginHandler::class);

View File

@ -0,0 +1,17 @@
<?php
/**
* @file
* Post update hooks for the farm_entity module.
*/
/**
* Enforce entity reference integrity on plan reference fields.
*/
function farm_entity_post_update_enforce_plan_eri(&$sandbox) {
$config = \Drupal::configFactory()->getEditable('entity_reference_integrity_enforce.settings');
$entity_types = $config->get('enabled_entity_type_ids');
$entity_types['plan'] = 'plan';
$config->set('enabled_entity_type_ids', $entity_types);
$config->save();
}

View File

@ -12,6 +12,9 @@ services:
plugin.manager.plan_type:
class: Drupal\farm_entity\PlanTypeManager
parent: default_plugin_manager
plugin.manager.plan_record_type:
class: Drupal\farm_entity\PlanRecordTypeManager
parent: default_plugin_manager
plugin.manager.quantity_type:
class: Drupal\farm_entity\QuantityTypeManager
parent: default_plugin_manager

View File

@ -39,7 +39,7 @@ function farm_entity_fields_asset_base_fields() {
'type' => 'text_long',
'label' => t('Notes'),
'weight' => [
'form' => 0,
'form' => 95,
'view' => 10,
],
],
@ -87,7 +87,7 @@ function farm_entity_fields_log_base_fields() {
'type' => 'text_long',
'label' => t('Notes'),
'weight' => [
'form' => 0,
'form' => 95,
'view' => 10,
],
],
@ -133,7 +133,7 @@ function farm_entity_fields_plan_base_fields() {
'type' => 'text_long',
'label' => t('Notes'),
'weight' => [
'form' => 0,
'form' => 95,
'view' => 10,
],
],

View File

@ -0,0 +1,34 @@
<?php
namespace Drupal\farm_entity\Annotation;
use Drupal\Component\Annotation\Plugin;
/**
* Defines the plan record relationship type plugin annotation object.
*
* Plugin namespace: Plugin\PlanRecord\PlanRecordType.
*
* @see plugin_api
*
* @Annotation
*/
class PlanRecordType extends Plugin {
/**
* The plugin ID.
*
* @var string
*/
public $id;
/**
* The plan record relationship type label.
*
* @var \Drupal\Core\Annotation\Translation
*
* @ingroup plugin_translatable
*/
public $label;
}

View File

@ -0,0 +1,49 @@
<?php
namespace Drupal\farm_entity;
use Drupal\Component\Plugin\Exception\PluginException;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Plugin\DefaultPluginManager;
/**
* Manages discovery and instantiation of plan record relationship type plugins.
*
* @see \Drupal\farm_entity\Annotation\PlanType
* @see plugin_api
*/
class PlanRecordTypeManager extends DefaultPluginManager {
/**
* Constructs a new PlanRecordTypeManager object.
*
* @param \Traversable $namespaces
* An object that implements \Traversable which contains the root paths
* keyed by the corresponding namespace to look for plugin implementations.
* @param \Drupal\Core\Cache\CacheBackendInterface $cache_backend
* The cache backend.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler.
*/
public function __construct(\Traversable $namespaces, CacheBackendInterface $cache_backend, ModuleHandlerInterface $module_handler) {
parent::__construct('Plugin/PlanRecord/PlanRecordType', $namespaces, $module_handler, 'Drupal\farm_entity\Plugin\PlanRecord\PlanRecordType\PlanRecordTypeInterface', 'Drupal\farm_entity\Annotation\PlanRecordType');
$this->alterInfo('plan_record_type_info');
$this->setCacheBackend($cache_backend, 'plan_record_type_plugins');
}
/**
* {@inheritdoc}
*/
public function processDefinition(&$definition, $plugin_id) {
parent::processDefinition($definition, $plugin_id);
foreach (['id', 'label'] as $required_property) {
if (empty($definition[$required_property])) {
throw new PluginException(sprintf('The plan record relationship type %s must define the %s property.', $plugin_id, $required_property));
}
}
}
}

View File

@ -0,0 +1,14 @@
<?php
namespace Drupal\farm_entity\Plugin\PlanRecord\PlanRecordType;
use Drupal\Core\StringTranslation\StringTranslationTrait;
/**
* Provides a farmOS plan record relationship type base class.
*/
class FarmPlanRecordType extends PlanRecordTypeBase {
use StringTranslationTrait;
}

View File

@ -0,0 +1,26 @@
<?php
namespace Drupal\farm_entity\Plugin\PlanRecord\PlanRecordType;
use Drupal\farm_entity\FarmEntityTypeBase;
/**
* Provides the base plan record relationship type class.
*/
abstract class PlanRecordTypeBase extends FarmEntityTypeBase implements PlanRecordTypeInterface {
/**
* {@inheritdoc}
*/
public function getLabel() {
return $this->pluginDefinition['label'];
}
/**
* {@inheritdoc}
*/
public function buildFieldDefinitions() {
return [];
}
}

View File

@ -0,0 +1,20 @@
<?php
namespace Drupal\farm_entity\Plugin\PlanRecord\PlanRecordType;
use Drupal\entity\BundlePlugin\BundlePluginInterface;
/**
* Defines the interface for plan record relationship types.
*/
interface PlanRecordTypeInterface extends BundlePluginInterface {
/**
* Gets the plan record relationship type label.
*
* @return string
* The plan record relationship type label.
*/
public function getLabel();
}

View File

@ -0,0 +1,5 @@
name: farmOS Export
description: Features for exporting records.
type: module
package: farmOS
core_version_requirement: ^10

View File

@ -0,0 +1,11 @@
langcode: en
status: true
dependencies:
module:
- asset
- farm_export_csv
id: asset_csv_action
label: 'Export CSV'
type: asset
plugin: entity:csv_action:asset
configuration: { }

View File

@ -0,0 +1,11 @@
langcode: en
status: true
dependencies:
module:
- farm_export_csv
- log
id: log_csv_action
label: 'Export CSV'
type: log
plugin: entity:csv_action:log
configuration: { }

View File

@ -0,0 +1,8 @@
name: farmOS Export CSV
description: Provides a CSV export action for farmOS.
type: module
package: farmOS
core_version_requirement: ^10
dependencies:
- farm:farm_export
- farm:farm_csv

View File

@ -0,0 +1,27 @@
<?php
/**
* @file
* The farmOS Export CSV module.
*/
use Drupal\farm_export_csv\Form\EntityCsvActionForm;
use Drupal\farm_export_csv\Routing\EntityCsvActionRouteProvider;
/**
* Implements hook_entity_type_build().
*/
function farm_export_csv_entity_type_build(array &$entity_types) {
/** @var \Drupal\Core\Entity\EntityTypeInterface[] $entity_types */
// Enable the entity CSV export action on assets and logs.
foreach (['asset', 'log'] as $entity_type) {
if (!empty($entity_types[$entity_type])) {
$route_providers = $entity_types[$entity_type]->getRouteProviderClasses();
$route_providers['csv'] = EntityCsvActionRouteProvider::class;
$entity_types[$entity_type]->setHandlerClass('route_provider', $route_providers);
$entity_types[$entity_type]->setLinkTemplate('csv-action-form', '/' . $entity_type . '/csv');
$entity_types[$entity_type]->setFormClass('csv-action-form', EntityCsvActionForm::class);
}
}
}

View File

@ -0,0 +1,381 @@
<?php
namespace Drupal\farm_export_csv\Form;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Entity\EntityFieldManagerInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\File\FileSystemInterface;
use Drupal\Core\File\FileUrlGeneratorInterface;
use Drupal\Core\Form\BaseFormIdInterface;
use Drupal\Core\Form\ConfirmFormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\TempStore\PrivateTempStoreFactory;
use Drupal\Core\Url;
use Drupal\file\FileRepositoryInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\Serializer\SerializerInterface;
/**
* Provides an entity CSV action form.
*
* @see \Drupal\farm_export_csv\Plugin\Action\EntityCsv
* @see \Drupal\Core\Entity\Form\DeleteMultipleForm
*/
class EntityCsvActionForm extends ConfirmFormBase implements BaseFormIdInterface {
/**
* The tempstore factory.
*
* @var \Drupal\Core\TempStore\SharedTempStore
*/
protected $tempStore;
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* The entity field manager.
*
* @var \Drupal\Core\Entity\EntityFieldManagerInterface
*/
protected $entityFieldManager;
/**
* The serializer service.
*
* @var \Symfony\Component\Serializer\SerializerInterface
*/
protected $serializer;
/**
* The file system service.
*
* @var \Drupal\Core\File\FileSystemInterface
*/
protected $fileSystem;
/**
* The default file scheme.
*
* @var string
*/
protected $defaultFileScheme;
/**
* The file repository service.
*
* @var \Drupal\file\FileRepositoryInterface
*/
protected $fileRepository;
/**
* The file URL generator.
*
* @var \Drupal\Core\File\FileUrlGeneratorInterface
*/
protected $fileUrlGenerator;
/**
* The current user.
*
* @var \Drupal\Core\Session\AccountInterface
*/
protected $user;
/**
* The entity type.
*
* @var \Drupal\Core\Entity\EntityTypeInterface
*/
protected $entityType;
/**
* The entities to export.
*
* @var \Drupal\Core\Entity\EntityInterface[]
*/
protected $entities;
/**
* Constructs an EntityCsvActionForm form object.
*
* @param \Drupal\Core\TempStore\PrivateTempStoreFactory $temp_store_factory
* The tempstore factory.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
* @param \Drupal\Core\Entity\EntityFieldManagerInterface $entity_field_manager
* The entity field manager.
* @param \Symfony\Component\Serializer\SerializerInterface $serializer
* The serializer service.
* @param \Drupal\Core\File\FileSystemInterface $file_system
* The file system service.
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* The config factory service.
* @param \Drupal\file\FileRepositoryInterface $file_repository
* The file repository service.
* @param \Drupal\Core\File\FileUrlGeneratorInterface $file_url_generator
* The file URL generator.
* @param \Drupal\Core\Session\AccountInterface $user
* The current user.
*/
public function __construct(PrivateTempStoreFactory $temp_store_factory, EntityTypeManagerInterface $entity_type_manager, EntityFieldManagerInterface $entity_field_manager, SerializerInterface $serializer, FileSystemInterface $file_system, ConfigFactoryInterface $config_factory, FileRepositoryInterface $file_repository, FileUrlGeneratorInterface $file_url_generator, AccountInterface $user) {
$this->tempStore = $temp_store_factory->get('entity_csv_confirm');
$this->entityTypeManager = $entity_type_manager;
$this->entityFieldManager = $entity_field_manager;
$this->serializer = $serializer;
$this->fileSystem = $file_system;
$this->defaultFileScheme = $config_factory->get('system.file')->get('default_scheme') ?? 'public';
$this->fileRepository = $file_repository;
$this->fileUrlGenerator = $file_url_generator;
$this->user = $user;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('tempstore.private'),
$container->get('entity_type.manager'),
$container->get('entity_field.manager'),
$container->get('serializer'),
$container->get('file_system'),
$container->get('config.factory'),
$container->get('file.repository'),
$container->get('file_url_generator'),
$container->get('current_user'),
);
}
/**
* {@inheritdoc}
*/
public function getBaseFormId() {
return 'entity_export_csv_action_form';
}
/**
* {@inheritdoc}
*/
public function getFormId() {
// Get entity type ID from the route because ::buildForm has not yet been
// called.
$entity_type_id = $this->getRouteMatch()->getParameter('entity_type_id');
return $entity_type_id . '_export_csv_action_form';
}
/**
* {@inheritdoc}
*/
public function getQuestion() {
return $this->formatPlural(count($this->entities), 'Export a CSV of @count @item?', 'Export a CSV of @count @items?', [
'@item' => $this->entityType->getSingularLabel(),
'@items' => $this->entityType->getPluralLabel(),
]);
}
/**
* {@inheritdoc}
*/
public function getCancelUrl() {
if ($this->entityType->hasLinkTemplate('collection')) {
return new Url('entity.' . $this->entityType->id() . '.collection');
}
else {
return new Url('<front>');
}
}
/**
* {@inheritdoc}
*/
public function getDescription() {
return '';
}
/**
* {@inheritdoc}
*/
public function getConfirmText() {
return $this->t('Export');
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state, $entity_type_id = NULL) {
// If we don't have an entity type or list of entities, redirect.
$this->entityType = $this->entityTypeManager->getDefinition($entity_type_id);
$this->entities = $this->tempStore->get($this->user->id() . ':' . $entity_type_id);
if (empty($entity_type_id) || empty($this->entities)) {
return new RedirectResponse($this->getCancelUrl()
->setAbsolute()
->toString());
}
// Make it clear that CSV exports are limited.
$message = $this->t('Note: CSV exports do not include all @item data.', ['@item' => $this->entityType->getSingularLabel()]);
$form['warning'] = [
'#type' => 'html_tag',
'#tag' => 'strong',
'#value' => $message,
];
// Delegate to the parent method.
return parent::buildForm($form, $form_state);
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
// Filter out entities the user doesn't have access to.
$inaccessible_entities = [];
$accessible_entities = [];
foreach ($this->entities as $entity) {
if (!$entity->access('view', $this->currentUser())) {
$inaccessible_entities[] = $entity;
continue;
}
$accessible_entities[] = $entity;
}
// Serialize the entities with the csv format.
$context = [
// Define the columns to include.
'include_columns' => $this->getIncludeColumns(),
// Return processed text from long text fields.
'processed_text' => TRUE,
// Return content entity labels and config entity IDs.
'content_entity_labels' => TRUE,
'config_entity_ids' => TRUE,
// Return RFC3339 dates.
'rfc3339_dates' => TRUE,
];
$output = $this->serializer->serialize($accessible_entities, 'csv', $context);
// Prepare the file directory.
$directory = $this->defaultFileScheme . '://csv';
$this->fileSystem->prepareDirectory($directory, FileSystemInterface::CREATE_DIRECTORY);
// Create the file.
$filename = 'csv_export-' . date('c') . '.csv';
$destination = "$directory/$filename";
try {
$file = $this->fileRepository->writeData($output, $destination);
}
// If file creation failed, bail with a warning.
catch (\Exception $e) {
$this->messenger()->addWarning($this->t('Could not create file.'));
return;
}
// Make the file temporary.
$file->status = 0;
$file->save();
// Add warning message for inaccessible entities.
if (!empty($inaccessible_entities)) {
$inaccessible_count = count($inaccessible_entities);
$this->messenger()->addWarning($this->formatPlural($inaccessible_count, 'Could not export @count @item because you do not have the necessary permissions.', 'Could not export @count @items because you do not have the necessary permissions.', [
'@item' => $this->entityType->getSingularLabel(),
'@items' => $this->entityType->getPluralLabel(),
]));
}
// Add confirmation message.
if (count($accessible_entities)) {
$this->messenger()->addStatus($this->formatPlural(count($accessible_entities), 'Exported @count @item.', 'Exported @count @items', [
'@item' => $this->entityType->getSingularLabel(),
'@items' => $this->entityType->getPluralLabel(),
]));
}
// Show a link to the file.
$url = $this->fileUrlGenerator->generateAbsoluteString($file->getFileUri());
$this->messenger()->addMessage($this->t('CSV file created: <a href=":url">%filename</a>', [
':url' => $url,
'%filename' => $file->label(),
]));
$this->tempStore->delete($this->currentUser()->id() . ':' . $this->entityType->id());
$form_state->setRedirectUrl($this->getCancelUrl());
}
/**
* Get a list of columns to include in CSV exports.
*
* @return string[]
* An array of column names.
*/
protected function getIncludeColumns() {
// Start with ID and UUID.
$columns = [
'id',
'uuid',
];
// Define which field types are supported.
$supported_field_types = [
'boolean',
'created',
'changed',
'entity_reference',
'list_string',
'state',
'string',
'text_long',
'timestamp',
];
// Add base field for supported field types.
$base_field_definitions = $this->entityFieldManager->getBaseFieldDefinitions($this->entityType->id());
foreach ($base_field_definitions as $field_name => $field_definition) {
if (!in_array($field_name, $columns) && in_array($field_definition->getType(), $supported_field_types)) {
$columns[] = $field_name;
}
}
// Add bundle fields for supported field types.
$bundles = $this->entityTypeManager->getStorage($this->entityType->getBundleEntityType())->loadMultiple();
foreach ($bundles as $bundle) {
if ($this->entityTypeManager->hasHandler($this->entityType->id(), 'bundle_plugin')) {
$bundle_fields = $this->entityTypeManager->getHandler($this->entityType->id(), 'bundle_plugin')->getFieldDefinitions($bundle->id());
foreach ($bundle_fields as $field_name => $field_definition) {
if (!in_array($field_name, $columns) && in_array($field_definition->getType(), $supported_field_types)) {
$columns[] = $field_name;
}
}
}
}
// Remove revision and language columns.
$remove_columns = [
'default_langcode',
'revision_translation_affected',
'revision_created',
'revision_user',
];
$columns = array_filter($columns, function ($name) use ($remove_columns) {
return !in_array($name, $remove_columns);
});
return $columns;
}
}

View File

@ -0,0 +1,42 @@
<?php
namespace Drupal\farm_export_csv\Plugin\Action\Derivative;
use Drupal\Core\Action\Plugin\Action\Derivative\EntityActionDeriverBase;
use Drupal\Core\Entity\EntityTypeInterface;
/**
* Provides an action deriver for the CSV action.
*
* @see \Drupal\farm_export_csv\Plugin\Action\EntityCsv
*/
class EntityCsvDeriver extends EntityActionDeriverBase {
/**
* {@inheritdoc}
*/
public function getDerivativeDefinitions($base_plugin_definition) {
if (empty($this->derivatives)) {
$definitions = [];
foreach ($this->getApplicableEntityTypes() as $entity_type_id => $entity_type) {
$definition = $base_plugin_definition;
$definition['type'] = $entity_type_id;
$definition['label'] = $this->t('Export @entity_type CSV', ['@entity_type' => $entity_type->getSingularLabel()]);
$definition['confirm_form_route_name'] = 'entity.' . $entity_type->id() . '.csv_form';
$definitions[$entity_type_id] = $definition;
}
$this->derivatives = $definitions;
}
return parent::getDerivativeDefinitions($base_plugin_definition);
}
/**
* {@inheritdoc}
*/
protected function isApplicable(EntityTypeInterface $entity_type) {
return in_array($entity_type->id(), ['log', 'asset']);
}
}

View File

@ -0,0 +1,94 @@
<?php
namespace Drupal\farm_export_csv\Plugin\Action;
use Drupal\Core\Action\Plugin\Action\EntityActionBase;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\TempStore\PrivateTempStoreFactory;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Action that exports a CSV file of entities.
*
* @Action(
* id = "entity:csv_action",
* action_label = @Translation("Export entity as CSV"),
* deriver = "Drupal\farm_export_csv\Plugin\Action\Derivative\EntityCsvDeriver",
* )
*/
class EntityCsv extends EntityActionBase {
/**
* The tempstore object.
*
* @var \Drupal\Core\TempStore\SharedTempStore
*/
protected $tempStore;
/**
* The current user.
*
* @var \Drupal\Core\Session\AccountInterface
*/
protected $currentUser;
/**
* Constructs a new EntityCsv object.
*
* @param array $configuration
* A configuration array containing information about the plugin instance.
* @param string $plugin_id
* The plugin ID for the plugin instance.
* @param mixed $plugin_definition
* The plugin implementation definition.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager service.
* @param \Drupal\Core\TempStore\PrivateTempStoreFactory $temp_store_factory
* The tempstore factory.
* @param \Drupal\Core\Session\AccountInterface $current_user
* Current user.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityTypeManagerInterface $entity_type_manager, PrivateTempStoreFactory $temp_store_factory, AccountInterface $current_user) {
parent::__construct($configuration, $plugin_id, $plugin_definition, $entity_type_manager);
$this->tempStore = $temp_store_factory->get('entity_csv_confirm');
$this->currentUser = $current_user;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('entity_type.manager'),
$container->get('tempstore.private'),
$container->get('current_user'),
);
}
/**
* {@inheritdoc}
*/
public function executeMultiple(array $entities) {
/** @var \Drupal\Core\Entity\EntityInterface[] $entities */
$this->tempStore->set($this->currentUser->id() . ':' . $this->getPluginDefinition()['type'], $entities);
}
/**
* {@inheritdoc}
*/
public function execute($object = NULL) {
$this->executeMultiple([$object]);
}
/**
* {@inheritdoc}
*/
public function access($object, AccountInterface $account = NULL, $return_as_object = FALSE) {
return $object->access('view', $account, $return_as_object);
}
}

View File

@ -0,0 +1,47 @@
<?php
namespace Drupal\farm_export_csv\Routing;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Entity\Routing\EntityRouteProviderInterface;
use Symfony\Component\Routing\Route;
use Symfony\Component\Routing\RouteCollection;
/**
* Provides routes for the entity CSV export action.
*/
class EntityCsvActionRouteProvider implements EntityRouteProviderInterface {
/**
* {@inheritdoc}
*/
public function getRoutes(EntityTypeInterface $entity_type) {
$collection = new RouteCollection();
$entity_type_id = $entity_type->id();
if ($route = $this->getEntityCsvFormRoute($entity_type)) {
$collection->add("entity.$entity_type_id.csv_form", $route);
}
return $collection;
}
/**
* Gets the entity CSV export form route.
*
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
* The entity type.
*
* @return \Symfony\Component\Routing\Route|null
* The generated route, if available.
*/
protected function getEntityCsvFormRoute(EntityTypeInterface $entity_type) {
if ($entity_type->hasLinkTemplate('csv-action-form')) {
$route = new Route($entity_type->getLinkTemplate('csv-action-form'));
$route->setDefault('_form', $entity_type->getFormClass('csv-action-form'));
$route->setDefault('entity_type_id', $entity_type->id());
$route->setRequirement('_user_is_logged_in', 'TRUE');
return $route;
}
}
}

View File

@ -3,7 +3,7 @@ status: true
dependencies:
module:
- asset
- farm_kml
- farm_export_kml
id: asset_kml_action
label: 'Export KML'
type: asset

View File

@ -2,7 +2,7 @@ langcode: en
status: true
dependencies:
module:
- farm_kml
- farm_export_kml
- log
id: log_kml_action
label: 'Export KML'

View File

@ -0,0 +1,8 @@
name: farmOS Export KML
description: Provides a KML export action for farmOS.
type: module
package: farmOS
core_version_requirement: ^10
dependencies:
- farm:farm_export
- farm:farm_kml

View File

@ -1,6 +1,6 @@
<?php
namespace Drupal\farm_kml\Plugin\Action\Derivative;
namespace Drupal\farm_export_kml\Plugin\Action\Derivative;
use Drupal\Core\Action\Plugin\Action\Derivative\EntityActionDeriverBase;
use Drupal\Core\Entity\EntityTypeInterface;
@ -8,7 +8,7 @@ use Drupal\Core\Entity\EntityTypeInterface;
/**
* Provides an action deriver for the KML action.
*
* @see \Drupal\farm_kml\Plugin\Action\EntityKml
* @see \Drupal\farm_export_kml\Plugin\Action\EntityKml
*/
class EntityKmlDeriver extends EntityActionDeriverBase {

View File

@ -1,6 +1,6 @@
<?php
namespace Drupal\farm_kml\Plugin\Action;
namespace Drupal\farm_export_kml\Plugin\Action;
use Drupal\Core\Action\Plugin\Action\EntityActionBase;
use Drupal\Core\Config\ConfigFactoryInterface;
@ -18,7 +18,7 @@ use Symfony\Component\Serializer\SerializerInterface;
* @Action(
* id = "entity:kml_action",
* action_label = @Translation("Export entity geometry as KML"),
* deriver = "Drupal\farm_kml\Plugin\Action\Derivative\EntityKmlDeriver",
* deriver = "Drupal\farm_export_kml\Plugin\Action\Derivative\EntityKmlDeriver",
* )
*/
class EntityKml extends EntityActionBase {

View File

@ -781,6 +781,14 @@ class FarmFieldFactory implements FarmFieldFactoryInterface {
$field->setSetting('size', $options['size']);
}
// Set the min/max constraints, if specified.
if (isset($options['min'])) {
$field->setSetting('min', $options['min']);
}
if (isset($options['max'])) {
$field->setSetting('max', $options['max']);
}
// Build form and view display settings.
$field->setDisplayOptions('form', [
'type' => 'number',

View File

@ -0,0 +1,25 @@
<?php
/**
* @file
* Post update functions for farm_kml module.
*/
use Drupal\system\Entity\Action;
/**
* Move KML export actions to new farm_export_kml module.
*/
function farm_kml_post_update_move_kml_export_actions(&$sandbox = NULL) {
// Delete the existing KML export action config entities.
$configs = Action::loadMultiple(['asset_kml_action', 'log_kml_action']);
foreach ($configs as $config) {
$config->delete();
}
// Install the farm_export_kml module. This will recreate the actions.
if (!\Drupal::service('module_handler')->moduleExists('farm_export_kml')) {
\Drupal::service('module_installer')->install(['farm_export_kml']);
}
}

View File

@ -141,7 +141,7 @@ class UserLoginTest extends FarmBrowserTestBase {
$this->submitForm([
'name' => $account->getEmail(),
'pass' => $account->passRaw,
], $this->t('Log in'));
], 'Log in');
if (isset($flood_trigger)) {
$this->assertSession()->statusCodeEquals(403);
$this->assertSession()->fieldNotExists('pass');
@ -187,7 +187,7 @@ class UserLoginTest extends FarmBrowserTestBase {
$this->submitForm([
'name' => $account->getEmail(),
'pass' => $account->passRaw,
], $this->t('Log in'));
], 'Log in');
// @see ::drupalUserIsLoggedIn()
$account->sessionId = $this->getSession()->getCookie(\Drupal::service('session_configuration')->getOptions(\Drupal::request())['name']);

View File

@ -72,6 +72,23 @@ class MapRenderEvent extends Event {
return $this->mapType;
}
/**
* Getter method for map behaviors.
*
* This returns a merged list of map behaviors from both the map type
* configuration and the map element's #behaviors property.
*
* @return string[]
* An array of map behavior IDs.
*/
public function getMapBehaviors() {
$behaviors = $this->getMapType()->getMapBehaviors();
if (!empty($this->element['#behaviors'])) {
$behaviors = array_merge($behaviors, $this->element['#behaviors']);
}
return $behaviors;
}
/**
* Add behavior to the map.
*
@ -92,8 +109,10 @@ class MapRenderEvent extends Event {
/** @var \Drupal\farm_map\Entity\MapBehaviorInterface $behavior */
$behavior = $this->entityTypeManager->getStorage('map_behavior')->load($behavior_name);
// Attach the library.
$this->element['#attached']['library'][] = $behavior->getLibrary();
// If the behavior has a library, attach it.
if (!empty($behavior->getLibrary())) {
$this->element['#attached']['library'][] = $behavior->getLibrary();
}
// Add behavior settings if supplied.
if (!empty($settings)) {

View File

@ -123,7 +123,7 @@ class MapBlock extends BlockBase implements ContainerFactoryPluginInterface {
return [
'#type' => 'farm_map',
'#map_type' => $this->configuration['map_type'] ?? 'default',
'#behaviors' => array_keys($this->configuration['map_behaviors']) ?? [],
'#behaviors' => $this->configuration['map_behaviors'] ?? [],
];
}

View File

@ -19,6 +19,20 @@ plan.type.*:
type: boolean
label: 'Create new revision'
plan.record.type.*:
type: config_entity
label: 'Plan record relationship type'
mapping:
id:
type: string
label: 'Machine-readable name'
label:
type: label
label: 'Type'
description:
type: text
label: 'Description'
condition.plugin.plan_type:
type: condition.plugin
mapping:

View File

@ -0,0 +1,18 @@
<?php
/**
* @file
* Post update hooks for the plan module.
*/
/**
* Install plan_record and plan_record_type entity types.
*/
function plan_post_update_install_plan_record(&$sandbox) {
\Drupal::entityDefinitionUpdateManager()->installEntityType(
\Drupal::entityTypeManager()->getDefinition('plan_record_type')
);
\Drupal::entityDefinitionUpdateManager()->installEntityType(
\Drupal::entityTypeManager()->getDefinition('plan_record')
);
}

View File

@ -0,0 +1,30 @@
<?php
namespace Drupal\plan\Access;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Entity\EntityAccessControlHandler;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Session\AccountInterface;
/**
* Defines plan_record access logic.
*/
class PlanRecordAccess extends EntityAccessControlHandler {
/**
* {@inheritdoc}
*/
protected function checkAccess(EntityInterface $entity, $operation, AccountInterface $account) {
// If a plan is referenced, access is based on access to the plan.
/** @var \Drupal\plan\Entity\PlanRecordInterface $plan */
if ($plan = $entity->getPlan()) {
return AccessResult::allowedIf($plan->access($operation, $account));
}
// Otherwise, delegate to the parent method.
return parent::checkAccess($entity, $operation, $account);
}
}

View File

@ -148,7 +148,7 @@ class Plan extends RevisionableContentEntityBase implements PlanInterface {
*/
public function getBundleLabel() {
/** @var \Drupal\plan\Entity\PlanTypeInterface $type */
$type = \Drupal::entityTypeManager()
$type = $this->entityTypeManager()
->getStorage('plan_type')
->load($this->bundle());
return $type->label();

View File

@ -0,0 +1,92 @@
<?php
namespace Drupal\plan\Entity;
use Drupal\Core\Entity\ContentEntityBase;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Field\BaseFieldDefinition;
/**
* Defines the Plan record relationship entity.
*
* This entity type can be used to create relationships between a plan and other
* record(s) along with additional metadata fields to describe the relationship.
*
* @ContentEntityType(
* id = "plan_record",
* label = @Translation("Plan record relationship"),
* bundle_label = @Translation("Plan record relationship type"),
* label_collection = @Translation("Plan record relationships"),
* label_singular = @Translation("plan record relationship"),
* label_plural = @Translation("plan record relationships"),
* label_count = @PluralTranslation(
* singular = "@count plan record relationship",
* plural = "@count plan record relationships",
* ),
* handlers = {
* "access" = "Drupal\plan\Access\PlanRecordAccess",
* "form" = {
* "edit" = "Drupal\Core\Entity\ContentEntityForm",
* },
* "route_provider" = {
* "default" = "Drupal\Core\Entity\Routing\AdminHtmlRouteProvider",
* },
* },
* base_table = "plan_record",
* data_table = "plan_record_data",
* entity_keys = {
* "id" = "id",
* "uuid" = "uuid",
* "label" = "uuid",
* "bundle" = "type",
* },
* bundle_entity_type = "plan_record_type",
* common_reference_target = TRUE,
* links = {
* "edit-form" = "/plan/record/{plan_record}/edit",
* },
* )
*/
class PlanRecord extends ContentEntityBase implements PlanRecordInterface {
/**
* {@inheritdoc}
*/
public function getBundleLabel() {
/** @var \Drupal\plan\Entity\PlanRecordTypeInterface $type */
$type = $this->entityTypeManager()
->getStorage('plan_record_type')
->load($this->bundle());
return $type->label();
}
/**
* {@inheritdoc}
*/
public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
$fields = parent::baseFieldDefinitions($entity_type);
$fields['plan'] = BaseFieldDefinition::create('entity_reference')
->setLabel(t('Plan'))
->setDescription(t('Associate this plan record relationship with a plan entity.'))
->setTranslatable(FALSE)
->setCardinality(1)
->setSetting('target_type', 'plan')
->setDisplayOptions('form', [
'type' => 'entity_reference',
'weight' => 12,
])
->setDisplayConfigurable('form', TRUE)
->setDisplayConfigurable('view', TRUE);
return $fields;
}
/**
* {@inheritdoc}
*/
public function getPlan(): ?PlanInterface {
return $this->get('plan')->first()?->entity;
}
}

View File

@ -0,0 +1,28 @@
<?php
namespace Drupal\plan\Entity;
use Drupal\Core\Entity\ContentEntityInterface;
/**
* Provides an interface for defining plan record relationship entities.
*/
interface PlanRecordInterface extends ContentEntityInterface {
/**
* Gets the label of the plan record relationship type.
*
* @return string
* The label of the plan record relationship type.
*/
public function getBundleLabel();
/**
* Returns the Plan entity the plan record is assigned to.
*
* @return \Drupal\plan\Entity\PlanInterface|null
* The plant entity or NULL if not assigned.
*/
public function getPlan(): ?PlanInterface;
}

View File

@ -0,0 +1,74 @@
<?php
namespace Drupal\plan\Entity;
use Drupal\Core\Config\Entity\ConfigEntityBundleBase;
/**
* Defines the Plan record relationship type entity.
*
* @ConfigEntityType(
* id = "plan_record_type",
* label = @Translation("Plan record relationship type"),
* label_collection = @Translation("Plan record relationship types"),
* label_singular = @Translation("Plan record relationship type"),
* label_plural = @Translation("plan record relationship types"),
* label_count = @PluralTranslation(
* singular = "@count plan record relationship type",
* plural = "@count plan record relationship types",
* ),
* handlers = {
* "access" = "\Drupal\entity\BundleEntityAccessControlHandler",
* },
* config_prefix = "record.type",
* bundle_of = "plan_record",
* entity_keys = {
* "id" = "id",
* "label" = "label",
* "uuid" = "uuid",
* },
* config_export = {
* "id",
* "label",
* "description",
* }
* )
*/
class PlanRecordType extends ConfigEntityBundleBase implements PlanRecordTypeInterface {
/**
* The Plan record relationship type ID.
*
* @var string
*/
protected $id;
/**
* The Plan record relationship type label.
*
* @var string
*/
protected $label;
/**
* A brief description of this plan record relationship type.
*
* @var string
*/
protected $description;
/**
* {@inheritdoc}
*/
public function getDescription() {
return $this->description;
}
/**
* {@inheritdoc}
*/
public function setDescription($description) {
return $this->set('description', $description);
}
}

View File

@ -0,0 +1,13 @@
<?php
namespace Drupal\plan\Entity;
use Drupal\Core\Config\Entity\ConfigEntityInterface;
use Drupal\Core\Entity\EntityDescriptionInterface;
/**
* Provides an interface for defining Plan record relationship type entities.
*/
interface PlanRecordTypeInterface extends ConfigEntityInterface, EntityDescriptionInterface {
}

View File

@ -113,7 +113,7 @@ class PlanType extends ConfigEntityBundleBase implements PlanTypeInterface {
// If the plan type id changed, update all existing plans of that type.
if ($update && $this->getOriginalId() != $this->id()) {
$update_count = \Drupal::entityTypeManager()->getStorage('plan')->updateType($this->getOriginalId(), $this->id());
$update_count = $this->entityTypeManager()->getStorage('plan')->updateType($this->getOriginalId(), $this->id());
if ($update_count) {
\Drupal::messenger()->addMessage(\Drupal::translation()->formatPlural($update_count,
'Changed the plan type of 1 post from %old-type to %type.',
@ -127,7 +127,7 @@ class PlanType extends ConfigEntityBundleBase implements PlanTypeInterface {
if ($update) {
// Clear the cached field definitions as some settings affect the field
// definitions.
\Drupal::entityTypeManager()->clearCachedDefinitions();
$this->entityTypeManager()->clearCachedDefinitions();
\Drupal::service('entity_field.manager')->clearCachedFieldDefinitions();
}
}

View File

@ -40,7 +40,7 @@ class PlanCRUDTest extends PlanTestBase {
];
$this->drupalGet('plan/add/default');
$this->submitForm($edit, $this->t('Save'));
$this->submitForm($edit, 'Save');
$result = \Drupal::entityTypeManager()
->getStorage('plan')
@ -85,7 +85,7 @@ class PlanCRUDTest extends PlanTestBase {
'name[0][value]' => $this->randomMachineName(),
];
$this->drupalGet($plan->toUrl('edit-form'));
$this->submitForm($edit, $this->t('Save'));
$this->submitForm($edit, 'Save');
$this->assertSession()->pageTextContains($edit['name[0][value]']);
}
@ -101,7 +101,7 @@ class PlanCRUDTest extends PlanTestBase {
$plan_id = $plan->id();
$this->drupalGet($plan->toUrl('delete-form'));
$this->submitForm([], $this->t('Delete'));
$this->submitForm([], 'Delete');
$this->assertSession()->responseContains($this->t('The @entity-type %label has been deleted.', [
'@entity-type' => $plan->getEntityType()->getSingularLabel(),
'%label' => $label,

View File

@ -118,7 +118,7 @@ class Quantity extends RevisionableContentEntityBase implements QuantityInterfac
*/
public function getBundleLabel() {
/** @var \Drupal\quantity\Entity\QuantityTypeInterface $type */
$type = \Drupal::entityTypeManager()
$type = $this->entityTypeManager()
->getStorage('quantity_type')
->load($this->bundle());
return $type->label();

View File

@ -8,5 +8,6 @@ dependencies:
- drupal:taxonomy
- farm:asset
- farm:farm_log_quantity
- farm:farm_setup
- farm:quantity
- log:log

View File

@ -0,0 +1,5 @@
farm_quick.add_page:
title: 'Add quick form'
route_name: farm_quick.add_page
appears_on:
- entity.quick_form.collection

View File

@ -1,3 +1,8 @@
farm.quick:
class: Drupal\Core\Menu\MenuLinkDefault
deriver: Drupal\farm_quick\Plugin\Derivative\QuickFormMenuLink
farm.quick_setup:
title: Quick Forms
description: Quick forms make it easy to record common activities.
parent: farm.setup
route_name: entity.quick_form.collection

View File

@ -1,5 +1,7 @@
farm_quick:
config_permissions:
- create quick_form
- update quick_form
- administer quick_form
default_permissions:
- view quick_form

View File

@ -5,7 +5,9 @@
* The farmOS Quick Form module.
*/
use Drupal\Component\Utility\Html;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Routing\RouteMatchInterface;
/**
@ -23,11 +25,16 @@ function farm_quick_help($route_name, RouteMatchInterface $route_match) {
if (strpos($route_name, 'farm.quick.') === 0) {
$quick_form_id = $route_match->getParameter('id');
if ($route_name == 'farm.quick.' . $quick_form_id) {
/** @var \Drupal\farm_quick\Entity\QuickFormInstanceInterface $quick_form */
$quick_form = \Drupal::service('quick_form.instance_manager')->getInstance($quick_form_id);
$help_text = $quick_form->getHelpText();
if (!empty($help_text)) {
$output .= '<p>' . $help_text . '</p>';
}
$output = [
'#type' => 'html_tag',
'#tag' => 'p',
'#value' => Html::escape($quick_form->getHelpText()),
'#cache' => [
'tags' => $quick_form->getCacheTags(),
],
];
}
}
@ -57,3 +64,42 @@ function farm_quick_farm_entity_bundle_field_info(EntityTypeInterface $entity_ty
return $fields;
}
/**
* Implements hook_form_alter().
*/
function farm_quick_form_alter(&$form, FormStateInterface $form_state, $form_id) {
// Only alter views_form_ forms.
if (!str_starts_with($form_id, 'views_form_')) {
return;
}
$target = NULL;
if (isset($form['header']['asset_bulk_form']['action'])) {
$target = 'asset_bulk_form';
}
if (isset($form['header']['log_bulk_form']['action'])) {
$target = 'log_bulk_form';
}
// Alter action options for the target entity type bulk form.
if ($target) {
// Check for disabled quick forms.
$disabled_quick_forms = \Drupal::entityTypeManager()->getStorage('quick_form')->getQuery()
->accessCheck(TRUE)
->condition('status', FALSE)
->execute();
if (empty($disabled_quick_forms)) {
return;
}
// Remove system actions that end with quick_* for a disabled quick form.
foreach (array_keys($form['header'][$target]['action']['#options']) as $option_id) {
if ((preg_match("/quick_(.*)/", $option_id, $matches)) && in_array($matches[1], $disabled_quick_forms)) {
unset($form['header'][$target]['action']['#options'][$option_id]);
}
}
}
}

View File

@ -6,16 +6,23 @@ farm.quick:
requirements:
_permission: 'view quick_form'
farm_quick.configure:
path: /quick/{quick_form}/configure
farm_quick.add_page:
path: 'setup/quick/add'
defaults:
_entity_form: quick_form.configure
_title_callback: \Drupal\farm_quick\Form\ConfigureQuickForm::getTitle
_controller: \Drupal\farm_quick\Controller\QuickFormAddPage::addPage
_title: 'Add quick form'
requirements:
_custom_access: \Drupal\farm_quick\Form\ConfigureQuickForm::access
_permission: 'create quick_form'
farm_quick.add_form:
path: '/setup/quick/add/{plugin}'
defaults:
_entity_form: quick_form.add
requirements:
_permission: 'create quick_form'
options:
parameters:
quick_form:
plugin:
type: string
route_callbacks:

View File

@ -0,0 +1,79 @@
<?php
namespace Drupal\farm_quick\Controller;
use Drupal\Component\Utility\Html;
use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\Link;
use Drupal\farm_quick\QuickFormPluginManager;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Page that renders links to create instances of quick form plugins.
*/
class QuickFormAddPage extends ControllerBase {
/**
* The quick form plugin manager.
*
* @var \Drupal\farm_quick\QuickFormPluginManager
*/
protected $quickFormPluginManager;
/**
* Constructs a new QuickFormAddPage object.
*/
public function __construct(QuickFormPluginManager $quick_form_plugin_manager) {
$this->quickFormPluginManager = $quick_form_plugin_manager;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('plugin.manager.quick_form'),
);
}
/**
* Add quick form page callback.
*
* @return array
* Render array.
*/
public function addPage(): array {
$render = [
'#theme' => 'entity_add_list',
'#bundles' => [],
'#cache' => [
'tags' => $this->quickFormPluginManager->getCacheTags(),
],
];
// Filter to configurable quick form plugins.
$plugins = array_filter($this->quickFormPluginManager->getDefinitions(), function (array $plugin) {
if (($instance = $this->quickFormPluginManager->createInstance($plugin['id'])) && $instance->isConfigurable()) {
return TRUE;
}
return FALSE;
});
if (empty($plugins)) {
$render['#add_bundle_message'] = $this->t('No quick forms are available. Enable a module that provides quick forms.');
}
// Add link for each configurable plugin.
foreach ($plugins as $plugin_id => $plugin) {
$render['#bundles'][$plugin_id] = [
'label' => Html::escape($plugin['label']),
'description' => Html::escape($plugin['description']) ?? '',
'add_link' => Link::createFromRoute($plugin['label'], 'farm_quick.add_form', ['plugin' => $plugin_id]),
];
}
return $render;
}
}

View File

@ -2,8 +2,10 @@
namespace Drupal\farm_quick\Controller;
use Drupal\Component\Utility\Html;
use Drupal\Core\Cache\CacheableMetadata;
use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\Render\Markup;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\Url;
use Drupal\farm_quick\QuickFormInstanceManagerInterface;
@ -63,8 +65,15 @@ class QuickFormController extends ControllerBase {
$url = Url::fromRoute('farm.quick.' . $id);
if ($url->access()) {
$items[] = [
'title' => $quick_form->getLabel(),
'description' => $quick_form->getDescription(),
// Wrap the title in Markup::create() because the template preprocess
// function for admin_block_content uses Link::fromTextAndUrl(), which
// sanitizes strings automatically. This avoids double-sanitization,
// but also ensures we are sanitizing consistently in this code, in
// case anything changes later.
// @see template_preprocess_admin_block_content()
// @see \Drupal\Core\Link::fromTextAndUrl()
'title' => Markup::create(Html::escape($quick_form->getLabel())),
'description' => Html::escape($quick_form->getDescription()),
'url' => $url,
];
}

View File

@ -23,13 +23,27 @@ use Drupal\farm_quick\QuickFormPluginCollection;
* handlers = {
* "access" = "\Drupal\entity\EntityAccessControlHandler",
* "permission_provider" = "\Drupal\entity\EntityPermissionProvider",
* "list_builder" = "Drupal\farm_quick\QuickFormListBuilder",
* "form" = {
* "add" = "Drupal\farm_quick\Form\QuickFormEntityForm",
* "edit" = "Drupal\farm_quick\Form\QuickFormEntityForm",
* "configure" = "Drupal\farm_quick\Form\ConfigureQuickForm",
* "delete" = "\Drupal\Core\Entity\EntityDeleteForm",
* },
* "route_provider" = {
* "default" = "Drupal\entity\Routing\DefaultHtmlRouteProvider",
* },
* },
* admin_permission = "administer quick_form",
* entity_keys = {
* "id" = "id",
* "status" = "status"
* "status" = "status",
* "label" = "label",
* },
* links = {
* "edit-form" = "/setup/quick/{quick_form}/edit",
* "delete-form" = "/setup/quick/{quick_form}/delete",
* "collection" = "/setup/quick"
* },
* config_export = {
* "id",

View File

@ -1,159 +0,0 @@
<?php
namespace Drupal\farm_quick\Form;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Entity\EntityForm;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Form\SubformState;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\farm_quick\Plugin\QuickForm\ConfigurableQuickFormInterface;
use Drupal\farm_quick\QuickFormInstanceManagerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\Routing\Exception\ResourceNotFoundException;
/**
* Form that renders quick form configuration forms.
*
* @ingroup farm
*/
class ConfigureQuickForm extends EntityForm {
/**
* The entity being used by this form.
*
* @var \Drupal\farm_quick\Entity\QuickFormInstanceInterface
*/
protected $entity;
/**
* The quick form instance manager.
*
* @var \Drupal\farm_quick\QuickFormInstanceManagerInterface
*/
protected $quickFormInstanceManager;
/**
* Class constructor.
*
* @param \Drupal\farm_quick\QuickFormInstanceManagerInterface $quick_form_instance_manager
* The quick form instance manager.
*/
public function __construct(QuickFormInstanceManagerInterface $quick_form_instance_manager) {
$this->quickFormInstanceManager = $quick_form_instance_manager;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('quick_form.instance_manager'),
);
}
/**
* Get the title of the quick form.
*
* @param string $quick_form
* The quick form ID.
*
* @return \Drupal\Core\StringTranslation\TranslatableMarkup
* Quick form title.
*/
public function getTitle(string $quick_form) {
$quick_form_title = NULL;
if ($quick_form = $this->getQuickFormInstance($quick_form)) {
$quick_form_title = $quick_form->getLabel();
}
return $this->t('Configure @quick_form', ['@quick_form' => $quick_form_title]);
}
/**
* Checks access for configuration of a specific quick form.
*
* @param \Drupal\Core\Session\AccountInterface $account
* Run access checks for this account.
* @param string|null $quick_form
* The quick form ID.
*
* @return \Drupal\Core\Access\AccessResultInterface
* The access result.
*/
public function access(AccountInterface $account, string $quick_form = NULL) {
// Get a quick form config entity.
if ($quick_form !== NULL) {
$quick_form = $this->getQuickFormInstance($quick_form);
}
// Raise 404 if no quick form exists. This is the case with a quick form
// ID that is not a valid quick form plugin ID.
if ($quick_form === NULL) {
throw new ResourceNotFoundException();
}
// Deny access if the quick form plugin is not configurable.
if (!$quick_form->getPlugin() instanceof ConfigurableQuickFormInterface) {
return AccessResult::forbidden();
}
// Check the update quick_form permission.
$configure_form_access = AccessResult::allowedIfHasPermissions($account, ['update quick_form']);
return $quick_form->getPlugin()->access($account)->andIf($configure_form_access);
}
/**
* {@inheritdoc}
*/
public function form(array $form, FormStateInterface $form_state) {
$form = parent::form($form, $form_state);
$form['settings'] = [
'#tree' => TRUE,
];
$form['settings'] = $this->entity->getPlugin()->buildConfigurationForm($form['settings'], SubformState::createForSubform($form['settings'], $form, $form_state));
return $form;
}
/**
* {@inheritdoc}
*/
public function validateForm(array &$form, FormStateInterface $form_state) {
parent::validateForm($form, $form_state);
$this->entity->getPlugin()->validateConfigurationForm($form['settings'], SubformState::createForSubform($form['settings'], $form, $form_state));
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
parent::submitForm($form, $form_state);
$this->entity->getPlugin()->submitConfigurationForm($form['settings'], SubformState::createForSubform($form['settings'], $form, $form_state));
}
/**
* {@inheritdoc}
*/
public function getEntityFromRouteMatch(RouteMatchInterface $route_match, $entity_type_id) {
$entity = NULL;
if ($route_match->getRawParameter($entity_type_id) !== NULL) {
$entity = $this->getQuickFormInstance($route_match->getParameter($entity_type_id));
}
return $entity;
}
/**
* Helper function to get a quick form instance.
*
* @param string $quick_form_id
* The quick form ID.
*
* @return \Drupal\farm_quick\Entity\QuickFormInstanceInterface|null
* The quick form instance or NULL if does not exist.
*/
protected function getQuickFormInstance(string $quick_form_id) {
return $this->quickFormInstanceManager->getInstance($quick_form_id);
}
}

View File

@ -0,0 +1,210 @@
<?php
namespace Drupal\farm_quick\Form;
use Drupal\Component\Utility\Html;
use Drupal\Core\Entity\EntityForm;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Form\SubformState;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\farm_quick\Entity\QuickFormInstance;
use Drupal\farm_quick\QuickFormPluginManager;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
/**
* Form that renders quick form configuration forms.
*/
class QuickFormEntityForm extends EntityForm {
/**
* The entity being used by this form.
*
* @var \Drupal\farm_quick\Entity\QuickFormInstanceInterface
*/
protected $entity;
/**
* The quick form plugin manager.
*
* @var \Drupal\farm_quick\QuickFormPluginManager
*/
protected $quickFormPluginManager;
/**
* Constructs a new QuickFormEntityForm object.
*
* @param \Drupal\farm_quick\QuickFormPluginManager $quick_form_plugin_manager
* The quick form plugin manager.
*/
public function __construct(QuickFormPluginManager $quick_form_plugin_manager) {
$this->quickFormPluginManager = $quick_form_plugin_manager;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('plugin.manager.quick_form'),
);
}
/**
* {@inheritdoc}
*/
public function form(array $form, FormStateInterface $form_state, string $plugin = NULL) {
$form = parent::form($form, $form_state);
// Add tabs if the quick form plugin is configurable.
$tab_group = NULL;
if ($this->entity->getPlugin()->isConfigurable()) {
$form['tabs'] = [
'#type' => 'vertical_tabs',
];
$form['quick_form'] = [
'#type' => 'details',
'#title' => $this->t('Quick form'),
'#group' => 'tabs',
];
$tab_group = 'quick_form';
// Render the plugin form in settings tab.
$form['settings_tab'] = [
'#type' => 'details',
'#title' => Html::escape($this->entity->getPlugin()->getLabel()),
'#group' => 'tabs',
'#weight' => 50,
];
$form['settings'] = [
'#tree' => TRUE,
'#type' => 'container',
'#group' => 'settings_tab',
];
$form['settings'] = $this->entity->getPlugin()->buildConfigurationForm($form['settings'], SubformState::createForSubform($form['settings'], $form, $form_state));
}
$form['label'] = [
'#type' => 'textfield',
'#title' => $this->t('Label'),
'#maxlength' => 255,
'#required' => TRUE,
'#group' => $tab_group,
];
$form['id'] = [
'#type' => 'machine_name',
'#machine_name' => [
'exists' => '\Drupal\farm_quick\Entity\QuickFormInstance::load',
],
'#disabled' => !$this->entity->isNew() || $this->getRequest()->get('override'),
'#group' => $tab_group,
];
// Provide default label and ID for existing config entities
// or if the override parameter is set.
if (!$this->entity->isNew() || $this->getRequest()->get('override')) {
$form['label']['#default_value'] = $this->entity->label();
$form['id']['#default_value'] = $this->entity->id();
}
// Adjust form title.
if ($this->entity->isNew()) {
$form['#title'] = $this->t('Add quick form: @label', ['@label' => $this->entity->getPlugin()->getLabel()]);
if ($this->getRequest()->get('override')) {
$form['#title'] = $this->t('Override quick form: @label', ['@label' => $this->entity->getPlugin()->getLabel()]);
}
}
else {
$form['#title'] = $this->t('Edit quick form: @label', ['@label' => $this->entity->label()]);
}
$form['description'] = [
'#type' => 'textfield',
'#title' => $this->t('Description'),
'#description' => $this->t('A brief description of this quick form.'),
'#default_value' => $this->entity->getDescription(),
'#group' => $tab_group,
];
$form['status'] = [
'#type' => 'checkbox',
'#title' => $this->t('Enabled'),
'#description' => $this->t('Enable the quick form.'),
'#default_value' => $this->entity->status(),
'#group' => $tab_group,
];
$form['helpText'] = [
'#type' => 'textarea',
'#title' => $this->t('Help Text'),
'#description' => $this->t('Help text to display for the quick form.'),
'#default_value' => $this->entity->getHelpText(),
'#group' => $tab_group,
];
return $form;
}
/**
* {@inheritdoc}
*/
public function validateForm(array &$form, FormStateInterface $form_state) {
parent::validateForm($form, $form_state);
// Validate plugin form.
if ($this->entity->getPlugin()->isConfigurable()) {
$this->entity->getPlugin()->validateConfigurationForm($form['settings'], SubformState::createForSubform($form['settings'], $form, $form_state));
}
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
parent::submitForm($form, $form_state);
// Submit plugin form.
if ($this->entity->getPlugin()->isConfigurable()) {
$this->entity->getPlugin()->submitConfigurationForm($form['settings'], SubformState::createForSubform($form['settings'], $form, $form_state));
}
}
/**
* {@inheritdoc}
*/
public function save(array $form, FormStateInterface $form_state) {
$status = parent::save($form, $form_state);
$entity_type_label = $this->entity->getEntityType()->getSingularLabel();
$this->messenger()->addMessage($this->t('Saved @entity_type_label: %label', ['@entity_type_label' => $entity_type_label, '%label' => $this->entity->label()]));
$form_state->setRedirect('entity.quick_form.collection');
return $status;
}
/**
* {@inheritdoc}
*/
public function getEntityFromRouteMatch(RouteMatchInterface $route_match, $entity_type_id) {
// Get existing quick form entity from route parameter.
if ($route_match->getRawParameter($entity_type_id) !== NULL) {
$entity = $route_match->getParameter($entity_type_id);
}
// Else create a new quick form entity, the plugin must be specified.
else {
if (($plugin = $route_match->getRawParameter('plugin')) && $this->quickFormPluginManager->hasDefinition($plugin)) {
$entity = QuickFormInstance::create(['plugin' => $plugin]);
if ($this->getRequest()->get('override')) {
$entity->set('id', $plugin);
}
}
}
if (empty($entity)) {
throw new NotFoundHttpException();
}
return $entity;
}
}

View File

@ -3,6 +3,7 @@
namespace Drupal\farm_quick\Plugin\Derivative;
use Drupal\Component\Plugin\Derivative\DeriverBase;
use Drupal\Component\Utility\Html;
use Drupal\Core\Plugin\Discovery\ContainerDeriverInterface;
use Drupal\farm_quick\QuickFormInstanceManagerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
@ -59,9 +60,16 @@ class QuickFormMenuLink extends DeriverBase implements ContainerDeriverInterface
// Add a link for each quick form.
foreach ($quick_forms as $id => $quick_form) {
// Skip disabled quick forms.
if (!$quick_form->status()) {
continue;
}
// Create link.
$route_id = 'farm.quick.' . $id;
$links[$route_id] = [
'title' => $quick_form->getLabel(),
'title' => Html::escape($quick_form->getLabel()),
'parent' => 'farm.quick:farm.quick',
'route_name' => $route_id,
] + $base_plugin_definition;

View File

@ -59,19 +59,6 @@ class QuickFormTaskLink extends DeriverBase implements ContainerDeriverInterface
'base_route' => $route_name,
'weight' => 0,
] + $base_plugin_definition;
// If the quick form is configurable, add a link to the config form.
if ($quick_form->getPlugin()->isConfigurable()) {
$links["farm.quick.$id.configure"] = [
'title' => $this->t('Configure'),
'route_name' => 'farm_quick.configure',
'route_parameters' => [
'quick_form' => $id,
],
'base_route' => $route_name,
'weight' => 100,
] + $base_plugin_definition;
}
}
return $links;

View File

@ -60,7 +60,7 @@ class QuickFormInstanceManager implements QuickFormInstanceManagerInterface {
// Load quick form instance configuration entities for this plugin.
// Exclude disabled quick forms.
/** @var \Drupal\farm_quick\Entity\QuickFormInstanceInterface[] $entities */
$entities = $this->entityTypeManager->getStorage('quick_form')->loadByProperties(['plugin' => $plugin['id'], 'status' => TRUE]);
$entities = $this->entityTypeManager->getStorage('quick_form')->loadByProperties(['plugin' => $plugin['id']]);
foreach ($entities as $entity) {
$entity->getPlugin()->setQuickId($entity->id());
$instances[$entity->id()] = $entity;

View File

@ -0,0 +1,157 @@
<?php
namespace Drupal\farm_quick;
use Drupal\Core\Config\Entity\ConfigEntityListBuilder;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Url;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides a listing of template entities.
*/
class QuickFormListBuilder extends ConfigEntityListBuilder {
/**
* The quick form instance manager.
*
* @var \Drupal\farm_quick\QuickFormInstanceManagerInterface
*/
protected $quickFormInstanceManager;
/**
* Constructs a new QuickFormListBuilder object.
*
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
* The entity type definition.
* @param \Drupal\Core\Entity\EntityStorageInterface $storage
* The entity storage class.
* @param \Drupal\farm_quick\QuickFormInstanceManagerInterface $quick_form_instance_manager
* The quick form instance manager.
*/
public function __construct(EntityTypeInterface $entity_type, EntityStorageInterface $storage, QuickFormInstanceManagerInterface $quick_form_instance_manager) {
parent::__construct($entity_type, $storage);
$this->quickFormInstanceManager = $quick_form_instance_manager;
}
/**
* {@inheritdoc}
*/
public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) {
return new static(
$entity_type,
$container->get('entity_type.manager')->getStorage($entity_type->id()),
$container->get('quick_form.instance_manager'),
);
}
/**
* {@inheritdoc}
*/
public function load() {
return $this->quickFormInstanceManager->getInstances();
}
/**
* {@inheritdoc}
*/
public function render() {
$render['table'] = [
'#type' => 'table',
'#header' => $this->buildHeader(),
'#caption' => $this->t('Configured quick forms'),
'#rows' => [],
'#empty' => $this->t('There are no configured @label.', ['@label' => $this->entityType->getPluralLabel()]),
'#cache' => [
'contexts' => $this->entityType->getListCacheContexts(),
'tags' => $this->entityType->getListCacheTags(),
],
];
$render['default'] = [
'#type' => 'table',
'#header' => $this->buildHeader(),
'#caption' => $this->t('Default quick forms'),
'#rows' => [],
'#empty' => $this->t('There are no default @label.', ['@label' => $this->entityType->getPluralLabel()]),
];
// Load all quick form instances into proper table.
$quick_form_instances = $this->load();
foreach ($quick_form_instances as $entity) {
$target = $entity->isNew() ? 'default' : 'table';
if ($row = $this->buildRow($entity)) {
$render[$target][$entity->id()] = $row;
}
}
return $render;
}
/**
* {@inheritdoc}
*/
public function buildHeader() {
$header['enabled'] = $this->t('Enabled');
$header['type'] = $this->t('Plugin');
$header['label'] = $this->t('Label');
$header['id'] = $this->t('ID');
$header['description'] = $this->t('Description');
return $header + parent::buildHeader();
}
/**
* {@inheritdoc}
*/
public function buildRow(EntityInterface $entity) {
/** @var \Drupal\farm_quick\Entity\QuickFormInstanceInterface $quick_form */
$quick_form = $entity;
$row['enabled'] = [
'#type' => 'checkbox',
'#checked' => $quick_form->status(),
'#attributes' => [
'disabled' => 'disabled',
],
];
$row['type'] = [
'#plain_text' => $quick_form->getPlugin()->getLabel(),
];
$row['label'] = [
'#plain_text' => $quick_form->getLabel(),
];
$row['id'] = [
'#plain_text' => $quick_form->id(),
];
$row['description'] = [
'#plain_text' => $quick_form->getDescription(),
];
return $row + parent::buildRow($entity);
}
/**
* {@inheritdoc}
*/
public function getDefaultOperations(EntityInterface $entity) {
$operations = parent::getDefaultOperations($entity);
// Override operations for default quick form instances.
if ($entity->isNew()) {
// Remove edit operation.
unset($operations['edit']);
// Add override operation.
$operations['override'] = [
'title' => $this->t('Override'),
'weight' => 0,
'url' => $this->ensureDestination(Url::fromRoute('farm_quick.add_form', ['plugin' => $entity->getPluginId()], ['query' => ['override' => TRUE]])),
];
}
return $operations;
}
}

View File

@ -52,6 +52,11 @@ class QuickFormRoutes implements ContainerInjectionInterface {
$quick_forms = $this->quickFormInstanceManager->getInstances();
foreach ($quick_forms as $id => $quick_form) {
// Skip quick forms that are disabled.
if (!$quick_form->status()) {
continue;
}
// Build a route for the quick form.
$route = new Route(
"/quick/$id",

View File

@ -91,21 +91,10 @@ class QuickFormTest extends FarmBrowserTestBase {
$this->assertSession()->statusCodeEquals(200);
$this->assertSession()->responseContains('value="100"');
// Go to the test configuration form and confirm that access is denied.
$this->drupalGet('quick/configurable_test/configure');
$this->assertSession()->statusCodeEquals(403);
// Create and login a test user with permission to create test logs and
// permission to update quick forms.
$user = $this->createUser(['view quick_form', 'create test log', 'update quick_form']);
$this->drupalLogin($user);
// Go to the default configurable_test quick form and confirm that the
// default value field is visible and the default value is 100.
$this->drupalGet('quick/configurable_test/configure');
$this->assertSession()->statusCodeEquals(200);
$this->assertSession()->pageTextContains($this->t('Default value'));
$this->assertSession()->responseContains('value="100"');
// Attempt to load the edit form for the unsaved configurable_test quick
// form and confirm 404 not found.
$this->drupalGet('setup/quick/foo/configurable_test');
$this->assertSession()->statusCodeEquals(404);
// Go to the configurable_test2 quick form and confirm access is granted and
// the default value is 500.
@ -113,9 +102,19 @@ class QuickFormTest extends FarmBrowserTestBase {
$this->assertSession()->statusCodeEquals(200);
$this->assertSession()->responseContains('value="500"');
// Attempt to load the edit form for saved configurable_test2 quick
// form and confirm 403.
$this->drupalGet('setup/quick/configurable_test2/edit');
$this->assertSession()->statusCodeEquals(403);
// Create and login a test user with permission to create test logs and
// permission to update quick forms.
$user = $this->createUser(['view quick_form', 'create test log', 'update quick_form']);
$this->drupalLogin($user);
// Go to the configurable_test2 quick form and confirm that the default
// value field is visible and the default value is 500.
$this->drupalGet('quick/configurable_test2/configure');
$this->drupalGet('setup/quick/configurable_test2/edit');
$this->assertSession()->statusCodeEquals(200);
$this->assertSession()->pageTextContains($this->t('Default value'));
$this->assertSession()->responseContains('value="500"');
@ -127,12 +126,12 @@ class QuickFormTest extends FarmBrowserTestBase {
$config_entity->save();
$this->drupalGet('quick/configurable_test2');
$this->assertSession()->responseContains('value="600"');
$this->drupalGet('quick/configurable_test2/configure');
$this->drupalGet('setup/quick/configurable_test2/edit');
$this->assertSession()->responseContains('value="600"');
// Attempt to load a configuration form for a non-existent quick form and
// Attempt to load an edit form for a non-existent quick form and
// confirm 404 not found.
$this->drupalGet('quick/foo/configure');
$this->drupalGet('setup/quick/foo/edit');
$this->assertSession()->statusCodeEquals(404);
// Go to the requires_entity_test quick form and confirm 404 not found.

View File

@ -3,7 +3,7 @@
namespace Drupal\Tests\farm_quick\Kernel;
use Drupal\Core\Form\FormState;
use Drupal\farm_quick\Form\ConfigureQuickForm;
use Drupal\farm_quick\Form\QuickFormEntityForm;
use Drupal\KernelTests\KernelTestBase;
/**
@ -152,11 +152,15 @@ class QuickFormTest extends KernelTestBase {
// Confirm that the config entity for this quick form has not been saved.
$this->assertTrue($quick_form->isNew());
// Programmatically submit the configurable_test config form.
$form = ConfigureQuickForm::create(\Drupal::getContainer());
// Programmatically submit the quick form entity form.
$form = QuickFormEntityForm::create(\Drupal::getContainer());
$form->setModuleHandler(\Drupal::moduleHandler());
$form->setEntity($quick_form);
$form_state = (new FormState())->setValues([
// Set the ID and label because no default value is provided for these
// in the form unless the override query param is set.
'id' => $quick_form->id(),
'label' => (string) $quick_form->label(),
'settings' => [
'test_default' => '101',
],

View File

@ -11,10 +11,3 @@ farm.settings:
parent: farm.setup
route_name: farm_settings.settings_page
weight: 100
farm.settings.farm_info:
title: Farm Info
description: Test
parent: farm.settings
route_name: farm_settings.settings_page
weight: -10

View File

@ -0,0 +1,12 @@
langcode: en
status: true
dependencies:
enforced:
module:
- farm_ui_location
id: locations
label: Locations
description: 'The farmOS locations map.'
behaviors:
- locations
options: { }

View File

@ -6,5 +6,6 @@ core_version_requirement: ^10
dependencies:
- farm:farm_entity
- farm:farm_location
- farm:farm_ui_map
- farm:farm_ui_menu
- inspire_tree:inspire_tree

View File

@ -0,0 +1,33 @@
<?php
/**
* @file
* Post update functions for farm_ui_location module.
*/
use Drupal\farm_map\Entity\MapType;
/**
* Add farmOS locations map type.
*/
function farm_ui_location_post_update_add_locations_map_type(&$sandbox = NULL) {
// Create locations map type.
$map_type = MapType::create([
'id' => 'locations',
'label' => 'Locations',
'description' => 'The farmOS locations map.',
'behaviors' => [
'location',
],
'options' => [],
'dependencies' => [
'enforced' => [
'module' => [
'farm_ui_location',
],
],
],
]);
$map_type->save();
}

View File

@ -12,7 +12,6 @@ farm.asset.locations:
_title_callback: '\Drupal\farm_ui_location\Form\LocationHierarchyForm::getTitle'
_form: '\Drupal\farm_ui_location\Form\LocationHierarchyForm'
requirements:
_entity_access: 'asset.edit'
_custom_access: '\Drupal\farm_ui_location\Form\LocationHierarchyForm::access'
_module_dependencies: 'asset'
asset: \d+

View File

@ -82,8 +82,13 @@ class LocationHierarchyForm extends FormBase {
return AccessResult::forbidden();
}
// If the asset does not have child locations, forbid access.
if (empty($this->getLocations($asset))) {
return AccessResult::forbidden();
}
// Allow access if the asset has child locations.
return AccessResult::allowedIf(!empty($this->getLocations($asset)));
return AccessResult::allowedIf($asset->access('view', $account));
}
/**
@ -107,6 +112,14 @@ class LocationHierarchyForm extends FormBase {
*/
public function buildForm(array $form, FormStateInterface $form_state, AssetInterface $asset = NULL) {
// If no asset was specified, show a map of all locations.
if (is_null($asset)) {
$form['map'] = [
'#type' => 'farm_map',
'#map_type' => 'locations',
];
}
// Add a DIV for the JavaScript content.
$form['content'] = [
'#type' => 'html_tag',
@ -238,6 +251,11 @@ class LocationHierarchyForm extends FormBase {
/** @var \Drupal\asset\Entity\AssetInterface[] $assets */
$assets = $storage->loadMultiple($asset_ids);
// Filter out assets that the user cannot view.
$assets = array_filter($assets, function ($asset) {
return $asset->access('view');
});
// Sort assets by name, using natural sort algorithm.
usort($assets, function ($a, $b) {
return strnatcmp($a->label(), $b->label());
@ -269,12 +287,22 @@ class LocationHierarchyForm extends FormBase {
// Maintain a list of assets that need to be saved.
$save_assets = [];
// Maintain a list of assets that were not editable by the user.
$restricted_assets = [];
// Iterate through the changes.
foreach ($changes as $change) {
// Load the asset.
$asset = $storage->load($change['asset_id']);
// If the user does not have permission to edit the asset, count it so
// that we can add a warning message later, and skip it.
if (!$asset->access('edit')) {
$restricted_assets[] = $asset;
continue;
}
// Remove the original parent.
if (!empty($asset->get('parent'))) {
/** @var \Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItem $parent */
@ -317,6 +345,12 @@ class LocationHierarchyForm extends FormBase {
// Show a summary of the results.
$message = $this->formatPlural(count($save_assets), 'Updated the parent hierarchy of %count asset.', 'Updated the parent hierarchy of %count assets.', ['%count' => count($save_assets)]);
$this->messenger()->addStatus($message);
// If any edits were restricted, show a warning.
if ($restricted_assets) {
$message = $this->formatPlural(count($restricted_assets), '%count asset could not be changed because you do not have permission.', '%count assets could not be changed because you do not have permission.', ['%count' => count($restricted_assets)]);
$this->messenger()->addWarning($message);
}
}
}

View File

@ -1,6 +1,9 @@
langcode: en
status: true
dependencies:
enforced:
module:
- farm_ui_map
module:
- asset
id: asset.map_popup

View File

@ -0,0 +1,11 @@
langcode: en
status: true
dependencies:
enforced:
module:
- farm_ui_map
id: locations
label: Location asset layers
description: 'Displays location asset geometries in layers by asset type.'
library: ''
settings: { }

View File

@ -7,5 +7,6 @@ dependencies:
id: dashboard
label: Dashboard
description: 'The farmOS dashboard map.'
behaviors: { }
behaviors:
- locations
options: { }

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