Compare commits

...

129 Commits

Author SHA1 Message Date
wotnak 76b945d4d8
Merge eb8a77607b 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
wotnak eb8a77607b
Split dev stage into two separate ones to improve dev image cacheability 2024-01-01 08:30:57 +01:00
wotnak e54d689376
Add GIT_COMMIT build arg to ensure cache invalidation between different commits on the same branch 2024-01-01 08:29:28 +01:00
wotnak 778de46e31
Use Buildx to build and cache docker images 2024-01-01 08:28:51 +01:00
AlMaVizca 3774afee32
Merge development image into a single Dockerfile
Changes based on PR review
2023-12-29 23:15:10 +07:00
AlMaVizca 53fd5ebbbb
Relocate php unit configuration 2023-12-23 14:00:19 +07:00
AlMaVizca 635adf1a04
Refactor composer dependencies to create the production image
Use docker stages to install dependencies and simplify the building script.
Prepare production image while keeping compatibility with the development image.

Fix typo

Relocate arguments to improve cache

Make single line for each env definition

Remove composer home path

Avoid installing recommended packages
2023-12-23 14:00:19 +07:00
AlMaVizca 3a25ca5d71
Avoid repeating common path
Use common path on entrypoint
2023-12-23 14:00:19 +07:00
AlMaVizca 4803e40043
Define baseimage with arguments required in multiple stages
Change versions, add common paths

Keep drupal path

Fix comment
2023-12-23 14:00:19 +07:00
132 changed files with 3869 additions and 709 deletions

View File

@ -34,10 +34,23 @@ jobs:
run: |
echo "FARMOS_VERSION=${GITHUB_HEAD_REF}" >> $GITHUB_ENV
echo "FARMOS_REPO=${{ github.event.pull_request.head.repo.full_name }}" >> $GITHUB_ENV
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build and save farmOS Docker image
run: |
docker build --build-arg FARMOS_REPO=https://github.com/${FARMOS_REPO} --build-arg FARMOS_VERSION=${FARMOS_VERSION} -t farmos/farmos:3.x docker
docker save farmos/farmos:3.x > /tmp/farmos.tar
uses: docker/build-push-action@v5
with:
push: false
context: docker
build-args: |
FARMOS_REPO=https://github.com/${{ env.FARMOS_REPO }}
FARMOS_VERSION=${{ env.FARMOS_VERSION }}
GIT_COMMIT=${{ github.sha }}
tags: farmos/farmos:3.x
cache-from: type=gha,scope=prod
cache-to: type=gha,scope=prod
outputs: type=docker,dest=/tmp/farmos.tar
- name: Cache farmOS Docker image
uses: actions/cache@v3
with:
@ -47,9 +60,21 @@ jobs:
# but notably it does NOT override the default PROJECT_VERSION, so the
# farmOS Composer project 3.x branch is always used.
- name: Build and save farmOS dev Docker image
run: |
docker build --build-arg FARMOS_REPO=https://github.com/${FARMOS_REPO} --build-arg FARMOS_VERSION=${FARMOS_VERSION} -t farmos/farmos:3.x-dev docker/dev
docker save farmos/farmos:3.x-dev > /tmp/farmos-dev.tar
uses: docker/build-push-action@v5
with:
push: false
context: docker
target: dev
build-args: |
FARMOS_REPO=https://github.com/${{ env.FARMOS_REPO }}
FARMOS_VERSION=${{ env.FARMOS_VERSION }}
GIT_COMMIT=${{ github.sha }}
tags: farmos/farmos:3.x-dev
cache-from: |
type=gha,scope=prod
type=gha,scope=dev
cache-to: type=gha,scope=dev
outputs: type=docker,dest=/tmp/farmos-dev.tar
- name: Cache farmOS dev Docker image
uses: actions/cache@v3
with:
@ -222,8 +247,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
@ -234,3 +259,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,7 +1,7 @@
{
"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",

View File

@ -1,7 +1,14 @@
# Use the official Drupal 10 image to build GEOS PHP extension.
FROM drupal:10.1 as php-dependencies
# Use the official Drupal 10 as base image.
FROM drupal:10.1 as baseimage
# Define common paths.
ENV FARMOS_PATH=/var/farmOS
ENV DRUPAL_PATH=/opt/drupal
##
# Build PHP extensions, GEOS and bcmath.
FROM baseimage as php-dependencies
# Build and install the GEOS PHP extension.
# See https://git.osgeo.org/gitea/geos/php-geos
ARG PHP_GEOS_VERSION=e77d5a16abbf89a59d947d1fe49381a944762c9d
ADD https://github.com/libgeos/php-geos/archive/${PHP_GEOS_VERSION}.tar.gz /opt/php-geos.tar.gz
@ -17,51 +24,127 @@ RUN apt-get update && apt-get install -y libgeos-dev \
# Install the BCMath PHP extension.
RUN docker-php-ext-install bcmath
# Inherit from the official Drupal 10 image.
FROM drupal:10.1
# Setup dependencies and sources for composer installations.
FROM baseimage as composer-file
# Set the farmOS and composer project repository URLs and versions.
ARG FARMOS_REPO=https://github.com/farmOS/farmOS.git
ARG FARMOS_VERSION=3.x
ARG PROJECT_REPO=https://github.com/farmOS/composer-project.git
ARG PROJECT_VERSION=3.x
ARG PROJECT_REPO=https://github.com/farmOS/composer-project/raw/${PROJECT_VERSION}/composer.json
# Set the COMPOSER_MEMORY_LIMIT environment variable to unlimited.
ENV COMPOSER_MEMORY_LIMIT=-1
# Allow root to install plugins.
ENV COMPOSER_ALLOW_SUPERUSER=1
ENV COMPOSER_NO_INTERACTION=1
# Install apt dependencies.
RUN apt-get update && apt-get install -y --no-install-recommends\
# Install git and unzip (needed by Composer).
git unzip
# Add the build-farmOS.sh script.
COPY build-farmOS.sh /usr/local/bin/
# Setup composer file for non interactive installation.
WORKDIR ${FARMOS_PATH}
RUN build-farmOS.sh
##
# Create layer with farmOS sources.
FROM composer-file as farmos-sources
# Declare the git commit hash argument used for build cache invalidation between
# different commits on the same branch in GitHub actions.
ARG GIT_COMMIT=unknown
# Install sources.
RUN composer install --no-dev
# Set the version in farm.info.yml.
RUN sed -i "s|version: 3.x|version: ${FARMOS_VERSION}|g" ${FARMOS_PATH}/web/profiles/farm/farm.info.yml
##
# Create layer with farmOS dev sources.
FROM farmos-sources as farmos-dev-sources
# Install sources.
RUN composer update
##
# Dependencies layer.
FROM baseimage as farmos-baseimage
# Set Apache ServerName directive globally to suppress AH00558 message.
RUN echo "ServerName localhost" >> /etc/apache2/apache2.conf
# Install and enable geos.
# Enable PHP dependencies.
COPY --from=php-dependencies /usr/local/lib/php/extensions/ /usr/local/lib/php/extensions/
RUN docker-php-ext-enable geos bcmath
# Add custom PHP configurations.
COPY conf.d/ /usr/local/etc/php/conf.d
# Install apt dependencies.
RUN apt-get update && apt-get install -y \
# Install git and unzip (needed by Composer).
git unzip \
# Install apt dependencies and clean up.
RUN apt-get update && apt-get install -y --no-install-recommends\
# Install postgresql-client so Drush can connect to the database.
postgresql-client \
# Install libgeos-c1v5 so geos php extension can use libgeos_c.so.1.
libgeos-c1v5 \
&& rm -rf /var/lib/apt/lists/* \
&& apt-get clean
# Set the COMPOSER_MEMORY_LIMIT environment variable to unlimited.
ENV COMPOSER_MEMORY_LIMIT=-1
# Add the build-farmOS.sh script.
COPY build-farmOS.sh /usr/local/bin/
RUN chmod a+x /usr/local/bin/build-farmOS.sh
# Build the farmOS codebase in /var/farmoS with the --no-dev flag.
# Change the ownership of the sites directory and copy the farmOS codebase into /opt/drupal.
RUN mkdir /var/farmOS \
&& /usr/local/bin/build-farmOS.sh --no-dev \
&& chown -R www-data:www-data /var/farmOS/web/sites \
&& rm -r /opt/drupal && cp -rp /var/farmOS /opt/drupal
# Install git and unzip (needed by Composer).
git unzip \
&& rm -rf /var/lib/apt/lists/* \
&& apt-get clean \
# Clean up ${DRUPAL_PATH}.
&& rm -r ${DRUPAL_PATH} \
&& mkdir ${DRUPAL_PATH}
# Set the entrypoint.
COPY docker-entrypoint.sh /usr/local/bin/
COPY --chown=www-data docker-entrypoint.sh /usr/local/bin/
# Set the working directory, entrypoint and command.
WORKDIR ${DRUPAL_PATH}
ENTRYPOINT ["docker-entrypoint.sh"]
CMD ["apache2-foreground"]
##
# Development base image.
FROM farmos-baseimage as farmos-dev-baseimage
# Change the user/group IDs of www-data inside the image to match the ID of the
# developer's user on the host machine. This allows Composer to create files
# owned by www-data inside the container, while keeping those files editable by
# the developer outside of the container.
# This defaults to 1000, based on the assumption that the developer is running
# as UID 1000 on the host machine. It can be overridden at image build time with:
# --build-arg WWW_DATA_ID=$(id -u)
ARG WWW_DATA_ID=1000
RUN usermod -u ${WWW_DATA_ID} www-data && groupmod -g ${WWW_DATA_ID} www-data
# Install and configure XDebug.
RUN yes | pecl install xdebug \
&& docker-php-ext-enable xdebug
# Add opcache revalidation frequency configuration.
COPY dev/conf.d/ /usr/local/etc/php/conf.d
# Add Configurations for PHP CodeSniffer, PHPStan
COPY --chown=www-data:www-data ./dev/files/ ${FARMOS_PATH}
##
# Development image.
FROM farmos-dev-baseimage as dev
# Add farmOS dev sources.
COPY --from=farmos-dev-sources --chown=www-data:www-data ${FARMOS_PATH} ${FARMOS_PATH}
# Configure PHPUnit.
RUN ${FARMOS_PATH}/phpunit.sh
##
# Production image.
FROM farmos-baseimage
# Add farmOS sources.
COPY --from=farmos-sources --chown=www-data:www-data ${FARMOS_PATH} ${DRUPAL_PATH}

35
docker/build-farmOS.sh Normal file → Executable file
View File

@ -2,26 +2,18 @@
set -e
###
# This script will build the farmOS codebase in /var/farmOS.
# This script will build the farmOS codebase in ${FARMOS_PATH},
# by default it is /var/farmOS.
###
# If /var/farmOS is not empty, bail.
if [ "$(ls -A /var/farmOS/)" ]; then
# If ${FARMOS_PATH} is not empty, bail.
if [ "$(ls -A ${FARMOS_PATH})" ]; then
echo "The ${FARMOS_PATH} is not empty, terminate."
exit 1
fi
# Make /var/farmOS the working directory.
cd /var/farmOS
# Generate an empty Composer project project and checkout a specific version.
git clone ${PROJECT_REPO} project
mv project/.git ./.git
rm -rf project
git checkout ${PROJECT_VERSION}
git reset --hard
# Create a temporary Composer cache directory.
export COMPOSER_HOME="$(mktemp -d)"
# Fetch composer template
curl -L ${PROJECT_REPO} -o composer.json
# If FARMOS_VERSION is a valid semantic versioning string, we assume that it is
# a tagged version.
@ -61,16 +53,3 @@ allowedPlugins=(
for plugin in ${allowedPlugins[@]}; do
composer config --no-plugins allow-plugins.$plugin true
done
# Run composer install with optional arguments passed into this script.
if [ $# -eq 0 ]; then
composer install
else
composer install "$*"
fi
# Set the version in farm.info.yml.
sed -i "s|version: 3.x|version: ${FARMOS_VERSION}|g" /var/farmOS/web/profiles/farm/farm.info.yml
# Remove the Composer cache directory.
rm -rf "$COMPOSER_HOME"

View File

@ -1,63 +0,0 @@
# Inherit from the farmOS 3.x image.
FROM farmos/farmos:3.x
# Set the farmOS and composer project repository URLs and versions.
ARG FARMOS_REPO=https://github.com/farmOS/farmOS.git
ARG FARMOS_VERSION=3.x
ARG PROJECT_REPO=https://github.com/farmOS/composer-project.git
ARG PROJECT_VERSION=3.x
# Install and enable XDebug extension.
RUN yes | pecl install xdebug \
&& docker-php-ext-enable xdebug
# Add opcache revalidation frequency configuration.
COPY conf.d/ /usr/local/etc/php/conf.d
# Change the user/group IDs of www-data inside the image to match the ID of the
# developer's user on the host machine. This allows Composer to create files
# owned by www-data inside the container, while keeping those files editable by
# the developer outside of the container.
# This defaults to 1000, based on the assumption that the developer is running
# as UID 1000 on the host machine. It can be overridden at image build time with:
# --build-arg WWW_DATA_ID=$(id -u)
ARG WWW_DATA_ID=1000
RUN usermod -u ${WWW_DATA_ID} www-data && groupmod -g ${WWW_DATA_ID} www-data
# Create a fresh /var/farmOS directory owned by www-data.
# We do this in two steps because of a known issue with Moby.
# @see https://github.com/farmOS/farmOS/pull/440
RUN rm -r /var/farmOS
RUN mkdir /var/farmOS && chown www-data:www-data /var/farmOS
# Change to the www-data user.
USER www-data
# Build the farmOS codebase in /var/farmOS.
RUN /usr/local/bin/build-farmOS.sh
# Add Configurartions for PHP CodeSniffer, PHPStan.
COPY --chown=www-data ./files/ /var/farmOS/
# Configure PHPUnit.
RUN cp -p /var/farmOS/web/core/phpunit.xml.dist /var/farmOS/phpunit.xml \
&& sed -i 's|bootstrap="tests/bootstrap.php"|bootstrap="web/core/tests/bootstrap.php"|g' /var/farmOS/phpunit.xml \
&& sed -i '/failOnWarning="true"/a \ failOnIncomplete="true"' /var/farmOS/phpunit.xml \
&& sed -i '/failOnWarning="true"/a \ failOnSkipped="true"' /var/farmOS/phpunit.xml \
&& sed -i 's|name="SIMPLETEST_BASE_URL" value=""|name="SIMPLETEST_BASE_URL" value="http://www"|g' /var/farmOS/phpunit.xml \
&& sed -i 's|name="SIMPLETEST_DB" value=""|name="SIMPLETEST_DB" value="pgsql://farm:farm@db/farm"|g' /var/farmOS/phpunit.xml \
&& sed -i 's|name="BROWSERTEST_OUTPUT_DIRECTORY" value=""|name="BROWSERTEST_OUTPUT_DIRECTORY" value="/var/www/html/sites/simpletest/browser_output"|g' /var/farmOS/phpunit.xml \
&& sed -i 's|name="MINK_DRIVER_ARGS_WEBDRIVER" value='\'''\''|name="MINK_DRIVER_ARGS_WEBDRIVER" value='\''["chrome", { "chromeOptions": { "w3c": false, "args": ["--disable-gpu","--headless", "--no-sandbox"] } }, "http://chrome:4444/wd/hub"]'\''|g' /var/farmOS/phpunit.xml \
&& sed -i 's|\./|\./web/core/|g' /var/farmOS/phpunit.xml \
&& sed -i 's|\.\./web/core/|\./web/|g' /var/farmOS/phpunit.xml \
&& sed -i 's| </php>| <env name="SYMFONY_DEPRECATIONS_HELPER" value="disabled"/>'"\n"' </php>|g' /var/farmOS/phpunit.xml \
&& mkdir -p /var/farmOS/web/sites/simpletest/browser_output
# Change back to the root user.
USER root
# Copy the farmOS codebase into /opt/drupal.
RUN rm -r /opt/drupal && cp -rp /var/farmOS /opt/drupal
# Create a Composer config directory for the www-data user.
RUN mkdir /var/www/.composer && chown www-data:www-data /var/www/.composer

19
docker/dev/files/phpunit.sh Executable file
View File

@ -0,0 +1,19 @@
#!/usr/bin/env sh
cp -p ${FARMOS_PATH}/web/core/phpunit.xml.dist ${FARMOS_PATH}/phpunit.xml
sed -i 's|bootstrap="tests/bootstrap.php"|bootstrap="web/core/tests/bootstrap.php"|g' ${FARMOS_PATH}/phpunit.xml
sed -i '/failOnWarning="true"/a \ failOnIncomplete="true"' ${FARMOS_PATH}/phpunit.xml
sed -i '/failOnWarning="true"/a \ failOnSkipped="true"' ${FARMOS_PATH}/phpunit.xml
sed -i 's|name="SIMPLETEST_BASE_URL" value=""|name="SIMPLETEST_BASE_URL" value="http://www"|g' ${FARMOS_PATH}/phpunit.xml
sed -i 's|name="SIMPLETEST_DB" value=""|name="SIMPLETEST_DB" value="pgsql://farm:farm@db/farm"|g' ${FARMOS_PATH}/phpunit.xml
sed -i 's|name="BROWSERTEST_OUTPUT_DIRECTORY" value=""|name="BROWSERTEST_OUTPUT_DIRECTORY" value="/var/www/html/sites/simpletest/browser_output"|g' ${FARMOS_PATH}/phpunit.xml
sed -i 's|name="MINK_DRIVER_ARGS_WEBDRIVER" value='\'''\''|name="MINK_DRIVER_ARGS_WEBDRIVER" value='\''["chrome", { "chromeOptions": { "w3c": false, "args": ["--disable-gpu","--headless", "--no-sandbox"] } }, "http://chrome:4444/wd/hub"]'\''|g' ${FARMOS_PATH}/phpunit.xml
sed -i 's|\./|\./web/core/|g' ${FARMOS_PATH}/phpunit.xml
sed -i 's|\.\./web/core/|\./web/|g' ${FARMOS_PATH}/phpunit.xml
sed -i 's| </php>| <env name="SYMFONY_DEPRECATIONS_HELPER" value="disabled"/>'"\n"' </php>|g' ${FARMOS_PATH}/phpunit.xml
# Create output directory for phpunit tests and permissions for testing user.
mkdir -p ${FARMOS_PATH}/web/sites/simpletest/browser_output
chown -R www-data:www-data ${FARMOS_PATH}/web/sites/simpletest
rm ${FARMOS_PATH}/phpunit.sh

View File

@ -8,15 +8,15 @@ set -e
###
# If the Drupal directory is empty, populate it from pre-built files.
if [ -d /opt/drupal ] && ! [ "$(ls -A /opt/drupal/)" ]; then
if [ -d ${DRUPAL_PATH} ] && ! [ "$(ls -A ${DRUPAL_PATH}/)" ]; then
echo "farmOS codebase not detected. Copying from pre-built files in the Docker image."
cp -rp /var/farmOS/. /opt/drupal
cp -rp ${FARMOS_PATH}/. ${DRUPAL_PATH}
fi
# If the sites directory is empty, populate it from pre-built files.
if [ -d /opt/drupal/web/sites ] && ! [ "$(ls -A /opt/drupal/web/sites/)" ]; then
if [ -d ${DRUPAL_PATH}/web/sites ] && ! [ "$(ls -A ${DRUPAL_PATH}/web/sites/)" ]; then
echo "farmOS sites directory not detected. Copying from pre-built files in the Docker image."
cp -rp /var/farmOS/web/sites/. /opt/drupal/web/sites
cp -rp ${FARMOS_PATH}/web/sites/. ${DRUPAL_PATH}/web/sites
fi
if [ -n "$FARMOS_FS_READY_SENTINEL_FILENAME" ]; then

View File

@ -17,7 +17,9 @@ Available arguments and their default values are described below:
check out.
- Default: `3.x`
The `3.x-dev` image also provides the following:
## Development image
The `3.x-dev` image also provides the following build arguments:
- `WWW_DATA_ID` - The ID to use for the `www-data` user and group inside the
image. Setting this to the ID of the developer's user on the host machine
@ -26,3 +28,8 @@ The `3.x-dev` image also provides the following:
container. If your user ID is not `1000`, build the image with:
`--build-arg WWW_DATA_ID=$(id -u)`
- Default: `1000`
To build the development image, you will have to define the target dev,
for example:
`docker build --build-arg WWW_DATA_ID=$(id -u) -t farmos/farmos:3.x-dev --target dev docker`

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

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