diff --git a/web/profiles/custom/yalesites_profile/composer.json b/web/profiles/custom/yalesites_profile/composer.json index 7128dde846..192465d0e6 100644 --- a/web/profiles/custom/yalesites_profile/composer.json +++ b/web/profiles/custom/yalesites_profile/composer.json @@ -106,7 +106,7 @@ "laminas/laminas-escaper": "2.12", "northernco/ckeditor5-anchor-drupal": "0.4.0", "yalesites-org/ai_engine": "1.2.4", - "yalesites-org/atomic": "1.36.0", + "yalesites-org/atomic": "1.38.0", "yalesites-org/yale_cas": "1.0.4" }, "minimum-stability": "dev", diff --git a/web/profiles/custom/yalesites_profile/config/sync/ai_engine_chat.settings.yml b/web/profiles/custom/yalesites_profile/config/sync/ai_engine_chat.settings.yml deleted file mode 100644 index c678f9ba23..0000000000 --- a/web/profiles/custom/yalesites_profile/config/sync/ai_engine_chat.settings.yml +++ /dev/null @@ -1,7 +0,0 @@ -_core: - default_config_hash: 2UNzD8lgl2QZFwoEWDqXPOjnX-1xrj2WOUPOyV36X4w -enable: false -azure_base_url: '' -initial_questions: { } -disclaimer: '' -footer: '' diff --git a/web/profiles/custom/yalesites_profile/config/sync/ai_engine_metadata.settings.yml b/web/profiles/custom/yalesites_profile/config/sync/ai_engine_metadata.settings.yml deleted file mode 100644 index d48968cc55..0000000000 --- a/web/profiles/custom/yalesites_profile/config/sync/ai_engine_metadata.settings.yml +++ /dev/null @@ -1,3 +0,0 @@ -_core: - default_config_hash: _M8zK1JzYowovWvVZ0BNgS8MXJ_VkFi1KuUjskwJ2h8 -enable: false diff --git a/web/profiles/custom/yalesites_profile/config/sync/config_ignore.settings.yml b/web/profiles/custom/yalesites_profile/config/sync/config_ignore.settings.yml index 814e7b3bd3..007ac237c4 100644 --- a/web/profiles/custom/yalesites_profile/config/sync/config_ignore.settings.yml +++ b/web/profiles/custom/yalesites_profile/config/sync/config_ignore.settings.yml @@ -8,3 +8,4 @@ ignored_config_entities: - ys_themes.theme_settings - 'ys_localist*' - 'ai_engine*' + - 'ys_servicenow*' diff --git a/web/profiles/custom/yalesites_profile/config/sync/core.entity_form_display.block_content.content_spotlight.default.yml b/web/profiles/custom/yalesites_profile/config/sync/core.entity_form_display.block_content.content_spotlight.default.yml index d967b1cae3..c41eaf9021 100644 --- a/web/profiles/custom/yalesites_profile/config/sync/core.entity_form_display.block_content.content_spotlight.default.yml +++ b/web/profiles/custom/yalesites_profile/config/sync/core.entity_form_display.block_content.content_spotlight.default.yml @@ -140,7 +140,7 @@ content: hide_help: '1' hide_guidelines: '1' maxlength: - maxlength_js: 500 + maxlength_js: 600 maxlength_js_label: 'Content limited to @limit characters, remaining: @remaining' maxlength_js_enforce: true info: diff --git a/web/profiles/custom/yalesites_profile/config/sync/core.entity_form_display.block_content.content_spotlight_portrait.default.yml b/web/profiles/custom/yalesites_profile/config/sync/core.entity_form_display.block_content.content_spotlight_portrait.default.yml index e06663272f..fcc3f2d1d2 100644 --- a/web/profiles/custom/yalesites_profile/config/sync/core.entity_form_display.block_content.content_spotlight_portrait.default.yml +++ b/web/profiles/custom/yalesites_profile/config/sync/core.entity_form_display.block_content.content_spotlight_portrait.default.yml @@ -136,7 +136,7 @@ content: hide_help: '1' hide_guidelines: '1' maxlength: - maxlength_js: 500 + maxlength_js: 650 maxlength_js_label: 'Content limited to @limit characters, remaining: @remaining' maxlength_js_enforce: true info: diff --git a/web/profiles/custom/yalesites_profile/config/sync/core.entity_form_display.block_content.image_banner.default.yml b/web/profiles/custom/yalesites_profile/config/sync/core.entity_form_display.block_content.image_banner.default.yml index a3492304ad..d328a37a37 100644 --- a/web/profiles/custom/yalesites_profile/config/sync/core.entity_form_display.block_content.image_banner.default.yml +++ b/web/profiles/custom/yalesites_profile/config/sync/core.entity_form_display.block_content.image_banner.default.yml @@ -5,6 +5,7 @@ dependencies: config: - block_content.type.image_banner - field.field.block_content.image_banner.field_media + - field.field.block_content.image_banner.field_style_variation module: - media_library - media_library_edit @@ -18,10 +19,18 @@ content: weight: 1 region: content settings: - media_types: { } + media_types: + - image + - background_video third_party_settings: media_library_edit: show_edit: '1' + field_style_variation: + type: options_select + weight: 11 + region: content + settings: { } + third_party_settings: { } info: type: string_textfield weight: 0 diff --git a/web/profiles/custom/yalesites_profile/config/sync/core.entity_form_display.node.profile.default.yml b/web/profiles/custom/yalesites_profile/config/sync/core.entity_form_display.node.profile.default.yml index f6e3a6e54c..c5aecefb5b 100644 --- a/web/profiles/custom/yalesites_profile/config/sync/core.entity_form_display.node.profile.default.yml +++ b/web/profiles/custom/yalesites_profile/config/sync/core.entity_form_display.node.profile.default.yml @@ -15,6 +15,7 @@ dependencies: - field.field.node.profile.field_media - field.field.node.profile.field_metatags - field.field.node.profile.field_position + - field.field.node.profile.field_pronouns - field.field.node.profile.field_subtitle - field.field.node.profile.field_tags - field.field.node.profile.field_teaser_media @@ -107,6 +108,7 @@ third_party_settings: - field_first_name - field_last_name - field_honorific_prefix + - field_pronouns - field_position - field_subtitle - field_department @@ -164,7 +166,7 @@ content: third_party_settings: { } field_department: type: string_textfield - weight: 33 + weight: 34 region: content settings: size: 60 @@ -235,6 +237,14 @@ content: use_details: true third_party_settings: { } field_position: + type: string_textfield + weight: 32 + region: content + settings: + size: 60 + placeholder: '' + third_party_settings: { } + field_pronouns: type: string_textfield weight: 31 region: content @@ -244,7 +254,7 @@ content: third_party_settings: { } field_subtitle: type: text_textfield - weight: 32 + weight: 33 region: content settings: size: 60 diff --git a/web/profiles/custom/yalesites_profile/config/sync/core.entity_view_display.block_content.image_banner.default.yml b/web/profiles/custom/yalesites_profile/config/sync/core.entity_view_display.block_content.image_banner.default.yml index e7362d3b0c..733e1b8063 100644 --- a/web/profiles/custom/yalesites_profile/config/sync/core.entity_view_display.block_content.image_banner.default.yml +++ b/web/profiles/custom/yalesites_profile/config/sync/core.entity_view_display.block_content.image_banner.default.yml @@ -5,6 +5,9 @@ dependencies: config: - block_content.type.image_banner - field.field.block_content.image_banner.field_media + - field.field.block_content.image_banner.field_style_variation + module: + - options id: block_content.image_banner.default targetEntityType: block_content bundle: image_banner @@ -14,10 +17,17 @@ content: type: entity_reference_entity_view label: hidden settings: - view_mode: banner_16_5 + view_mode: default link: false third_party_settings: { } weight: 0 region: content + field_style_variation: + type: list_key + label: hidden + settings: { } + third_party_settings: { } + weight: 5 + region: content hidden: search_api_excerpt: true diff --git a/web/profiles/custom/yalesites_profile/config/sync/core.entity_view_display.node.profile.card.yml b/web/profiles/custom/yalesites_profile/config/sync/core.entity_view_display.node.profile.card.yml index 344f6fc635..7db4d41818 100644 --- a/web/profiles/custom/yalesites_profile/config/sync/core.entity_view_display.node.profile.card.yml +++ b/web/profiles/custom/yalesites_profile/config/sync/core.entity_view_display.node.profile.card.yml @@ -16,6 +16,7 @@ dependencies: - field.field.node.profile.field_media - field.field.node.profile.field_metatags - field.field.node.profile.field_position + - field.field.node.profile.field_pronouns - field.field.node.profile.field_subtitle - field.field.node.profile.field_tags - field.field.node.profile.field_teaser_media @@ -128,6 +129,7 @@ hidden: field_last_name: true field_login_required: true field_metatags: true + field_pronouns: true field_telephone: true layout_builder__layout: true links: true diff --git a/web/profiles/custom/yalesites_profile/config/sync/core.entity_view_display.node.profile.condensed.yml b/web/profiles/custom/yalesites_profile/config/sync/core.entity_view_display.node.profile.condensed.yml index 1ad780139e..258573abef 100644 --- a/web/profiles/custom/yalesites_profile/config/sync/core.entity_view_display.node.profile.condensed.yml +++ b/web/profiles/custom/yalesites_profile/config/sync/core.entity_view_display.node.profile.condensed.yml @@ -16,6 +16,7 @@ dependencies: - field.field.node.profile.field_media - field.field.node.profile.field_metatags - field.field.node.profile.field_position + - field.field.node.profile.field_pronouns - field.field.node.profile.field_subtitle - field.field.node.profile.field_tags - field.field.node.profile.field_teaser_media @@ -47,6 +48,11 @@ targetEntityType: node bundle: profile mode: condensed content: + content_moderation_control: + settings: { } + third_party_settings: { } + weight: -20 + region: content field_external_source: type: link_separate label: hidden @@ -78,6 +84,7 @@ hidden: field_login_required: true field_media: true field_metatags: true + field_pronouns: true field_subtitle: true field_tags: true field_teaser_media: true @@ -87,3 +94,4 @@ hidden: layout_builder__layout: true links: true search_api_excerpt: true + workflow_buttons: true diff --git a/web/profiles/custom/yalesites_profile/config/sync/core.entity_view_display.node.profile.default.yml b/web/profiles/custom/yalesites_profile/config/sync/core.entity_view_display.node.profile.default.yml index 77771730b1..8b0cdfa720 100644 --- a/web/profiles/custom/yalesites_profile/config/sync/core.entity_view_display.node.profile.default.yml +++ b/web/profiles/custom/yalesites_profile/config/sync/core.entity_view_display.node.profile.default.yml @@ -15,6 +15,7 @@ dependencies: - field.field.node.profile.field_media - field.field.node.profile.field_metatags - field.field.node.profile.field_position + - field.field.node.profile.field_pronouns - field.field.node.profile.field_subtitle - field.field.node.profile.field_tags - field.field.node.profile.field_teaser_media @@ -323,6 +324,14 @@ content: third_party_settings: { } weight: 107 region: content + field_pronouns: + type: string + label: above + settings: + link_to_entity: false + third_party_settings: { } + weight: 119 + region: content field_subtitle: type: text_default label: above diff --git a/web/profiles/custom/yalesites_profile/config/sync/core.entity_view_display.node.profile.directory.yml b/web/profiles/custom/yalesites_profile/config/sync/core.entity_view_display.node.profile.directory.yml index 7ea14769c7..8ea2fb6955 100644 --- a/web/profiles/custom/yalesites_profile/config/sync/core.entity_view_display.node.profile.directory.yml +++ b/web/profiles/custom/yalesites_profile/config/sync/core.entity_view_display.node.profile.directory.yml @@ -16,6 +16,7 @@ dependencies: - field.field.node.profile.field_media - field.field.node.profile.field_metatags - field.field.node.profile.field_position + - field.field.node.profile.field_pronouns - field.field.node.profile.field_subtitle - field.field.node.profile.field_tags - field.field.node.profile.field_teaser_media @@ -27,6 +28,7 @@ dependencies: module: - layout_builder - layout_builder_restrictions + - link - text - user third_party_settings: @@ -47,6 +49,11 @@ targetEntityType: node bundle: profile mode: directory content: + content_moderation_control: + settings: { } + third_party_settings: { } + weight: -20 + region: content field_affiliation: type: entity_reference_label label: hidden @@ -130,9 +137,11 @@ hidden: field_last_name: true field_login_required: true field_metatags: true + field_pronouns: true field_tags: true field_teaser_text: true field_teaser_title: true layout_builder__layout: true links: true search_api_excerpt: true + workflow_buttons: true diff --git a/web/profiles/custom/yalesites_profile/config/sync/core.entity_view_display.node.profile.list_item.yml b/web/profiles/custom/yalesites_profile/config/sync/core.entity_view_display.node.profile.list_item.yml index b797a59601..9ad2bca6fb 100644 --- a/web/profiles/custom/yalesites_profile/config/sync/core.entity_view_display.node.profile.list_item.yml +++ b/web/profiles/custom/yalesites_profile/config/sync/core.entity_view_display.node.profile.list_item.yml @@ -16,6 +16,7 @@ dependencies: - field.field.node.profile.field_media - field.field.node.profile.field_metatags - field.field.node.profile.field_position + - field.field.node.profile.field_pronouns - field.field.node.profile.field_subtitle - field.field.node.profile.field_tags - field.field.node.profile.field_teaser_media @@ -128,6 +129,7 @@ hidden: field_last_name: true field_login_required: true field_metatags: true + field_pronouns: true field_telephone: true layout_builder__layout: true links: true diff --git a/web/profiles/custom/yalesites_profile/config/sync/core.entity_view_display.node.profile.search_result.yml b/web/profiles/custom/yalesites_profile/config/sync/core.entity_view_display.node.profile.search_result.yml index 8ca5f5fdef..c4154d40db 100644 --- a/web/profiles/custom/yalesites_profile/config/sync/core.entity_view_display.node.profile.search_result.yml +++ b/web/profiles/custom/yalesites_profile/config/sync/core.entity_view_display.node.profile.search_result.yml @@ -16,6 +16,7 @@ dependencies: - field.field.node.profile.field_media - field.field.node.profile.field_metatags - field.field.node.profile.field_position + - field.field.node.profile.field_pronouns - field.field.node.profile.field_subtitle - field.field.node.profile.field_tags - field.field.node.profile.field_teaser_media @@ -26,6 +27,7 @@ dependencies: - node.type.profile module: - layout_builder + - link - text - user third_party_settings: @@ -37,6 +39,11 @@ targetEntityType: node bundle: profile mode: search_result content: + content_moderation_control: + settings: { } + third_party_settings: { } + weight: -20 + region: content field_external_source: type: link_separate label: hidden @@ -73,6 +80,7 @@ hidden: field_media: true field_metatags: true field_position: true + field_pronouns: true field_subtitle: true field_tags: true field_teaser_media: true @@ -80,3 +88,4 @@ hidden: field_telephone: true layout_builder__layout: true links: true + workflow_buttons: true diff --git a/web/profiles/custom/yalesites_profile/config/sync/core.entity_view_display.node.profile.single.yml b/web/profiles/custom/yalesites_profile/config/sync/core.entity_view_display.node.profile.single.yml index 518a1cb1fa..6c7a65ee20 100644 --- a/web/profiles/custom/yalesites_profile/config/sync/core.entity_view_display.node.profile.single.yml +++ b/web/profiles/custom/yalesites_profile/config/sync/core.entity_view_display.node.profile.single.yml @@ -16,6 +16,7 @@ dependencies: - field.field.node.profile.field_media - field.field.node.profile.field_metatags - field.field.node.profile.field_position + - field.field.node.profile.field_pronouns - field.field.node.profile.field_subtitle - field.field.node.profile.field_tags - field.field.node.profile.field_teaser_media @@ -163,6 +164,11 @@ targetEntityType: node bundle: profile mode: single content: + content_moderation_control: + settings: { } + third_party_settings: { } + weight: -20 + region: content field_address: type: text_default label: hidden @@ -288,7 +294,9 @@ content: hidden: field_login_required: true field_metatags: true + field_pronouns: true field_tags: true layout_builder__layout: true links: true search_api_excerpt: true + workflow_buttons: true diff --git a/web/profiles/custom/yalesites_profile/config/sync/core.entity_view_display.node.profile.teaser.yml b/web/profiles/custom/yalesites_profile/config/sync/core.entity_view_display.node.profile.teaser.yml index eabe3d2873..3a21a64a51 100644 --- a/web/profiles/custom/yalesites_profile/config/sync/core.entity_view_display.node.profile.teaser.yml +++ b/web/profiles/custom/yalesites_profile/config/sync/core.entity_view_display.node.profile.teaser.yml @@ -16,6 +16,7 @@ dependencies: - field.field.node.profile.field_media - field.field.node.profile.field_metatags - field.field.node.profile.field_position + - field.field.node.profile.field_pronouns - field.field.node.profile.field_subtitle - field.field.node.profile.field_tags - field.field.node.profile.field_teaser_media @@ -31,6 +32,11 @@ targetEntityType: node bundle: profile mode: teaser content: + content_moderation_control: + settings: { } + third_party_settings: { } + weight: -20 + region: content links: settings: { } third_party_settings: { } @@ -49,6 +55,7 @@ hidden: field_media: true field_metatags: true field_position: true + field_pronouns: true field_subtitle: true field_tags: true field_teaser_media: true @@ -57,3 +64,4 @@ hidden: field_telephone: true layout_builder__layout: true search_api_excerpt: true + workflow_buttons: true diff --git a/web/profiles/custom/yalesites_profile/config/sync/core.extension.yml b/web/profiles/custom/yalesites_profile/config/sync/core.extension.yml index 9cd2218d72..86a2966cd4 100644 --- a/web/profiles/custom/yalesites_profile/config/sync/core.extension.yml +++ b/web/profiles/custom/yalesites_profile/config/sync/core.extension.yml @@ -148,6 +148,7 @@ module: ys_localist: 0 ys_mail: 0 ys_node_access: 0 + ys_servicenow: 0 ys_starterkit: 0 ys_toolbar: 0 ys_views_basic: 0 diff --git a/web/profiles/custom/yalesites_profile/config/sync/field.field.block_content.image_banner.field_media.yml b/web/profiles/custom/yalesites_profile/config/sync/field.field.block_content.image_banner.field_media.yml index ed5e599347..7431310e3a 100644 --- a/web/profiles/custom/yalesites_profile/config/sync/field.field.block_content.image_banner.field_media.yml +++ b/web/profiles/custom/yalesites_profile/config/sync/field.field.block_content.image_banner.field_media.yml @@ -27,5 +27,5 @@ settings: field: _none direction: ASC auto_create: true - auto_create_bundle: background_video + auto_create_bundle: image field_type: entity_reference diff --git a/web/profiles/custom/yalesites_profile/config/sync/field.field.block_content.image_banner.field_style_variation.yml b/web/profiles/custom/yalesites_profile/config/sync/field.field.block_content.image_banner.field_style_variation.yml new file mode 100644 index 0000000000..f95053e5cc --- /dev/null +++ b/web/profiles/custom/yalesites_profile/config/sync/field.field.block_content.image_banner.field_style_variation.yml @@ -0,0 +1,21 @@ +uuid: 746c6e8d-b767-4afe-bc09-8bf928f39ae4 +langcode: en +status: true +dependencies: + config: + - block_content.type.image_banner + - field.storage.block_content.field_style_variation + module: + - options +id: block_content.image_banner.field_style_variation +field_name: field_style_variation +entity_type: block_content +bundle: image_banner +label: 'Media Size' +description: '' +required: true +translatable: false +default_value: { } +default_value_callback: ys_themes_default_value_function +settings: { } +field_type: list_string diff --git a/web/profiles/custom/yalesites_profile/config/sync/field.field.node.profile.field_pronouns.yml b/web/profiles/custom/yalesites_profile/config/sync/field.field.node.profile.field_pronouns.yml new file mode 100644 index 0000000000..751f6fef47 --- /dev/null +++ b/web/profiles/custom/yalesites_profile/config/sync/field.field.node.profile.field_pronouns.yml @@ -0,0 +1,19 @@ +uuid: 47bd0833-5f70-4c13-a2d9-25b0f01dcfff +langcode: en +status: true +dependencies: + config: + - field.storage.node.field_pronouns + - node.type.profile +id: node.profile.field_pronouns +field_name: field_pronouns +entity_type: node +bundle: profile +label: Pronouns +description: '' +required: false +translatable: false +default_value: { } +default_value_callback: '' +settings: { } +field_type: string diff --git a/web/profiles/custom/yalesites_profile/config/sync/field.storage.node.field_pronouns.yml b/web/profiles/custom/yalesites_profile/config/sync/field.storage.node.field_pronouns.yml new file mode 100644 index 0000000000..dd884e313a --- /dev/null +++ b/web/profiles/custom/yalesites_profile/config/sync/field.storage.node.field_pronouns.yml @@ -0,0 +1,21 @@ +uuid: 78b31208-ff3a-404a-ba52-8fba4c418766 +langcode: en +status: true +dependencies: + module: + - node +id: node.field_pronouns +field_name: field_pronouns +entity_type: node +type: string +settings: + max_length: 255 + case_sensitive: false + is_ascii: false +module: core +locked: false +cardinality: 1 +translatable: true +indexes: { } +persist_with_no_fields: false +custom_storage: false diff --git a/web/profiles/custom/yalesites_profile/config/sync/ys_themes.component_overrides.yml b/web/profiles/custom/yalesites_profile/config/sync/ys_themes.component_overrides.yml index d9d548e0bd..aa97e20d67 100644 --- a/web/profiles/custom/yalesites_profile/config/sync/ys_themes.component_overrides.yml +++ b/web/profiles/custom/yalesites_profile/config/sync/ys_themes.component_overrides.yml @@ -254,3 +254,9 @@ video: left: Left center: Center default: left +image_banner: + field_style_variation: + values: + tall: Tall + short: Short + default: tall diff --git a/web/profiles/custom/yalesites_profile/modules/custom/ys_core/src/Plugin/Action/ModeratedPublish.php b/web/profiles/custom/yalesites_profile/modules/custom/ys_core/src/Plugin/Action/ModeratedPublish.php index 65cb92234d..039c6f77a9 100644 --- a/web/profiles/custom/yalesites_profile/modules/custom/ys_core/src/Plugin/Action/ModeratedPublish.php +++ b/web/profiles/custom/yalesites_profile/modules/custom/ys_core/src/Plugin/Action/ModeratedPublish.php @@ -27,7 +27,7 @@ public function execute($entity = NULL) { /** * {@inheritdoc} */ - public function access($object, AccountInterface $account = NULL, $return_as_object = FALSE) { + public function access($object, ?AccountInterface $account = NULL, $return_as_object = FALSE) { $key = $object->getEntityType()->getKey('published'); /** @var \Drupal\Core\Entity\EntityInterface $object */ diff --git a/web/profiles/custom/yalesites_profile/modules/custom/ys_core/src/Plugin/Action/ModeratedUnpublish.php b/web/profiles/custom/yalesites_profile/modules/custom/ys_core/src/Plugin/Action/ModeratedUnpublish.php index 20325818bb..460b10bbc5 100644 --- a/web/profiles/custom/yalesites_profile/modules/custom/ys_core/src/Plugin/Action/ModeratedUnpublish.php +++ b/web/profiles/custom/yalesites_profile/modules/custom/ys_core/src/Plugin/Action/ModeratedUnpublish.php @@ -27,7 +27,7 @@ public function execute($entity = NULL) { /** * {@inheritdoc} */ - public function access($object, AccountInterface $account = NULL, $return_as_object = FALSE) { + public function access($object, ?AccountInterface $account = NULL, $return_as_object = FALSE) { $key = $object->getEntityType()->getKey('published'); /** @var \Drupal\Core\Entity\EntityInterface $object */ diff --git a/web/profiles/custom/yalesites_profile/modules/custom/ys_layouts/src/Plugin/Block/ProfileMetaBlock.php b/web/profiles/custom/yalesites_profile/modules/custom/ys_layouts/src/Plugin/Block/ProfileMetaBlock.php index 31e97bde96..e8cc91ff9c 100644 --- a/web/profiles/custom/yalesites_profile/modules/custom/ys_layouts/src/Plugin/Block/ProfileMetaBlock.php +++ b/web/profiles/custom/yalesites_profile/modules/custom/ys_layouts/src/Plugin/Block/ProfileMetaBlock.php @@ -101,6 +101,7 @@ public function build() { $position = NULL; $subtitle = NULL; $department = NULL; + $pronouns = NULL; $mediaId = NULL; $request = $this->requestStack->getCurrentRequest(); @@ -125,6 +126,7 @@ public function build() { $position = $node->get('field_position')->getValue()[0]['value'] ?? NULL; $subtitle = $node->get('field_subtitle')->getValue()[0]['value'] ?? NULL; $department = $node->get('field_department')->getValue()[0]['value'] ?? NULL; + $pronouns = $node->get('field_pronouns')->getValue()[0]['value'] ?? NULL; $mediaId = $node->get('field_media')->getValue()[0]['target_id'] ?? NULL; } @@ -134,10 +136,12 @@ public function build() { '#profile_meta__title_line' => $position, '#profile_meta__subtitle_line' => $subtitle, '#profile_meta__department' => $department, + '#profile_meta__pronouns' => $pronouns, '#media_id' => $mediaId, '#profile_meta__image_orientation' => $this->configuration['image_orientation'] ?? 'portrait', '#profile_meta__image_style' => $this->configuration['image_style'] ?? 'inline', '#profile_meta__image_alignment' => $this->configuration['image_alignment'] ?? 'left', + ]; } diff --git a/web/profiles/custom/yalesites_profile/modules/custom/ys_layouts/src/Service/LayoutUpdater.php b/web/profiles/custom/yalesites_profile/modules/custom/ys_layouts/src/Service/LayoutUpdater.php index 2bcaf6660d..a75694c344 100644 --- a/web/profiles/custom/yalesites_profile/modules/custom/ys_layouts/src/Service/LayoutUpdater.php +++ b/web/profiles/custom/yalesites_profile/modules/custom/ys_layouts/src/Service/LayoutUpdater.php @@ -2,7 +2,6 @@ namespace Drupal\ys_layouts\Service; -use Drupal\block_content\Entity\BlockContent; use Drupal\Core\Config\ConfigFactoryInterface; use Drupal\Core\Database\Connection; use Drupal\Core\Entity\EntityFieldManager; @@ -10,6 +9,7 @@ use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\Messenger\MessengerInterface; use Drupal\Core\StringTranslation\StringTranslationTrait; +use Drupal\block_content\Entity\BlockContent; use Drupal\node\NodeInterface; use Psr\Log\LoggerInterface; diff --git a/web/profiles/custom/yalesites_profile/modules/custom/ys_layouts/ys_layouts.module b/web/profiles/custom/yalesites_profile/modules/custom/ys_layouts/ys_layouts.module index ade2e3546d..b029a58559 100644 --- a/web/profiles/custom/yalesites_profile/modules/custom/ys_layouts/ys_layouts.module +++ b/web/profiles/custom/yalesites_profile/modules/custom/ys_layouts/ys_layouts.module @@ -57,6 +57,7 @@ function ys_layouts_theme($existing, $type, $theme, $path): array { 'profile_meta__title_line' => NULL, 'profile_meta__subtitle_line' => NULL, 'profile_meta__department' => NULL, + 'profile_meta__pronouns' => NULL, 'media_id' => NULL, 'profile_meta__image_orientation' => NULL, 'profile_meta__image_style' => NULL, diff --git a/web/profiles/custom/yalesites_profile/modules/custom/ys_localist/src/MetaFieldsManager.php b/web/profiles/custom/yalesites_profile/modules/custom/ys_localist/src/MetaFieldsManager.php index 7df6cb1ae2..5b2cfa4dc6 100644 --- a/web/profiles/custom/yalesites_profile/modules/custom/ys_localist/src/MetaFieldsManager.php +++ b/web/profiles/custom/yalesites_profile/modules/custom/ys_localist/src/MetaFieldsManager.php @@ -2,11 +2,11 @@ namespace Drupal\ys_localist; -use Drupal\calendar_link\Twig\CalendarLinkTwigExtension; use Drupal\Core\Datetime\DateFormatter; use Drupal\Core\Entity\EntityTypeManager; use Drupal\Core\Plugin\ContainerFactoryPluginInterface; use Drupal\Core\Url; +use Drupal\calendar_link\Twig\CalendarLinkTwigExtension; use Drupal\node\NodeInterface; use Symfony\Component\DependencyInjection\ContainerInterface; diff --git a/web/profiles/custom/yalesites_profile/modules/custom/ys_localist/src/Plugin/migrate_plus/data_parser/LocalistJson.php b/web/profiles/custom/yalesites_profile/modules/custom/ys_localist/src/Plugin/migrate_plus/data_parser/LocalistJson.php index c1d7ca7ab7..094fd64fdc 100644 --- a/web/profiles/custom/yalesites_profile/modules/custom/ys_localist/src/Plugin/migrate_plus/data_parser/LocalistJson.php +++ b/web/profiles/custom/yalesites_profile/modules/custom/ys_localist/src/Plugin/migrate_plus/data_parser/LocalistJson.php @@ -49,7 +49,7 @@ protected function getSourceData(string $url): array { // If json_decode() has returned NULL, it might be that the data isn't // valid utf8 see http://php.net/manual/en/function.json-decode.php#86997. if (is_null($source_data)) { - $utf8response = utf8_encode($response); + $utf8response = mb_convert_encoding($response, 'UTF-8', 'ISO-8859-1'); $source_data = json_decode($utf8response, TRUE, 512, JSON_THROW_ON_ERROR); } diff --git a/web/profiles/custom/yalesites_profile/modules/custom/ys_servicenow/README.md b/web/profiles/custom/yalesites_profile/modules/custom/ys_servicenow/README.md new file mode 100644 index 0000000000..e560f49ab6 --- /dev/null +++ b/web/profiles/custom/yalesites_profile/modules/custom/ys_servicenow/README.md @@ -0,0 +1,40 @@ +# YaleSites ServiceNow + +This is a YaleSites integration to allow ServiceNow knowledge base articles to import into the platform. The ultimate goal of this is to allow it to then go through our AI pipeline so that you can ask questions that will consider the articles synced. + +## Features + +Features include: + +- Key-based authentication +- Block-based syncing + +## Usage + +1. Install the module +2. Create a new key in the keys module with the ServiceNow endpoint username and password credentials (see pantheon secrets below) +3. Visit the ServiceNow Settings under Configuration +4. Enable the module +5. Select the key you created from the drop down +6. Enter the endpoint URL you were given by the ServiceNow Team + 1. Ensure that the following fields are present in the JSON output: + 1. number: The KB article number + 2. short_description: The title of the article + 3. text: The body of the article + 4. workflow_state: The state of the article (Published, etc) +7. Save +8. Upon reload, you'll notice a Sync button; click this button to do a manual sync + +The service once turned on will attempt to sync hourly. + +## Pantheon Secrets + +Pantheon secrets can be used with their Drupal module to interact with the keys module. To do this you'd want to add a pantheon secret at the site level first with empty data: + +`terminus secrets:site:set --scope web,user servicenow_auth ""` + +From there, you can then specify the multidev specific information. The key must ultimately be a JSON payload of the following: + +`terminus secrets:site:set . servicenow_auth '{"username":"username","password":"password"}'` + +Then simply sync pantheon secrets in the keys configuration to bring in the key; remember that there is a time delay on when that becomes available for sync. diff --git a/web/profiles/custom/yalesites_profile/modules/custom/ys_servicenow/migrations/servicenow_knowledge_base_article_block.yml b/web/profiles/custom/yalesites_profile/modules/custom/ys_servicenow/migrations/servicenow_knowledge_base_article_block.yml new file mode 100644 index 0000000000..20d4459b56 --- /dev/null +++ b/web/profiles/custom/yalesites_profile/modules/custom/ys_servicenow/migrations/servicenow_knowledge_base_article_block.yml @@ -0,0 +1,49 @@ +id: servicenow_knowledge_base_article_block +label: 'ServiceNow knowledge base article blocks' +source: + plugin: url + data_fetcher_plugin: http + data_parser_plugin: json + headers: + Accept: 'application/json; charset=utf-8' + Content-Type: application/json + authentication: + plugin: servicenow_auth + track_changes: true + urls: + callback: ys_servicenow_url_endpoint + item_selector: result + fields: + - name: servicenow_number + label: 'ServiceNow number' + selector: number + - name: servicenow_title + label: 'ServiceNow title' + selector: short_description + - name: servicenow_text + label: 'ServiceNow text' + selector: text + ids: + servicenow_number: + type: string +process: + field_text/value: servicenow_text + field_text/format: + plugin: default_value + default_value: basic_html + info: servicenow_title + reusable: + plugin: default_value + default_value: 0 +destination: + plugin: 'entity:block_content' + default_bundle: text + overwrite_properties: + - field_text + - info +dependencies: + enforced: + module: + - migrate_plus + - migrate_tools + - layout_builder diff --git a/web/profiles/custom/yalesites_profile/modules/custom/ys_servicenow/migrations/servicenow_knowledge_base_articles.yml b/web/profiles/custom/yalesites_profile/modules/custom/ys_servicenow/migrations/servicenow_knowledge_base_articles.yml new file mode 100644 index 0000000000..ebc6d2053e --- /dev/null +++ b/web/profiles/custom/yalesites_profile/modules/custom/ys_servicenow/migrations/servicenow_knowledge_base_articles.yml @@ -0,0 +1,59 @@ +id: servicenow_knowledge_base_articles +label: 'ServiceNow knowledge base articles' +source: + plugin: url + data_fetcher_plugin: http + data_parser_plugin: json + headers: + Accept: 'application/json; charset=utf-8' + Content-Type: application/json + authentication: + plugin: servicenow_auth + track_changes: true + urls: + callback: ys_servicenow_url_endpoint + item_selector: result + fields: + - name: servicenow_number + label: 'ServiceNow number' + selector: number + - name: servicenow_title + label: 'ServiceNow title' + selector: short_description + - name: servicenow_text + label: 'ServiceNow text' + selector: text + - name: servicenow_workflow_state + label: 'ServiceNow workflow state' + selector: workflow_state + ids: + servicenow_number: + type: string +process: + title: servicenow_title + block_id: + plugin: migration_lookup + migration: servicenow_knowledge_base_article_block + source: servicenow_number + layout_builder__layout: + source: servicenow_title + plugin: layout_builder_sections + moderation_state: + plugin: callback + callable: ys_servicenow_moderation_state_transformation + source: servicenow_workflow_state +destination: + plugin: 'entity:node' + default_bundle: page + overwrite_properties: + - title + - moderation_state +migration_dependencies: + required: + - servicenow_knowledge_base_article_block +dependencies: + enforced: + module: + - migrate_plus + - migrate_tools + - layout_builder diff --git a/web/profiles/custom/yalesites_profile/modules/custom/ys_servicenow/src/BasicAuthWithKeys.php b/web/profiles/custom/yalesites_profile/modules/custom/ys_servicenow/src/BasicAuthWithKeys.php new file mode 100644 index 0000000000..3e3d72d3ad --- /dev/null +++ b/web/profiles/custom/yalesites_profile/modules/custom/ys_servicenow/src/BasicAuthWithKeys.php @@ -0,0 +1,108 @@ +keyId = $key_id; + $this->configuration = $configuration; + } + + /** + * Get the authentication options. + * + * @return array + * The authentication options. + */ + public function getAuthenticationOptions() { + if (!$this->keyId) { + throw new \Exception("Key not set"); + } + $key = $this->getKey($this->keyId); + $key_object = $this->getKeyValues($key); + return [ + 'auth' => [ + $key_object->username, + $key_object->password, + ], + ]; + } + + /** + * Given a key ID, return the key object. + * + * @param string $key_id + * The key ID. + * + * @return \Drupal\key\KeyInterface + * The key object + */ + protected function getKey($key_id) { + $key = \Drupal::service('key.repository')->getKey($key_id); + + if (!$key) { + throw new \Exception("Key '$key_id' not found"); + } + + return $key; + } + + /** + * Given a key, return the key values. + * + * @param \Drupal\key\KeyInterface $key + * The key object. + * + * @return object + * The key values. + */ + protected function getKeyValues($key) { + $json_key = $key->getKeyValue(); + + if (!$json_key) { + throw new \Exception("Key has no value"); + } + + $decoded_object = json_decode($json_key); + + if (!$decoded_object) { + throw new \Exception("Key value is not valid JSON. Could you have accidentally used single quotes vs double?"); + } + + return $decoded_object; + } + +} diff --git a/web/profiles/custom/yalesites_profile/modules/custom/ys_servicenow/src/Controller/RunMigrations.php b/web/profiles/custom/yalesites_profile/modules/custom/ys_servicenow/src/Controller/RunMigrations.php new file mode 100644 index 0000000000..33bf45b569 --- /dev/null +++ b/web/profiles/custom/yalesites_profile/modules/custom/ys_servicenow/src/Controller/RunMigrations.php @@ -0,0 +1,83 @@ +servicenowConfig = $config_factory->get('ys_servicenow.settings'); + $this->servicenowManager = $servicenow_manager; + $this->messenger = $messenger; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('config.factory'), + $container->get('ys_servicenow.manager'), + $container->get('messenger'), + ); + + } + + /** + * Runs all ServiceNow migrations. + */ + public function runAllMigrations() { + if ($this->servicenowConfig->get('enable_servicenow_sync')) { + $this->messenger->addMessage('Running ServiceNow migrations...'); + $this->servicenowManager->runAllMigrations(); + $this->messenger->addMessage('ServiceNow migrations complete.'); + } + else { + $this->messenger->addMessage('ServiceNow sync is disabled. No sync was performed.'); + } + + $redirectUrl = Url::fromRoute('ys_servicenow.settings')->toString(); + $response = new RedirectResponse($redirectUrl); + return $response; + } + +} diff --git a/web/profiles/custom/yalesites_profile/modules/custom/ys_servicenow/src/Form/ServiceNowSettings.php b/web/profiles/custom/yalesites_profile/modules/custom/ys_servicenow/src/Form/ServiceNowSettings.php new file mode 100644 index 0000000000..3a81b8d2df --- /dev/null +++ b/web/profiles/custom/yalesites_profile/modules/custom/ys_servicenow/src/Form/ServiceNowSettings.php @@ -0,0 +1,170 @@ +entityTypeManager = $entity_type_manager; + $this->servicenowManager = $servicenow_manager; + $this->currentUserSession = $current_user_session; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('config.factory'), + $container->get('entity_type.manager'), + $container->get('ys_servicenow.manager'), + $container->get('current_user'), + ); + } + + /** + * {@inheritdoc} + */ + public function getFormId() { + return 'ys_servicenow_settings'; + } + + /** + * {@inheritdoc} + */ + protected function getEditableConfigNames() { + return ['ys_servicenow.settings']; + } + + /** + * {@inheritdoc} + */ + public function buildForm(array $form, FormStateInterface $form_state) { + $config = $this->config('ys_servicenow.settings'); + + $allowSecretItems = function_exists('ys_core_allow_secret_items') ? ys_core_allow_secret_items($this->currentUserSession) : FALSE; + + if ( + $config->get('enable_servicenow_sync') && + $config->get('servicenow_endpoint') && + $config->get('servicenow_auth_key') + ) { + $form['sync_now_button'] = [ + '#type' => 'markup', + '#markup' => 'Sync now', + ]; + } + + $form['enable_servicenow_sync'] = [ + '#type' => 'checkbox', + '#title' => $this->t('Enable ServiceNow sync'), + '#description' => $this->t('Once enabled, ServiceNow data will sync knowledge base articles roughly every hour.'), + '#default_value' => $config->get('enable_servicenow_sync') ?: FALSE, + '#disabled' => !$allowSecretItems, + ]; + + $form['servicenow_endpoint'] = [ + '#type' => 'textarea', + '#title' => $this->t('ServiceNow Endpoint'), + '#default_value' => $config->get('servicenow_endpoint') ?: '', + ]; + + $form['servicenow_auth_key'] = [ + '#type' => 'key_select', + '#title' => $this->t('ServiceNow Authentication Credentials'), + '#default_value' => $config->get('servicenow_auth_key') ?: '', + ]; + + return parent::buildForm($form, $form_state); + } + + /** + * {@inheritdoc} + */ + public function validateForm(array &$form, FormStateInterface $form_state) { + $enabled = $form_state->getValue('enable_servicenow_sync'); + if ($enabled) { + $requiredFields = [ + 'servicenow_auth_key', + 'servicenow_endpoint', + ]; + + foreach ($requiredFields as $field) { + if (!$form_state->getValue($field)) { + $form_state->setErrorByName( + $field, + $this->t("%required_field is required.", ['%required_field' => $form_state->getCompleteForm()[$field]['#title']->__toString()]) + ); + } + } + + } + + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + + $this->configFactory->getEditable('ys_servicenow.settings') + // Set the submitted configuration setting. + ->set('enable_servicenow_sync', $form_state->getValue('enable_servicenow_sync')) + ->set('servicenow_auth_key', $form_state->getValue('servicenow_auth_key')) + ->set('servicenow_endpoint', $form_state->getValue('servicenow_endpoint')) + ->save(); + + parent::submitForm($form, $form_state); + } + +} diff --git a/web/profiles/custom/yalesites_profile/modules/custom/ys_servicenow/src/MetaFieldsManager.php b/web/profiles/custom/yalesites_profile/modules/custom/ys_servicenow/src/MetaFieldsManager.php new file mode 100644 index 0000000000..e69de29bb2 diff --git a/web/profiles/custom/yalesites_profile/modules/custom/ys_servicenow/src/Plugin/migrate/process/LayoutBuilderSections.php b/web/profiles/custom/yalesites_profile/modules/custom/ys_servicenow/src/Plugin/migrate/process/LayoutBuilderSections.php new file mode 100644 index 0000000000..a361b5b327 --- /dev/null +++ b/web/profiles/custom/yalesites_profile/modules/custom/ys_servicenow/src/Plugin/migrate/process/LayoutBuilderSections.php @@ -0,0 +1,84 @@ +getStorage('block_content')->getQuery(); + $entityQuery->condition('info', $value) + ->accessCheck(FALSE); + $ids = $entityQuery->execute(); + + $block_content = NULL; + if (!empty($ids)) { + $block_content = $entityTypeManager->getStorage('block_content')->load(reset($ids)); + } + + if (is_null($block_content)) { + \Drupal::messenger()->addError("Could not load " . $value . ' ???', 'status', TRUE); + return NULL; + } + + $config = [ + 'id' => 'inline_block:text', + 'label' => $block_content->label(), + 'provider' => 'layout_builder', + 'label_display' => FALSE, + 'view_mode' => 'full', + 'block_revision_id' => $block_content->getRevisionId(), + 'block_serialized' => serialize($block_content), + 'context_mapping' => [], + ]; + + $components[] = new SectionComponent($generator->generate(), 'content', $config); + + // If you were doing multiple sections, you'd want this to be an array + // somehow. @TODO figure out how to do that ;) + // PARAMS: $layout_id, $layout_settings, $components. + $sections = new Section('layout_onecol', [], $components); + + return $sections; + } + + /** + * {@inheritdoc} + */ + public function multiple() { + // Perhaps if multiple() returned TRUE this would help allow + // multiple Sections. ;) + return FALSE; + } + +} diff --git a/web/profiles/custom/yalesites_profile/modules/custom/ys_servicenow/src/Plugin/migrate_plus/authentication/ServiceNowAuth.php b/web/profiles/custom/yalesites_profile/modules/custom/ys_servicenow/src/Plugin/migrate_plus/authentication/ServiceNowAuth.php new file mode 100644 index 0000000000..b4b4f1d530 --- /dev/null +++ b/web/profiles/custom/yalesites_profile/modules/custom/ys_servicenow/src/Plugin/migrate_plus/authentication/ServiceNowAuth.php @@ -0,0 +1,31 @@ +get('servicenow_auth_key'); + $basicAuthWithKeys = new BasicAuthWithKeys($servicenow_config, $servicenow_key_id); + return $basicAuthWithKeys->getAuthenticationOptions(); + } + +} diff --git a/web/profiles/custom/yalesites_profile/modules/custom/ys_servicenow/src/ServiceNowManager.php b/web/profiles/custom/yalesites_profile/modules/custom/ys_servicenow/src/ServiceNowManager.php new file mode 100644 index 0000000000..c2fe83ef3a --- /dev/null +++ b/web/profiles/custom/yalesites_profile/modules/custom/ys_servicenow/src/ServiceNowManager.php @@ -0,0 +1,203 @@ +servicenowConfig = $config_factory->get('ys_servicenow.settings'); + $this->httpClient = $http_client; + $this->entityTypeManager = $entity_type_manager; + $this->migrationManager = $migration_manager; + $this->moduleHandler = $module_handler; + $this->time = $time; + $this->messenger = $messenger; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('config.factory'), + $container->get('http_client'), + $container->get('entity_type.manager'), + $container->get('plugin.manager.migration'), + $container->get('module_handler'), + $container->get('date.formatter'), + $container->get('messenger'), + ); + + } + + /** + * Runs specific migration. + * + * @param string $migration + * The migration ID. + */ + public function runMigration($migration) { + // Loop over the list of the migrations and check if they require + // execution. + // Prevent non-existent migrations from breaking cron. + $migrationInstance = $this->migrationManager->createInstance($migration); + if ($migrationInstance) { + // Check if the migration status is IDLE, if not, make it so. + $status = $migrationInstance->getStatus(); + if ($status !== MigrationInterface::STATUS_IDLE) { + $migrationInstance->setStatus(MigrationInterface::STATUS_IDLE); + } + + /* + * @todo Possibly implement the following flags, if needed. + * Runs migration with the --update flag. + * $migration_update = $migration->getIdMap(); + * $migration_update->prepareUpdate(); + * Runs migration with the --sync flag. + * The problem here is if editor adds layout builder, this will wipe those + * changes out before recreating. So, this not be a good idea. + * $migrationInstance->set('syncSource', TRUE); + */ + + // We choose to use syncSource since we want to recreate the block for + // update. + /*if ($migration === 'servicenow_knowledge_base_article_block') {*/ + /* $migrationInstance->set('syncSource', TRUE);*/ + /* $migration_update = $migrationInstance->getIdMap();*/ + /* $migration_update->prepareUpdate();*/ + /*}*/ + $message = new MigrateMessage(); + $executable = new MigrateExecutable($migrationInstance, $message); + $executable->import(); + + /* If using migrate_plus module, update the migrate_last_imported value + * for the migration. + */ + + if ($this->moduleHandler->moduleExists('migrate_plus')) { + $migrate_last_imported_store = $this->keyValue('migrate_last_imported'); + $migrate_last_imported_store->set($migrationInstance->id(), round($this->time->getCurrentMicroTime() * 1000)); + } + } + } + + /** + * Gets the migration status such as number of items imported. + * + * @return int + * For now, just the number of items imported. + */ + public function getMigrationStatus($migration_id) { + $migration = $this->migrationManager->createInstance($migration_id); + $map = $migration->getIdMap(); + $imported = $map->importedCount(); + return $imported; + } + + /** + * Runs all Localist migrations. + * + * @return array + * Array of status of all migrations run. + */ + public function runAllMigrations() { + foreach (self::SERVICENOW_MIGRATIONS as $migration) { + $this->runMigration($migration); + $messageData[$migration] = [ + 'imported' => $this->getMigrationStatus($migration), + ]; + } + + return $messageData; + } + +} diff --git a/web/profiles/custom/yalesites_profile/modules/custom/ys_servicenow/ys_servicenow.info.yml b/web/profiles/custom/yalesites_profile/modules/custom/ys_servicenow/ys_servicenow.info.yml new file mode 100644 index 0000000000..7a3d69928d --- /dev/null +++ b/web/profiles/custom/yalesites_profile/modules/custom/ys_servicenow/ys_servicenow.info.yml @@ -0,0 +1,10 @@ +name: 'YS ServiceNow' +type: module +description: 'Provides integration with the ServiceNow knowledge base platform' +package: YaleSites +core_version_requirement: ^10 +dependencies: + - drupal:migrate + - migrate_plus:migrate_plus + - migrate_tools:migrate_tools + - drupal:key diff --git a/web/profiles/custom/yalesites_profile/modules/custom/ys_servicenow/ys_servicenow.links.menu.yml b/web/profiles/custom/yalesites_profile/modules/custom/ys_servicenow/ys_servicenow.links.menu.yml new file mode 100644 index 0000000000..c97fc72830 --- /dev/null +++ b/web/profiles/custom/yalesites_profile/modules/custom/ys_servicenow/ys_servicenow.links.menu.yml @@ -0,0 +1,6 @@ +ys_servicenow.settings: + title: 'ServiceNow settings' + description: 'Configure ServiceNow sync settings' + route_name: ys_servicenow.settings + parent: system.admin_config_services + weight: 0 diff --git a/web/profiles/custom/yalesites_profile/modules/custom/ys_servicenow/ys_servicenow.module b/web/profiles/custom/yalesites_profile/modules/custom/ys_servicenow/ys_servicenow.module new file mode 100644 index 0000000000..5609e3bc6a --- /dev/null +++ b/web/profiles/custom/yalesites_profile/modules/custom/ys_servicenow/ys_servicenow.module @@ -0,0 +1,56 @@ +get('enable_servicenow_sync')) { + + $state = \Drupal::state(); + + $next_execution_time = $state->get("servicenow_migrations", 0); + $current_time = \Drupal::time()->getRequestTime(); + if ($current_time > $next_execution_time) { + $state->set("servicenow_migrations", $current_time + 3600); + $servicenowManager = \Drupal::service('ys_servicenow.manager'); + $servicenowManager->runAllMigrations(); + } + } +} + +/** + * Provide the URL endpoint for migrations in servicenow from config. + */ +function ys_servicenow_url_endpoint(MigrationInterface $migration): array { + // Get the servicenow configuration. + $servicenow_config = \Drupal::config('ys_servicenow.settings'); + // Get the endpoint type from the migration. + $endpoint = $servicenow_config->get('servicenow_endpoint'); + + if ($endpoint) { + return [$endpoint]; + } + + return []; +} + +/** + * Any state other than "Published" is unpublished. + */ +function ys_servicenow_moderation_state_transformation($value) { + $unpublished_state = 'archive'; + + $published_states = [ + 'Published' => 'published', + ]; + + return $published_states[$value] ?? $unpublished_state; +} diff --git a/web/profiles/custom/yalesites_profile/modules/custom/ys_servicenow/ys_servicenow.permissions.yml b/web/profiles/custom/yalesites_profile/modules/custom/ys_servicenow/ys_servicenow.permissions.yml new file mode 100644 index 0000000000..f4935e13c7 --- /dev/null +++ b/web/profiles/custom/yalesites_profile/modules/custom/ys_servicenow/ys_servicenow.permissions.yml @@ -0,0 +1,4 @@ +# Permission to manage YaleSites ServiceNow. +yalesites manage servicenow: + title: 'Manage YaleSites ServiceNow' + description: 'Set and change YaleSites ServiceNow settings.' diff --git a/web/profiles/custom/yalesites_profile/modules/custom/ys_servicenow/ys_servicenow.routing.yml b/web/profiles/custom/yalesites_profile/modules/custom/ys_servicenow/ys_servicenow.routing.yml new file mode 100644 index 0000000000..36c48a1363 --- /dev/null +++ b/web/profiles/custom/yalesites_profile/modules/custom/ys_servicenow/ys_servicenow.routing.yml @@ -0,0 +1,14 @@ +ys_servicenow.settings: + path: '/admin/yalesites/servicenow' + defaults: + _form: '\Drupal\ys_servicenow\Form\ServiceNowSettings' + _title: 'ServiceNow Settings' + requirements: + _permission: 'yalesites manage servicenow' +ys_servicenow.run_migrations: + path: '/admin/yalesites/servicenow/sync' + defaults: + _controller: '\Drupal\ys_servicenow\Controller\RunMigrations::runAllMigrations' + _title: 'Import ServiceNow events now' + requirements: + _permission: 'yalesites manage servicenow' diff --git a/web/profiles/custom/yalesites_profile/modules/custom/ys_servicenow/ys_servicenow.services.yml b/web/profiles/custom/yalesites_profile/modules/custom/ys_servicenow/ys_servicenow.services.yml new file mode 100644 index 0000000000..a14c27b508 --- /dev/null +++ b/web/profiles/custom/yalesites_profile/modules/custom/ys_servicenow/ys_servicenow.services.yml @@ -0,0 +1,5 @@ +services: + # Service for interacting with ServiceNow sync and configuration. + ys_servicenow.manager: + class: Drupal\ys_servicenow\ServiceNowManager + arguments: ['@config.factory', '@http_client', '@entity_type.manager', '@plugin.manager.migration', '@module_handler', '@datetime.time', '@messenger'] diff --git a/web/profiles/custom/yalesites_profile/modules/custom/ys_themes/ys_themes.module b/web/profiles/custom/yalesites_profile/modules/custom/ys_themes/ys_themes.module index d675ad1140..af8277d0fc 100644 --- a/web/profiles/custom/yalesites_profile/modules/custom/ys_themes/ys_themes.module +++ b/web/profiles/custom/yalesites_profile/modules/custom/ys_themes/ys_themes.module @@ -17,15 +17,13 @@ use Drupal\field\Entity\FieldStorageConfig; * The field definition. * @param \Drupal\Core\Entity\ContentEntityInterface|null $entity * The entity being created, if applicable. - * @param bool $cacheable - * Boolean indicating if the results are cache-able. * * @return array * An array of possible key and value options. * * @see options_allowed_values() */ -function ys_themes_allowed_values_function(FieldStorageConfig $definition, ContentEntityInterface $entity = NULL, $cacheable) { +function ys_themes_allowed_values_function(FieldStorageConfig $definition, ?ContentEntityInterface $entity = NULL) { $options = []; $config = \Drupal::config('ys_themes.component_overrides'); diff --git a/web/profiles/custom/yalesites_profile/modules/custom/ys_views_basic/assets/js/views-basic.js b/web/profiles/custom/yalesites_profile/modules/custom/ys_views_basic/assets/js/views-basic.js index c6a8580b13..8dbcc3a121 100644 --- a/web/profiles/custom/yalesites_profile/modules/custom/ys_views_basic/assets/js/views-basic.js +++ b/web/profiles/custom/yalesites_profile/modules/custom/ys_views_basic/assets/js/views-basic.js @@ -75,6 +75,28 @@ displayElement.addEventListener('change', updateLimitElement); updateLimitElement(); } + + // Unified selectors to handle both cases + const entityTypesSelector = 'input[name="settings[block_form][group_user_selection][entity_and_view_mode][entity_types]"], input[name="block_form[group_user_selection][entity_and_view_mode][entity_types]"]'; + const viewModeSelector = 'input[name="settings[block_form][group_user_selection][entity_and_view_mode][view_mode]"], input[name="block_form[group_user_selection][entity_and_view_mode][view_mode]"]'; + const eventTimePeriod = document.querySelector('#edit-event-time-period'); + + const entityTypes = document.querySelectorAll(entityTypesSelector); + const viewModes = document.querySelectorAll(viewModeSelector); + + // Function to handle visibility based on conditions + function updateVisibility() { + const entityType = Array.from(entityTypes).find(input => input.checked)?.value; + const viewMode = Array.from(viewModes).find(input => input.checked)?.value; + + if (eventTimePeriod) { + eventTimePeriod.style.display = (entityType === 'event' && viewMode === 'calendar') ? 'none' : ''; + } + } + + entityTypes.forEach(input => input.addEventListener('change', updateVisibility)); + viewModes.forEach(input => input.addEventListener('change', updateVisibility)); + updateVisibility(); }, }; })(Drupal); diff --git a/web/profiles/custom/yalesites_profile/modules/custom/ys_views_basic/src/Controller/EventsCalendarController.php b/web/profiles/custom/yalesites_profile/modules/custom/ys_views_basic/src/Controller/EventsCalendarController.php new file mode 100644 index 0000000000..e43e8968db --- /dev/null +++ b/web/profiles/custom/yalesites_profile/modules/custom/ys_views_basic/src/Controller/EventsCalendarController.php @@ -0,0 +1,66 @@ +get('ys_views_basic.events_calendar'), + ); + } + + /** + * Builds the response. + */ + public function __invoke(Request $request): AjaxResponse { + $response = new AjaxResponse(); + + if (!$request->request->has('calendar_id') || !$request->request->has('month') || !$request->request->has('year')) { + return $response; + } + + // Calendar wrapper that needs to be updated. + $calendar_id = $request->request->get('calendar_id'); + $month = $request->request->get('month'); + $year = $request->request->get('year'); + + $events_calendar = $this->eventsCalendar + ->getCalendar($month, $year); + + $calendar = [ + '#theme' => 'views_basic_events_calendar', + '#month_data' => $events_calendar, + '#cache' => [ + 'tags' => ['node_list:event'], + ], + ]; + + $response->addCommand(new ReplaceCommand($calendar_id, $calendar)); + + return $response; + } + +} diff --git a/web/profiles/custom/yalesites_profile/modules/custom/ys_views_basic/src/Plugin/Field/FieldFormatter/ViewsBasicDefaultFormatter.php b/web/profiles/custom/yalesites_profile/modules/custom/ys_views_basic/src/Plugin/Field/FieldFormatter/ViewsBasicDefaultFormatter.php index 3428c952e7..56ffeb9444 100644 --- a/web/profiles/custom/yalesites_profile/modules/custom/ys_views_basic/src/Plugin/Field/FieldFormatter/ViewsBasicDefaultFormatter.php +++ b/web/profiles/custom/yalesites_profile/modules/custom/ys_views_basic/src/Plugin/Field/FieldFormatter/ViewsBasicDefaultFormatter.php @@ -6,7 +6,7 @@ use Drupal\Core\Field\FieldItemListInterface; use Drupal\Core\Field\FormatterBase; use Drupal\Core\Plugin\ContainerFactoryPluginInterface; -use Drupal\Core\Render\Renderer; +use Drupal\ys_views_basic\Service\EventsCalendarInterface; use Drupal\ys_views_basic\ViewsBasicManager; use Symfony\Component\DependencyInjection\ContainerInterface; @@ -28,14 +28,14 @@ class ViewsBasicDefaultFormatter extends FormatterBase implements ContainerFacto * * @var \Drupal\ys_views_basic\ViewsBasicManager */ - protected $viewsBasicManager; + protected ViewsBasicManager $viewsBasicManager; /** - * The renderer service. + * The Events Calendar service. * - * @var \Drupal\Core\Render\Renderer + * @var \Drupal\ys_views_basic\Service\EventsCalendarInterface */ - protected $rendererService; + protected EventsCalendarInterface $eventsCalendar; /** * Constructs an views basic default formatter object. @@ -54,10 +54,10 @@ class ViewsBasicDefaultFormatter extends FormatterBase implements ContainerFacto * The view mode. * @param array $third_party_settings * Any third party settings. - * @param \Drupal\ys_views_basic\Plugin\ViewsBasicManager $viewsBasicManager + * @param \Drupal\ys_views_basic\ViewsBasicManager $viewsBasicManager * The views basic manager service. - * @param \Drupal\Core\Render\Renderer $renderer_service - * Drupal Core renderer service. + * @param \Drupal\ys_views_basic\Service\EventsCalendarInterface $eventsCalendar + * The Events Calendar service. */ public function __construct( string $plugin_id, @@ -68,7 +68,7 @@ public function __construct( string $view_mode, array $third_party_settings, ViewsBasicManager $viewsBasicManager, - Renderer $renderer_service, + EventsCalendarInterface $eventsCalendar, ) { parent::__construct( $plugin_id, @@ -78,20 +78,15 @@ public function __construct( $label, $view_mode, $third_party_settings, - $this->rendererService = $renderer_service, ); $this->viewsBasicManager = $viewsBasicManager; + $this->eventsCalendar = $eventsCalendar; } /** * {@inheritdoc} */ - public static function create( - ContainerInterface $container, - array $configuration, - $plugin_id, - $plugin_definition, - ) { + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { return new static( $plugin_id, $plugin_definition, @@ -101,33 +96,57 @@ public static function create( $configuration['view_mode'], $configuration['third_party_settings'], $container->get('ys_views_basic.views_basic_manager'), - $container->get('renderer'), + $container->get('ys_views_basic.events_calendar') ); } /** * Define how the field type is showed. */ - public function viewElements(FieldItemListInterface $items, $langcode) { + public function viewElements(FieldItemListInterface $items, $langcode): array { $elements = []; + foreach ($items as $delta => $item) { + // Get decoded parameters. + $paramsDecoded = json_decode($item->getValue()['params'], TRUE); + + if ($paramsDecoded['filters']['types'][0] === 'event' && $paramsDecoded['view_mode'] === 'calendar') { + // Calculate the remaining time until the end of the current month. + $now = new \DateTime(); + $end_of_month = new \DateTime('last day of this month 23:59:59'); + $remaining_time_in_seconds = $end_of_month->getTimestamp() - $now->getTimestamp(); - $view = $this->viewsBasicManager->getView('rendered', $item->getValue()['params']); + $events_calendar = $this->eventsCalendar + ->getCalendar(date('m'), date('Y')); - $elements[$delta] = [ - '#theme' => 'views_basic_formatter_default', - '#view' => $view, - // Extract exposed filters from the view and place them separately. - // This is necessary because we are conditionally displaying - // specific exposed filters based on field configuration. - // By placing the exposed filters outside of the view rendering - // context, we ensure that they do not get re-rendered - // when AJAX operations are performed on the view, - // allowing for better control over which filters are displayed - // and maintaining the expected user interface behavior. - '#exposed' => $view['#view']->exposed_widgets, - ]; + $elements[$delta] = [ + '#theme' => 'views_basic_events_calendar', + '#month_data' => $events_calendar, + '#cache' => [ + 'tags' => ['node_list:event'], + // Set max-age to the remaining time until the end of the month. + 'max-age' => $remaining_time_in_seconds, + 'contexts' => ['timezone'], + ], + ]; + } + else { + $view = $this->viewsBasicManager->getView('rendered', $item->getValue()['params']); + $elements[$delta] = [ + '#theme' => 'views_basic_formatter_default', + '#view' => $view, + // Extract exposed filters from the view and place them separately. + // This is necessary because we are conditionally displaying + // specific exposed filters based on field configuration. + // By placing the exposed filters outside of the view rendering + // context, we ensure that they do not get re-rendered + // when AJAX operations are performed on the view, + // allowing for better control over which filters are displayed + // and maintaining the expected user interface behavior. + '#exposed' => $view['#view']->exposed_widgets, + ]; + } } return $elements; diff --git a/web/profiles/custom/yalesites_profile/modules/custom/ys_views_basic/src/Plugin/Field/FieldWidget/ViewsBasicDefaultWidget.php b/web/profiles/custom/yalesites_profile/modules/custom/ys_views_basic/src/Plugin/Field/FieldWidget/ViewsBasicDefaultWidget.php index 468fbe71df..7680c5f2dc 100644 --- a/web/profiles/custom/yalesites_profile/modules/custom/ys_views_basic/src/Plugin/Field/FieldWidget/ViewsBasicDefaultWidget.php +++ b/web/profiles/custom/yalesites_profile/modules/custom/ys_views_basic/src/Plugin/Field/FieldWidget/ViewsBasicDefaultWidget.php @@ -219,6 +219,11 @@ public function formElement( '#suffix' => '', ]; + // Define the state for when the view mode is 'calendar' once. + $calendarViewInvisibleState = [ + $formSelectors['view_mode_input_selector'] => ['value' => 'calendar'], + ]; + $fieldOptionValue = ($items[$delta]->params) ? $this->viewsBasicManager->getDefaultParamValue('field_options', $items[$delta]->params) : []; $fieldOptionDefaultValue = $fieldOptionValue ?? ['show_thumbnail' => 'show_thumbnail']; $isNewForm = str_contains($formState->getCompleteForm()['#id'], 'layout-builder-add-block'); @@ -234,6 +239,7 @@ public function formElement( '#title' => $this->t('Field Display Options'), '#tree' => TRUE, '#default_value' => ($isNewForm && empty($fieldOptionValue)) ? ['show_thumbnail'] : $fieldOptionDefaultValue, + '#states' => ['invisible' => $calendarViewInvisibleState], 'show_thumbnail' => [ '#states' => [ 'visible' => [ @@ -256,14 +262,9 @@ public function formElement( '#title' => $this->t('Exposed Filter Options'), '#tree' => TRUE, '#default_value' => ($items[$delta]->params) ? $this->viewsBasicManager->getDefaultParamValue('exposed_filter_options', $items[$delta]->params) : [], + '#states' => ['invisible' => $calendarViewInvisibleState], 'show_year_filter' => [ - '#states' => [ - 'visible' => [ - $formSelectors['entity_types_ajax'] => [ - 'value' => 'post', - ], - ], - ], + '#states' => ['visible' => [$formSelectors['entity_types_ajax'] => ['value' => 'post']]], ], ]; @@ -273,9 +274,8 @@ public function formElement( '#description' => $this->t("Enter a custom label for the Category Filter. This label will be displayed to users as the filter's name. If left blank, the default label Category will be used."), '#default_value' => ($items[$delta]->params) ? $this->viewsBasicManager->getDefaultParamValue('category_filter_label', $items[$delta]->params) : NULL, '#states' => [ - 'visible' => [ - $formSelectors['show_category_filter_selector'] => ['checked' => TRUE], - ], + 'visible' => [$formSelectors['show_category_filter_selector'] => ['checked' => TRUE]], + 'invisible' => $calendarViewInvisibleState, ], ]; @@ -291,9 +291,8 @@ public function formElement( '#prefix' => '
', '#suffix' => '
', '#states' => [ - 'visible' => [ - $formSelectors['show_category_filter_selector'] => ['checked' => TRUE], - ], + 'visible' => [$formSelectors['show_category_filter_selector'] => ['checked' => TRUE]], + 'invisible' => $calendarViewInvisibleState, ], ]; @@ -306,6 +305,7 @@ public function formElement( '#tags' => TRUE, '#target_type' => 'taxonomy_term', '#default_value' => ($items[$delta]->params) ? $this->viewsBasicManager->getDefaultParamValue('terms_include', $items[$delta]->params) : [], + '#states' => ['invisible' => $calendarViewInvisibleState], ]; $form['group_user_selection']['filter_and_sort']['terms_exclude'] = [ @@ -317,6 +317,7 @@ public function formElement( '#tags' => TRUE, '#target_type' => 'taxonomy_term', '#default_value' => ($items[$delta]->params) ? $this->viewsBasicManager->getDefaultParamValue('terms_exclude', $items[$delta]->params) : [], + '#states' => ['invisible' => $calendarViewInvisibleState], ]; $form['group_user_selection']['filter_and_sort']['term_operator'] = [ @@ -333,7 +334,7 @@ public function formElement( 'term-operator-item', ], ], - + '#states' => ['invisible' => $calendarViewInvisibleState], ]; // Gets the view mode options based on Ajax callbacks or initial load. @@ -349,6 +350,7 @@ public function formElement( '#validated' => 'true', '#prefix' => '
', '#suffix' => '
', + '#states' => ['invisible' => $calendarViewInvisibleState], ]; $form['group_user_selection']['entity_specific']['event_time_period'] = [ @@ -360,13 +362,8 @@ public function formElement( 'all' => $this->t('All Events') . 'All Events icon showing a calendar.', ], '#default_value' => ($items[$delta]->params) ? $this->viewsBasicManager->getDefaultParamValue('event_time_period', $items[$delta]->params) : 'future', - '#states' => [ - 'visible' => [ - $formSelectors['entity_types_ajax'] => [ - 'value' => 'event', - ], - ], - ], + '#prefix' => '
', + '#suffix' => '
', ]; $displayValue = ($items[$delta]->params) ? $this->viewsBasicManager->getDefaultParamValue('display', $items[$delta]->params) : 'all'; @@ -381,6 +378,7 @@ public function formElement( 'limit' => $this->t('Limit to'), 'pager' => $this->t('Pagination after'), ], + '#states' => ['invisible' => $calendarViewInvisibleState], ]; $limitTitle = $this->t('Items'); @@ -401,6 +399,7 @@ public function formElement( '#required' => TRUE, '#prefix' => '
', '#suffix' => '
', + '#states' => ['invisible' => $calendarViewInvisibleState], ]; $form['group_user_selection']['options']['offset'] = [ @@ -412,6 +411,7 @@ public function formElement( '#attributes' => [ 'placeholder' => 0, ], + '#states' => ['invisible' => $calendarViewInvisibleState], ]; $element['group_params']['params'] = [ @@ -424,6 +424,7 @@ public function formElement( 'views-basic--params', ], ], + '#states' => ['invisible' => $calendarViewInvisibleState], ]; $form['#attached']['library'][] = 'ys_views_basic/ys_views_basic'; diff --git a/web/profiles/custom/yalesites_profile/modules/custom/ys_views_basic/src/Plugin/views/filter/PostYearFilter.php b/web/profiles/custom/yalesites_profile/modules/custom/ys_views_basic/src/Plugin/views/filter/PostYearFilter.php index bb9dc2b371..5c9b2f9abc 100644 --- a/web/profiles/custom/yalesites_profile/modules/custom/ys_views_basic/src/Plugin/views/filter/PostYearFilter.php +++ b/web/profiles/custom/yalesites_profile/modules/custom/ys_views_basic/src/Plugin/views/filter/PostYearFilter.php @@ -56,7 +56,7 @@ public static function create(ContainerInterface $container, array $configuratio /** * {@inheritdoc} */ - public function init(ViewExecutable $view, DisplayPluginBase $display, array &$options = NULL) { + public function init(ViewExecutable $view, DisplayPluginBase $display, ?array &$options = NULL) { parent::init($view, $display, $options); $this->valueTitle = $this->t('Post Year Filter'); diff --git a/web/profiles/custom/yalesites_profile/modules/custom/ys_views_basic/src/Service/EventsCalendar.php b/web/profiles/custom/yalesites_profile/modules/custom/ys_views_basic/src/Service/EventsCalendar.php new file mode 100644 index 0000000000..9f54e55849 --- /dev/null +++ b/web/profiles/custom/yalesites_profile/modules/custom/ys_views_basic/src/Service/EventsCalendar.php @@ -0,0 +1,280 @@ +entityTypeManager = $entity_type_manager; + $this->aliasManager = $alias_manager; + $this->nodeStorage = $entity_type_manager->getStorage('node'); + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container): static { + return new static( + $container->get('entity_type.manager'), + $container->get('path_alias.manager') + ); + } + + /** + * {@inheritdoc} + */ + public function getCalendar(string $month, string $year): array { + // Create a date object for the first day of the given month and year. + $firstDayOfMonth = new DrupalDateTime("$year-$month-01"); + $totalDaysInMonth = (int) $firstDayOfMonth->format('t'); + $startDayOfWeek = (int) $firstDayOfMonth->format('w'); + $lastDayOfMonth = new DrupalDateTime("$year-$month-$totalDaysInMonth"); + $endDayOfWeek = (int) $lastDayOfMonth->format('w'); + + $paddingStart = $startDayOfWeek; + $paddingEnd = 6 - $endDayOfWeek; + $totalCells = $totalDaysInMonth + $paddingStart + $paddingEnd; + $totalRows = (int) ceil($totalCells / 7); + $calendarRows = []; + + // Calculate the previous month and year. + $previousMonthDate = clone $firstDayOfMonth; + $previousMonthDate->modify('-1 month'); + $daysInPreviousMonth = (int) $previousMonthDate->format('t'); + $previousMonth = $previousMonthDate->format('m'); + $previousYear = $previousMonthDate->format('Y'); + + // Calculate the next month and year. + $nextMonthDate = clone $lastDayOfMonth; + $nextMonthDate->modify('+1 month'); + $nextMonth = $nextMonthDate->format('m'); + $nextYear = $nextMonthDate->format('Y'); + + // Load all events for the given month and year. + $monthlyEvents = $this->loadMonthlyEvents($month, $year); + + $currentDay = 1; + + for ($row = 0; $row < $totalRows; $row++) { + $calendarRows[$row] = []; + for ($cell = 0; $cell < 7; $cell++) { + if ($row == 0 && $cell < $paddingStart) { + // Fill in days from the previous month. + $day = $daysInPreviousMonth - ($paddingStart - $cell - 1); + $calendarRows[$row][] = $this->createCalendarCell($day, $previousMonth, $previousYear, $monthlyEvents); + } + elseif ($row == $totalRows - 1 && $cell > $endDayOfWeek) { + // Fill in days from the next month. + $day = $cell - $endDayOfWeek; + $calendarRows[$row][] = $this->createCalendarCell($day, $nextMonth, $nextYear, $monthlyEvents); + } + else { + // Normal date cell within the current month. + $calendarRows[$row][] = $this->createCalendarCell($currentDay, $month, $year, $monthlyEvents); + $currentDay++; + } + } + } + + return $calendarRows; + } + + /** + * {@inheritdoc} + */ + public function createCalendarCell(int $day, string $month, string $year, array $events): array { + return [ + 'date' => [ + 'day' => str_pad($day, 2, '0', STR_PAD_LEFT), + 'month' => $month, + 'year' => $year, + ], + 'events' => $this->getEvents($day, $month, $year, $events), + ]; + } + + /** + * {@inheritdoc} + */ + public function getEvents(int $day, string $month, string $year, array $events): array { + $startDate = new DrupalDateTime("$year-$month-$day 00:00:00"); + $endDate = new DrupalDateTime("$year-$month-$day 23:59:59"); + + $startTimestamp = $startDate->getTimestamp(); + $endTimestamp = $endDate->getTimestamp(); + + $events_data = []; + foreach ($events as $event) { + if (!$event->get('field_event_date')->isEmpty()) { + // Handle recurrence rules if present. + if ($event->field_event_date?->rrule) { + + /** @var \Drupal\smart_date_recur\Entity\SmartDateRule $rule */ + $rule = $this->entityTypeManager->getStorage('smart_date_rule') + ->load($event->field_event_date->rrule); + + if ($rule instanceof SmartDateRule) { + // Iterate over the stored instances to find occurrences for the + // current day. + foreach ($rule->getStoredInstances() as $instance) { + $instanceStartTimestamp = $instance['value']; + $instanceEndTimestamp = $instance['end_value']; + + // Check if the instance overlaps with the current day. + if ($instanceStartTimestamp <= $endTimestamp && $instanceEndTimestamp >= $startTimestamp) { + $time = $this->isAllDay($instanceStartTimestamp, $instanceEndTimestamp) + ? $this->t('All Day') + : $this->t('@start to @end', [ + '@start' => date('g:iA', $instanceStartTimestamp), + '@end' => date('g:iA', $instanceEndTimestamp), + ]); + + $events_data[] = $this->createEventArray($event, $time, $instanceStartTimestamp); + } + } + } + } + else { + // Iterate through the nodes to extract event details. + foreach ($event->get('field_event_date')->getValue() as $eventDate) { + $eventStartTimestamp = $eventDate['value']; + $eventEndTimestamp = $eventDate['end_value']; + + // Check if the event overlaps with the current day. + if ($eventStartTimestamp <= $endTimestamp && $eventEndTimestamp >= $startTimestamp) { + if (date('Y-m-d', $eventStartTimestamp) !== date('Y-m-d', $eventEndTimestamp)) { + $time = $this->t('Multi-day Event'); + } + else { + $time = $this->isAllDay($eventStartTimestamp, $eventEndTimestamp) + ? $this->t('All Day') + : $this->t('@start to @end', [ + '@start' => date('g:iA', $eventStartTimestamp), + '@end' => date('g:iA', $eventEndTimestamp), + ]); + } + + // Add event to the list if it overlaps with the current day. + $events_data[] = $this->createEventArray($event, $time, $eventStartTimestamp); + } + } + } + } + } + + // Sort events by the start timestamp. + usort($events_data, function ($a, $b) { + return $a['timestamp'] <=> $b['timestamp']; + }); + + return $events_data; + } + + /** + * {@inheritdoc} + */ + public function createEventArray($node, string $time, int $timestamp): array { + // Extract the event's categories. + $categories = implode(' | ', array_map(function ($term) { + return $term->label(); + }, $node->get('field_category')->referencedEntities())); + + // Extract the event's tags. + $tags = array_map(fn($term) => $term->label(), $node->get('field_tags')->referencedEntities()); + + // Build and return the event array. + return [ + 'category' => $categories, + 'title' => $node->label(), + 'url' => $this->aliasManager->getAliasByPath('/node/' . $node->id()), + 'time' => $time, + 'type' => $tags, + 'timestamp' => $timestamp, + ]; + } + + /** + * {@inheritdoc} + */ + public function isAllDay(int $start_ts, int $end_ts, ?string $timezone = NULL): bool { + if ($timezone) { + $default_tz = date_default_timezone_get(); + date_default_timezone_set($timezone); + } + + $temp_start = date('H:i', $start_ts); + $temp_end = date('H:i', $end_ts); + + if ($timezone) { + date_default_timezone_set($default_tz); + } + + return $temp_start == '00:00' && $temp_end == '23:59'; + } + + /** + * {@inheritdoc} + */ + public function loadMonthlyEvents(string $month, string $year): array { + $startDate = new DrupalDateTime("$year-$month-01 00:00:00"); + $endDate = clone $startDate; + $endDate->modify('last day of this month 23:59:59'); + + // Query to fetch event nodes that overlap with the given month. + $query = $this->nodeStorage->getQuery() + ->accessCheck() + ->condition('type', 'event') + ->condition('status', 1) + ->condition('field_event_date.value', $endDate->getTimestamp(), '<=') + ->condition('field_event_date.end_value', $startDate->getTimestamp(), '>='); + + $nids = $query->execute(); + return $this->nodeStorage->loadMultiple($nids); + } + +} diff --git a/web/profiles/custom/yalesites_profile/modules/custom/ys_views_basic/src/Service/EventsCalendarInterface.php b/web/profiles/custom/yalesites_profile/modules/custom/ys_views_basic/src/Service/EventsCalendarInterface.php new file mode 100644 index 0000000000..4c46404786 --- /dev/null +++ b/web/profiles/custom/yalesites_profile/modules/custom/ys_views_basic/src/Service/EventsCalendarInterface.php @@ -0,0 +1,102 @@ + '/profiles/custom/yalesites_profile/modules/custom/ys_views_basic/assets/icons/display-type-condensed.svg', 'img_alt' => 'Icon showing 3 generic list items one on top of the other with no images on the items.', ], + 'calendar' => [ + 'label' => 'Calendar', + 'img' => '/profiles/custom/yalesites_profile/modules/custom/ys_views_basic/assets/icons/content-type-event.svg', + 'img_alt' => 'Calendar', + ], ], 'sort_by' => [ 'field_event_date:DESC' => 'Event Date - newer first', diff --git a/web/profiles/custom/yalesites_profile/modules/custom/ys_views_basic/templates/views-basic-events-calendar.html.twig b/web/profiles/custom/yalesites_profile/modules/custom/ys_views_basic/templates/views-basic-events-calendar.html.twig new file mode 100644 index 0000000000..7d089c2a19 --- /dev/null +++ b/web/profiles/custom/yalesites_profile/modules/custom/ys_views_basic/templates/views-basic-events-calendar.html.twig @@ -0,0 +1,5 @@ +{{ attach_library('atomic/calendar') }} + +{% include "@organisms/calendar/yds-calendar.twig" with { + month:month_data +} %} diff --git a/web/profiles/custom/yalesites_profile/modules/custom/ys_views_basic/ys_views_basic.module b/web/profiles/custom/yalesites_profile/modules/custom/ys_views_basic/ys_views_basic.module index a58c13d946..70ad89fb59 100644 --- a/web/profiles/custom/yalesites_profile/modules/custom/ys_views_basic/ys_views_basic.module +++ b/web/profiles/custom/yalesites_profile/modules/custom/ys_views_basic/ys_views_basic.module @@ -32,6 +32,11 @@ function ys_views_basic_theme($existing, $type, $theme, $path): array { 'contentType' => NULL, ], ], + 'views_basic_events_calendar' => [ + 'variables' => [ + 'month_data' => [], + ], + ], ]; } diff --git a/web/profiles/custom/yalesites_profile/modules/custom/ys_views_basic/ys_views_basic.routing.yml b/web/profiles/custom/yalesites_profile/modules/custom/ys_views_basic/ys_views_basic.routing.yml new file mode 100644 index 0000000000..818b81880f --- /dev/null +++ b/web/profiles/custom/yalesites_profile/modules/custom/ys_views_basic/ys_views_basic.routing.yml @@ -0,0 +1,7 @@ +ys_views_basic.events_calendar: + path: '/events-calendar' + defaults: + _title: 'Events Calendar' + _controller: '\Drupal\ys_views_basic\Controller\EventsCalendarController' + requirements: + _permission: 'access content' diff --git a/web/profiles/custom/yalesites_profile/modules/custom/ys_views_basic/ys_views_basic.services.yml b/web/profiles/custom/yalesites_profile/modules/custom/ys_views_basic/ys_views_basic.services.yml index 4039bf8a2f..0a37a9f47d 100644 --- a/web/profiles/custom/yalesites_profile/modules/custom/ys_views_basic/ys_views_basic.services.yml +++ b/web/profiles/custom/yalesites_profile/modules/custom/ys_views_basic/ys_views_basic.services.yml @@ -3,3 +3,7 @@ services: ys_views_basic.views_basic_manager: class: Drupal\ys_views_basic\ViewsBasicManager arguments: ['@entity_type.manager', '@entity_display.repository'] + + ys_views_basic.events_calendar: + class: Drupal\ys_views_basic\Service\EventsCalendar + arguments: ['@entity_type.manager', '@path_alias.manager']