diff --git a/docs/development/module/entities.md b/docs/development/module/entities.md index c380c7c1d0..2b7fb3b5b3 100644 --- a/docs/development/module/entities.md +++ b/docs/development/module/entities.md @@ -1,14 +1,15 @@ # Entity types -Assets, logs, plans, taxonomy terms, users, etc are all types of "entities" in -farmOS/Drupal terminology. Entities can have sub-types called "bundles", which -represent "bundles of fields". Some fields may be common across all bundles of -a given entity type, and some fields may be bundle-specific. +Assets, logs, plans, taxonomy terms, users, organizations, etc are all types of +"entities" in farmOS/Drupal terminology. Entities can have sub-types called +"bundles", which represent "bundles of fields". Some fields may be common +across all bundles of a given entity type, and some fields may be +bundle-specific. -## Adding asset, log, and plan types +## Adding asset, log, organization, and plan types -Asset types, log types, and plan types can be provided by adding two files to a -module: +Asset types, log types, organization types, and plan types can be provided by +adding two files to a module: 1. An entity type config file (YAML), and: 2. A bundle plugin class (PHP). @@ -54,6 +55,13 @@ class Activity extends FarmLogType { } ``` +For more examples, refer to the modules that come with farmOS in the +`modules/[type]/[bundle]` directories of the repository. Each bundle of each +type is declared in a separate module. Note that these modules may also contain +other features and logic related to the bundle, but only the two files described +above (in addition to the basic [module files](/development/module)) are +necessary for declaring a new bundle. + ## Bundle fields Bundles can declare field definitions in their plugin class via the diff --git a/docs/model/index.md b/docs/model/index.md index 97854f5571..46fd72d3e6 100644 --- a/docs/model/index.md +++ b/docs/model/index.md @@ -24,6 +24,7 @@ keeping data types are **Assets** and **Logs**. Other types include - [Terms](/model/type/term) - [Plans](/model/type/plan) - [Users](/model/type/user) +- [Organizations](/model/type/organization) ## Logic diff --git a/docs/model/type/asset.md b/docs/model/type/asset.md index ea3754e40b..e48539ffe8 100644 --- a/docs/model/type/asset.md +++ b/docs/model/type/asset.md @@ -215,8 +215,16 @@ Shapefiles, PDFs, CSVs, or other files associated with the Asset. Assets *may* contain additional relationships: +- Farm - Group membership +#### Farm + +Assets can specify which Farm they are associated with. + +This field is added to all Asset types by default only if the Farm organization +module is enabled. + #### Group membership The group membership of an Asset references Group Assets which the Asset is a diff --git a/docs/model/type/organization.md b/docs/model/type/organization.md new file mode 100644 index 0000000000..e8f64b2e92 --- /dev/null +++ b/docs/model/type/organization.md @@ -0,0 +1,97 @@ +# Organizations + +Organizations represent the legal or informal entities that other records are +associated with. A Farm is type of Organization. Modules can provide additional +Organization types. + +## Type + +Each Organization must have a type. All Organization types have a common set of +attributes and relationships. Specific Organization types may also add +additional attributes and relationships (collectively referred to as "fields"). +Organization types are defined by modules, and are only available if their +module is enabled. The modules included with farmOS define the following +Organization types: + +- Farm + +## ID + +Each Organization will be assigned two unique IDs in the database: a universally +unique identifier (UUID), and an internal numeric ID. + +The UUID will be unique **across** farmOS databases. The internal ID will only +be unique to a **single** farmOS database. Therefore, the farmOS API uses UUIDs +to ensure that IDs pulled from multiple farmOS databases do not conflict. +Internally, farmOS modules use the internal IDs to perform CRUD operations. + +## Attributes + +Organizations have a number of attributes that serve to describe their meta +information. All Organizations have the same standard set of attributes. Modules +can add additional attributes. + +### Standard attributes + +Attributes that are common to all Organization types include: + +- Name +- Status +- Notes +- Data + +#### Name + +Organizations must have a name that describes them. The name is used in lists of +Organizations to easily identify them at quick glance. + +#### Status + +Organizations can be marked as "active" or "archived" to indicate their status. +Archived Organizations will be hidden from most lists in farmOS unless they are +explicitly requested. + +#### Notes + +Notes can be added to an Organization to describe it in more detail. This is a +freeform text field that allows a limited set of HTML tags, including links, +lists, blockquotes, emphasis, etc. + +#### Data + +Organizations have a hidden "data" field on them that is only accessible via the +API. This provides a freeform plain text field that can be used to store +additional data in any format (eg: JSON, YAML, XML). One use case for this field +is to store remote system IDs that correspond to the Organization. So if the +Organization is created or managed by software outside of farmOS, it can be +identified easily. It can also be used to store additional structured metadata +that does not fit into the standard Asset attributes. + +## Relationships + +Organizations can be related to other records in farmOS. These relationships are +stored as reference fields on Organization records. + +All Organizations have the same standard set of relationships. Modules can add +additional relationships. + +Relationships that are common to all Organization types include: + +- Images +- Files +- Users + +#### Images + +Images can be attached to Organizations. This provides a place to store photos +of the Organization. + +#### Files + +Files can be attached to Organizations. This provides a place to put documents +such as Shapefiles, PDFs, CSVs, or other files associated with the Organization. + +#### Users + +Users can be assigned to Organizations. This provides a way to group users based +on the Organization(s) they are associated with. diff --git a/farm.profile b/farm.profile index 69e5b52952..d27a27778b 100644 --- a/farm.profile +++ b/farm.profile @@ -65,6 +65,7 @@ function farm_modules() { 'farm_comment_asset' => t('Asset comments'), 'farm_comment_log' => t('Log comments'), 'farm_comment_plan' => t('Plan comments'), + 'farm_farm' => t('Farm organizations'), 'farm_map_mapbox' => t('Mapbox map layers: Satellite, Outdoors'), 'farm_api_default_consumer' => t('Default API Consumer'), 'farm_fieldkit' => t('Field Kit integration'), diff --git a/modules/core/asset/asset.info.yml b/modules/core/asset/asset.info.yml index 82621f8b74..a37d66220e 100644 --- a/modules/core/asset/asset.info.yml +++ b/modules/core/asset/asset.info.yml @@ -4,7 +4,7 @@ type: module package: Asset core_version_requirement: ^10 dependencies: - - drupal:system (>=8.8.0) + - drupal:system - drupal:user - drupal:views - entity:entity diff --git a/modules/core/entity/farm_entity.api.php b/modules/core/entity/farm_entity.api.php index eb4ab192fa..7295e42646 100644 --- a/modules/core/entity/farm_entity.api.php +++ b/modules/core/entity/farm_entity.api.php @@ -17,7 +17,7 @@ */ /** - * Allows modules to add field definitions to asset, log, and plan bundles. + * Allows modules to add fields to asset, log, organization, and plan bundles. * * @todo https://www.drupal.org/project/farm/issues/3194206 * diff --git a/modules/core/entity/farm_entity.install b/modules/core/entity/farm_entity.install index 07db3dcad1..b41d03da5a 100644 --- a/modules/core/entity/farm_entity.install +++ b/modules/core/entity/farm_entity.install @@ -18,6 +18,7 @@ function farm_entity_install() { 'data_stream', 'file', 'log', + 'organization', 'plan', 'quantity', 'taxonomy_term', diff --git a/modules/core/entity/farm_entity.managed_role_permissions.yml b/modules/core/entity/farm_entity.managed_role_permissions.yml index d7f6df6523..5082bbbc71 100644 --- a/modules/core/entity/farm_entity.managed_role_permissions.yml +++ b/modules/core/entity/farm_entity.managed_role_permissions.yml @@ -3,5 +3,6 @@ farm_entity: - view asset_type - view data_stream_type - view log_type + - view organization_type - view plan_type - view quantity_type diff --git a/modules/core/entity/farm_entity.module b/modules/core/entity/farm_entity.module index 9ebe46e5b5..b6789c662d 100644 --- a/modules/core/entity/farm_entity.module +++ b/modules/core/entity/farm_entity.module @@ -57,7 +57,7 @@ function farm_entity_entity_type_build(array &$entity_types) { /** @var \Drupal\Core\Entity\EntityTypeInterface[] $entity_types */ // Allow the "view label" operation on the bundle entity type. - foreach (['asset', 'log', 'plan', 'quantity', 'data_stream'] as $entity_type) { + foreach (['asset', 'log', 'organization', 'plan', 'quantity', 'data_stream'] as $entity_type) { if (!empty($entity_types[$entity_type])) { $bundle_entity_type = $entity_types[$entity_type]->getBundleEntityType(); $entity_types[$bundle_entity_type]->setHandlerClass('access', EntityAccessControlHandler::class); @@ -66,7 +66,7 @@ function farm_entity_entity_type_build(array &$entity_types) { } // Enable the use of bundle plugins on specific entity types. - foreach (['asset', 'log', 'plan', 'plan_record', 'quantity'] as $entity_type) { + foreach (['asset', 'log', 'organization', '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); @@ -90,7 +90,7 @@ function farm_entity_entity_type_build(array &$entity_types) { function farm_entity_entity_field_storage_info_alter(&$fields, EntityTypeInterface $entity_type) { // Bail if not a farm entity type that allows bundle plugins. - if (!in_array($entity_type->id(), ['log', 'asset', 'plan', 'quantity'])) { + if (!in_array($entity_type->id(), ['log', 'asset', 'organization', 'plan', 'quantity'])) { return; } @@ -132,6 +132,7 @@ function farm_entity_entity_presave(EntityInterface $entity) { $entity_types = [ 'asset', 'log', + 'organization', 'plan', 'quantity', ]; @@ -182,6 +183,7 @@ function farm_entity_form_alter(&$form, FormStateInterface $form_state, $form_id $entity_types = [ 'asset', 'log', + 'organization', 'plan', 'quantity', ]; diff --git a/modules/core/entity/farm_entity.post_update.php b/modules/core/entity/farm_entity.post_update.php index 494dc5044a..25bb8706fa 100644 --- a/modules/core/entity/farm_entity.post_update.php +++ b/modules/core/entity/farm_entity.post_update.php @@ -24,3 +24,14 @@ function farm_entity_post_update_enforce_plan_eri(&$sandbox) { function farm_entity_post_update_rebuild_bundle_field_maps(&$sandbox = NULL) { \Drupal::service('entity_field.manager')->rebuildBundleFieldMap(); } + +/** + * Enforce entity reference integrity on organization reference fields. + */ +function farm_entity_post_update_enforce_organization_eri(&$sandbox) { + $config = \Drupal::configFactory()->getEditable('entity_reference_integrity_enforce.settings'); + $entity_types = $config->get('enabled_entity_type_ids'); + $entity_types['organization'] = 'organization'; + $config->set('enabled_entity_type_ids', $entity_types); + $config->save(); +} diff --git a/modules/core/entity/farm_entity.services.yml b/modules/core/entity/farm_entity.services.yml index 437e7ab895..0b81d6f7f4 100644 --- a/modules/core/entity/farm_entity.services.yml +++ b/modules/core/entity/farm_entity.services.yml @@ -9,6 +9,9 @@ services: plugin.manager.log_type: class: Drupal\farm_entity\LogTypeManager parent: default_plugin_manager + plugin.manager.organization_type: + class: Drupal\farm_entity\OrganizationTypeManager + parent: default_plugin_manager plugin.manager.plan_type: class: Drupal\farm_entity\PlanTypeManager parent: default_plugin_manager diff --git a/modules/core/entity/modules/fields/farm_entity_fields.base_fields.inc b/modules/core/entity/modules/fields/farm_entity_fields.base_fields.inc index 79fdfa1756..7e5e14cf6d 100644 --- a/modules/core/entity/modules/fields/farm_entity_fields.base_fields.inc +++ b/modules/core/entity/modules/fields/farm_entity_fields.base_fields.inc @@ -101,6 +101,64 @@ function farm_entity_fields_log_base_fields() { return $fields; } +/** + * Define common organization base fields. + */ +function farm_entity_fields_organization_base_fields() { + $field_info = [ + 'data' => [ + 'type' => 'string_long', + 'label' => t('Data'), + 'hidden' => TRUE, + ], + 'file' => [ + 'type' => 'file', + 'label' => t('Files'), + 'file_directory' => 'farm/organization/[date:custom:Y]-[date:custom:m]', + 'multiple' => TRUE, + 'weight' => [ + 'form' => 90, + 'view' => 90, + ], + ], + 'image' => [ + 'type' => 'image', + 'label' => t('Images'), + 'file_directory' => 'farm/organization/[date:custom:Y]-[date:custom:m]', + 'multiple' => TRUE, + 'weight' => [ + 'form' => 89, + 'view' => 89, + ], + ], + 'notes' => [ + 'type' => 'text_long', + 'label' => t('Notes'), + 'weight' => [ + 'form' => 95, + 'view' => 10, + ], + ], + 'user' => [ + 'type' => 'entity_reference', + 'label' => t('Users'), + 'target_type' => 'user', + 'multiple' => TRUE, + 'weight' => [ + 'form' => 100, + 'view' => 100, + ], + ], + ]; + /** @var \Drupal\Core\Field\BaseFieldDefinition[] $fields */ + $fields = []; + foreach ($field_info as $name => $info) { + $fields[$name] = \Drupal::service('farm_field.factory')->baseFieldDefinition($info); + } + + return $fields; +} + /** * Define common plan base fields. */ diff --git a/modules/core/entity/modules/fields/farm_entity_fields.module b/modules/core/entity/modules/fields/farm_entity_fields.module index a267b2b098..ac7195f4ec 100644 --- a/modules/core/entity/modules/fields/farm_entity_fields.module +++ b/modules/core/entity/modules/fields/farm_entity_fields.module @@ -27,6 +27,11 @@ function farm_entity_fields_entity_base_field_info(EntityTypeInterface $entity_t return farm_entity_fields_log_base_fields(); } + // Add common base fields to all organization types. + elseif ($entity_type->id() == 'organization') { + return farm_entity_fields_organization_base_fields(); + } + // Add common base fields to all plan types. elseif ($entity_type->id() == 'plan') { return farm_entity_fields_plan_base_fields(); @@ -45,8 +50,8 @@ function farm_entity_fields_entity_base_field_info(EntityTypeInterface $entity_t */ function farm_entity_fields_entity_base_field_info_alter(&$fields, EntityTypeInterface $entity_type) { - // Only alter asset, log, and plan fields. - if (!in_array($entity_type->id(), ['asset', 'log', 'plan'])) { + // Only alter asset, log, organization, and plan fields. + if (!in_array($entity_type->id(), ['asset', 'log', 'organization', 'plan'])) { return; } diff --git a/modules/core/entity/modules/views/farm_entity_views.module b/modules/core/entity/modules/views/farm_entity_views.module index 35e9ffcd14..54f9571264 100644 --- a/modules/core/entity/modules/views/farm_entity_views.module +++ b/modules/core/entity/modules/views/farm_entity_views.module @@ -46,7 +46,7 @@ function farm_entity_views_entity_type_build(array &$entity_types) { /** @var \Drupal\Core\Entity\EntityTypeInterface[] $entity_types */ // Set the views data handler class to FarmEntityViewsData. - foreach (['asset', 'log', 'plan', 'plan_record', 'quantity'] as $entity_type) { + foreach (['asset', 'log', 'organization', 'plan', 'plan_record', 'quantity'] as $entity_type) { if (!empty($entity_types[$entity_type])) { // Use the correct class for each entity type. diff --git a/modules/core/entity/modules/views/farm_entity_views.views.inc b/modules/core/entity/modules/views/farm_entity_views.views.inc index ff92788202..681bda09e8 100644 --- a/modules/core/entity/modules/views/farm_entity_views.views.inc +++ b/modules/core/entity/modules/views/farm_entity_views.views.inc @@ -26,6 +26,8 @@ function farm_entity_views_views_data_alter(array &$data) { 'asset_field_revision', 'log_field_data', 'log_field_revision', + 'organization_field_data', + 'organization_field_revision', 'plan_field_data', 'plan_field_revision', ]; diff --git a/modules/core/entity/src/Annotation/OrganizationType.php b/modules/core/entity/src/Annotation/OrganizationType.php new file mode 100644 index 0000000000..6e1294732c --- /dev/null +++ b/modules/core/entity/src/Annotation/OrganizationType.php @@ -0,0 +1,36 @@ +alterInfo('organization_type_info'); + $this->setCacheBackend($cache_backend, 'organization_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 organization type %s must define the %s property.', $plugin_id, $required_property)); + } + } + } + +} diff --git a/modules/core/entity/src/Plugin/Organization/OrganizationType/FarmOrganizationType.php b/modules/core/entity/src/Plugin/Organization/OrganizationType/FarmOrganizationType.php new file mode 100644 index 0000000000..42544b8c09 --- /dev/null +++ b/modules/core/entity/src/Plugin/Organization/OrganizationType/FarmOrganizationType.php @@ -0,0 +1,16 @@ +pluginDefinition['label']; + } + + /** + * {@inheritdoc} + */ + public function getWorkflowId() { + return $this->pluginDefinition['workflow']; + } + + /** + * {@inheritdoc} + */ + public function buildFieldDefinitions() { + return []; + } + +} diff --git a/modules/core/entity/src/Plugin/Organization/OrganizationType/OrganizationTypeInterface.php b/modules/core/entity/src/Plugin/Organization/OrganizationType/OrganizationTypeInterface.php new file mode 100644 index 0000000000..37fce14eda --- /dev/null +++ b/modules/core/entity/src/Plugin/Organization/OrganizationType/OrganizationTypeInterface.php @@ -0,0 +1,30 @@ +assertArrayHasKey($field_name, $fields, "The plan $field_name field exists."); } + // Test organization field storage definitions. + $fields = $this->entityFieldManager->getFieldStorageDefinitions('organization'); + $field_names = [ + 'data', + 'file', + 'image', + 'notes', + 'user', + ]; + foreach ($field_names as $field_name) { + $this->assertArrayHasKey($field_name, $fields, "The organization $field_name field exists."); + } + // Test taxonomy term field storage definitions. $fields = $this->entityFieldManager->getFieldStorageDefinitions('taxonomy_term'); $field_names = [ diff --git a/modules/core/field/src/FarmFieldFactory.php b/modules/core/field/src/FarmFieldFactory.php index 3654f99092..303418950a 100644 --- a/modules/core/field/src/FarmFieldFactory.php +++ b/modules/core/field/src/FarmFieldFactory.php @@ -467,6 +467,32 @@ protected function modifyEntityReferenceField(BaseFieldDefinition &$field, array ]; break; + // Organization reference. + case 'organization': + $handler = 'default:organization'; + $handler_settings = [ + 'target_bundles' => NULL, + 'sort' => [ + 'field' => 'name', + 'direction' => 'asc', + ], + 'auto_create' => FALSE, + 'auto_create_bundle' => '', + ]; + $form_display_options = [ + 'type' => 'options_select', + 'weight' => $options['weight']['form'] ?? 0, + ]; + $view_display_options = [ + 'label' => 'inline', + 'type' => 'entity_reference_label', + 'weight' => $options['weight']['view'] ?? 0, + 'settings' => [ + 'link' => TRUE, + ], + ]; + break; + // Data stream reference. case 'data_stream': $handler = 'default:data_stream'; diff --git a/modules/core/organization/config/optional/system.action.organization_activate_action.yml b/modules/core/organization/config/optional/system.action.organization_activate_action.yml new file mode 100644 index 0000000000..01ac9e51c4 --- /dev/null +++ b/modules/core/organization/config/optional/system.action.organization_activate_action.yml @@ -0,0 +1,10 @@ +langcode: en +status: true +dependencies: + module: + - organization +id: organization_activate_action +label: 'Unarchive organization' +type: organization +plugin: 'organization_activate_action' +configuration: { } diff --git a/modules/core/organization/config/optional/system.action.organization_archive_action.yml b/modules/core/organization/config/optional/system.action.organization_archive_action.yml new file mode 100644 index 0000000000..415f84818a --- /dev/null +++ b/modules/core/organization/config/optional/system.action.organization_archive_action.yml @@ -0,0 +1,10 @@ +langcode: en +status: true +dependencies: + module: + - organization +id: organization_archive_action +label: 'Archive organization' +type: organization +plugin: 'organization_archive_action' +configuration: { } diff --git a/modules/core/organization/config/optional/system.action.organization_clone_action.yml b/modules/core/organization/config/optional/system.action.organization_clone_action.yml new file mode 100644 index 0000000000..31dc56695a --- /dev/null +++ b/modules/core/organization/config/optional/system.action.organization_clone_action.yml @@ -0,0 +1,10 @@ +langcode: en +status: true +dependencies: + module: + - organization +id: organization_clone_action +label: 'Clone organization' +type: organization +plugin: 'organization_clone_action' +configuration: { } diff --git a/modules/core/organization/config/optional/system.action.organization_delete_action.yml b/modules/core/organization/config/optional/system.action.organization_delete_action.yml new file mode 100644 index 0000000000..76c58f40da --- /dev/null +++ b/modules/core/organization/config/optional/system.action.organization_delete_action.yml @@ -0,0 +1,10 @@ +langcode: en +status: true +dependencies: + module: + - organization +id: organization_delete_action +label: 'Delete organization' +type: organization +plugin: entity:delete_action:organization +configuration: { } diff --git a/modules/core/organization/config/optional/views.view.organization_admin.yml b/modules/core/organization/config/optional/views.view.organization_admin.yml new file mode 100644 index 0000000000..bf939b644f --- /dev/null +++ b/modules/core/organization/config/optional/views.view.organization_admin.yml @@ -0,0 +1,729 @@ +langcode: en +status: true +dependencies: + module: + - organization + - options + - user +id: organization_admin +label: 'Organization admin' +module: views +description: 'Find and manage organization entities.' +tag: default +base_table: organization_field_data +base_field: id +display: + default: + id: default + display_title: Master + display_plugin: default + position: 0 + display_options: + title: Organizations + fields: + organization_bulk_form: + id: organization_bulk_form + table: organization + field: organization_bulk_form + relationship: none + group_type: group + admin_label: '' + entity_type: organization + plugin_id: bulk_form + label: 'Bulk update' + exclude: false + alter: + alter_text: false + text: '' + make_link: false + path: '' + absolute: false + external: false + replace_spaces: false + path_case: none + trim_whitespace: false + alt: '' + rel: '' + link_class: '' + prefix: '' + suffix: '' + target: '' + nl2br: false + max_length: 0 + word_boundary: true + ellipsis: true + more_link: false + more_link_text: '' + more_link_path: '' + strip_tags: false + trim: false + preserve_tags: '' + html: false + element_type: '' + element_class: '' + element_label_type: '' + element_label_class: '' + element_label_colon: true + element_wrapper_type: '' + element_wrapper_class: '' + element_default_classes: true + empty: '' + hide_empty: false + empty_zero: false + hide_alter_empty: true + action_title: 'With selection' + include_exclude: exclude + selected_actions: { } + status: + id: status + table: organization_field_data + field: status + relationship: none + group_type: group + admin_label: '' + entity_type: organization + entity_field: status + plugin_id: field + label: Status + exclude: false + alter: + alter_text: false + text: '' + make_link: false + path: '' + absolute: false + external: false + replace_spaces: false + path_case: none + trim_whitespace: false + alt: '' + rel: '' + link_class: '' + prefix: '' + suffix: '' + target: '' + nl2br: false + max_length: 0 + word_boundary: true + ellipsis: true + more_link: false + more_link_text: '' + more_link_path: '' + strip_tags: false + trim: false + preserve_tags: '' + html: false + element_type: '' + element_class: '' + element_label_type: '' + element_label_class: '' + element_label_colon: true + element_wrapper_type: '' + element_wrapper_class: '' + element_default_classes: true + empty: '' + hide_empty: false + empty_zero: false + hide_alter_empty: true + click_sort_column: value + type: list_default + settings: { } + group_column: value + group_columns: { } + group_rows: true + delta_limit: 0 + delta_offset: 0 + delta_reversed: false + delta_first_last: false + multi_type: separator + separator: ', ' + field_api_classes: false + id: + id: id + table: organization_field_data + field: id + relationship: none + group_type: group + admin_label: '' + entity_type: organization + entity_field: id + plugin_id: field + label: ID + exclude: false + alter: + alter_text: false + text: '' + make_link: false + path: '' + absolute: false + external: false + replace_spaces: false + path_case: none + trim_whitespace: false + alt: '' + rel: '' + link_class: '' + prefix: '' + suffix: '' + target: '' + nl2br: false + max_length: 0 + word_boundary: true + ellipsis: true + more_link: false + more_link_text: '' + more_link_path: '' + strip_tags: false + trim: false + preserve_tags: '' + html: false + element_type: '' + element_class: '' + element_label_type: '' + element_label_class: '' + element_label_colon: true + element_wrapper_type: '' + element_wrapper_class: '' + element_default_classes: true + empty: '' + hide_empty: false + empty_zero: false + hide_alter_empty: true + click_sort_column: value + type: number_integer + settings: + thousand_separator: '' + prefix_suffix: true + group_column: value + group_columns: { } + group_rows: true + delta_limit: 0 + delta_offset: 0 + delta_reversed: false + delta_first_last: false + multi_type: separator + separator: ', ' + field_api_classes: false + name: + id: name + table: organization_field_data + field: name + relationship: none + group_type: group + admin_label: '' + entity_type: organization + entity_field: name + plugin_id: field + label: Name + exclude: false + alter: + alter_text: false + text: '' + make_link: false + path: '' + absolute: false + external: false + replace_spaces: false + path_case: none + trim_whitespace: false + alt: '' + rel: '' + link_class: '' + prefix: '' + suffix: '' + target: '' + nl2br: false + max_length: 0 + word_boundary: true + ellipsis: true + more_link: false + more_link_text: '' + more_link_path: '' + strip_tags: false + trim: false + preserve_tags: '' + html: false + element_type: '' + element_class: '' + element_label_type: '' + element_label_class: '' + element_label_colon: true + element_wrapper_type: '' + element_wrapper_class: '' + element_default_classes: true + empty: '' + hide_empty: false + empty_zero: false + hide_alter_empty: true + click_sort_column: value + type: string + settings: + link_to_entity: true + group_column: value + group_columns: { } + group_rows: true + delta_limit: 0 + delta_offset: 0 + delta_reversed: false + delta_first_last: false + multi_type: separator + separator: ', ' + field_api_classes: false + type: + id: type + table: organization_field_data + field: type + relationship: none + group_type: group + admin_label: '' + entity_type: organization + entity_field: type + plugin_id: field + label: 'Organization type' + exclude: false + alter: + alter_text: false + text: '' + make_link: false + path: '' + absolute: false + external: false + replace_spaces: false + path_case: none + trim_whitespace: false + alt: '' + rel: '' + link_class: '' + prefix: '' + suffix: '' + target: '' + nl2br: false + max_length: 0 + word_boundary: true + ellipsis: true + more_link: false + more_link_text: '' + more_link_path: '' + strip_tags: false + trim: false + preserve_tags: '' + html: false + element_type: '' + element_class: '' + element_label_type: '' + element_label_class: '' + element_label_colon: true + element_wrapper_type: '' + element_wrapper_class: '' + element_default_classes: true + empty: '' + hide_empty: false + empty_zero: false + hide_alter_empty: true + click_sort_column: target_id + type: entity_reference_label + settings: + link: true + group_column: target_id + group_columns: { } + group_rows: true + delta_limit: 0 + delta_offset: 0 + delta_reversed: false + delta_first_last: false + multi_type: separator + separator: ', ' + field_api_classes: false + uid: + id: uid + table: organization_field_data + field: uid + relationship: none + group_type: group + admin_label: '' + entity_type: organization + entity_field: uid + plugin_id: field + label: 'Authored by' + exclude: false + alter: + alter_text: false + text: '' + make_link: false + path: '' + absolute: false + external: false + replace_spaces: false + path_case: none + trim_whitespace: false + alt: '' + rel: '' + link_class: '' + prefix: '' + suffix: '' + target: '' + nl2br: false + max_length: 0 + word_boundary: true + ellipsis: true + more_link: false + more_link_text: '' + more_link_path: '' + strip_tags: false + trim: false + preserve_tags: '' + html: false + element_type: '' + element_class: '' + element_label_type: '' + element_label_class: '' + element_label_colon: true + element_wrapper_type: '' + element_wrapper_class: '' + element_default_classes: true + empty: '' + hide_empty: false + empty_zero: false + hide_alter_empty: true + click_sort_column: target_id + type: entity_reference_label + settings: + link: true + group_column: target_id + group_columns: { } + group_rows: true + delta_limit: 0 + delta_offset: 0 + delta_reversed: false + delta_first_last: false + multi_type: separator + separator: ', ' + field_api_classes: false + operations: + id: operations + table: organization + field: operations + relationship: none + group_type: group + admin_label: '' + entity_type: null + entity_field: null + plugin_id: entity_operations + label: 'Operations links' + exclude: false + alter: + alter_text: false + text: '' + make_link: false + path: '' + absolute: false + external: false + replace_spaces: false + path_case: none + trim_whitespace: false + alt: '' + rel: '' + link_class: '' + prefix: '' + suffix: '' + target: '' + nl2br: false + max_length: 0 + word_boundary: true + ellipsis: true + more_link: false + more_link_text: '' + more_link_path: '' + strip_tags: false + trim: false + preserve_tags: '' + html: false + element_type: '' + element_class: '' + element_label_type: '' + element_label_class: '' + element_label_colon: true + element_wrapper_type: '' + element_wrapper_class: '' + element_default_classes: true + empty: '' + hide_empty: false + empty_zero: false + hide_alter_empty: true + destination: true + pager: + type: full + options: + offset: 0 + items_per_page: 25 + total_pages: null + id: 0 + tags: + next: 'Next ›' + previous: '‹ Previous' + first: '« First' + last: 'Last »' + expose: + items_per_page: false + items_per_page_label: 'Items per page' + items_per_page_options: '5, 10, 25, 50' + items_per_page_options_all: false + items_per_page_options_all_label: '- All -' + offset: false + offset_label: Offset + quantity: 9 + exposed_form: + type: basic + options: + submit_button: Filter + reset_button: false + reset_button_label: Reset + exposed_sorts_label: 'Sort by' + expose_sort_order: true + sort_asc_label: Asc + sort_desc_label: Desc + access: + type: perm + options: + perm: 'administer organizations' + cache: + type: tag + options: { } + empty: + area_text_custom: + id: area_text_custom + table: views + field: area_text_custom + relationship: none + group_type: group + admin_label: '' + plugin_id: text_custom + empty: true + content: 'No organizations available.' + tokenize: false + sorts: + created: + id: created + table: organization_field_data + field: created + relationship: none + group_type: group + admin_label: '' + entity_type: organization + entity_field: created + plugin_id: date + order: ASC + expose: + label: '' + field_identifier: created + exposed: false + granularity: second + arguments: { } + filters: + name: + id: name + table: organization_field_data + field: name + relationship: none + group_type: group + admin_label: '' + entity_type: organization + entity_field: name + plugin_id: string + operator: contains + value: '' + group: 1 + exposed: true + expose: + operator_id: name_op + label: Name + description: '' + use_operator: false + operator: name_op + operator_limit_selection: false + operator_list: { } + identifier: name + required: false + remember: false + multiple: false + remember_roles: + authenticated: authenticated + placeholder: '' + is_grouped: false + group_info: + label: '' + description: '' + identifier: '' + optional: true + widget: select + multiple: false + remember: false + default_group: All + default_group_multiple: { } + group_items: { } + type: + id: type + table: organization_field_data + field: type + relationship: none + group_type: group + admin_label: '' + entity_type: organization + entity_field: type + plugin_id: bundle + operator: in + value: { } + group: 1 + exposed: true + expose: + operator_id: type_op + label: 'Organization type' + description: '' + use_operator: false + operator: type_op + operator_limit_selection: false + operator_list: { } + identifier: type + required: false + remember: false + multiple: false + remember_roles: + authenticated: authenticated + reduce: false + is_grouped: false + group_info: + label: '' + description: '' + identifier: '' + optional: true + widget: select + multiple: false + remember: false + default_group: All + default_group_multiple: { } + group_items: { } + filter_groups: + operator: AND + groups: + 1: AND + style: + type: table + options: + grouping: { } + row_class: '' + default_row_class: true + columns: + organization_bulk_form: organization_bulk_form + status: status + id: id + name: name + type: type + uid: uid + operations: operations + default: '-1' + info: + organization_bulk_form: + align: '' + separator: '' + empty_column: false + responsive: '' + status: + sortable: true + default_sort_order: asc + align: '' + separator: '' + empty_column: false + responsive: '' + id: + sortable: true + default_sort_order: asc + align: '' + separator: '' + empty_column: false + responsive: '' + name: + sortable: true + default_sort_order: asc + align: '' + separator: '' + empty_column: false + responsive: '' + type: + sortable: true + default_sort_order: asc + align: '' + separator: '' + empty_column: false + responsive: '' + uid: + sortable: false + default_sort_order: asc + align: '' + separator: '' + empty_column: false + responsive: '' + operations: + align: '' + separator: '' + empty_column: false + responsive: '' + override: true + sticky: false + summary: '' + empty_table: true + caption: '' + description: '' + row: + type: fields + query: + type: views_query + options: + query_comment: '' + disable_sql_rewrite: false + distinct: false + replica: false + query_tags: { } + relationships: { } + header: { } + footer: { } + display_extenders: { } + cache_metadata: + max-age: 0 + contexts: + - 'languages:language_content' + - 'languages:language_interface' + - url + - url.query_args + - user.permissions + tags: { } + collection: + id: collection + display_title: Page + display_plugin: page + position: 1 + display_options: + display_extenders: { } + path: admin/content/organization + menu: + type: tab + title: Organizations + description: '' + weight: 0 + expanded: false + menu_name: admin + parent: '' + context: '0' + tab_options: + type: normal + title: organization + description: '' + weight: 0 + cache_metadata: + max-age: 0 + contexts: + - 'languages:language_content' + - 'languages:language_interface' + - url + - url.query_args + - user.permissions + tags: { } diff --git a/modules/core/organization/config/schema/organization.schema.yml b/modules/core/organization/config/schema/organization.schema.yml new file mode 100644 index 0000000000..d2a805ff10 --- /dev/null +++ b/modules/core/organization/config/schema/organization.schema.yml @@ -0,0 +1,40 @@ +# Schema for the configuration files of the organization module. +organization.type.*: + type: config_entity + label: 'Organization type' + mapping: + id: + type: string + label: 'Machine-readable name' + label: + type: label + label: 'Type' + description: + type: text + label: 'Description' + workflow: + type: string + label: 'Workflow' + new_revision: + type: boolean + label: 'Create new revision' + +condition.plugin.organization_type: + type: condition.plugin + mapping: + bundles: + type: sequence + sequence: + type: string + +action.configuration.organization_activate_action: + type: action_configuration_default + label: 'Configuration for the organization activate action' + +action.configuration.organization_archive_action: + type: action_configuration_default + label: 'Configuration for the organization archive action' + +action.configuration.organization_clone_action: + type: action_configuration_default + label: 'Configuration for the organization clone action' diff --git a/modules/core/organization/organization.info.yml b/modules/core/organization/organization.info.yml new file mode 100644 index 0000000000..fa94ee8774 --- /dev/null +++ b/modules/core/organization/organization.info.yml @@ -0,0 +1,11 @@ +name: Organization +description: Provides an organization entity type for real world record keeping. +type: module +package: Organization +core_version_requirement: ^10 +dependencies: + - drupal:system + - drupal:user + - drupal:views + - entity:entity + - state_machine:state_machine diff --git a/modules/core/organization/organization.links.action.yml b/modules/core/organization/organization.links.action.yml new file mode 100644 index 0000000000..25376490fa --- /dev/null +++ b/modules/core/organization/organization.links.action.yml @@ -0,0 +1,11 @@ +entity.organization.add_page: + route_name: 'entity.organization.add_page' + title: 'Add organization' + appears_on: + - entity.organization.collection + +entity.organization_type.add_form: + route_name: 'entity.organization_type.add_form' + title: 'Add organization type' + appears_on: + - entity.organization_type.collection diff --git a/modules/core/organization/organization.links.menu.yml b/modules/core/organization/organization.links.menu.yml new file mode 100644 index 0000000000..17e1aec7b7 --- /dev/null +++ b/modules/core/organization/organization.links.menu.yml @@ -0,0 +1,5 @@ +entity.organization_type.collection: + title: 'Organization types' + route_name: entity.organization_type.collection + description: 'List of organization types' + parent: system.admin_structure diff --git a/modules/core/organization/organization.links.task.yml b/modules/core/organization/organization.links.task.yml new file mode 100644 index 0000000000..bd707d7080 --- /dev/null +++ b/modules/core/organization/organization.links.task.yml @@ -0,0 +1,4 @@ +entity.organization.collection: + route_name: entity.organization.collection + title: 'Organizations' + base_route: system.admin_content diff --git a/modules/core/organization/organization.module b/modules/core/organization/organization.module new file mode 100644 index 0000000000..c4f8bee041 --- /dev/null +++ b/modules/core/organization/organization.module @@ -0,0 +1,126 @@ +' . t('About') . ''; + $output .= '
' . t('Provides organization entity') . '
'; + } + + return $output; +} + +/** + * Implements hook_ENTITY_TYPE_presave(). + */ +function organization_organization_presave(OrganizationInterface $organization) { + + // Dispatch an event on organization presave. + // @todo Replace this with core event via https://www.drupal.org/node/2551893. + $event = new OrganizationEvent($organization); + $event_dispatcher = \Drupal::service('event_dispatcher'); + $event_dispatcher->dispatch($event, OrganizationEvent::PRESAVE); +} + +/** + * Implements hook_ENTITY_TYPE_insert(). + */ +function organization_organization_insert(OrganizationInterface $organization) { + + // Dispatch an event on organization insert. + // @todo Replace this with core event via https://www.drupal.org/node/2551893. + $event = new OrganizationEvent($organization); + $event_dispatcher = \Drupal::service('event_dispatcher'); + $event_dispatcher->dispatch($event, OrganizationEvent::INSERT); +} + +/** + * Implements hook_ENTITY_TYPE_update(). + */ +function organization_organization_update(OrganizationInterface $organization) { + + // Dispatch an event on organization update. + // @todo Replace this with core event via https://www.drupal.org/node/2551893. + $event = new OrganizationEvent($organization); + $event_dispatcher = \Drupal::service('event_dispatcher'); + $event_dispatcher->dispatch($event, OrganizationEvent::UPDATE); +} + +/** + * Implements hook_ENTITY_TYPE_delete(). + */ +function organization_organization_delete(OrganizationInterface $organization) { + + // Dispatch an event on organization delete. + // @todo Replace this with core event via https://www.drupal.org/node/2551893. + $event = new OrganizationEvent($organization); + $event_dispatcher = \Drupal::service('event_dispatcher'); + $event_dispatcher->dispatch($event, OrganizationEvent::DELETE); +} + +/** + * Implements hook_theme(). + */ +function organization_theme() { + return [ + 'organization' => [ + 'render element' => 'elements', + ], + ]; +} + +/** + * Implements hook_theme_suggestions_HOOK(). + */ +function organization_theme_suggestions_organization(array $variables) { + $suggestions = []; + $organization = $variables['elements']['#organization']; + $sanitized_view_mode = strtr($variables['elements']['#view_mode'], '.', '_'); + + $suggestions[] = 'organization__' . $sanitized_view_mode; + $suggestions[] = 'organization__' . $organization->bundle(); + $suggestions[] = 'organization__' . $organization->bundle() . '__' . $sanitized_view_mode; + $suggestions[] = 'organization__' . $organization->id(); + $suggestions[] = 'organization__' . $organization->id() . '__' . $sanitized_view_mode; + + return $suggestions; +} + +/** + * Prepares variables for organization templates. + * + * Default template: organization.html.twig. + * + * @param array $variables + * An associative array containing: + * - elements: An associative array containing the organization information + * and any fields attached to the organization. Properties used: + * - #organization: A \Drupal\organization\Entity\Organization object. The + * organization entity. + * - attributes: HTML attributes for the containing element. + */ +function template_preprocess_organization(array &$variables) { + $variables['organization'] = $variables['elements']['#organization']; + // Helpful $content variable for templates. + foreach (Element::children($variables['elements']) as $key) { + $variables['content'][$key] = $variables['elements'][$key]; + } +} diff --git a/modules/core/organization/organization.permissions.yml b/modules/core/organization/organization.permissions.yml new file mode 100644 index 0000000000..720a29e109 --- /dev/null +++ b/modules/core/organization/organization.permissions.yml @@ -0,0 +1,19 @@ +administer organizations: + title: 'Administer organizations' + description: 'Admin access to all organization entities.' + restrict access: true + +administer organization types: + title: 'Administer organization types' + description: 'Maintain the types of content available and the fields that are associated with those types.' + restrict access: true + +view all organization revisions: + title: 'View all organization revisions' + description: 'Allow viewing organization entity revisions.' + restrict access: true + +revert all organization revisions: + title: 'Revert all organization revisions' + description: 'Allow reverting to a previous organization entity revision.' + restrict access: true diff --git a/modules/core/organization/organization.workflow_groups.yml b/modules/core/organization/organization.workflow_groups.yml new file mode 100644 index 0000000000..b979e62a3a --- /dev/null +++ b/modules/core/organization/organization.workflow_groups.yml @@ -0,0 +1,3 @@ +organization: + label: organization + entity_type: organization diff --git a/modules/core/organization/organization.workflows.yml b/modules/core/organization/organization.workflows.yml new file mode 100644 index 0000000000..43c2c44816 --- /dev/null +++ b/modules/core/organization/organization.workflows.yml @@ -0,0 +1,18 @@ +organization_default: + id: organization_default + group: organization + label: 'Default' + states: + active: + label: Active + archived: + label: Archived + transitions: + archive: + label: 'Archive' + from: [active] + to: archived + to_active: + label: 'Make active' + from: [archived] + to: active diff --git a/modules/core/organization/src/Entity/Organization.php b/modules/core/organization/src/Entity/Organization.php new file mode 100644 index 0000000000..5fdd444b43 --- /dev/null +++ b/modules/core/organization/src/Entity/Organization.php @@ -0,0 +1,287 @@ +getName(); + } + + /** + * {@inheritdoc} + */ + public function getName() { + return $this->get('name')->value; + } + + /** + * {@inheritdoc} + */ + public function setName($name) { + $this->set('name', $name); + return $this; + } + + /** + * {@inheritdoc} + */ + public function getCreatedTime() { + return $this->get('created')->value; + } + + /** + * {@inheritdoc} + */ + public function setCreatedTime($timestamp) { + $this->set('created', $timestamp); + return $this; + } + + /** + * {@inheritdoc} + */ + public function getArchivedTime() { + return $this->get('archived')->value; + } + + /** + * {@inheritdoc} + */ + public function setArchivedTime($timestamp) { + $this->set('archived', $timestamp); + return $this; + } + + /** + * {@inheritdoc} + */ + public function getBundleLabel() { + /** @var \Drupal\organization\Entity\OrganizationTypeInterface $type */ + $type = $this->entityTypeManager() + ->getStorage('organization_type') + ->load($this->bundle()); + return $type->label(); + } + + /** + * {@inheritdoc} + */ + public static function getCurrentUserId() { + return [\Drupal::currentUser()->id()]; + } + + /** + * {@inheritdoc} + */ + public static function getRequestTime() { + return \Drupal::time()->getRequestTime(); + } + + /** + * {@inheritdoc} + */ + public static function baseFieldDefinitions(EntityTypeInterface $entity_type) { + $fields = parent::baseFieldDefinitions($entity_type); + $fields += static::ownerBaseFieldDefinitions($entity_type); + $fields += static::revisionLogBaseFieldDefinitions($entity_type); + + $fields['name'] = BaseFieldDefinition::create('string') + ->setLabel(t('Name')) + ->setDescription(t('The name of the organization.')) + ->setRevisionable(TRUE) + ->setTranslatable(TRUE) + ->setRequired(TRUE) + ->setSetting('max_length', 255) + ->setSetting('text_processing', 0) + ->setDisplayOptions('view', [ + 'label' => 'hidden', + 'type' => 'string', + 'weight' => -5, + ]) + ->setDisplayOptions('form', [ + 'type' => 'string_textfield', + 'weight' => -5, + ]) + ->setDisplayConfigurable('form', TRUE); + + $fields['status'] = BaseFieldDefinition::create('state') + ->setLabel(t('Status')) + ->setDescription(t('Indicates the status of the organization.')) + ->setRevisionable(TRUE) + ->setRequired(TRUE) + ->setSetting('max_length', 255) + ->setDisplayOptions('view', [ + 'label' => 'hidden', + 'type' => 'state_transition_form', + 'weight' => 10, + ]) + ->setDisplayOptions('form', [ + 'type' => 'options_select', + 'weight' => 11, + ]) + ->setDisplayConfigurable('form', TRUE) + ->setDisplayConfigurable('view', TRUE) + ->setSetting('workflow_callback', ['\Drupal\organization\Entity\Organization', 'getWorkflowId']); + + $fields['uid'] = BaseFieldDefinition::create('entity_reference') + ->setLabel(t('Authored by')) + ->setDescription(t('The user ID of author of the organization.')) + ->setRevisionable(TRUE) + ->setSetting('target_type', 'user') + ->setSetting('handler', 'default') + ->setDefaultValueCallback('Drupal\organization\Entity\Organization::getCurrentUserId') + ->setDisplayOptions('view', [ + 'label' => 'hidden', + 'type' => 'author', + 'weight' => 0, + ]) + ->setDisplayOptions('form', [ + 'type' => 'entity_reference_autocomplete', + 'weight' => 12, + 'settings' => [ + 'match_operator' => 'CONTAINS', + 'size' => '60', + 'autocomplete_type' => 'tags', + 'placeholder' => '', + ], + ]) + ->setDisplayConfigurable('form', TRUE) + ->setDisplayConfigurable('view', TRUE); + + $fields['created'] = BaseFieldDefinition::create('created') + ->setLabel(t('Authored on')) + ->setDescription(t('The time that the organization was created.')) + ->setRevisionable(TRUE) + ->setDefaultValueCallback(static::class . '::getRequestTime') + ->setDisplayOptions('view', [ + 'label' => 'hidden', + 'type' => 'timestamp', + 'weight' => 0, + ]) + ->setDisplayOptions('form', [ + 'type' => 'datetime_timestamp', + 'weight' => 13, + ]) + ->setDisplayConfigurable('form', TRUE); + + $fields['changed'] = BaseFieldDefinition::create('changed') + ->setLabel(t('Changed')) + ->setDescription(t('The time the organization was last edited.')) + ->setRevisionable(TRUE); + + $fields['archived'] = BaseFieldDefinition::create('timestamp') + ->setLabel(t('Timestamp')) + ->setDescription(t('The time the organization was archived.')) + ->setRevisionable(TRUE); + + return $fields; + } + + /** + * Gets the workflow ID for the state field. + * + * @param \Drupal\organization\Entity\OrganizationInterface $organization + * The organization entity. + * + * @return string + * The workflow ID. + */ + public static function getWorkflowId(OrganizationInterface $organization) { + $workflow = OrganizationType::load($organization->bundle())->getWorkflowId(); + return $workflow; + } + +} diff --git a/modules/core/organization/src/Entity/OrganizationInterface.php b/modules/core/organization/src/Entity/OrganizationInterface.php new file mode 100644 index 0000000000..b5b816a258 --- /dev/null +++ b/modules/core/organization/src/Entity/OrganizationInterface.php @@ -0,0 +1,84 @@ +description; + } + + /** + * {@inheritdoc} + */ + public function setDescription($description) { + return $this->set('description', $description); + } + + /** + * {@inheritdoc} + */ + public function postSave(EntityStorageInterface $storage, $update = TRUE) { + parent::postSave($storage, $update); + + // If the organization type id changed, update all existing organizations of + // that type. + if ($update && $this->getOriginalId() != $this->id()) { + $update_count = $this->entityTypeManager()->getStorage('organization')->updateType($this->getOriginalId(), $this->id()); + if ($update_count) { + \Drupal::messenger()->addMessage(\Drupal::translation()->formatPlural($update_count, + 'Changed the organization type of 1 post from %old-type to %type.', + 'Changed the organization type of @count posts from %old-type to %type.', + [ + '%old-type' => $this->getOriginalId(), + '%type' => $this->id(), + ])); + } + } + if ($update) { + // Clear the cached field definitions as some settings affect the field + // definitions. + $this->entityTypeManager()->clearCachedDefinitions(); + \Drupal::service('entity_field.manager')->clearCachedFieldDefinitions(); + } + } + + /** + * {@inheritdoc} + */ + public function getWorkflowId() { + return $this->workflow; + } + + /** + * {@inheritdoc} + */ + public function setWorkflowId($workflow_id) { + $this->workflow = $workflow_id; + return $this; + } + + /** + * {@inheritdoc} + */ + public function calculateDependencies() { + parent::calculateDependencies(); + + // The organization type must depend on the module that provides the + // workflow. + $workflow_manager = \Drupal::service('plugin.manager.workflow'); + $workflow = $workflow_manager->createInstance($this->getWorkflowId()); + $this->calculatePluginDependencies($workflow); + + return $this; + } + + /** + * {@inheritdoc} + */ + public function shouldCreateNewRevision() { + return $this->new_revision; + } + + /** + * {@inheritdoc} + */ + public function setNewRevision($new_revision) { + return $this->set('new_revision', $new_revision); + } + +} diff --git a/modules/core/organization/src/Entity/OrganizationTypeInterface.php b/modules/core/organization/src/Entity/OrganizationTypeInterface.php new file mode 100644 index 0000000000..cb4c7f2560 --- /dev/null +++ b/modules/core/organization/src/Entity/OrganizationTypeInterface.php @@ -0,0 +1,14 @@ +organization = $organization; + } + +} diff --git a/modules/core/organization/src/Form/OrganizationForm.php b/modules/core/organization/src/Form/OrganizationForm.php new file mode 100644 index 0000000000..3a300e4ac8 --- /dev/null +++ b/modules/core/organization/src/Form/OrganizationForm.php @@ -0,0 +1,28 @@ +entity->toUrl()->setAbsolute()->toString(); + $this->messenger()->addMessage($this->t('Saved organization: %label', [':url' => $entity_url, '%label' => $this->entity->label()])); + $form_state->setRedirectUrl($this->entity->toUrl()); + return $status; + } + +} diff --git a/modules/core/organization/src/Form/OrganizationTypeForm.php b/modules/core/organization/src/Form/OrganizationTypeForm.php new file mode 100644 index 0000000000..0b4efdc12a --- /dev/null +++ b/modules/core/organization/src/Form/OrganizationTypeForm.php @@ -0,0 +1,117 @@ +workflowManager = $workflow_manager; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('plugin.manager.workflow') + ); + } + + /** + * {@inheritdoc} + */ + public function form(array $form, FormStateInterface $form_state) { + $form = parent::form($form, $form_state); + $organization_type = $this->entity; + + $form['label'] = [ + '#type' => 'textfield', + '#title' => $this->t('Label'), + '#maxlength' => 255, + '#default_value' => $organization_type->label(), + '#description' => $this->t('Label for the organization type.'), + '#required' => TRUE, + ]; + + $form['id'] = [ + '#type' => 'machine_name', + '#default_value' => $organization_type->id(), + '#machine_name' => [ + 'exists' => '\Drupal\organization\Entity\OrganizationType::load', + ], + '#disabled' => !$organization_type->isNew(), + ]; + + $form['description'] = [ + '#type' => 'textarea', + '#title' => $this->t('Description'), + '#default_value' => $organization_type->getDescription(), + ]; + + $form['workflow'] = [ + '#type' => 'select', + '#title' => $this->t('Workflow'), + '#options' => $this->workflowManager->getGroupedLabels('organization'), + '#default_value' => $organization_type->getWorkflowId(), + '#description' => $this->t('Used by all organizations of this type.'), + ]; + + $form['new_revision'] = [ + '#type' => 'checkbox', + '#title' => $this->t('Create new revision'), + '#default_value' => $organization_type->shouldCreateNewRevision(), + ]; + + return $form; + } + + /** + * {@inheritdoc} + */ + public function save(array $form, FormStateInterface $form_state) { + $organization_type = $this->entity; + $status = $organization_type->save(); + + switch ($status) { + case SAVED_NEW: + $this->messenger()->addMessage($this->t('Created the %label organization type.', [ + '%label' => $organization_type->label(), + ])); + break; + + default: + $this->messenger()->addMessage($this->t('Saved the %label organization type.', [ + '%label' => $organization_type->label(), + ])); + } + $form_state->setRedirectUrl($organization_type->toUrl('collection')); + + return $status; + } + +} diff --git a/modules/core/organization/src/OrganizationListBuilder.php b/modules/core/organization/src/OrganizationListBuilder.php new file mode 100644 index 0000000000..43dce72ff0 --- /dev/null +++ b/modules/core/organization/src/OrganizationListBuilder.php @@ -0,0 +1,38 @@ +t('Organization ID'); + $header['label'] = $this->t('Label'); + $header['type'] = $this->t('Type'); + return $header + parent::buildHeader(); + } + + /** + * {@inheritdoc} + */ + public function buildRow(EntityInterface $entity) { + /** @var \Drupal\organization\Entity\OrganizationInterface $entity */ + $row['id'] = ['#markup' => $entity->id()]; + $row['name'] = $entity->toLink($entity->label(), 'canonical')->toRenderable(); + $row['type'] = ['#markup' => $entity->getBundleLabel()]; + return $row + parent::buildRow($entity); + } + +} diff --git a/modules/core/organization/src/OrganizationStorage.php b/modules/core/organization/src/OrganizationStorage.php new file mode 100644 index 0000000000..51e37093af --- /dev/null +++ b/modules/core/organization/src/OrganizationStorage.php @@ -0,0 +1,130 @@ +time = $time; + } + + /** + * {@inheritdoc} + */ + public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) { + return new static( + $entity_type, + $container->get('database'), + $container->get('entity_field.manager'), + $container->get('cache.entity'), + $container->get('language_manager'), + $container->get('entity.memory_cache'), + $container->get('entity_type.bundle.info'), + $container->get('entity_type.manager'), + $container->get('datetime.time'), + ); + } + + /** + * {@inheritdoc} + */ + protected function doPreSave(EntityInterface $entity) { + /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */ + $id = parent::doPreSave($entity); + + // If there is no original entity, bail. + if (empty($entity->original)) { + return $id; + } + + // Load new and original states. + $new_state = $entity->get('status')->first()->getString(); + $old_state = $entity->original->get('status')->first()->getString(); + + $state_unchanged = $new_state == $old_state; + + // If the entity is not archived and this would otherwise not be a state + // transition but the archive timestamp is set, then transition to the + // archived state. + if ($state_unchanged && $old_state != 'archived' && $entity->getArchivedTime() != NULL) { + $entity->get('status')->first()->applyTransitionById('archive'); + } + + // If the entity is archived and this would otherwise not be a state + // transition but the archive timestemp is NULL, then transition to the + // active state. + if ($state_unchanged && $old_state == 'archived' && $entity->getArchivedTime() == NULL) { + $entity->get('status')->first()->applyTransitionById('to_active'); + } + + // If the state has not changed, bail. + if ($state_unchanged) { + return $id; + } + + // If the state has changed to archived and no archived timestamp was + // specified, set it to the current time. + if ($new_state == 'archived' && $entity->getArchivedTime() == NULL) { + $entity->setArchivedTime($this->time->getRequestTime()); + } + + // Or, if the state has changed from archived, set a null value. + elseif ($old_state == 'archived') { + $entity->setArchivedTime(NULL); + } + + return $id; + } + +} diff --git a/modules/core/organization/src/OrganizationTypeListBuilder.php b/modules/core/organization/src/OrganizationTypeListBuilder.php new file mode 100644 index 0000000000..90e3f707ae --- /dev/null +++ b/modules/core/organization/src/OrganizationTypeListBuilder.php @@ -0,0 +1,59 @@ +t('Organization type'); + $header['id'] = $this->t('Machine name'); + return $header + parent::buildHeader(); + } + + /** + * {@inheritdoc} + */ + public function buildRow(EntityInterface $entity) { + $row['label'] = $entity->label(); + $row['id'] = $entity->id(); + // You probably want a few more properties here... + return $row + parent::buildRow($entity); + } + + /** + * {@inheritdoc} + */ + public function getDefaultOperations(EntityInterface $entity) { + $operations = parent::getDefaultOperations($entity); + // Place the edit operation after the operations added by field_ui.module + // which have the weights 15, 20, 25. + if (isset($operations['edit'])) { + $operations['edit']['weight'] = 30; + } + return $operations; + } + + /** + * {@inheritdoc} + */ + public function render() { + $build = parent::render(); + $build['table']['#empty'] = $this->t('No organization types available. Add organization type.', [ + ':link' => Url::fromRoute('entity.organization_type.add_form')->toString(), + ]); + return $build; + } + +} diff --git a/modules/core/organization/src/Plugin/Action/OrganizationActivate.php b/modules/core/organization/src/Plugin/Action/OrganizationActivate.php new file mode 100644 index 0000000000..47c2bfabcf --- /dev/null +++ b/modules/core/organization/src/Plugin/Action/OrganizationActivate.php @@ -0,0 +1,23 @@ +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('current_user'), + ); + } + + /** + * {@inheritdoc} + */ + public function execute(?OrganizationInterface $organization = NULL) { + if ($organization) { + $cloned_organization = $organization->createDuplicate(); + $new_name = $organization->getName() . ' ' . $this->t('(clone of organization #@id)', ['@id' => $organization->id()]); + $cloned_organization->setOwnerId($this->currentUser->id()); + $cloned_organization->setName($new_name); + $cloned_organization->save(); + $this->messenger()->addMessage($this->t('Organization saved: %organization_label', [':uri' => $cloned_organization->toUrl()->toString(), '%organization_label' => $cloned_organization->label()])); + } + } + + /** + * {@inheritdoc} + */ + public function access($object, ?AccountInterface $account = NULL, $return_as_object = FALSE) { + /** @var \Drupal\organization\Entity\OrganizationInterface $object */ + $result = $object->access('view', $account, TRUE) + ->andIf($object->access('create', $account, TRUE)); + + return $return_as_object ? $result : $result->isAllowed(); + } + +} diff --git a/modules/core/organization/src/Plugin/Action/OrganizationStateChangeBase.php b/modules/core/organization/src/Plugin/Action/OrganizationStateChangeBase.php new file mode 100644 index 0000000000..ef1a60faee --- /dev/null +++ b/modules/core/organization/src/Plugin/Action/OrganizationStateChangeBase.php @@ -0,0 +1,101 @@ +get('status')->first(); + if ($state_item->getOriginalId() !== $this->targetState && $transition = $state_item->getWorkflow()->findTransition($state_item->getOriginalId(), $this->targetState)) { + $state_item->applyTransition($transition); + $organization->setNewRevision(TRUE); + + // Validate the entity before saving. + $violations = $organization->validate(); + if ($violations->count() > 0) { + $this->messenger()->addWarning( + $this->t('Could not change the status of %entity_label: validation failed.', + [ + ':entity_link' => $organization->toUrl()->setAbsolute()->toString(), + '%entity_label' => $organization->label(), + ], + ), + ); + return; + } + + $organization->save(); + } + } + + /** + * {@inheritdoc} + */ + public function access($object, ?AccountInterface $account = NULL, $return_as_object = FALSE) { + /** @var \Drupal\organization\Entity\OrganizationInterface $object */ + // First check entity and state field access. + $result = $object->get('status')->access('edit', $account, TRUE) + ->andIf($object->access('update', $account, TRUE)); + + // Save the state field. + /** @var \Drupal\state_machine\Plugin\Field\FieldType\StateItemInterface $state_item */ + $state_item = $object->get('status')->first(); + + // If the state field is already in the target state, return early. + // The workflow will not allow a transition to the same state but the + // action itself does not need to fail. + if ($state_item->getOriginalId() === $this->targetState) { + return $return_as_object ? $result : $result->isAllowed(); + } + + // Check that the target state exists for the workflow. + $workflow = $state_item->getWorkflow(); + $target_state = $workflow->getState($this->targetState); + + // Deny access if the workflow does not support the target state. + if (empty($target_state)) { + $result = $result->orIf(AccessResult::forbidden( + $this->t('The %workflow workflow does not support the %target_state state.', ['%workflow' => $workflow->getLabel(), '%target_state' => $this->targetState]), + )); + } + // Else check that a transition exists to the desired target state. + else { + $transition = $workflow->findTransition($state_item->getOriginalId(), $this->targetState); + $result = $result->orIf(AccessResult::forbiddenIf( + empty($transition) || !$state_item->isTransitionAllowed($transition->getId()), + $this->t('The state transition from %original_state to %target_state is not allowed.', ['%original' => $state_item->getOriginalLabel(), '%target_state' => $target_state->getLabel()]), + )); + } + + return $return_as_object ? $result : $result->isAllowed(); + } + +} diff --git a/modules/core/organization/templates/organization.html.twig b/modules/core/organization/templates/organization.html.twig new file mode 100644 index 0000000000..83888ec20c --- /dev/null +++ b/modules/core/organization/templates/organization.html.twig @@ -0,0 +1,22 @@ +{# +/** + * @file organization.html.twig + * Default theme implementation to present organization data. + * + * This template is used when viewing organization pages. + * + * + * Available variables: + * - content: A list of content items. Use 'content' to print all content, or + * - attributes: HTML attributes for the container element. + * + * @see template_preprocess_organization() + * + * @ingroup themeable + */ +#} +