Merge pull request #577 from tainacan/feature/358

Related Items - Feature/358
This commit is contained in:
Vinícius Nunes Medeiros 2021-07-05 16:03:15 -03:00 committed by GitHub
commit 4d2e1e3674
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
51 changed files with 2763 additions and 770 deletions

View File

@ -40,7 +40,9 @@ sass -E 'UTF-8' --cache-location .tmp/sass-cache-15 src/views/gutenberg-blocks/t
sass -E 'UTF-8' --cache-location .tmp/sass-cache-16 src/views/gutenberg-blocks/tainacan-items/item-submission-form/item-submission-form.scss:src/assets/css/tainacan-gutenberg-block-item-submission-form.css sass -E 'UTF-8' --cache-location .tmp/sass-cache-16 src/views/gutenberg-blocks/tainacan-items/item-submission-form/item-submission-form.scss:src/assets/css/tainacan-gutenberg-block-item-submission-form.css
sass -E 'UTF-8' --cache-location .tmp/sass-cache-17 src/views/gutenberg-blocks/gutenberg-blocks-style.scss:src/assets/css/tainacan-gutenberg-block-common-styles.css sass -E 'UTF-8' --cache-location .tmp/sass-cache-17 src/views/gutenberg-blocks/tainacan-items/carousel-related-items/carousel-related-items.scss:src/assets/css/tainacan-gutenberg-block-carousel-related-items.css
sass -E 'UTF-8' --cache-location .tmp/sass-cache-18 src/views/gutenberg-blocks/gutenberg-blocks-style.scss:src/assets/css/tainacan-gutenberg-block-common-styles.css
echo "Compilação do Sass Concluído!" echo "Compilação do Sass Concluído!"
exit 0 exit 0

View File

@ -122,7 +122,7 @@
color: var(--tainacan-block-gray5, #454647); color: var(--tainacan-block-gray5, #454647);
font-weight: bold; font-weight: bold;
text-decoration: none; text-decoration: none;
padding: 8px 16px; padding: 8px 12px;
display: block; display: block;
line-height: 1.2em; line-height: 1.2em;
word-break: break-word; } word-break: break-word; }

View File

@ -129,7 +129,7 @@
color: var(--tainacan-block-gray5, #454647); color: var(--tainacan-block-gray5, #454647);
font-weight: bold; font-weight: bold;
text-decoration: none; text-decoration: none;
padding: 8px 16px; padding: 8px 12px;
display: block; display: block;
line-height: 1.2em; line-height: 1.2em;
word-break: break-word; } word-break: break-word; }

View File

@ -0,0 +1,51 @@
.wp-block-tainacan-carousel-related-items {
margin: 0.5em auto;
width: 100%; }
.wp-block-tainacan-carousel-related-items .spinner-container {
min-height: 56px;
padding: 1em;
display: flex;
justify-content: center;
align-items: center;
color: var(--tainacan-block-gray4, #555758); }
@-webkit-keyframes skeleton-animation {
0% {
opacity: 1.0; }
50% {
opacity: 0.2; }
100% {
opacity: 1.0; } }
@-moz-keyframes skeleton-animation {
0% {
opacity: 1.0; }
50% {
opacity: 0.2; }
100% {
opacity: 1.0; } }
@-o-keyframes skeleton-animation {
0% {
opacity: 1.0; }
50% {
opacity: 0.2; }
100% {
opacity: 1.0; } }
@keyframes skeleton-animation {
0% {
opacity: 1.0; }
50% {
opacity: 0.2; }
100% {
opacity: 1.0; } }
.wp-block-tainacan-carousel-related-items .skeleton {
border-radius: 2px;
background: var(--tainacan-block-gray1, #f2f2f2);
-webkit-animation: skeleton-animation 1.8s ease infinite;
-moz-animation: skeleton-animation 1.8s ease infinite;
-o-animation: skeleton-animation 1.8s ease infinite;
animation: skeleton-animation 1.8s ease infinite; }
.wp-block-tainacan-carousel-related-items .carousel-related-items-edit-container {
position: relative; }
.wp-block-tainacan-carousel-related-items .carousel-related-items-edit-container .skeleton {
min-height: 150px; }
/*# sourceMappingURL=tainacan-gutenberg-block-carousel-related-items.css.map */

View File

@ -0,0 +1,7 @@
{
"version": 3,
"mappings": "AAEA,yCAA0C;EACtC,MAAM,EAAE,UAAU;EAClB,KAAK,EAAE,IAAI;EAGX,4DAAmB;IACf,UAAU,EAAE,IAAI;IAChB,OAAO,EAAE,GAAG;IACZ,OAAO,EAAE,IAAI;IACb,eAAe,EAAE,MAAM;IACvB,WAAW,EAAE,MAAM;IACnB,KAAK,EAAE,oCAAmC;AAI9C,qCAIC;EAHG,EAAE;IAAC,OAAO,EAAE,GAAG;EACf,GAAG;IAAC,OAAO,EAAE,GAAG;EAChB,IAAI;IAAC,OAAO,EAAE,GAAG;AAErB,kCAIC;EAHG,EAAE;IAAC,OAAO,EAAE,GAAG;EACf,GAAG;IAAC,OAAO,EAAE,GAAG;EAChB,IAAI;IAAC,OAAO,EAAE,GAAG;AAErB,gCAIC;EAHG,EAAE;IAAC,OAAO,EAAE,GAAG;EACf,GAAG;IAAC,OAAO,EAAE,GAAG;EAChB,IAAI;IAAC,OAAO,EAAE,GAAG;AAErB,6BAIC;EAHG,EAAE;IAAC,OAAO,EAAE,GAAG;EACf,GAAG;IAAC,OAAO,EAAE,GAAG;EAChB,IAAI;IAAC,OAAO,EAAE,GAAG;EAErB,mDAAU;IACN,aAAa,EAAE,GAAG;IAClB,UAAU,EAAE,oCAAmC;IAE/C,iBAAiB,EAAE,qCAAqC;IACxD,cAAc,EAAE,qCAAqC;IACrD,YAAY,EAAE,qCAAqC;IACnD,SAAS,EAAE,qCAAqC;EAIpD,gFAAuC;IACnC,QAAQ,EAAE,QAAQ;IAElB,0FAAY;MACR,UAAU,EAAE,KAAK",
"sources": ["../../views/gutenberg-blocks/tainacan-items/carousel-related-items/carousel-related-items.scss"],
"names": [],
"file": "tainacan-gutenberg-block-carousel-related-items.css"
}

View File

@ -122,7 +122,7 @@
color: var(--tainacan-block-gray5, #454647); color: var(--tainacan-block-gray5, #454647);
font-weight: bold; font-weight: bold;
text-decoration: none; text-decoration: none;
padding: 8px 16px; padding: 8px 12px;
display: block; display: block;
line-height: 1.2em; line-height: 1.2em;
word-break: break-word; } word-break: break-word; }

View File

@ -189,6 +189,24 @@ class REST_Items_Controller extends REST_Controller {
return $item_array; return $item_array;
} }
/**
* @param \Tainacan\Entities\Item $item|int
*
* @return array
*/
public function get_context_edit($item) {
if(is_numeric($item))
$item = new Entities\Item($item);
return array(
'current_user_can_edit' => $item->can_edit(),
'current_user_can_delete' => $item->can_delete(),
'nonces' => array(
'update-post_' . $item->get_id() => wp_create_nonce('update-post_' . $item->get_id())
)
);
}
/** /**
* @param mixed $item * @param mixed $item
* @param \WP_REST_Request $request * @param \WP_REST_Request $request
@ -256,6 +274,12 @@ class REST_Items_Controller extends REST_Controller {
$attributes_to_filter .= ',id,collection_id'; $attributes_to_filter .= ',id,collection_id';
} }
if ( $request['context'] === 'edit' ) {
add_filter( 'taiancan_add_related_item', function( $related_item ) {
return array_merge($related_item, $this->get_context_edit($related_item['id']));
}, 10, 2 );
}
$item_arr = $this->filter_object_by_attributes($item, $attributes_to_filter); $item_arr = $this->filter_object_by_attributes($item, $attributes_to_filter);
$item_arr = array_merge($extra_metadata_values, $item_arr); $item_arr = array_merge($extra_metadata_values, $item_arr);
@ -274,11 +298,7 @@ class REST_Items_Controller extends REST_Controller {
} }
if ( $request['context'] === 'edit' ) { if ( $request['context'] === 'edit' ) {
$item_arr['current_user_can_edit'] = $item->can_edit(); $item_arr = array_merge($item_arr, $this->get_context_edit($item));
$item_arr['current_user_can_delete'] = $item->can_delete();
$item_arr['nonces'] = array(
'update-post_' . $item->get_id() => wp_create_nonce('update-post_' . $item->get_id())
);
} }
if( isset($item_arr['thumbnail']) ) { if( isset($item_arr['thumbnail']) ) {
$item_arr['thumbnail_alt'] = get_post_meta( $item->get__thumbnail_id(), '_wp_attachment_image_alt', true ); $item_arr['thumbnail_alt'] = get_post_meta( $item->get__thumbnail_id(), '_wp_attachment_image_alt', true );

View File

@ -816,4 +816,15 @@ class Item extends Entity {
return $link; return $link;
} }
/**
* Return related items withs the item
*
* @return array
*/
public function get_related_items($args = []) {
$Tainacan_Items = \Tainacan\Repositories\Items::get_instance();
$related_items = $Tainacan_Items->fetch_related_items($this, $args);
return $related_items;
}
} }

View File

@ -523,7 +523,6 @@ class Test_Importer extends Importer {
'type' => 'Tainacan\Metadata_Types\Relationship', 'type' => 'Tainacan\Metadata_Types\Relationship',
'options' => [ 'options' => [
'collection_id' => $col2->get_id(), 'collection_id' => $col2->get_id(),
'repeated' => 'yes',
'search' => $col2_core_title->get_id() 'search' => $col2_core_title->get_id()
] ]
], $col1 ); ], $col1 );

View File

@ -116,7 +116,7 @@ class Items extends Repository {
'description' => __( 'Item comment status: "open" means comments are allowed, "closed" means comments are not allowed.', 'tainacan' ), 'description' => __( 'Item comment status: "open" means comments are allowed, "closed" means comments are not allowed.', 'tainacan' ),
'default' => get_default_comment_status(Entities\Collection::get_post_type()), 'default' => get_default_comment_status(Entities\Collection::get_post_type()),
'validation' => v::optional(v::stringType()->in( [ 'open', 'closed' ] )), 'validation' => v::optional(v::stringType()->in( [ 'open', 'closed' ] )),
] ],
] ); ] );
} }
@ -573,5 +573,95 @@ class Items extends Repository {
return $caps; return $caps;
} }
private function get_related_items_by_collection($item, $collection, $metadata, $args=[]) {
$Tainacan_Metadata = \Tainacan\Repositories\Metadata::get_instance();
if (!$collection instanceof \Tainacan\Entities\Collection || !$metadata instanceof \Tainacan\Entities\Metadatum || !$Tainacan_Metadata->metadata_is_enabled($collection, $metadata))
return false;
$prepared_items = array();
$items = $this->fetch(array_merge([
'meta_query' => [
[
'key' => $metadata->get_id(),
'value' => $item->get_id()
]
]
], $args), $collection->get_id(), 'WP_Query');
if ($items->have_posts()) {
while ( $items->have_posts() ) {
$items->the_post();
$item_related = new \Tainacan\Entities\Item($items->post);
$item_arr = $item_related->_toArray();
$item_arr['thumbnail'] = $item_related->get_thumbnail();
array_push($prepared_items, apply_filters( 'taiancan_add_related_item', $item_arr ) );
}
wp_reset_postdata();
}
return array(
"found_posts" => $items->found_posts,
'items' => $prepared_items
);
}
public function fetch_related_items($item, $args=[]) {
$Tainacan_Metadata = \Tainacan\Repositories\Metadata::get_instance();
$Tainacan_Collections = \Tainacan\Repositories\Collections::get_instance();
$current_collection = $item->get_collection();
$metadatas = $Tainacan_Metadata->fetch([
'meta_query' => [
[
'key' => 'metadata_type',
'value' => 'Tainacan\Metadata_Types\Relationship'
],
[
'key' => '_option_collection_id',
'value' => $current_collection->get_id()
],
[
'key' => '_option_display_in_related_items',
'value' => 'yes'
]
]
], 'OBJECT');
$response = array();
foreach($metadatas as $metadata) {
if($metadata->get_collection_id() == $Tainacan_Metadata->get_default_metadata_attribute()) {
$collections = $Tainacan_Collections->fetch([], 'OBJECT');
foreach($collections as $collection) {
$related_items = $this->get_related_items_by_collection($item, $collection, $metadata, $args);
if($related_items == false) continue;
$response[$metadata->get_id() . '_' . $collection->get_id()] = array(
'collection_id' => $collection->get_id(),
'collection_name' => $collection->get_name(),
'collection_url' => $collection->get_url(),
'collection_slug' => $collection->get_slug(),
'metadata_id' => $metadata->get_id(),
'metadata_name' => $metadata->get_name(),
'total_items' => $related_items['found_posts'],
'items' => $related_items['items']
);
}
} else {
$collection = $metadata->get_collection();
$related_items = $this->get_related_items_by_collection($item, $collection, $metadata, $args);
if($related_items == false) continue;
$response[$metadata->get_id()] = array(
'collection_id' => $collection->get_id(),
'collection_name' => $collection->get_name(),
'collection_url' => $collection->get_url(),
'collection_slug' => $collection->get_slug(),
'metadata_id' => $metadata->get_id(),
'metadata_name' => $metadata->get_name(),
'total_items' => $related_items['found_posts'],
'items' => $related_items['items']
);
}
}
return $response;
}
} }

View File

@ -87,7 +87,7 @@ class Metadata extends Repository {
'map' => 'post_content', 'map' => 'post_content',
'title' => __( 'Description', 'tainacan' ), 'title' => __( 'Description', 'tainacan' ),
'type' => 'string', 'type' => 'string',
'description' => __( 'The metadata description', 'tainacan' ), 'description' => __( 'The metadatum description. This may provide information on how to fill this metadatum, which will appear inside a tooltip alongside the input label.', 'tainacan' ),
'default' => '', 'default' => '',
//'on_error' => __('The description should be a text value', 'tainacan'), //'on_error' => __('The description should be a text value', 'tainacan'),
//'validation' => v::stringType()->notEmpty(), //'validation' => v::stringType()->notEmpty(),
@ -134,7 +134,7 @@ class Metadata extends Repository {
'type' => ['string', 'number'], 'type' => ['string', 'number'],
'description' => __( 'Number of multiples possible metadata', 'tainacan' ), 'description' => __( 'Number of multiples possible metadata', 'tainacan' ),
'on_error' => __( 'This number of multiples metadata is not allowed', 'tainacan' ), 'on_error' => __( 'This number of multiples metadata is not allowed', 'tainacan' ),
'validation' => v::numeric()->positive(), //'validation' => v::numeric()->positive(),
'default' => 1 'default' => 1
], ],
'mask' => [ 'mask' => [
@ -1681,4 +1681,28 @@ class Metadata extends Repository {
} }
/**
* Test if a metadata is enabled on collection.
*
* @param \Tainacan\Entities\Collection $collection
* @param \Tainacan\Entities\Metadatum $metadata
*
* @return boolean
*/
public function metadata_is_enabled($collection, $metadata) {
$order = $collection->get_metadata_order();
if($order == false) return true;
$order = ( is_array( $order ) ) ? $order : unserialize( $order );
if( is_array($order) ) {
foreach ($order as $metadata_order) {
if( $metadata_order['id'] == $metadata->get_id() || $metadata_order['id'] == $metadata->get_parent() ) {
if($metadata_order['enabled'] == false)
return false;
return true;
}
}
}
return false;
}
} }

View File

@ -50,6 +50,8 @@ class Theme_Helper {
add_shortcode( 'tainacan-search', array($this, 'search_shortcode')); add_shortcode( 'tainacan-search', array($this, 'search_shortcode'));
add_shortcode( 'tainacan-item-submission', array($this, 'item_submission_shortcode')); add_shortcode( 'tainacan-item-submission', array($this, 'item_submission_shortcode'));
add_shortcode( 'tainacan-items-carousel', array($this, 'get_tainacan_items_carousel'));
add_shortcode( 'tainacan-related-items-carousel', array($this, 'get_tainacan_related_items_carousel'));
add_action( 'generate_rewrite_rules', array( &$this, 'rewrite_rules' ), 10, 1 ); add_action( 'generate_rewrite_rules', array( &$this, 'rewrite_rules' ), 10, 1 );
add_filter( 'query_vars', array( &$this, 'rewrite_rules_query_vars' ) ); add_filter( 'query_vars', array( &$this, 'rewrite_rules_query_vars' ) );
@ -122,6 +124,49 @@ class Theme_Helper {
wp_localize_script('tainacan-search', 'tainacan_plugin', \Tainacan\Admin::get_instance()->get_admin_js_localization_params()); wp_localize_script('tainacan-search', 'tainacan_plugin', \Tainacan\Admin::get_instance()->get_admin_js_localization_params());
} }
} }
public function enqueue_items_carousel_scripts() {
global $post;
global $TAINACAN_BASE_URL;
global $TAINACAN_VERSION;
global $wp_version;
$settings = [
'wp_version' => $wp_version,
'root' => esc_url_raw( rest_url() ) . 'tainacan/v2',
'nonce' => is_user_logged_in() ? wp_create_nonce( 'wp_rest' ) : false,
'base_url' => $TAINACAN_BASE_URL,
'admin_url' => admin_url(),
'site_url' => site_url(),
'theme_items_list_url' => esc_url_raw( get_site_url() ) . '/' . \Tainacan\Theme_Helper::get_instance()->get_items_list_slug()
];
wp_enqueue_script(
'carousel-items-list-theme',
$TAINACAN_BASE_URL . '/assets/js/block_carousel_items_list_theme.js',
array('wp-components')
);
wp_enqueue_style(
'carousel-items-list',
$TAINACAN_BASE_URL . '/assets/css/tainacan-gutenberg-block-' . 'carousel-items-list' . '.css',
array('tainacan-blocks-common-styles'),
$TAINACAN_VERSION
);
wp_set_script_translations('carousel-items-list-theme', 'tainacan');
wp_localize_script('carousel-items-list-theme', 'tainacan_blocks', $settings);
}
public function enqueue_related_items_carousel_scripts() {
global $TAINACAN_BASE_URL;
global $TAINACAN_VERSION;
wp_enqueue_style(
'carousel-related-items',
$TAINACAN_BASE_URL . '/assets/css/tainacan-gutenberg-block-' . 'carousel-related-items' . '.css',
array('tainacan-blocks-common-styles'),
$TAINACAN_VERSION
);
}
public function is_post_an_item(\WP_Post $post) { public function is_post_an_item(\WP_Post $post) {
$post_type = $post->post_type; $post_type = $post->post_type;
@ -428,7 +473,6 @@ class Theme_Helper {
public function search_shortcode($args) { public function search_shortcode($args) {
return $this->get_tainacan_items_list($args, true); return $this->get_tainacan_items_list($args, true);
} }
public function get_tainacan_items_list($args, $force_enqueue = false) { public function get_tainacan_items_list($args, $force_enqueue = false) {
$props = ' '; $props = ' ';
@ -828,5 +872,175 @@ class Theme_Helper {
return $adjacent_items; return $adjacent_items;
} }
/**
* Returns the div used by Vue to render the Carousel of Related Items
*
* @param array $args {
* Optional. Array of arguments.
* @type string $collection_id The Collection ID
* @type string $search_URL A query string to fetch items from, if load strategy is 'search'
* @type array $selected_items An array of item IDs to fetch items from, if load strategy is 'selection' and an array of items, if the load strategy is 'parent'
* @type string $load_strategy Either 'search' or 'selection', to determine how items will be fetch
* @type integer $max_items_number Maximum number of items to be fetch
* @type integer $max_tems_per_screen Maximum columns of items to be displayed on a row of the carousel
* @type string $arrows_position How the arrows will be positioned regarding the carousel ('around', 'left', 'right')
* @type bool $large_arrows Should large arrows be displayed?
* @type bool $auto_play Should the Caroulsel start automatically to slide?
* @type integer $auto_play_speed The time in s to translate to the next slide automatically
* @type bool $loop_slides Should slides loop when reached the end of the Carousel?
* @type bool $hide_title Should the title of the items be displayed?
* @type bool $crop_images_to_square Should it use the `tainacan-medium-size` instead of the `tainacan-medium-large-size`?
* @type bool $show_collection_header Should it display a small version of the collection header?
* @type bool $show_collection_label Should it displar a 'Collection' label before the collection name on the collection header?
* @type string $collection_background_color Color of the collection header background
* @type string $collection_text_color Color of the collection header text
* @type string $tainacan_api_root Path of the Tainacan api root (to make the items request)
* @type string $tainacan_base_url Path of the Tainacan base URL (to make the links to the items)
* @type string $class_name Extra class to add to the wrapper, besides the default wp-block-tainacan-carousel-items-list
* @return string The HTML div to be used for rendering the items carousel vue component
*/
public function get_tainacan_items_carousel($args = []) {
if (!is_array($args))
return __('There are missing parameters for Tainacan Items Carousel shortcode', 'tainacan');
$defaults = array(
'max_items_number' => 12,
'max_items_per_screen' => 7,
'arrows_position' => 'around',
'large_arrows' => false,
'auto_play' => false,
'auto_play_speed' => 3,
'loop_slides' => false,
'hide_title' => false,
'crop_images_to_square' => true,
'show_collection_header' => false,
'show_collection_label' => false,
'collection_background_color' => '#454647',
'collection_text_color' => '#ffffff',
'tainacan_api_root' => '',
'tainacan_base_url' => '',
'class_name' => '',
);
$args = wp_parse_args($args, $defaults);
$props = ' ';
// Always pass the class needed by Vue to mount the component;
$args['class'] = $args['class_name'] . ' wp-block-tainacan-carousel-items-list';
unset($args['class_name']);
// Builds parameters to the html div rendered by Vue
foreach ($args as $key => $value) {
if (is_bool($value))
$value = $value ? 'true' : 'false';
// Changes from PHP '_' notation to HTML '-' notation
$props .= (str_replace('_', '-', $key) . "='" . $value . "' ");
}
$this->enqueue_items_carousel_scripts();
return "<div id='tainacan-items-carousel-shortcode' $props ></div>";
}
/**
* Returns a group of related items carousels
* For each metatada, the collection name, the metadata name and a button linking
* the items list filtered is presented
*
* @param array $args {
* Optional. Array of arguments.
* @type string $item_id The Item ID
* @type string $class_name Extra class to add to the wrapper, besides the default wp-block-tainacan-carousel-related-items
* @type string $collection_heading_class_name Extra class to add to the collection name wrapper. Defaults to ''
* @type string $collection_heading_tag Tag to be used as wrapper of the collection name. Defaults to h2
* @type string $metadata_label_class_name Extra class to add to the metadata label wrapper. Defaults to ''
* @type string $metadata_label_tag Tag to be used as wrapper of the metadata label. Defaults to p
* @type array $carousel_args Array of arguments to be passed to the get_tainacan_items_carousel function
* @return string The HTML div to be used for rendering the related items vue component
*/
public function get_tainacan_related_items_carousel($args = []) {
$defaults = array(
'class_name' => '',
'collection_heading_class_name' => '',
'collection_heading_tag' => 'h2',
'metadata_label_class_name' => '',
'metadata_label_tag' => 'p',
'carousel_args' => []
);
$args = wp_parse_args($args, $defaults);
// Gets the current Item
$item = isset($args['item_id']) ? $this->tainacan_get_item($args['item_id']) : $this->tainacan_get_item();
if (!$item)
return;
// Then fetches related ones
$related_items = $item->get_related_items();
if (!count($related_items))
return;
// Enqueues necessary CSS
$this->enqueue_related_items_carousel_scripts();
// Always pass the default class;
$output = '<div class="' . $args['class_name'] . ' wp-block-tainacan-carousel-related-items' . '">';
foreach($related_items as $collection_id => $related_group) {
if ( isset($related_group['items']) && isset($related_group['total_items']) && $related_group['total_items'] ) {
// Adds a heading with the collection name
$collection_heading = '';
if ( isset($related_group['collection_name']) ) {
$collection_heading = '<' . $args['collection_heading_tag'] . ' class="' . $args['collection_heading_class_name'] . '">' . $related_group['collection_name'] . '</' . $args['collection_heading_tag'] . '>';
}
// Adds a paragraph with the metadata name
$metadata_label = '';
if ( isset($related_group['metadata_name']) ) {
$metadata_label = '<' . $args['metadata_label_tag'] . ' class="' . $args['metadata_label_class_name'] . '">' . $related_group['metadata_name'] . '</' . $args['metadata_label_tag'] . '>';
}
// Sets the carousel, from the items carousel template tag.
$carousel_div = '';
if ( isset($related_group['collection_id']) ) {
$carousel_args = wp_parse_args([
'collection_id' => $related_group['collection_id'],
'load_strategy' => 'parent',
'selected_items' => json_encode($related_group['items'])
], $args['carousel_args']);
$carousel_div = $this->get_tainacan_items_carousel($carousel_args);
}
$output .= '<div class="wp-block-group">
<div class="wp-block-group__inner-container">' .
$collection_heading .
$metadata_label .
$carousel_div .
(
$related_group['total_items'] > 1 ?
'<div class="wp-block-buttons">
<div class="wp-block-button">
<a class="wp-block-button__link" href="/' . $related_group['collection_slug'] . '?metaquery[0][key]=' . $related_group['metadata_id'] . '&metaquery[0][value][0]=' . $item->get_ID() . '&metaquery[0][compare]=IN">
' . sprintf( __('View all %s related items', 'tainacan'), $related_group['total_items'] ) . '
</a>
</div>
</div>'
: ''
)
. '<div style="height:30px" aria-hidden="true" class="wp-block-spacer">
</div>
</div>
</div>';
}
}
$output .= '</div>';
return $output;
}
} }

View File

@ -998,3 +998,70 @@ function tainacan_get_the_mime_type_icon($mime_type, $image_size = 'medium') {
return $images_path . $icon_file . $image_size . '.png'; return $images_path . $icon_file . $image_size . '.png';
} }
/**
* Displays a carousel of items, the same of the gutenberg block
*
* @param array $args {
* Optional. Array of arguments.
* @type string $collection_id The Collection ID
* @type string $search_URL A query string to fetch items from, if load strategy is 'search'
* @type array $selected_items An array of item IDs to fetch items from, if load strategy is 'selection' and an array of items, if the load strategy is 'parent'
* @type string $load_strategy Either 'search' or 'selection', to determine how items will be fetch
* @type integer $max_items_number Maximum number of items to be fetch
* @type integer $max_tems_per_screen Maximum columns of items to be displayed on a row of the carousel
* @type string $arrows_position How the arrows will be positioned regarding the carousel ('around', 'left', 'right')
* @type bool $large_arrows Should large arrows be displayed?
* @type bool $auto_play Should the Caroulsel start automatically to slide?
* @type integer $auto_play_speed The time in s to translate to the next slide automatically
* @type bool $loop_slides Should slides loop when reached the end of the Carousel?
* @type bool $hide_title Should the title of the items be displayed?
* @type bool $crop_images_to_square Should it use the `tainacan-medium-size` instead of the `tainacan-medium-large-size`?
* @type bool $show_collection_header Should it display a small version of the collection header?
* @type bool $show_collection_label Should it displar a 'Collection' label before the collection name on the collection header?
* @type string $collection_background_color Color of the collection header background
* @type string $collection_text_color Color of the collection header text
* @type string $tainacan_api_root Path of the Tainacan api root (to make the items request)
* @type string $tainacan_base_url Path of the Tainacan base URL (to make the links to the items)
* @type string $class_name Extra class to add to the wrapper, besides the default wp-block-tainacan-carousel-items-list
* @return void The HTML div to be used for rendering the items carousel vue component
*/
function tainacan_the_items_carousel($args = []) {
echo \Tainacan\Theme_Helper::get_instance()->get_tainacan_items_carousel($args);
}
/**
* Displays a group of related items carousels
* For each metatada, the collection name, the metadata name and a button linking
* the items list filtered is presented
*
* @param array $args {
* Optional. Array of arguments.
* @type string $item_id The Item ID
* @return void
*/
function tainacan_the_related_items_carousel($args = []) {
echo \Tainacan\Theme_Helper::get_instance()->get_tainacan_related_items_carousel($args);
}
/**
* Checks if the current item has or not related items
*/
function tainacan_has_related_items($item_id = false) {
// Gets the current Item
$item = $item_id ? \Tainacan\Theme_Helper::get_instance()->tainacan_get_item($item_id) : \Tainacan\Theme_Helper::get_instance()->tainacan_get_item();
if (!$item)
return;
// Then fetches related ones
$related_items = $item->get_related_items();// TODO: handle this inside the item so we don't have to load things here.
if ( !$related_items || !is_array($related_items) || !count($related_items) )
return false;
// If we have at least one total_items, there are related items
foreach($related_items as $related_group) {
if ( isset($related_group['total_items']) && (int)$related_group['total_items'] > 0 )
return true;
}
return false;
}

View File

@ -3,39 +3,40 @@
id="tainacan-admin-app" id="tainacan-admin-app"
class="columns is-fullheight" class="columns is-fullheight"
:class="{ :class="{
'tainacan-admin-iframe-mode': $route.query.iframemode, 'tainacan-admin-iframe-mode': isIframeMode,
'tainacan-admin-read-mode': $route.query.readmode 'tainacan-admin-read-mode': isReadMode
}"> }">
<template v-if="activeRoute == 'HomePage'"> <template v-if="activeRoute == 'HomePage'">
<tainacan-header /> <tainacan-header />
<router-view /> <router-view />
</template> </template>
<template v-else> <template v-else>
<primary-menu <template v-if="!isIframeMode">
:active-route="activeRoute" <primary-menu
:is-menu-compressed="isMenuCompressed"/> :active-route="activeRoute"
<button :is-menu-compressed="isMenuCompressed"/>
class="is-hidden-mobile" <button
id="menu-compress-button" class="is-hidden-mobile"
@click="isMenuCompressed = !isMenuCompressed"> id="menu-compress-button"
<span @click="isMenuCompressed = !isMenuCompressed">
v-tooltip="{ <span
content: isMenuCompressed ? $i18n.get('label_expand_menu') : $i18n.get('label_shrink_menu'), v-tooltip="{
autoHide: true, content: $i18n.get('label_shrink_menu'),
placement: 'auto-end', autoHide: true,
classes: ['tooltip', 'repository-tooltip'] placement: 'auto-end',
}" classes: ['tooltip', 'repository-tooltip']
class="icon"> }"
<i class="icon">
:class="{ 'tainacan-icon-arrowleft' : !isMenuCompressed, 'tainacan-icon-arrowright' : isMenuCompressed }" <i
class="tainacan-icon tainacan-icon-1-25em" :class="{ 'tainacan-icon-arrowleft' : !isMenuCompressed, 'tainacan-icon-arrowright' : isMenuCompressed }"
/> class="tainacan-icon tainacan-icon-1-25em"/>
</span> </span>
</button> </button>
<tainacan-header /> <tainacan-header />
<tainacan-repository-subheader <tainacan-repository-subheader
:is-repository-level="isRepositoryLevel" :is-repository-level="isRepositoryLevel"
:is-menu-compressed="isMenuCompressed"/> :is-menu-compressed="isMenuCompressed"/>
</template>
<div <div
id="repository-container" id="repository-container"
class="column is-main-content"> class="column is-main-content">
@ -65,6 +66,14 @@
activeRoute: '/collections' activeRoute: '/collections'
} }
}, },
computed: {
isReadMode () {
return this.$route && this.$route.query && this.$route.query.readmode;
},
isIframeMode () {
return this.$route && this.$route.query && this.$route.query.iframemode;
}
},
watch: { watch: {
'$route' (to) { '$route' (to) {
this.isMenuCompressed = (to.params.collectionId != undefined); this.isMenuCompressed = (to.params.collectionId != undefined);

File diff suppressed because it is too large Load Diff

View File

@ -451,6 +451,10 @@
.field > .field:not(:last-child) { .field > .field:not(:last-child) {
margin-bottom: 0em; margin-bottom: 0em;
} }
.field:first-child {
-webkit-column-span: all;
column-span: all;
}
.help-wrapper { .help-wrapper {
font-size: 1.25em; font-size: 1.25em;
} }

View File

@ -80,7 +80,7 @@
<!-- Context menu for right click selection --> <!-- Context menu for right click selection -->
<div <div
v-if="cursorPosY > 0 && cursorPosX > 0 && !$route.query.readmode" v-if="cursorPosY > 0 && cursorPosX > 0 && !isReadMode"
class="context-menu"> class="context-menu">
<!-- Backdrop for escaping context menu --> <!-- Backdrop for escaping context menu -->
@ -95,12 +95,12 @@
trap-focus> trap-focus>
<b-dropdown-item <b-dropdown-item
@click="openItem()" @click="openItem()"
v-if="!isOnTrash && !$route.query.iframemode"> v-if="!isOnTrash && !isIframeMode">
{{ $i18n.getFrom('items','view_item') }} {{ $i18n.getFrom('items','view_item') }}
</b-dropdown-item> </b-dropdown-item>
<b-dropdown-item <b-dropdown-item
@click="openItemOnNewTab()" @click="openItemOnNewTab()"
v-if="!isOnTrash && !$route.query.iframemode"> v-if="!isOnTrash && !isIframeMode">
{{ $i18n.get('label_open_item_new_tab') }} {{ $i18n.get('label_open_item_new_tab') }}
</b-dropdown-item> </b-dropdown-item>
<b-dropdown-item <b-dropdown-item
@ -110,22 +110,22 @@
</b-dropdown-item> </b-dropdown-item>
<b-dropdown-item <b-dropdown-item
@click="goToItemEditPage(contextMenuItem)" @click="goToItemEditPage(contextMenuItem)"
v-if="contextMenuItem != null && contextMenuItem.current_user_can_edit && !$route.query.iframemode"> v-if="contextMenuItem != null && contextMenuItem.current_user_can_edit && !isIframeMode">
{{ $i18n.getFrom('items','edit_item') }} {{ $i18n.getFrom('items','edit_item') }}
</b-dropdown-item> </b-dropdown-item>
<b-dropdown-item <b-dropdown-item
@click="makeCopiesOfOneItem(contextMenuItem.id)" @click="makeCopiesOfOneItem(contextMenuItem.id)"
v-if="contextMenuItem != null && contextMenuItem.current_user_can_edit && !$route.query.iframemode"> v-if="contextMenuItem != null && contextMenuItem.current_user_can_edit && !isIframeMode">
{{ $i18n.get('label_make_copies_of_item') }} {{ $i18n.get('label_make_copies_of_item') }}
</b-dropdown-item> </b-dropdown-item>
<b-dropdown-item <b-dropdown-item
@click="deleteOneItem(contextMenuItem.id)" @click="deleteOneItem(contextMenuItem.id)"
v-if="contextMenuItem != null && contextMenuItem.current_user_can_edit && !$route.query.iframemode"> v-if="contextMenuItem != null && contextMenuItem.current_user_can_edit && !isIframeMode">
{{ $i18n.get('label_delete_item') }} {{ $i18n.get('label_delete_item') }}
</b-dropdown-item> </b-dropdown-item>
</b-dropdown> </b-dropdown>
</div> </div>
<!-- GRID (THUMBNAILS) VIEW MODE --> <!-- GRID (THUMBNAILS) VIEW MODE -->
<div <div
role="list" role="list"
@ -142,17 +142,23 @@
<!-- Checkbox --> <!-- Checkbox -->
<!-- TODO: Remove v-if="collectionId" from this element when the bulk edit in repository is done --> <!-- TODO: Remove v-if="collectionId" from this element when the bulk edit in repository is done -->
<div <div
v-if="collectionId && !$route.query.readmode && ($route.query.iframemode || collection && collection.current_user_can_bulk_edit)" v-if="collectionId && !isReadMode && (isIframeMode || collection && collection.current_user_can_bulk_edit)"
:class="{ 'is-selecting': isSelectingItems }" :class="{ 'is-selecting': isSelectingItems }"
class="grid-item-checkbox"> class="grid-item-checkbox">
<b-checkbox <b-checkbox
v-if="!isSingleSelectionMode"
:value="getSelectedItemChecked(item.id)" :value="getSelectedItemChecked(item.id)"
@input="setSelectedItemChecked(item.id)"/> @input="setSelectedItemChecked(item.id)"/>
<b-radio
v-else
name="item-single-selection"
:native-value="item.id"
v-model="singleItemSelection"/>
</div> </div>
<!-- Title --> <!-- Title -->
<div <div
:style="{ 'padding-left': !collectionId || !($route.query.iframemode || collection && collection.current_user_can_bulk_edit) || $route.query.readmode ? '0.5em !important' : (isOnAllItemsTabs ? '1.875em' : '2.75em') }" :style="{ 'padding-left': !collectionId || !(isIframeMode || collection && collection.current_user_can_bulk_edit) || isReadMode? '0.5em !important' : (isOnAllItemsTabs ? '1.875em' : '2.75em') }"
class="metadata-title"> class="metadata-title">
<p <p
v-tooltip="{ v-tooltip="{
@ -203,7 +209,7 @@
<!-- Actions --> <!-- Actions -->
<div <div
v-if="item.current_user_can_edit && !$route.query.iframemode" v-if="item.current_user_can_edit && !isIframeMode"
class="actions-area" class="actions-area"
:label="$i18n.get('label_actions')"> :label="$i18n.get('label_actions')">
<a <a
@ -277,16 +283,23 @@
<!-- Checkbox --> <!-- Checkbox -->
<!-- TODO: Remove v-if="collectionId" from this element when the bulk edit in repository is done --> <!-- TODO: Remove v-if="collectionId" from this element when the bulk edit in repository is done -->
<div <div
v-if="collectionId && !$route.query.readmode && ($route.query.iframemode || collection && collection.current_user_can_bulk_edit)" v-if="collectionId && !isReadMode && (isIframeMode || collection && collection.current_user_can_bulk_edit)"
:class="{ 'is-selecting': isSelectingItems }" :class="{ 'is-selecting': isSelectingItems }"
class="masonry-item-checkbox"> class="masonry-item-checkbox">
<label <label
tabindex="0" tabindex="0"
class="b-checkbox checkbox is-small"> :class="(!isSingleSelectionMode ? 'b-checkbox checkbox' : 'b-radio radio') + ' is-small'">
<input <input
v-if="!isSingleSelectionMode"
type="checkbox" type="checkbox"
:checked="getSelectedItemChecked(item.id)" :checked="getSelectedItemChecked(item.id)"
@input="setSelectedItemChecked(item.id)"> @input="setSelectedItemChecked(item.id)">
<input
v-else
type="radio"
name="item-single-selection"
:value="item.id"
v-model="singleItemSelection">
<span class="check" /> <span class="check" />
<span class="control-label" /> <span class="control-label" />
</label> </label>
@ -295,7 +308,7 @@
<!-- Title --> <!-- Title -->
<div <div
:style="{ :style="{
'padding-left': !collectionId || !($route.query.iframemode || collection && collection.current_user_can_bulk_edit) || $route.query.readmode ? '0 !important' : (isOnAllItemsTabs ? '0.5em' : '1em') 'padding-left': !collectionId || !(isIframeMode || collection && collection.current_user_can_bulk_edit) || isReadMode ? '0 !important' : (isOnAllItemsTabs ? '0.5em' : '1em')
}" }"
@click.left="onClickItem($event, item)" @click.left="onClickItem($event, item)"
@click.right="onRightClickItem($event, item)" @click.right="onRightClickItem($event, item)"
@ -336,7 +349,7 @@
<!-- Actions --> <!-- Actions -->
<div <div
v-if="item.current_user_can_edit && !$route.query.iframemode" v-if="item.current_user_can_edit && !isIframeMode"
class="actions-area" class="actions-area"
:label="$i18n.get('label_actions')"> :label="$i18n.get('label_actions')">
<a <a
@ -405,18 +418,24 @@
<!-- Checkbox --> <!-- Checkbox -->
<!-- TODO: Remove v-if="collectionId" from this element when the bulk edit in repository is done --> <!-- TODO: Remove v-if="collectionId" from this element when the bulk edit in repository is done -->
<div <div
v-if="collectionId && !$route.query.readmode && ($route.query.iframemode || collection && collection.current_user_can_bulk_edit)" v-if="collectionId && !isReadMode && (isIframeMode || collection && collection.current_user_can_bulk_edit)"
:class="{ 'is-selecting': isSelectingItems }" :class="{ 'is-selecting': isSelectingItems }"
class="card-checkbox"> class="card-checkbox">
<b-checkbox <b-checkbox
v-if="!isSingleSelectionMode"
:value="getSelectedItemChecked(item.id)" :value="getSelectedItemChecked(item.id)"
@input="setSelectedItemChecked(item.id)"/> @input="setSelectedItemChecked(item.id)"/>
<b-radio
v-else
name="item-single-selection"
:native-value="item.id"
v-model="singleItemSelection"/>
</div> </div>
<!-- Title --> <!-- Title -->
<div <div
:style="{ :style="{
'padding-left': !collectionId || $route.query.readmode || !($route.query.iframemode || collection && collection.current_user_can_bulk_edit) ? '0.5em !important' : (isOnAllItemsTabs ? '2.125em' : '2.75em'), 'padding-left': !collectionId || !(isIframeMode || collection && collection.current_user_can_bulk_edit) || isReadMode ? '0.5em !important' : (isOnAllItemsTabs ? '2.125em' : '2.75em'),
}" }"
class="metadata-title"> class="metadata-title">
<p <p
@ -451,7 +470,7 @@
</div> </div>
<!-- Actions --> <!-- Actions -->
<div <div
v-if="item.current_user_can_edit && !$route.query.iframemode" v-if="item.current_user_can_edit && !isIframeMode"
class="actions-area" class="actions-area"
:label="$i18n.get('label_actions')"> :label="$i18n.get('label_actions')">
<a <a
@ -592,16 +611,23 @@
<!-- Checkbox --> <!-- Checkbox -->
<!-- TODO: Remove v-if="collectionId" from this element when the bulk edit in repository is done --> <!-- TODO: Remove v-if="collectionId" from this element when the bulk edit in repository is done -->
<div <div
v-if="collectionId && !$route.query.readmode && ($route.query.iframemode || collection && collection.current_user_can_bulk_edit)" v-if="collectionId && !isReadMode && (isIframeMode || collection && collection.current_user_can_bulk_edit)"
:class="{ 'is-selecting': isSelectingItems }" :class="{ 'is-selecting': isSelectingItems }"
class="record-checkbox"> class="record-checkbox">
<label <label
tabindex="0" tabindex="0"
class="b-checkbox checkbox is-small"> :class="(!isSingleSelectionMode ? 'b-checkbox checkbox' : 'b-radio radio') + ' is-small'">
<input <input
v-if="!isSingleSelectionMode"
type="checkbox" type="checkbox"
:checked="getSelectedItemChecked(item.id)" :checked="getSelectedItemChecked(item.id)"
@input="setSelectedItemChecked(item.id)"> @input="setSelectedItemChecked(item.id)">
<input
v-else
type="radio"
name="item-single-selection"
:value="item.id"
v-model="singleItemSelection">
<span class="check" /> <span class="check" />
<span class="control-label" /> <span class="control-label" />
</label> </label>
@ -611,7 +637,7 @@
<div <div
class="metadata-title" class="metadata-title"
:style="{ :style="{
'padding-left': !collectionId || !($route.query.iframemode || collection && collection.current_user_can_bulk_edit) || $route.query.readmode ? '1.5em !important' : '2.75em' 'padding-left': !collectionId || !(isIframeMode || collection && collection.current_user_can_bulk_edit) || isReadMode ? '1.5em !important' : '2.75em'
}"> }">
<span <span
v-if="isOnAllItemsTabs && $statusHelper.hasIcon(item.status)" v-if="isOnAllItemsTabs && $statusHelper.hasIcon(item.status)"
@ -664,7 +690,7 @@
</div> </div>
<!-- Actions --> <!-- Actions -->
<div <div
v-if="item.current_user_can_edit && !$route.query.iframemode" v-if="item.current_user_can_edit && !isIframeMode"
class="actions-area" class="actions-area"
:label="$i18n.get('label_actions')"> :label="$i18n.get('label_actions')">
<a <a
@ -779,7 +805,7 @@
<tr> <tr>
<!-- Checking list --> <!-- Checking list -->
<th <th
v-if="collectionId && !$route.query.readmode && ($route.query.iframemode || collection && collection.current_user_can_bulk_edit)"> v-if="collectionId && !isReadMode && (isIframeMode || collection && collection.current_user_can_bulk_edit)">
&nbsp; &nbsp;
<!-- nothing to show on header for checkboxes --> <!-- nothing to show on header for checkboxes -->
</th> </th>
@ -828,12 +854,18 @@
<!-- Checking list --> <!-- Checking list -->
<!-- TODO: Remove v-if="collectionId" from this element when the bulk edit in repository is done --> <!-- TODO: Remove v-if="collectionId" from this element when the bulk edit in repository is done -->
<td <td
v-if="collectionId && !$route.query.readmode && ($route.query.iframemode || collection && collection.current_user_can_bulk_edit)" v-if="collectionId && !isReadMode && (isIframeMode || collection && collection.current_user_can_bulk_edit)"
:class="{ 'is-selecting': isSelectingItems }" :class="{ 'is-selecting': isSelectingItems }"
class="checkbox-cell"> class="checkbox-cell">
<b-checkbox <b-checkbox
v-if="!isSingleSelectionMode"
:value="getSelectedItemChecked(item.id)" :value="getSelectedItemChecked(item.id)"
@input="setSelectedItemChecked(item.id)"/> @input="setSelectedItemChecked(item.id)"/>
<b-radio
v-else
name="item-single-selection"
:native-value="item.id"
v-model="singleItemSelection"/>
</td> </td>
<td <td
v-if="isOnAllItemsTabs" v-if="isOnAllItemsTabs"
@ -972,7 +1004,7 @@
<!-- Actions --> <!-- Actions -->
<td <td
v-if="(item.current_user_can_edit || item.current_user_can_delete) && !$route.query.iframemode" v-if="(item.current_user_can_edit || item.current_user_can_delete) && !isIframeMode"
class="actions-cell" class="actions-cell"
:label="$i18n.get('label_actions')"> :label="$i18n.get('label_actions')">
<div class="actions-container"> <div class="actions-container">
@ -1043,16 +1075,23 @@
:class="{ 'selected-list-item': getSelectedItemChecked(item.id) == true }"> :class="{ 'selected-list-item': getSelectedItemChecked(item.id) == true }">
<div <div
v-if="collectionId && !$route.query.readmode && ($route.query.iframemode || collection && collection.current_user_can_bulk_edit)" v-if="collectionId && !isReadMode && (isIframeMode || collection && collection.current_user_can_bulk_edit)"
:class="{ 'is-selecting': isSelectingItems }" :class="{ 'is-selecting': isSelectingItems }"
class="list-checkbox"> class="list-checkbox">
<label <label
tabindex="0" tabindex="0"
class="b-checkbox checkbox is-small"> :class="(!isSingleSelectionMode ? 'b-checkbox checkbox' : 'b-radio radio') + ' is-small'">
<input <input
v-if="!isSingleSelectionMode"
type="checkbox" type="checkbox"
:checked="getSelectedItemChecked(item.id)" :checked="getSelectedItemChecked(item.id)"
@input="setSelectedItemChecked(item.id)"> @input="setSelectedItemChecked(item.id)">
<input
v-else
type="radio"
name="item-single-selection"
:value="item.id"
v-model="singleItemSelection">
<span class="check" /> <span class="check" />
<span class="control-label" /> <span class="control-label" />
</label> </label>
@ -1099,7 +1138,7 @@
<!-- Actions --> <!-- Actions -->
<div <div
v-if="item.current_user_can_edit && !$route.query.iframemode" v-if="item.current_user_can_edit && !isIframeMode"
class="actions-area" class="actions-area"
:label="$i18n.get('label_actions')"> :label="$i18n.get('label_actions')">
<a <a
@ -1227,6 +1266,7 @@ export default {
cursorPosX: -1, cursorPosX: -1,
cursorPosY: -1, cursorPosY: -1,
contextMenuItem: null, contextMenuItem: null,
singleItemSelection: false
} }
}, },
computed: { computed: {
@ -1237,7 +1277,7 @@ export default {
return this.getHighlightedItem(); return this.getHighlightedItem();
}, },
selectedItems () { selectedItems () {
if (this.$route.query.iframemode) if (this.isIframeMode)
this.$eventBusSearch.setSelectedItemsForIframe(this.getSelectedItems()); this.$eventBusSearch.setSelectedItemsForIframe(this.getSelectedItems());
return this.getSelectedItems(); return this.getSelectedItems();
@ -1259,7 +1299,16 @@ export default {
return this.getItemsPerPage(); return this.getItemsPerPage();
}, },
totalPages(){ totalPages(){
return Math.ceil(Number(this.totalItems)/Number(this.itemsPerPage)); return Math.ceil(Number(this.totalItems)/Number(this.itemsPerPage));
},
isIframeMode () {
return this.$route && this.$route.query && this.$route.query.iframemode;
},
isReadMode () {
return this.$route && this.$route.query && this.$route.query.readmode;
},
isSingleSelectionMode () {
return this.$route && this.$route.query && this.$route.query.singleselectionmode;
}, },
isOnAllItemsTabs() { isOnAllItemsTabs() {
const currentStatus = this.getStatus(); const currentStatus = this.getStatus();
@ -1281,6 +1330,11 @@ export default {
allItemsOnPageSelected(value) { allItemsOnPageSelected(value) {
if (!value) if (!value)
this.queryAllItemsSelected = {}; this.queryAllItemsSelected = {};
},
singleItemSelection() {
if (this.isSingleSelectionMode && this.isIframeMode)
this.$eventBusSearch.setSelectedItemsForIframe([this.singleItemSelection], true);
} }
}, },
mounted() { mounted() {
@ -1321,14 +1375,18 @@ export default {
'getItemsPerPage' 'getItemsPerPage'
]), ]),
setSelectedItemChecked(itemId) { setSelectedItemChecked(itemId) {
if (this.selectedItems.find((item) => item == itemId) != undefined) if (this.isSingleSelectionMode) {
this.removeSelectedItem(itemId); this.singleItemSelection = itemId;
else { } else {
this.addSelectedItem(itemId); if (this.selectedItems.find((item) => item == itemId) != undefined)
this.removeSelectedItem(itemId);
else {
this.addSelectedItem(itemId);
}
} }
}, },
getSelectedItemChecked(itemId) { getSelectedItemChecked(itemId) {
return this.selectedItems.find(item => item == itemId) != undefined; return this.isSingleSelectionMode ? this.singleItemSelection == itemId : this.selectedItems.find(item => item == itemId) != undefined;
}, },
openBulkEditionModal(){ openBulkEditionModal(){
this.$buefy.modal.open({ this.$buefy.modal.open({
@ -1558,9 +1616,9 @@ export default {
} }
} else { } else {
if (this.$route.query.iframemode && !this.$route.query.readmode) { if (this.isIframeMode && !this.isReadMode) {
this.setSelectedItemChecked(item.id) this.setSelectedItemChecked(item.id)
} else if (!this.$route.query.iframemode && !this.$route.query.readmode) { } else if (!this.isIframeMode && !this.isReadMode) {
if(this.isOnTrash){ if(this.isOnTrash){
this.$buefy.toast.open({ this.$buefy.toast.open({
duration: 3000, duration: 3000,
@ -1575,7 +1633,7 @@ export default {
} }
}, },
onRightClickItem($event, item) { onRightClickItem($event, item) {
if (!this.$route.query.readmode) { if (!this.isReadMode) {
$event.preventDefault(); $event.preventDefault();
this.cursorPosX = $event.clientX; this.cursorPosX = $event.clientX;

View File

@ -0,0 +1,292 @@
<template>
<div>
<div class="table-container">
<b-loading
is-full-page="false"
:active.sync="displayLoading" />
<div class="table-wrapper">
<div class="related-items-list">
<div
v-for="(relatedItemGroup, index) of relatedItemsArray"
:key="index"
class="related-item-group">
<div class="columns is-vcentered is-multiline">
<div class="column is-narrow">
<div class="section-status">
<div class="field has-addons">
<span>
<span class="icon">
<i class="tainacan-icon tainacan-icon-collection"/>
</span>
{{ relatedItemGroup.collection_name ? relatedItemGroup.collection_name : '' }}
</span>
</div>
</div>
</div>
<div class="column is-narrow">
<div class="section-status">
<div class="field has-addons">
<span>
<span class="icon">
<i class="tainacan-icon tainacan-icon-metadata"/>
</span>
{{ relatedItemGroup.metadata_name ? relatedItemGroup.metadata_name : '' }}
</span>
</div>
</div>
</div>
<div
v-if="relatedItemGroup.total_items && relatedItemGroup.total_items > 1"
style="margin-left: auto;"
class="column is-narrow">
<div class="section-status">
<div class="field has-addons">
<b-button
class="button is-secondary"
tag="router-link"
:to="$routerHelper.getCollectionItemsPath(collectionId, { metaquery: [{ key: relatedItemGroup.metadata_id, value: itemId, compare: 'IN' }] })">
{{ $i18n.getWithVariables('label_view_all_%s_related_items', [relatedItemGroup.total_items]) }}
</b-button>
</div>
</div>
</div>
</div>
<ul class="related-item-group__items-list">
<li
v-for="(relatedItem, itemIndex) of relatedItemGroup.items"
:key="itemIndex">
<div
class="status-cell"
@click="openItemOnNewTab(relatedItem)">
<span
v-if="$statusHelper.hasIcon(relatedItem.status)"
class="icon has-text-gray"
v-tooltip="{
content: $i18n.get('status_' + relatedItem.status),
autoHide: true,
placement: 'auto-start'
}">
<i
class="tainacan-icon tainacan-icon-1em"
:class="$statusHelper.getIcon(relatedItem.status)"
/>
</span>
</div>
<div @click="openItemOnNewTab(relatedItem)">
<span class="table-thumb">
<blur-hash-image
:width="$thumbHelper.getWidth(relatedItem['thumbnail'], 'tainacan-small', 40)"
:height="$thumbHelper.getHeight(relatedItem['thumbnail'], 'tainacan-small', 40)"
:hash="$thumbHelper.getBlurhashString(relatedItem['thumbnail'], 'tainacan-small')"
:src="$thumbHelper.getSrc(relatedItem['thumbnail'], 'tainacan-small', relatedItem.document_mimetype)"
:alt="relatedItem.thumbnail_alt ? relatedItem.thumbnail_alt : $i18n.get('label_thumbnail')"
:transition-duration="500"
/>
</span>
</div>
<div
@click="openItemOnNewTab(relatedItem)"
style="width: 100%">
<p
v-tooltip="{
delay: {
show: 500,
hide: 300,
},
content: relatedItem.title != undefined && relatedItem.title != '' ? relatedItem.title : `<span class='has-text-gray3 is-italic'>` + $i18n.get('label_value_not_provided') + `</span>`,
html: true,
autoHide: false,
placement: 'auto-start'
}"
v-html="(relatedItem.title != undefined && relatedItem.title != '') ? relatedItem.title : `<span class='has-text-gray3 is-italic'>` + $i18n.get('label_value_not_provided') + `</span>`"/>
</div>
<div
v-if="isEditable && relatedItem.current_user_can_edit"
class="actions-cell"
:label="$i18n.get('label_actions')">
<div class="actions-container">
<a
v-if="!relatedItem.status != 'trash'"
id="button-edit"
@click.prevent.stop="editItemModal = true; editItemId = relatedItem.id; editMetadataId = relatedItemGroup.metadata_id;"
:aria-label="$i18n.getFrom('items','edit_item')">
<span
v-tooltip="{
content: $i18n.get('edit'),
autoHide: true,
placement: 'auto'
}"
class="icon">
<i class="has-text-secondary tainacan-icon tainacan-icon-1-25em tainacan-icon-edit" />
</span>
</a>
</div>
</div>
</li>
</ul>
</div>
</div>
<b-modal
:width="1200"
:active.sync="editItemModal"
@close="reloadRelatedItems"
custom-class="tainacan-modal">
<iframe
width="100%"
style="height: 85vh"
:src="adminFullURL + $routerHelper.getItemEditPath(collectionId, editItemId) + '?iframemode=true&editingmetadata=' + editMetadataId" />
</b-modal>
</div>
</div>
</div>
</template>
<script>
import { mapActions } from 'vuex';
export default {
name: 'RelatedItemsList',
props: {
relatedItems: Object,
isLoading: Boolean,
isEditable: Boolean,
itemId: String,
collectionId: String
},
data() {
return {
editMetadataId: false,
editItemId: false,
editItemModal: false,
adminFullURL: tainacan_plugin.admin_url + 'admin.php?page=tainacan_admin#',
isUpdatingRelatedItems: false
}
},
computed: {
relatedItemsArray() {
return this.relatedItems ? Object.values(this.relatedItems).filter((aRelatedItemGroup) => aRelatedItemGroup.total_items) : [];
},
displayLoading() {
return this.isLoading || this.isUpdatingRelatedItems;
}
},
watch: {
editItemModal() {
if (this.editItemModal) {
window.addEventListener('message', this.updateReloadItemsAfterModal, false);
} else {
this.editItemId = false;
this.editMetadataId = false;
window.removeEventListener('message', this.updateReloadItemsAfterModal);
}
}
},
methods: {
...mapActions('item', [
'fetchOnlyRelatedItems'
]),
openItemOnNewTab(relatedItem) {
if (relatedItem && relatedItem.id) {
let routeData = this.$router.resolve(this.$routerHelper.getItemPath(this.collectionId, relatedItem.id));
window.open(routeData.href, '_blank');
}
},
reloadRelatedItems() {
this.isUpdatingRelatedItems = true;
this.fetchOnlyRelatedItems({
itemId: this.itemId,
contextEdit: true
})
.then(() => this.isUpdatingRelatedItems = false)
.catch((error) => {
this.$console.error(error);
this.isUpdatingRelatedItems = false;
});
},
updateReloadItemsAfterModal(event) {
const message = event.message ? 'message' : 'data';
const data = event[message];
if (data.type == 'itemCreationMessage') {
this.editItemModal = false;
}
}
}
}
</script>
<style lang="scss" scoped>
.section-status {
margin-left: -0.875rem;
font-size: 0.875em;
.field {
.icon {
font-size: 1.125em !important;
color: var(--tainacan-info-color);
}
}
}
.related-items-list {
.related-item-group {
&:not(:last-child) {
border-bottom: 1px dashed var(--tainacan-info-color);
margin-bottom: 2rem;
}
.related-item-group__items-list {
list-style: none;
padding: 0px;
margin-bottom: 1rem;
-moz-column-count: 2;
-webkit-column-count: 2;
column-count: 2;
@media screen and (max-width: 768px) {
-moz-column-count: 1;
-webkit-column-count: 1;
column-count: 1;
}
li {
display: flex;
align-items: center;
padding: 8px 32px 8px 6px;
width: 100%;
position: relative;
overflow-x: hidden;
&:hover {
cursor: pointer;
background-color: var(--tainacan-item-hover-background-color);
.actions-cell {
opacity: 1;
visibility: 1;
right: 0;
}
}
}
.actions-cell {
position: absolute;
right: -32px;
padding: 0 6px;
min-height: 28px;
min-width: 28px;
opacity: 0;
visibility: 0;
transition: right 0.3s ease;
}
.table-thumb {
display: block;
min-height: 28px;
min-width: 28px;
margin-left: 2px;
margin-right: 8px;
}
}
}
}
</style>

View File

@ -52,6 +52,7 @@
</div> </div>
<tainacan-form-item <tainacan-form-item
v-else v-else
v-show="(metadataNameFilterString == '' || filterByMetadatumName(childItemMetadatum))"
:key="groupIndex + '-' + childIndex" :key="groupIndex + '-' + childIndex"
:item-metadatum="childItemMetadatum" :item-metadatum="childItemMetadatum"
:is-collapsed="childItemMetadatum.collapse" :is-collapsed="childItemMetadatum.collapse"
@ -105,7 +106,8 @@
props: { props: {
itemMetadatum: Object, itemMetadatum: Object,
value: [String, Number, Array], value: [String, Number, Array],
disabled: false disabled: false,
metadataNameFilterString: ''
}, },
data() { data() {
return { return {
@ -306,6 +308,9 @@
} else { } else {
this.childItemMetadataGroups.splice(groupIndex, 1); this.childItemMetadataGroups.splice(groupIndex, 1);
} }
},
filterByMetadatumName(itemMetadatum) {
return itemMetadatum.metadatum.name.toString().toLowerCase().indexOf(this.metadataNameFilterString.toString().toLowerCase()) >= 0;
} }
} }
} }
@ -331,7 +336,7 @@
.field { .field {
padding-right: 0; padding-right: 0;
margin-left: 3px; margin-left: 3px;
margin-bottom: 0.875em; margin-bottom: 0em !important;
} }
.is-last-input.field { .is-last-input.field {
border-bottom: none; border-bottom: none;

View File

@ -17,7 +17,8 @@
v-model="collection" v-model="collection"
@change.native="emitValues()" @change.native="emitValues()"
@focus="clear()" @focus="clear()"
:loading="loading"> :loading="loading"
expanded>
<option <option
v-for="option in collections" v-for="option in collections"
:value="option.id" :value="option.id"
@ -40,42 +41,36 @@
:title="$i18n.getHelperTitle('tainacan-relationship', 'search')" :title="$i18n.getHelperTitle('tainacan-relationship', 'search')"
:message="$i18n.getHelperMessage('tainacan-relationship', 'search')"/> :message="$i18n.getHelperMessage('tainacan-relationship', 'search')"/>
</label> </label>
<b-select <b-select
name="metadata_type_relationship[search]" name="metadata_type_relationship[search]"
v-model="modelSearch"> v-model="modelSearch"
expanded>
<option <option
v-for="(option, index) in metadata" v-for="(option, index) in metadata"
:key="index" :key="index"
:value="option.id" :value="option.id"
class="field"> class="field">
{{ option.name }} {{ option.name }}
</option> </option>
</b-select>
</b-select>
</b-field> </b-field>
</transition> </transition>
<b-field
<b-field :addons="false"> :addons="false"
<label class="label"> :label="$i18n.getHelperTitle('tainacan-relationship', 'display_in_related_items')">
{{ $i18n.get('label_allow_repeated_items') }} &nbsp;
<help-button <b-switch
:title="$i18n.getHelperTitle('tainacan-relationship', 'repeated')" size="is-small"
:message="$i18n.getHelperMessage('tainacan-relationship', 'repeated')"/> v-model="modelDisplayInRelatedItems"
</label> @input="emitValues()"
<div class="block"> true-value="yes"
<b-checkbox false-value="no" />
v-model="modelRepeated" <help-button
@input="emitValues()" :title="$i18n.getHelperTitle('tainacan-relationship', 'display_in_related_items')"
true-value="yes" :message="$i18n.getHelperMessage('tainacan-relationship', 'display_in_related_items')"/>
false-value="no">
{{ labelRepeated() }}
</b-checkbox>
</div>
</b-field> </b-field>
</section> </section>
</template> </template>
@ -86,7 +81,6 @@
props: { props: {
search: [ String ], search: [ String ],
collection_id: [ Number ], collection_id: [ Number ],
repeated: [ String ],
value: [ String, Object, Array ], value: [ String, Object, Array ],
metadatum: [ String, Object ], metadatum: [ String, Object ],
errors: [ String, Object, Array ] errors: [ String, Object, Array ]
@ -100,7 +94,7 @@
collection: '', collection: '',
hasMetadata: false, hasMetadata: false,
loadingMetadata: false, loadingMetadata: false,
modelRepeated: 'yes', modelDisplayInRelatedItems: 'no',
modelSearch:'', modelSearch:'',
collectionType: '', collectionType: '',
collectionMessage: '' collectionMessage: ''
@ -114,7 +108,7 @@
this.setErrorsAttributes( '', '' ); this.setErrorsAttributes( '', '' );
} }
return true; return true;
}, }
}, },
watch:{ watch:{
collection( value ){ collection( value ){
@ -125,7 +119,7 @@
this.metadata = []; this.metadata = [];
this.hasMetadata = false; this.hasMetadata = false;
this.modelSearch = ''; this.modelSearch = '';
this.modelDisplayInRelatedItems = 'no';
this.emitValues(); this.emitValues();
} }
}, },
@ -143,11 +137,7 @@
} }
}); });
if( this.repeated ){ this.modelDisplayInRelatedItems = this.value && this.value.display_in_related_items ? this.value.display_in_related_items : 'no';
this.modelRepeated = this.repeated;
} else if( this.value ) {
this.modelRepeated = this.value.repeated;
}
}, },
methods:{ methods:{
setErrorsAttributes( type, message ){ setErrorsAttributes( type, message ){
@ -157,14 +147,10 @@
fetchCollections(){ fetchCollections(){
return axios.get('/collections?nopaging=1') return axios.get('/collections?nopaging=1')
.then(res => { .then(res => {
let collections = res.data; const collections = res.data;
this.loading = false;
if( collections ){ this.loading = false;
this.collections = collections; this.collections = collections ? collections : [];
} else {
this.collections = [];
}
}) })
.catch(error => { .catch(error => {
this.$console.log(error); this.$console.log(error);
@ -231,9 +217,6 @@
} }
} }
}, },
labelRepeated(){
return ( this.modelRepeated === 'yes' ) ? this.$i18n.get('label_yes') : this.$i18n.get('label_no');
},
clear(){ clear(){
this.collectionType = ''; this.collectionType = '';
this.collectionMessage = ''; this.collectionMessage = '';
@ -242,7 +225,7 @@
this.$emit('input',{ this.$emit('input',{
collection_id: this.collection, collection_id: this.collection,
search: this.modelSearch, search: this.modelSearch,
repeated: this.modelRepeated display_in_related_items: this.modelDisplayInRelatedItems
}); });
} }
} }
@ -251,6 +234,9 @@
<style scoped> <style scoped>
.help-wrapper { .help-wrapper {
font-size: 1.25em; font-size: 1em;
}
.switch.is-small {
margin-top: -0.5em;
} }
</style> </style>

View File

@ -59,25 +59,25 @@ class Relationship extends Metadata_Type {
'title' => __( 'Related Metadatum', 'tainacan' ), 'title' => __( 'Related Metadatum', 'tainacan' ),
'description' => __( 'Select the metadata to use as search criteria in the target collection and as a label when representing the relationship', 'tainacan' ), 'description' => __( 'Select the metadata to use as search criteria in the target collection and as a label when representing the relationship', 'tainacan' ),
], ],
'repeated' => [ 'display_in_related_items' => [
'title' =>__( 'Allow repeated items', 'tainacan' ), 'title' =>__( 'Display in related items', 'tainacan' ),
'description' => __( 'Allows different items to be related to the same item selected in another collection.', 'tainacan' ), 'description' => __( 'Include items on related item list.', 'tainacan' ),
] ]
]; ];
} }
/** /**
* Gets print-ready version of the options list in html * Gets print-ready version of the options list in html
* *
* Checks if at least one option exists, otherwise return an empty string * Checks if at least one option exists, otherwise return an empty string
* *
* @return string An html content with labels and values for the options or an empty string * @return string An html content with labels and values for the options or an empty string
*/ */
public function get_options_as_html() { public function get_options_as_html() {
$options_as_html = ''; $options_as_html = '';
$options = $this->get_options(); $options = $this->get_options();
if ( count($options) > 0 ) { if ( count($options) > 0 ) {
// Remove this option that is not relevant for the user // Remove this option that is not relevant for the user
if ( isset($options['related_primitive_type']) ) if ( isset($options['related_primitive_type']) )
@ -110,7 +110,7 @@ class Relationship extends Metadata_Type {
$readable_option_value = $option_value; $readable_option_value = $option_value;
break; break;
case 'repeated': case 'display_in_related_items':
if ($option_value == 'yes') if ($option_value == 'yes')
$readable_option_value = __('Yes', 'tainacan'); $readable_option_value = __('Yes', 'tainacan');
else if ($option_value == 'no') else if ($option_value == 'no')
@ -124,10 +124,10 @@ class Relationship extends Metadata_Type {
} }
$options_as_html .= '<div class="value">' . $readable_option_value . '</div></div>'; $options_as_html .= '<div class="value">' . $readable_option_value . '</div></div>';
} }
} }
} }
return $options_as_html; return $options_as_html;
} }
public function validate_options(\Tainacan\Entities\Metadatum $metadatum) { public function validate_options(\Tainacan\Entities\Metadatum $metadatum) {
if ( !in_array($metadatum->get_status(), apply_filters('tainacan-status-require-validation', ['publish','future','private'])) ) if ( !in_array($metadatum->get_status(), apply_filters('tainacan-status-require-validation', ['publish','future','private'])) )
@ -148,6 +148,12 @@ class Relationship extends Metadata_Type {
'search' => __('Search option must be a numeric Metadatum ID','tainacan') 'search' => __('Search option must be a numeric Metadatum ID','tainacan')
]; ];
} }
// empty is ok
if ( !empty($this->get_option('display_in_related_items')) && !in_array($this->get_option('display_in_related_items'), ['yes', 'no']) ) {
return [
'search' => __('Display in related items must be a option yes or no','tainacan')
];
}
return true; return true;
} }

View File

@ -1,6 +1,10 @@
<template> <template>
<b-field <b-field
:class="hideCollapses ? 'has-collapses-hidden' : ''" :class="{
'has-collapses-hidden': hideCollapses,
'hightlighted-metadatum': isHighlightedMetadatum
}"
:ref="isHighlightedMetadatum ? 'hightlighted-metadatum': 'null'"
:addons="false" :addons="false"
:message="errorMessage" :message="errorMessage"
:type="errorMessage ? 'is-danger' : ''"> :type="errorMessage ? 'is-danger' : ''">
@ -17,17 +21,7 @@
}" }"
class="has-text-secondary tainacan-icon tainacan-icon-1-25em"/> class="has-text-secondary tainacan-icon tainacan-icon-1-25em"/>
</span> </span>
<label <label class="label">
v-tooltip="{
delay: {
show: 500,
hide: 300,
},
content: itemMetadatum.metadatum.name,
autoHide: false,
placement: 'auto-end'
}"
class="label">
{{ itemMetadatum.metadatum.name }} {{ itemMetadatum.metadatum.name }}
</label> </label>
<span <span
@ -50,12 +44,13 @@
<div <div
v-show="hideCollapses || (isCollapsed || errorMessage)" v-show="hideCollapses || (isCollapsed || errorMessage)"
v-if="isTextInputComponent"> v-if="isTextInputComponent">
<component <component
:is="metadatumComponent" :is="metadatumComponent"
v-model="values[0]" v-model="values[0]"
:item-metadatum="itemMetadatum" :item-metadatum="itemMetadatum"
@input="changeValue" @input="changeValue"
@blur="performValueChange"/> @blur="performValueChange"
:metadata-name-filter-string="metadataNameFilterString" />
<template v-if="isMultiple && values.length > 1"> <template v-if="isMultiple && values.length > 1">
<transition-group <transition-group
name="filter-item" name="filter-item"
@ -68,7 +63,8 @@
v-model="values[index]" v-model="values[index]"
:item-metadatum="itemMetadatum" :item-metadatum="itemMetadatum"
@input="changeValue" @input="changeValue"
@blur="performValueChange"/> @blur="performValueChange"
:metadata-name-filter-string="metadataNameFilterString" />
<a <a
v-if="index > 0" v-if="index > 0"
@click="removeValue(index)" @click="removeValue(index)"
@ -105,7 +101,8 @@
:item-metadatum="itemMetadatum" :item-metadatum="itemMetadatum"
@input="changeValue" @input="changeValue"
@blur="performValueChange" @blur="performValueChange"
:is-last-metadatum="isLastMetadatum" /> :is-last-metadatum="isLastMetadatum"
:metadata-name-filter-string="metadataNameFilterString" />
</div> </div>
</transition> </transition>
</b-field> </b-field>
@ -120,12 +117,14 @@
itemMetadatum: Object, itemMetadatum: Object,
isCollapsed: true, isCollapsed: true,
hideCollapses: false, hideCollapses: false,
isLastMetadatum: false isLastMetadatum: false,
metadataNameFilterString: ''
}, },
data(){ data(){
return { return {
values: [], values: [],
errorMessage: '' errorMessage: '',
isHighlightedMetadatum: false
} }
}, },
computed: { computed: {
@ -157,6 +156,20 @@
if (this.itemMetadatum && this.itemMetadatum.metadatum) if (this.itemMetadatum && this.itemMetadatum.metadatum)
eventBusItemMetadata.$off('updateErrorMessageOf#' + (this.itemMetadatum.parent_meta_id ? this.itemMetadatum.metadatum.id + '-' + this.itemMetadatum.parent_meta_id : this.itemMetadatum.metadatum.id)); eventBusItemMetadata.$off('updateErrorMessageOf#' + (this.itemMetadatum.parent_meta_id ? this.itemMetadatum.metadatum.id + '-' + this.itemMetadatum.parent_meta_id : this.itemMetadatum.metadatum.id));
}, },
mounted () {
if (this.$route && this.$route.query && this.$route.query.editingmetadata) {
this.isHighlightedMetadatum = this.$route.query.editingmetadata == (this.itemMetadatum.parent_meta_id ? this.itemMetadatum.metadatum.id + '-' + this.itemMetadatum.parent_meta_id : this.itemMetadatum.metadatum.id);
if (this.isHighlightedMetadatum) {
this.$nextTick(() => {
let highlightedMetadatum = this.$refs['hightlighted-metadatum'];
if (highlightedMetadatum && highlightedMetadatum.$el && highlightedMetadatum.$el.scrollIntoView)
setTimeout(() => highlightedMetadatum.$el.scrollIntoView(), 500);
});
}
}
},
methods: { methods: {
// 'this.values' is always an array for this component, even if it is single valued. // 'this.values' is always an array for this component, even if it is single valued.
setInitialValues() { setInitialValues() {
@ -256,6 +269,14 @@
border-bottom: 1px solid var(--tainacan-input-border-color); border-bottom: 1px solid var(--tainacan-input-border-color);
padding: 10px var(--tainacan-container-padding); padding: 10px var(--tainacan-container-padding);
&.hightlighted-metadatum {
background-color: var(--tainacan-white);
transition: background-color 0.8s;
animation-name: metadatum-highlight;
animation-duration: 3s;
animation-iteration-count: 2;
}
&.has-collapses-hidden { &.has-collapses-hidden {
border-bottom: none; border-bottom: none;
padding: 10px !important; padding: 10px !important;
@ -275,25 +296,21 @@
margin-left: 15px; margin-left: 15px;
margin-bottom: 0; margin-bottom: 0;
margin-top: 0.15em; margin-top: 0.15em;
max-width: 50%;
} }
.metadata-type { .metadata-type {
font-size: 0.8125em; font-size: 0.8125em;
font-weight: 400; font-weight: 400;
color: var(--tainacan-info-color); color: var(--tainacan-info-color);
opacity: 0.75; opacity: 0.75;
top: -0.1em;
position: relative; position: relative;
} }
.help-wrapper {
top: -0.2em;
}
.collapse-handle { .collapse-handle {
cursor: pointer; cursor: pointer;
position: relative;
margin-left: -42px; margin-left: -42px;
bottom: 0.1em; line-height: 1.5em;
white-space: nowrap; }
.collapse-handle+div {
margin-top: 0.5em;
} }
.add-link { .add-link {
font-size: 0.75em; font-size: 0.75em;

View File

@ -18,7 +18,8 @@
v-model="taxonomy_id" v-model="taxonomy_id"
@input="emitValues()" @input="emitValues()"
@focus="clear" @focus="clear"
:loading="loading"> :loading="loading"
expanded>
<option value="">{{ $i18n.get('label_selectbox_init') }}...</option> <option value="">{{ $i18n.get('label_selectbox_init') }}...</option>
<option <option
v-for="option in taxonomies" v-for="option in taxonomies"
@ -41,7 +42,8 @@
name="metadata_type_options[component_type]" name="metadata_type_options[component_type]"
placeholder="Select the input type for the taxonomy metadatum" placeholder="Select the input type for the taxonomy metadatum"
@input="emitValues()" @input="emitValues()"
v-model="input_type"> v-model="input_type"
expanded>
<option <option
v-for="(option, index) in single_types" v-for="(option, index) in single_types"
:value="index" :value="index"
@ -55,7 +57,8 @@
placeholder="Select the input type for the taxonomy metadatum" placeholder="Select the input type for the taxonomy metadatum"
v-model="input_type" v-model="input_type"
@input="emitValues()" @input="emitValues()"
v-else> v-else
expanded>
<option <option
v-for="(option, index) in multiple_types" v-for="(option, index) in multiple_types"
:value="index" :value="index"

View File

@ -7,9 +7,7 @@
tabindex="-1" tabindex="-1"
aria-modal aria-modal
ref="availableExportersModal"> ref="availableExportersModal">
<div <div style="width: auto">
class="tainacan-modal-content"
style="width: auto">
<header class="tainacan-modal-title"> <header class="tainacan-modal-title">
<h2>{{ this.$i18n.get('exporters') }}</h2> <h2>{{ this.$i18n.get('exporters') }}</h2>
<hr> <hr>

View File

@ -1,8 +1,10 @@
<template> <template>
<div> <div>
<figure <figure
class="document-item">
<!-- <figure
class="document-item" class="document-item"
@click.prevent="isPreviewModalActive = true"> @click.prevent="isPreviewModalActive = true"> -->
<div <div
class="image-wrapper" class="image-wrapper"
v-html="documentHtml" /> v-html="documentHtml" />
@ -15,6 +17,7 @@
trap-focus trap-focus
aria-modal aria-modal
aria-role="dialog" aria-role="dialog"
:append-to-body="true"
custom-class="tainacan-modal"> custom-class="tainacan-modal">
<!-- <div class="tainacan-modal-content"> <!-- <div class="tainacan-modal-content">
<div class="tainacan-modal-title"> <div class="tainacan-modal-title">

View File

@ -333,7 +333,11 @@ export default {
this.$store.dispatch('search/setAdminViewMode', adminViewMode); this.$store.dispatch('search/setAdminViewMode', adminViewMode);
this.updateURLQueries(); this.updateURLQueries();
}, },
setSelectedItemsForIframe(selectedItems) { setSelectedItemsForIframe(selectedItems, singleSelection) {
if (singleSelection)
this.$store.dispatch('search/cleanSelectedItems');
this.$store.dispatch('search/setSelectedItems', selectedItems); this.$store.dispatch('search/setSelectedItems', selectedItems);
let currentSelectedItems = this.$store.getters['search/getSelectedItems']; let currentSelectedItems = this.$store.getters['search/getSelectedItems'];

View File

@ -200,6 +200,26 @@ export const updateItemDocument = ({ commit }, { item_id, document, document_typ
}); });
}; };
export const fetchOnlyRelatedItems = ({ commit }, { itemId, contextEdit } ) => {
let endpoint = '/items/'+ itemId + '?';
if (contextEdit)
endpoint += '&context=edit';
endpoint += '&fetch_only=related_items'
return new Promise((resolve, reject) => {
axios.tainacan.get(endpoint)
.then(res => {
let relatedItems = res.data && res.data.related_items ? res.data.related_items : [];
commit('setOnlyRelatedItemsToItem', {itemId: itemId, relatedItems: relatedItems });
resolve( relatedItems );
})
.catch((thrown) => reject(thrown));
});
};
// Attachments ======================================= // Attachments =======================================
export const sendAttachment = ( { commit }, { item_id, file }) => { export const sendAttachment = ( { commit }, { item_id, file }) => {
commit('cleanAttachment'); commit('cleanAttachment');

View File

@ -62,6 +62,12 @@ export const cleanItemMetadata = (state) => {
state.itemMetadata = []; state.itemMetadata = [];
} }
export const setOnlyRelatedItemsToItem = (state, { itemId, relatedItems }) => {
if (state.item && state.item.id && state.item.id == itemId) {
state.item.related_items = relatedItems;
}
}
export const setSingleMetadatum = (state, itemMetadatum) => { export const setSingleMetadatum = (state, itemMetadatum) => {
if (itemMetadatum.metadatum.parent <= 0) { if (itemMetadatum.metadatum.parent <= 0) {

View File

@ -9,7 +9,7 @@
<!-- PAGE TITLE --------------------- --> <!-- PAGE TITLE --------------------- -->
<tainacan-title <tainacan-title
v-if="!$route.query.iframemode && !$route.query.readmode && !openAdvancedSearch" v-if="!isIframeMode && !isReadMode && !openAdvancedSearch"
:bread-crumb-items="[{ path: '', label: this.$i18n.get('items') }]"/> :bread-crumb-items="[{ path: '', label: this.$i18n.get('items') }]"/>
<div <div
v-else-if="openAdvancedSearch" v-else-if="openAdvancedSearch"
@ -101,7 +101,7 @@
<!-- Item Creation Dropdown, only on Admin --> <!-- Item Creation Dropdown, only on Admin -->
<div <div
class="search-control-item" class="search-control-item"
v-if="!$route.query.iframemode && v-if="!isIframeMode &&
!openAdvancedSearch && !openAdvancedSearch &&
collection && collection &&
collection.current_user_can_edit_items"> collection.current_user_can_edit_items">
@ -413,7 +413,7 @@
<!-- Exposers or alternative links modal button --> <!-- Exposers or alternative links modal button -->
<div <div
v-if="!$route.query.iframemode" v-if="!isIframeMode"
class="search-control-item"> class="search-control-item">
<button <button
class="button is-white" class="button is-white"
@ -494,7 +494,7 @@
<!-- STATUS TABS, only on Admin -------- --> <!-- STATUS TABS, only on Admin -------- -->
<items-status-tabs <items-status-tabs
v-if="!openAdvancedSearch && !$route.query.iframemode" v-if="!openAdvancedSearch && !isIframeMode"
:is-repository-level="isRepositoryLevel"/> :is-repository-level="isRepositoryLevel"/>
<!-- FILTERS TAG LIST--> <!-- FILTERS TAG LIST-->
@ -595,7 +595,7 @@
</p> </p>
<router-link <router-link
v-if="!isRepositoryLevel && !isSortingByCustomMetadata && !hasFiltered && (status == undefined || status == '') && !$route.query.iframemode" v-if="!isRepositoryLevel && !isSortingByCustomMetadata && !hasFiltered && (status == undefined || status == '') && !isIframeMode"
id="button-create-item" id="button-create-item"
tag="button" tag="button"
class="button is-secondary" class="button is-secondary"
@ -603,7 +603,7 @@
{{ $i18n.getFrom('items', 'add_new') }} {{ $i18n.getFrom('items', 'add_new') }}
</router-link> </router-link>
<button <button
v-else-if="isRepositoryLevel && !isSortingByCustomMetadata && !hasFiltered && (status == undefined || status == '') && !$route.query.iframemode" v-else-if="isRepositoryLevel && !isSortingByCustomMetadata && !hasFiltered && (status == undefined || status == '') && !isIframeMode"
id="button-create-item" id="button-create-item"
class="button is-secondary" class="button is-secondary"
@click="onOpenCollectionsModal"> @click="onOpenCollectionsModal">
@ -731,7 +731,13 @@
} }
} }
return ''; return '';
} },
isReadMode () {
return this.$route && this.$route.query && this.$route.query.readmode;
},
isIframeMode () {
return this.$route && this.$route.query && this.$route.query.iframemode;
},
}, },
watch: { watch: {
displayedMetadata() { displayedMetadata() {

View File

@ -23,104 +23,8 @@
</div> </div>
<div class="tainacan-form"> <div class="tainacan-form">
<div class="columns"> <div class="columns">
<div class="column is-5">
<!-- Hook for extra Form options --> <div class="column is-7">
<template
v-if="formHooks != undefined &&
formHooks['view-item'] != undefined &&
formHooks['view-item']['begin-left'] != undefined">
<div
id="view-item-begin-left"
class="form-hook-region"
v-html="formHooks['view-item']['begin-left'].join('')"/>
</template>
<!-- Document -------------------------------- -->
<div class="section-label">
<label>{{ item.document !== undefined && item.document !== null && item.document !== ''
?
$i18n.get('label_document') : $i18n.get('label_document_empty') }}</label>
</div>
<div class="section-box document-field">
<div
v-if="item.document !== undefined && item.document !== null &&
item.document_type !== undefined && item.document_type !== null &&
item.document !== '' && item.document_type !== 'empty'">
<div v-if="item.document_type === 'attachment'">
<!-- <div v-html="item.document_as_html"/> -->
<document-item :document-html="item.document_as_html"/>
</div>
<div v-else-if="item.document_type === 'text'">
<div v-html="item.document_as_html"/>
</div>
<div v-else-if="item.document_type === 'url'">
<div v-html="item.document_as_html"/>
</div>
</div>
<div v-else>
<p>{{ $i18n.get('info_no_document_to_item') }}</p>
</div>
</div>
<!-- Thumbnail -------------------------------- -->
<div class="section-label">
<label>{{ $i18n.get('label_thumbnail') }}</label>
</div>
<div class="section-box section-thumbnail">
<div class="thumbnail-field">
<file-item
v-if="item.thumbnail != undefined && ((item.thumbnail['tainacan-medium'] != undefined && item.thumbnail['tainacan-medium'] != false) || (item.thumbnail.medium != undefined && item.thumbnail.medium != false))"
:show-name="false"
:modal-on-click="false"
:size="178"
:file="{
media_type: 'image',
thumbnails: { 'tainacan-medium': [ $thumbHelper.getSrc(item['thumbnail'], 'tainacan-medium', item.document_mimetype) ] },
title: $i18n.get('label_thumbnail'),
description: `<img alt='` + $i18n.get('label_thumbnail') + `' src='` + $thumbHelper.getSrc(item['thumbnail'], 'full', item.document_mimetype) + `'/>`
}"/>
<figure
v-if="item.thumbnail == undefined || ((item.thumbnail.medium == undefined || item.thumbnail.medium == false) && (item.thumbnail['tainacan-medium'] == undefined || item.thumbnail['tainacan-medium'] == false))"
class="image">
<span
v-if="item.document_type == 'empty'"
class="image-placeholder">
{{ $i18n.get('label_empty_thumbnail') }}
</span>
<img
:alt="$i18n.get('label_thumbnail')"
:src="$thumbHelper.getEmptyThumbnailPlaceholder(item.document_mimetype)">
</figure>
</div>
<br>
<div
v-if="item.thumbnail_id"
class="thumbnail-alt-input">
<label class="label">{{ $i18n.get('label_thumbnail_alt') }}</label>
<help-button
:title="$i18n.get('label_thumbnail_alt')"
:message="$i18n.get('info_thumbnail_alt')"/>
<p> {{ item.thumbnail_alt }}</p>
</div>
</div>
<!-- Hook for extra Form options -->
<template
v-if="formHooks != undefined &&
formHooks['view-item'] != undefined &&
formHooks['view-item']['end-left'] != undefined">
<div
id="view-item-end-left"
class="form-hook-region"
v-html="formHooks['view-item']['end-left'].join('')"/>
</template>
</div>
<div class="column is-7">
<!-- Hook for extra Form options --> <!-- Hook for extra Form options -->
<template <template
@ -253,7 +157,7 @@
<b-tabs v-model="activeTab"> <b-tabs v-model="activeTab">
<b-tab-item> <b-tab-item>
<template slot="header"> <template slot="header">
<span class="icon has-text-gray4"> <span class="icon has-text-gray5">
<i class="tainacan-icon tainacan-icon-18px tainacan-icon-metadata"/> <i class="tainacan-icon tainacan-icon-18px tainacan-icon-metadata"/>
</span> </span>
<span>{{ $i18n.get('metadata') }}</span> <span>{{ $i18n.get('metadata') }}</span>
@ -291,9 +195,38 @@
</template> </template>
</b-tab-item> </b-tab-item>
<!-- Related items -->
<b-tab-item v-if="totalRelatedItems">
<template slot="header">
<span class="icon has-text-gray5">
<i class="tainacan-icon tainacan-icon-1-25em tainacan-icon-processes tainacan-icon-rotate-270"/>
</span>
<span>
{{ $i18n.get('label_related_items') }}
<span class="has-text-gray">
({{ totalRelatedItems }})
</span>
</span>
</template>
<div class="attachments-list-heading">
<p>
{{ $i18n.get("info_related_items") }}
</p>
</div>
<related-items-list
:item-id="itemId"
:collection-id="collectionId"
:related-items="item.related_items"
:is-editable="false"
:is-loading.sync="isLoading" />
</b-tab-item>
<b-tab-item> <b-tab-item>
<template slot="header"> <template slot="header">
<span class="icon has-text-gray4"> <span class="icon has-text-gray5">
<i class="tainacan-icon tainacan-icon-18px tainacan-icon-attachments"/> <i class="tainacan-icon tainacan-icon-18px tainacan-icon-attachments"/>
</span> </span>
<span> <span>
@ -315,16 +248,118 @@
<b-tab-item> <b-tab-item>
<template slot="header"> <template slot="header">
<span class="icon has-text-gray4"> <span class="icon has-text-gray5">
<i class="tainacan-icon tainacan-icon-18px tainacan-icon-activities"/> <i class="tainacan-icon tainacan-icon-18px tainacan-icon-activities"/>
</span> </span>
<span>{{ $i18n.get('activities') }}</span> <span>{{ $i18n.get('activities') }}</span>
</template> </template>
<activities-page v-if="activeTab == 2"/> <activities-page v-if="activeTab == 3"/>
</b-tab-item> </b-tab-item>
</b-tabs> </b-tabs>
</div> </div>
<div class="column is-5">
<div class="sticky-container">
<!-- Hook for extra Form options -->
<template
v-if="formHooks != undefined &&
formHooks['view-item'] != undefined &&
formHooks['view-item']['begin-left'] != undefined">
<div
id="view-item-begin-left"
class="form-hook-region"
v-html="formHooks['view-item']['begin-left'].join('')"/>
</template>
<!-- Document -------------------------------- -->
<div class="section-label">
<label>{{ item.document !== undefined && item.document !== null && item.document !== ''
?
$i18n.get('label_document') : $i18n.get('label_document_empty') }}</label>
</div>
<div class="section-box document-field">
<div
v-if="item.document !== undefined && item.document !== null &&
item.document_type !== undefined && item.document_type !== null &&
item.document !== '' && item.document_type !== 'empty'">
<div v-if="item.document_type === 'attachment'">
<!-- <div v-html="item.document_as_html"/> -->
<document-item :document-html="item.document_as_html"/>
</div>
<div v-else-if="item.document_type === 'text'">
<div v-html="item.document_as_html"/>
</div>
<div v-else-if="item.document_type === 'url'">
<div v-html="item.document_as_html"/>
</div>
</div>
<div v-else>
<p>{{ $i18n.get('info_no_document_to_item') }}</p>
</div>
</div>
<!-- Thumbnail -------------------------------- -->
<div class="section-label">
<label>{{ $i18n.get('label_thumbnail') }}</label>
</div>
<div class="section-box section-thumbnail">
<div class="thumbnail-field">
<file-item
v-if="item.thumbnail != undefined && ((item.thumbnail['tainacan-medium'] != undefined && item.thumbnail['tainacan-medium'] != false) || (item.thumbnail.medium != undefined && item.thumbnail.medium != false))"
:show-name="false"
:modal-on-click="false"
:size="148"
:file="{
media_type: 'image',
thumbnails: { 'tainacan-medium': [ $thumbHelper.getSrc(item['thumbnail'], 'tainacan-medium', item.document_mimetype) ] },
title: $i18n.get('label_thumbnail'),
description: `<img alt='` + $i18n.get('label_thumbnail') + `' src='` + $thumbHelper.getSrc(item['thumbnail'], 'full', item.document_mimetype) + `'/>`
}"/>
<figure
v-if="item.thumbnail == undefined || ((item.thumbnail.medium == undefined || item.thumbnail.medium == false) && (item.thumbnail['tainacan-medium'] == undefined || item.thumbnail['tainacan-medium'] == false))"
class="image">
<span
v-if="item.document_type == 'empty'"
class="image-placeholder">
{{ $i18n.get('label_empty_thumbnail') }}
</span>
<img
:alt="$i18n.get('label_thumbnail')"
:src="$thumbHelper.getEmptyThumbnailPlaceholder(item.document_mimetype)">
</figure>
</div>
<br>
<div
v-if="item.thumbnail_id"
class="thumbnail-alt-input">
<label class="label">{{ $i18n.get('label_thumbnail_alt') }}</label>
<help-button
:title="$i18n.get('label_thumbnail_alt')"
:message="$i18n.get('info_thumbnail_alt')"/>
<p> {{ item.thumbnail_alt }}</p>
</div>
</div>
<!-- Hook for extra Form options -->
<template
v-if="formHooks != undefined &&
formHooks['view-item'] != undefined &&
formHooks['view-item']['end-left'] != undefined">
<div
id="view-item-end-left"
class="form-hook-region"
v-html="formHooks['view-item']['end-left'].join('')"/>
</template>
</div>
</div>
</div> </div>
<footer class="footer"> <footer class="footer">
<div class="form-submission-footer"> <div class="form-submission-footer">
@ -380,6 +415,7 @@
import ActivitiesPage from '../lists/activities-page.vue'; import ActivitiesPage from '../lists/activities-page.vue';
import ExposersModal from '../../components/modals/exposers-modal.vue'; import ExposersModal from '../../components/modals/exposers-modal.vue';
import AttachmentsList from '../../components/lists/attachments-list.vue'; import AttachmentsList from '../../components/lists/attachments-list.vue';
import RelatedItemsList from '../../components/lists/related-items-list.vue';
export default { export default {
name: 'ItemPage', name: 'ItemPage',
@ -387,6 +423,7 @@
FileItem, FileItem,
DocumentItem, DocumentItem,
ActivitiesPage, ActivitiesPage,
RelatedItemsList,
AttachmentsList AttachmentsList
}, },
mixins: [formHooks], mixins: [formHooks],
@ -414,6 +451,9 @@
metadatumList() { metadatumList() {
return JSON.parse(JSON.stringify(this.getItemMetadata())); return JSON.parse(JSON.stringify(this.getItemMetadata()));
}, },
totalRelatedItems() {
return (this.item && this.item.related_items) ? Object.values(this.item.related_items).reduce((totalItems, aRelatedItemsGroup) => totalItems + parseInt(aRelatedItemsGroup.total_items), 0) : false;
},
totalAttachments() { totalAttachments() {
return this.getTotalAttachments(); return this.getTotalAttachments();
}, },
@ -437,7 +477,7 @@
this.fetchItem({ this.fetchItem({
itemId: this.itemId, itemId: this.itemId,
contextEdit: true, contextEdit: true,
fetchOnly: 'title,thumbnail,status,modification_date,document_type,document_mimetype,document,comment_status,document_as_html' fetchOnly: 'title,thumbnail,status,modification_date,document_type,document_mimetype,document,comment_status,document_as_html,related_items'
}) })
.then((resp) => { .then((resp) => {
resp.request.then((item) => { resp.request.then((item) => {
@ -558,13 +598,20 @@
padding-left: var(--tainacan-one-column); padding-left: var(--tainacan-one-column);
padding-right: var(--tainacan-one-column); padding-right: var(--tainacan-one-column);
.sticky-container {
position: relative;
position: sticky;
top: 0;
margin: 3px 0;
}
@media screen and (max-width: 769px) { @media screen and (max-width: 769px) {
width: 100%; width: 100%;
} }
} }
.column.is-7 { .column.is-7 {
padding-left: 0; padding-left: var(--tainacan-one-column);
padding-right: var(--tainacan-one-column); padding-right: 0;
.columns { .columns {
flex-wrap: wrap; flex-wrap: wrap;
@ -580,7 +627,7 @@
} }
@media screen and (max-width: 769px) { @media screen and (max-width: 769px) {
padding-left: var(--tainacan-one-column); padding-right: var(--tainacan-one-column);
width: 100%; width: 100%;
} }
} }
@ -625,8 +672,8 @@
.section-box { .section-box {
background-color: var(--tainacan-white); background-color: var(--tainacan-white);
padding: 0 var(--tainacan-one-column) 0 0; padding: 0 var(--tainacan-one-column) 0 0;
margin-top: 18px; margin-top: 12px;
margin-bottom: 32px; margin-bottom: 18px;
ul { ul {
display: flex; display: flex;
@ -700,20 +747,31 @@
font-size: 0.8em; font-size: 0.8em;
} }
img { img {
height: 178px; height: 148px;
width: 178px; width: 148px;
} }
.image-placeholder { .image-placeholder {
position: absolute; position: absolute;
margin-left: 45px; margin-left: 32px;
margin-right: 45px; margin-right: 32px;
font-size: 0.8em; font-size: 0.8em;
font-weight: bold; font-weight: bold;
z-index: 99; z-index: 99;
text-align: center; text-align: center;
color: var(--tainacan-info-color); color: var(--tainacan-info-color);
top: 70px; top: 60px;
max-width: 90px; max-width: 84px;
}
}
.attachments-list-heading {
display: flex;
align-items: center;
margin-top: 12px;
margin-bottom: 24px;
button {
margin-right: 12px;
} }
} }

View File

@ -548,4 +548,13 @@
-moz-animation: skeleton-animation 1.8s ease infinite; -moz-animation: skeleton-animation 1.8s ease infinite;
-o-animation: skeleton-animation 1.8s ease infinite; -o-animation: skeleton-animation 1.8s ease infinite;
animation: skeleton-animation 1.8s ease infinite; animation: skeleton-animation 1.8s ease infinite;
}
@keyframes metadatum-highlight {
from {
background-color: var(--tainacan-primary1);
}
to {
background-color: var(--tainacan-white);
}
} }

View File

@ -76,7 +76,8 @@
.tainacan-table { .tainacan-table {
tr.selected-row { tr.selected-row {
background-color: var(--tainacan-blue1) !important; background-color: var(--tainacan-blue1) !important;
.checkbox-cell .checkbox, .actions-cell .actions-container { .checkbox-cell .checkbox, .actions-cell .actions-container,
.checkbox-cell .radio, .actions-cell .actions-container {
background-color: var(--tainacan-blue2) !important; background-color: var(--tainacan-blue2) !important;
} }
} }

View File

@ -59,7 +59,8 @@
top: auto; top: auto;
display: table-cell; display: table-cell;
label.checkbox { label.checkbox,
label.radio {
border-radius: 0px; border-radius: 0px;
background-color: var(--tainacan-white); background-color: var(--tainacan-white);
padding: 0; padding: 0;
@ -69,7 +70,8 @@
justify-content: center; justify-content: center;
transition: background-color 0.15s ease; transition: background-color 0.15s ease;
} }
.b-checkbox.checkbox .control-label { .b-checkbox.checkbox .control-label,
.b-radio.radio .control-label {
display: none; display: none;
} }
&.is-selecting { &.is-selecting {
@ -258,6 +260,9 @@
.checkbox { .checkbox {
background-color: var(--tainacan-item-heading-hover-background-color) !important; background-color: var(--tainacan-item-heading-hover-background-color) !important;
} }
.radio {
background-color: var(--tainacan-item-heading-hover-background-color) !important;
}
} }
.actions-cell { .actions-cell {
.actions-container { .actions-container {

View File

@ -29,11 +29,8 @@
text-transform: inherit; text-transform: inherit;
font-size: 0.875em; font-size: 0.875em;
color: var(--tainacan-label-color); color: var(--tainacan-label-color);
display: inline-block; display: inline;
white-space: nowrap; line-height: 2.0em;
text-overflow: ellipsis;
overflow: hidden;
vertical-align: top;
} }
.section-label>label { .section-label>label {
cursor: default; cursor: default;

View File

@ -41,6 +41,7 @@
bottom: 0; bottom: 0;
margin-bottom: -25px; margin-bottom: -25px;
padding-right: 12px; padding-right: 12px;
margin-bottom: -25px;
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
visibility: hidden; visibility: hidden;

View File

@ -12,6 +12,7 @@ const TAINACAN_BLOCKS = [
'carousel-items-list' => [ 'has_theme_script' => true ], 'carousel-items-list' => [ 'has_theme_script' => true ],
'carousel-terms-list' => [ 'has_theme_script' => true ], 'carousel-terms-list' => [ 'has_theme_script' => true ],
'carousel-collections-list' => [ 'has_theme_script' => true ], 'carousel-collections-list' => [ 'has_theme_script' => true ],
'carousel-related-items' => [],
'terms-list' => [ 'extra_editor_script_deps' => array('undescore') ], 'terms-list' => [ 'extra_editor_script_deps' => array('undescore') ],
]; ];

View File

@ -165,7 +165,7 @@
<path d="M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z"/> <path d="M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z"/>
<path <path
d="M0 0h24v24H0z" d="M0 0h24v24H0z"
fill="none"/> fill="none"/>
</svg> </svg>
</button> </button>
</div> </div>

View File

@ -142,7 +142,7 @@
color: var(--tainacan-block-gray5, $gray5); color: var(--tainacan-block-gray5, $gray5);
font-weight: bold; font-weight: bold;
text-decoration: none; text-decoration: none;
padding: 8px 16px; padding: 8px 12px;
display: block; display: block;
line-height: 1.2em; line-height: 1.2em;
word-break: break-word; word-break: break-word;

View File

@ -11,7 +11,7 @@ document.addEventListener("DOMContentLoaded", () => {
// Gets all divs with content created by our block; // Gets all divs with content created by our block;
let blocks = document.getElementsByClassName('wp-block-tainacan-carousel-items-list'); let blocks = document.getElementsByClassName('wp-block-tainacan-carousel-items-list');
if (blocks) { if (blocks) {
let blockIds = Object.values(blocks).map((block) => block.id); let blockIds = Object.values(blocks).map((block) => block.id);

View File

@ -284,7 +284,15 @@ export default {
this.itemsRequestSource = axios.CancelToken.source(); this.itemsRequestSource = axios.CancelToken.source();
if (this.loadStrategy == 'selection') { if (this.loadStrategy == 'parent') {
for (let item of this.selectedItems)
this.items.push(item);
this.isLoading = false;
this.totalItems = this.items.length;
} else if (this.loadStrategy == 'selection') {
let endpoint = '/collection/' + this.collectionId + '/items?' + qs.stringify({ postin: this.selectedItems, perpage: this.selectedItems.length }) + '&fetch_only=title,url,thumbnail'; let endpoint = '/collection/' + this.collectionId + '/items?' + qs.stringify({ postin: this.selectedItems, perpage: this.selectedItems.length }) + '&fetch_only=title,url,thumbnail';
this.tainacanAxios.get(endpoint, { cancelToken: this.itemsRequestSource.token }) this.tainacanAxios.get(endpoint, { cancelToken: this.itemsRequestSource.token })
@ -354,7 +362,7 @@ export default {
fetchCollectionForHeader() { fetchCollectionForHeader() {
if (this.showCollectionHeader) { if (this.showCollectionHeader) {
this.isLoadingCollection = true; this.isLoadingCollection = true;
this.tainacanAxios.get('/collections/' + this.collectionId + '?fetch_only=name,thumbnail,header_image') this.tainacanAxios.get('/collections/' + this.collectionId + '?fetch_only=name,thumbnail,header_image')
.then(response => { .then(response => {

View File

@ -154,7 +154,7 @@
color: var(--tainacan-block-gray5, $gray5); color: var(--tainacan-block-gray5, $gray5);
font-weight: bold; font-weight: bold;
text-decoration: none; text-decoration: none;
padding: 8px 16px; padding: 8px 12px;
display: block; display: block;
line-height: 1.2em; line-height: 1.2em;
word-break: break-word; word-break: break-word;

View File

@ -219,23 +219,35 @@ registerBlockType('tainacan/carousel-items-list', {
); );
} }
function setContent(){ function setContent() {
isLoading = true; isLoading = true;
setAttributes({ setAttributes({
isLoading: isLoading isLoading: isLoading
}); });
if (itemsRequestSource != undefined && typeof itemsRequestSource == 'function')
itemsRequestSource.cancel('Previous items search canceled.');
itemsRequestSource = axios.CancelToken.source();
items = []; items = [];
if (loadStrategy == 'selection') { if (loadStrategy == 'parent') {
let endpoint = '/collection/' + collectionId + '/items?'+ qs.stringify({ postin: selectedItems, perpage: selectedItems.length }) + '&fetch_only=title,url,thumbnail';
for (let item of selectedItems)
items.push(prepareItem(item));
setAttributes({
content: <div></div>,
items: items,
isLoading: false
});
} else if (loadStrategy == 'selection') {
if (itemsRequestSource != undefined && typeof itemsRequestSource == 'function')
itemsRequestSource.cancel('Previous items search canceled.');
itemsRequestSource = axios.CancelToken.source();
let endpoint = '/collection/' + collectionId + '/items?'+ qs.stringify({ postin: selectedItems, perpage: selectedItems.length }) + '&fetch_only=title,url,thumbnail';
tainacan.get(endpoint, { cancelToken: itemsRequestSource.token }) tainacan.get(endpoint, { cancelToken: itemsRequestSource.token })
.then(response => { .then(response => {
@ -255,6 +267,11 @@ registerBlockType('tainacan/carousel-items-list', {
let query = endpoint.split('?')[1]; let query = endpoint.split('?')[1];
let queryObject = qs.parse(query); let queryObject = qs.parse(query);
if (itemsRequestSource != undefined && typeof itemsRequestSource == 'function')
itemsRequestSource.cancel('Previous items search canceled.');
itemsRequestSource = axios.CancelToken.source();
// Set up max items to be shown // Set up max items to be shown
if (maxItemsNumber != undefined && maxItemsNumber > 0) if (maxItemsNumber != undefined && maxItemsNumber > 0)
queryObject.perpage = maxItemsNumber; queryObject.perpage = maxItemsNumber;
@ -365,32 +382,35 @@ registerBlockType('tainacan/carousel-items-list', {
{ items.length ? { items.length ?
<BlockControls> <BlockControls>
{ loadStrategy != 'search' ? { loadStrategy != 'parent' ?
TainacanBlocksCompatToolbar({ (
label: __('Add more items', 'tainacan'), loadStrategy != 'search' ?
icon: <svg TainacanBlocksCompatToolbar({
xmlns="http://www.w3.org/2000/svg" label: __('Add more items', 'tainacan'),
viewBox="0 0 24 24" icon: <svg
height="24px"
width="24px">
<path d="M16,6H12a2,2,0,0,0-2,2v6.52A6,6,0,0,1,12,19a6,6,0,0,1-.73,2.88A1.92,1.92,0,0,0,12,22h8a2,2,0,0,0,2-2V12Zm-1,6V7.5L19.51,12ZM15,2V4H8v9.33A5.8,5.8,0,0,0,6,13V4A2,2,0,0,1,8,2ZM10.09,19.05,7,22.11V16.05L8,17l2,2ZM5,16.05v6.06L2,19.11Z"/>
</svg>,
onClick: openCarouseltemsModal,
onClickParams: 'selection'
})
:
TainacanBlocksCompatToolbar({
label: __('Configure a search', 'tainacan'),
icon: <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24" viewBox="0 0 24 24"
height="24px" height="24px"
width="24px"> width="24px">
<path d="M16,6H12a2,2,0,0,0-2,2v6.52A6,6,0,0,1,12,19a6,6,0,0,1-.73,2.88A1.92,1.92,0,0,0,12,22h8a2,2,0,0,0,2-2V12Zm-1,6V7.5L19.51,12ZM15,2V4H8v9.33A5.8,5.8,0,0,0,6,13V4A2,2,0,0,1,8,2ZM10.09,19.05,7,22.11V16.05L8,17l2,2ZM5,16.05v6.06L2,19.11Z"/> <path d="M16,6H12a2,2,0,0,0-2,2v6.52A6,6,0,0,1,12,19a6,6,0,0,1-.73,2.88A1.92,1.92,0,0,0,12,22h8a2,2,0,0,0,2-2V12Zm-1,6V7.5L19.51,12ZM15,2V4H8v9.33A5.8,5.8,0,0,0,6,13V4A2,2,0,0,1,8,2ZM10.09,19.05,7,22.11V16.05L8,17l2,2ZM5,16.05v6.06L2,19.11Z"/>
</svg>, </svg>,
onClick: openCarouseltemsModal, onClick: openCarouseltemsModal,
onClickParams: 'search' onClickParams: 'selection'
}) })
:
TainacanBlocksCompatToolbar({
label: __('Configure a search', 'tainacan'),
icon: <svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
height="24px"
width="24px">
<path d="M16,6H12a2,2,0,0,0-2,2v6.52A6,6,0,0,1,12,19a6,6,0,0,1-.73,2.88A1.92,1.92,0,0,0,12,22h8a2,2,0,0,0,2-2V12Zm-1,6V7.5L19.51,12ZM15,2V4H8v9.33A5.8,5.8,0,0,0,6,13V4A2,2,0,0,1,8,2ZM10.09,19.05,7,22.11V16.05L8,17l2,2ZM5,16.05v6.06L2,19.11Z"/>
</svg>,
onClick: openCarouseltemsModal,
onClickParams: 'search'
})
) : null
} }
</BlockControls> </BlockControls>
: null } : null }
@ -461,18 +481,22 @@ registerBlockType('tainacan/carousel-items-list', {
initialOpen={ true } initialOpen={ true }
> >
<div> <div>
<RangeControl {
label={ __('Maximum items per slide on a wide screen', 'tainacan') } loadStrategy != 'parent' ?
help={ maxItemsPerScreen <= 4 ? __('Warning: with such a small number of items per slide, the image size is greater, thus the cropped version of the thumbnail won\'t be used.', 'tainacan') : null } <RangeControl
value={ maxItemsPerScreen ? maxItemsPerScreen : 7 } label={ __('Maximum items per slide on a wide screen', 'tainacan') }
onChange={ ( aMaxItemsPerScreen ) => { help={ maxItemsPerScreen <= 4 ? __('Warning: with such a small number of items per slide, the image size is greater, thus the cropped version of the thumbnail won\'t be used.', 'tainacan') : null }
maxItemsPerScreen = aMaxItemsPerScreen; value={ maxItemsPerScreen ? maxItemsPerScreen : 7 }
setAttributes( { maxItemsPerScreen: aMaxItemsPerScreen } ); onChange={ ( aMaxItemsPerScreen ) => {
setContent(); maxItemsPerScreen = aMaxItemsPerScreen;
}} setAttributes( { maxItemsPerScreen: aMaxItemsPerScreen } );
min={ 1 } setContent();
max={ 9 } }}
/> min={ 1 }
max={ 9 }
/>
: null
}
<ToggleControl <ToggleControl
label={__('Crop Images', 'tainacan')} label={__('Crop Images', 'tainacan')}
help={ cropImagesToSquare && maxItemsPerScreen > 4 ? __('Do not use square cropeed version of the item thumbnail.', 'tainacan') : __('Toggle to use square cropped version of the item thumbnail.', 'tainacan') } help={ cropImagesToSquare && maxItemsPerScreen > 4 ? __('Do not use square cropeed version of the item thumbnail.', 'tainacan') : __('Toggle to use square cropped version of the item thumbnail.', 'tainacan') }
@ -696,19 +720,25 @@ registerBlockType('tainacan/carousel-items-list', {
</svg> </svg>
{__('List items on a Carousel, using search or item selection.', 'tainacan')} {__('List items on a Carousel, using search or item selection.', 'tainacan')}
</p> </p>
<Button {
isPrimary loadStrategy != 'parent' ?
type="button" <div>
onClick={ () => openCarouseltemsModal('selection') }> <Button
{__('Select Items', 'tainacan')} isPrimary
</Button> type="button"
<p style={{ margin: '0 12px' }}>{__('or', 'tainacan')}</p> onClick={ () => openCarouseltemsModal('selection') }>
<Button {__('Select Items', 'tainacan')}
isPrimary </Button>
type="button" <p style={{ margin: '0 12px' }}>{__('or', 'tainacan')}</p>
onClick={ () => openCarouseltemsModal('search') }> <Button
{__('Configure a search', 'tainacan')} isPrimary
</Button> type="button"
onClick={ () => openCarouseltemsModal('search') }>
{__('Configure a search', 'tainacan')}
</Button>
</div>
: null
}
</Placeholder> </Placeholder>
) : null ) : null
} }

View File

@ -0,0 +1,348 @@
import tainacan from '../../js/axios.js';
import axios from 'axios';
const { __ } = wp.i18n;
const { TextControl, Button, Modal, RadioControl, SelectControl, Spinner } = wp.components;
export default class CarouselRelatedItemsModal extends React.Component {
constructor(props) {
super(props);
// Initialize state
this.state = {
collectionsPerPage: 24,
collectionId: undefined,
itemId: undefined,
collectionName: '',
isLoadingCollections: false,
modalCollections: [],
totalModalCollections: 0,
collectionOrderBy: 'date-desc',
collectionPage: 1,
temporaryCollectionId: '',
temporaryItemId: '',
searchCollectionName: '',
collections: [],
collectionsRequestSource: undefined,
searchURL: '',
itemsPerPage: 12
};
// Bind events
this.resetCollections = this.resetCollections.bind(this);
this.selectCollection = this.selectCollection.bind(this);
this.fetchCollections = this.fetchCollections.bind(this);
this.fetchModalCollections = this.fetchModalCollections.bind(this);
this.fetchCollection = this.fetchCollection.bind(this);
this.applyRelatedItem = this.applyRelatedItem.bind(this);
}
componentWillMount() {
this.setState({
collectionId: this.props.existingCollectionId,
itemId: this.props.existingItemId
});
if (this.props.existingCollectionId != null && this.props.existingCollectionId != undefined) {
this.fetchCollection(this.props.existingCollectionId);
this.setState({
searchURL: tainacan_blocks.admin_url + 'admin.php?page=tainacan_admin#/collections/'+ this.props.existingCollectionId + '/items/?singleselectionmode=true&iframemode=true&status=publish'
});
} else {
this.setState({ collectionPage: 1 });
this.fetchModalCollections();
}
}
// COLLECTIONS RELATED --------------------------------------------------
fetchModalCollections() {
let someModalCollections = this.state.modalCollections;
if (this.state.collectionPage <= 1)
someModalCollections = [];
let endpoint = '/collections/?perpage=' + this.state.collectionsPerPage + '&paged=' + this.state.collectionPage;
if (this.state.collectionOrderBy == 'date')
endpoint += '&orderby=date&order=asc';
else if (this.state.collectionOrderBy == 'date-desc')
endpoint += '&orderby=date&order=desc';
else if (this.state.collectionOrderBy == 'title')
endpoint += '&orderby=title&order=asc';
else if (this.state.collectionOrderBy == 'title-desc')
endpoint += '&orderby=title&order=desc';
this.setState({
isLoadingCollections: true,
collectionPage: this.state.collectionPage + 1,
modalCollections: someModalCollections
});
tainacan.get(endpoint)
.then(response => {
let otherModalCollections = this.state.modalCollections;
for (let collection of response.data) {
otherModalCollections.push({
name: collection.name,
id: collection.id
});
}
this.setState({
isLoadingCollections: false,
modalCollections: otherModalCollections,
totalModalCollections: response.headers['x-wp-total']
});
return otherModalCollections;
})
.catch(error => {
console.log('Error trying to fetch collections: ' + error);
});
}
fetchCollection(collectionId) {
tainacan.get('/collections/' + collectionId)
.then((response) => {
this.setState({ collectionName: response.data.name });
}).catch(error => {
console.log('Error trying to fetch collection: ' + error);
});
}
selectCollection(selectedCollectionId) {
this.setState({
collectionId: selectedCollectionId,
searchURL: tainacan_blocks.admin_url + 'admin.php?page=tainacan_admin#/collections/' + selectedCollectionId + '/items/?singleselectionmode=true&iframemode=true&status=publish'
});
this.props.onSelectCollection(selectedCollectionId);
this.fetchCollection(selectedCollectionId);
}
fetchCollections(name) {
if (this.state.collectionsRequestSource != undefined)
this.state.collectionsRequestSource.cancel('Previous collections search canceled.');
let aCollectionRequestSource = axios.CancelToken.source();
this.setState({
collectionsRequestSource: aCollectionRequestSource,
isLoadingCollections: true,
collections: [],
items: []
});
let endpoint = '/collections/?perpage=' + this.state.collectionsPerPage;
if (name != undefined && name != '')
endpoint += '&search=' + name;
if (this.state.collectionOrderBy == 'date')
endpoint += '&orderby=date&order=asc';
else if (this.state.collectionOrderBy == 'date-desc')
endpoint += '&orderby=date&order=desc';
else if (this.state.collectionOrderBy == 'title')
endpoint += '&orderby=title&order=asc';
else if (this.state.collectionOrderBy == 'title-desc')
endpoint += '&orderby=title&order=desc';
tainacan.get(endpoint, { cancelToken: aCollectionRequestSource.token })
.then(response => {
let someCollections = response.data.map((collection) => ({ name: collection.name, id: collection.id + '' }));
this.setState({
isLoadingCollections: false,
collections: someCollections
});
return someCollections;
})
.catch(error => {
console.log('Error trying to fetch collections: ' + error);
});
}
applyRelatedItem() {
let iframe = document.getElementById("itemsFrame");
if (iframe) {
let params = new URLSearchParams(iframe.contentWindow.location.search);
let selectedItems = params.getAll('selecteditems');
params.delete('selecteditems')
this.props.onApplyRelatedItem(selectedItems[0]);
}
}
resetCollections() {
this.setState({
collectionId: null,
collectionPage: 1,
modalCollections: []
});
this.fetchModalCollections();
}
cancelSelection() {
this.setState({
modalCollections: []
});
this.props.onCancelSelection();
}
render() {
return this.state.collectionId != null && this.state.collectionId != undefined ? (
// Items modal
<Modal
className="wp-block-tainacan-modal dynamic-modal"
title={ __('Select one item that has relations', 'tainacan') }
onRequestClose={ () => this.cancelSelection() }
shouldCloseOnClickOutside={ false }
contentLabel={ __('Select onte item that has relations', 'tainacan') }>
<iframe
id="itemsFrame"
src={ this.state.searchURL } />
<div className="modal-footer-area">
<Button
isSecondary
onClick={ () => { this.resetCollections() }}>
{__('Switch collection', 'tainacan')}
</Button>
<Button
style={{ marginLeft: 'auto' }}
isPrimary
onClick={ () => this.applyRelatedItem() }>
{__('Get relations of this item', 'tainacan')}
</Button>
</div>
</Modal>
) : (
// Collections modal
<Modal
className="wp-block-tainacan-modal"
title={__('Select a collection to fetch items from', 'tainacan')}
onRequestClose={ () => this.cancelSelection() }
shouldCloseOnClickOutside={ false }
contentLabel={__('Select item', 'tainacan')}>
<div>
<div className="modal-search-area">
<TextControl
label={__('Search for a collection', 'tainacan')}
placeholder={ __('Search by collection\'s name', 'tainacan') }
value={ this.state.searchCollectionName }
onChange={(value) => {
this.setState({
searchCollectionName: value
});
_.debounce(this.fetchCollections(value), 300);
}}/>
<SelectControl
label={__('Order by', 'tainacan')}
value={ this.state.collectionOrderBy }
options={ [
{ label: __('Latest', 'tainacan'), value: 'date-desc' },
{ label: __('Oldest', 'tainacan'), value: 'date' },
{ label: __('Name (A-Z)', 'tainacan'), value: 'title' },
{ label: __('Name (Z-A)', 'tainacan'), value: 'title-desc' }
] }
onChange={ ( aCollectionOrderBy ) => {
this.state.collectionOrderBy = aCollectionOrderBy;
this.state.collectionPage = 1;
this.setState({
collectionOrderBy: this.state.collectionOrderBy,
collectionPage: this.state.collectionPage
});
if (this.state.searchCollectionName && this.state.searchCollectionName != '') {
this.fetchCollections(this.state.searchCollectionName);
} else {
this.fetchModalCollections();
}
}}/>
</div>
{(
this.state.searchCollectionName != '' ? (
this.state.collections.length > 0 ?
(
<div>
<div className="modal-radio-list">
{
<RadioControl
selected={ this.state.temporaryCollectionId }
options={
this.state.collections.map((collection) => {
return { label: collection.name, value: '' + collection.id }
})
}
onChange={ ( aCollectionId ) => {
this.setState({ temporaryCollectionId: aCollectionId });
} } />
}
</div>
</div>
) :
this.state.isLoadingCollections ? (
<Spinner />
) :
<div className="modal-loadmore-section">
<p>{ __('Sorry, no collection found.', 'tainacan') }</p>
</div>
):
this.state.modalCollections.length > 0 ?
(
<div>
<div className="modal-radio-list">
{
<RadioControl
selected={ this.state.temporaryCollectionId }
options={
this.state.modalCollections.map((collection) => {
return { label: collection.name, value: '' + collection.id }
})
}
onChange={ ( aCollectionId ) => {
this.setState({ temporaryCollectionId: aCollectionId });
} } />
}
</div>
<div className="modal-loadmore-section">
<p>{ __('Showing', 'tainacan') + " " + this.state.modalCollections.length + " " + __('of', 'tainacan') + " " + this.state.totalModalCollections + " " + __('collections', 'tainacan') + "."}</p>
{
this.state.modalCollections.length < this.state.totalModalCollections ? (
<Button
isSecondary
isSmall
onClick={ () => this.fetchModalCollections() }>
{__('Load more', 'tainacan')}
</Button>
) : null
}
</div>
</div>
) : this.state.isLoadingCollections ? <Spinner/> :
<div className="modal-loadmore-section">
<p>{ __('Sorry, no collection found.', 'tainacan') }</p>
</div>
)}
<div className="modal-footer-area">
<Button
isSecondary
onClick={ () => { this.cancelSelection() }}>
{__('Cancel', 'tainacan')}
</Button>
<Button
isPrimary
disabled={ this.state.temporaryCollectionId == undefined || this.state.temporaryCollectionId == null || this.state.temporaryCollectionId == ''}
onClick={ () => { this.selectCollection(this.state.temporaryCollectionId); } }>
{ __('Select item', 'tainacan') }
</Button>
</div>
</div>
</Modal>
);
}
}

View File

@ -0,0 +1,56 @@
@import '../../gutenberg-blocks-variables.scss';
.wp-block-tainacan-carousel-related-items {
margin: 0.5em auto;
width: 100%;
// Spinner
.spinner-container {
min-height: 56px;
padding: 1em;
display: flex;
justify-content: center;
align-items: center;
color: var(--tainacan-block-gray4, $gray4);
}
// Skeleton loading
@-webkit-keyframes skeleton-animation {
0%{opacity: 1.0}
50%{opacity: 0.2}
100%{opacity: 1.0}
}
@-moz-keyframes skeleton-animation {
0%{opacity: 1.0}
50%{opacity: 0.2}
100%{opacity: 1.0}
}
@-o-keyframes skeleton-animation {
0%{opacity: 1.0}
50%{opacity: 0.2}
100%{opacity: 1.0}
}
@keyframes skeleton-animation {
0%{opacity: 1.0}
50%{opacity: 0.2}
100%{opacity: 1.0}
}
.skeleton {
border-radius: 2px;
background: var(--tainacan-block-gray1, $gray1);
-webkit-animation: skeleton-animation 1.8s ease infinite;
-moz-animation: skeleton-animation 1.8s ease infinite;
-o-animation: skeleton-animation 1.8s ease infinite;
animation: skeleton-animation 1.8s ease infinite;
}
// Placeholder on editor side ----------------------------------------------------
.carousel-related-items-edit-container {
position: relative;
& .skeleton {
min-height: 150px;
}
}
}

View File

@ -0,0 +1,288 @@
const { registerBlockType } = wp.blocks;
const { __ } = wp.i18n;
const { Spinner, Button, Placeholder } = wp.components;
const { InnerBlocks} = ( tainacan_blocks.wp_version < '5.2' ? wp.editor : wp.blockEditor );
import CarouselRelatedItemsModal from './carousel-related-items-modal.js';
import tainacan from '../../js/axios.js';
import axios from 'axios';
import DeprecatedBlocks from './carousel-related-items-deprecated.js';
import 'swiper/css/swiper.min.css';
registerBlockType('tainacan/carousel-related-items', {
title: __('Tainacan Related Items Carousel', 'tainacan'),
icon:
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
height="24px"
width="24px">
<path
fill="#298596"
d="M16,6H12a2,2,0,0,0-2,2v6.52A6,6,0,0,1,12,19a6,6,0,0,1-.73,2.88A1.92,1.92,0,0,0,12,22h8a2,2,0,0,0,2-2V12Zm-1,6V7.5L19.51,12ZM15,2V4H8v9.33A5.8,5.8,0,0,0,6,13V4A2,2,0,0,1,8,2ZM10.09,19.05,7,22.11V16.05L8,17l2,2ZM5,16.05v6.06L2,19.11Z"/>
</svg>,
category: 'tainacan-blocks',
keywords: [ __( 'items', 'tainacan' ), __( 'carousel', 'tainacan' ), __( 'slider', 'tainacan' ), __( 'relationship', 'tainacan' ) ],
description: __('A set of carousels to list items related to a certain item via relationship metadata.', 'tainacan'),
example: {
attributes: {
content: 'preview'
}
},
parent: [], // Hides this block while we manage better update logic for its inner blocks.
attributes: {
content: {
type: 'array',
source: 'children',
selector: 'div'
},
collectionId: {
type: String,
default: undefined
},
itemId: {
type: String,
default: undefined
},
isModalOpen: {
type: Boolean,
default: false
},
relatedItems: {
type: Array,
default: []
},
relatedItemsTemplate: {
type: Array,
default: []
},
itemRequestSource: {
type: String,
default: undefined
},
},
supports: {
align: ['full', 'wide'],
html: false,
multiple: true,
},
edit({ attributes, setAttributes, className, isSelected }) {
let {
content,
collectionId,
itemId,
isModalOpen,
relatedItems,
isLoading,
itemRequestSource,
relatedItemsTemplate
} = attributes;
function setContent(){
isLoading = true;
setAttributes({
isLoading: isLoading
});
if (itemRequestSource != undefined && typeof itemRequestSource == 'function')
itemRequestSource.cancel('Previous items search canceled.');
itemRequestSource = axios.CancelToken.source();
let endpoint = '/items/'+ itemId + '?fetch_only=related_items';
tainacan.get(endpoint, { cancelToken: itemRequestSource.token })
.then(response => {
relatedItems = response.data && response.data.related_items ? Object.values(response.data.related_items) : [];
setAttributes({
relatedItems: relatedItems,
isLoading: false,
itemRequestSource: itemRequestSource
});
getRelatedItemsTemplates();
});
}
function openRelatedItemsModal() {
isModalOpen = true;
setAttributes( {
isModalOpen: isModalOpen
} );
}
function getRelatedItemsTemplates() {
relatedItemsTemplate = [];
relatedItems.forEach((collection) => {
if (collection.total_items) {
relatedItemsTemplate.push([
'core/group',
{},
[
[
'core/heading',
{
placeholder: __( 'Collection name', 'tainacan' ),
content: collection.collection_name
}
],
[
'core/paragraph',
{
placeholder: __( 'Relationship metadadum name', 'tainacan' ),
content: collection.metadata_name
}
],
[
'tainacan/carousel-items-list',
{
content: [{ type: 'innerblock' }],
selectedItems: collection.items,
loadStrategy: 'parent',
collectionId: collection.collection_id
}
],
[
'core/buttons',
{},
[
[
'core/button',
{
text: __( 'View all related items', 'tainacan' ),
url: collection.collection_slug ? (collection.collection_slug + '?metaquery[0][key]=' + collection.metadata_id + '&metaquery[0][value][0]=' + itemId + '&metaquery[0][compare]=IN') : ''
}
]
]
],
[
'core/spacer',
{ height: 30 }
]
]
]);
}
});
setAttributes({ relatedItemsTemplate: relatedItemsTemplate});
}
// Executed only on the first load of page
if(content && content.length && content[0].type)
setContent();
return content == 'preview' ?
<div className={className}>
<img
width="100%"
src={ `${tainacan_blocks.base_url}/assets/images/related-carousel-items.png` } />
</div>
: (
<div className={className}>
{ isSelected ?
(
<div>
{ isModalOpen ?
<CarouselRelatedItemsModal
existingCollectionId={ collectionId }
existingItemId={ itemId }
onSelectCollection={ (selectedCollectionId) => {
if (collectionId != selectedCollectionId)
relatedItems = [];
collectionId = selectedCollectionId;
setAttributes({
collectionId: collectionId,
relatedItems: relatedItems
});
}}
onApplyRelatedItem={ (selectedItemId) => {
if (itemId != selectedItemId) {
relatedItems = [];
relatedItemsTemplate = [];
}
itemId = selectedItemId;
setAttributes({
itemId: itemId,
relatedItems: relatedItems,
relatedItemsTemplate: relatedItemsTemplate,
isModalOpen: false
});
setContent();
}}
onCancelSelection={ () => setAttributes({ isModalOpen: false }) }/>
: null
}
</div>
) : null
}
{ !relatedItems.length && !isLoading ? (
<Placeholder
className="tainacan-block-placeholder"
icon={(
<img
width={148}
src={ `${tainacan_blocks.base_url}/assets/images/tainacan_logo_header.svg` }
alt="Tainacan Logo"/>
)}>
<p>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
height="24px"
width="24px">
<path d="M16,6H12a2,2,0,0,0-2,2v6.52A6,6,0,0,1,12,19a6,6,0,0,1-.73,2.88A1.92,1.92,0,0,0,12,22h8a2,2,0,0,0,2-2V12Zm-1,6V7.5L19.51,12ZM15,2V4H8v9.33A5.8,5.8,0,0,0,6,13V4A2,2,0,0,1,8,2ZM10.09,19.05,7,22.11V16.05L8,17l2,2ZM5,16.05v6.06L2,19.11Z"/>
</svg>
{__('List items on a Carousel, using search or item selection.', 'tainacan')}
</p>
<Button
isPrimary
type="button"
onClick={ () => openRelatedItemsModal() }>
{__('Select Item', 'tainacan')}
</Button>
</Placeholder>
) : null
}
{ isLoading ?
<div class="spinner-container">
<Spinner />
</div> :
<div>
{ relatedItems.length ? (
<div className={ 'carousel-related-items-edit-container' }>
<InnerBlocks
allowedBlocks={[
'core/heading',
'core/paragraph',
'tainacan/carousel-items-list',
'core/buttons'
]}
template={ relatedItemsTemplate } />
</div>
) : null
}
</div>
}
</div>
);
},
save({ className }){
return <div className={ className }><InnerBlocks.Content /></div>
},
deprecated: DeprecatedBlocks
});

View File

@ -142,7 +142,7 @@
color: var(--tainacan-block-gray5, $gray5); color: var(--tainacan-block-gray5, $gray5);
font-weight: bold; font-weight: bold;
text-decoration: none; text-decoration: none;
padding: 8px 16px; padding: 8px 12px;
display: block; display: block;
line-height: 1.2em; line-height: 1.2em;
word-break: break-word; word-break: break-word;

View File

@ -736,10 +736,10 @@ export default {
max-width: 100%; max-width: 100%;
} }
.field:not(:last-child) { .field:not(:last-child) {
margin-bottom: 0.5em; margin-bottom: 0em;
} }
.field { .field {
padding: 10px 0px 14px 34px; padding: 12px 0px 12px 34px;
} }
.columns { .columns {

View File

@ -220,7 +220,6 @@ return apply_filters( 'tainacan-admin-i18n', [
'label_approved' => __( 'Approved', 'tainacan' ), 'label_approved' => __( 'Approved', 'tainacan' ),
'label_collection_related' => __( 'Collection related', 'tainacan' ), 'label_collection_related' => __( 'Collection related', 'tainacan' ),
'label_metadata_for_search' => __( 'Metadata for search', 'tainacan' ), 'label_metadata_for_search' => __( 'Metadata for search', 'tainacan' ),
'label_allow_repeated_items' => __( 'Allow repeated items', 'tainacan' ),
'label_select_taxonomy' => __( 'Select taxonomy', 'tainacan' ), 'label_select_taxonomy' => __( 'Select taxonomy', 'tainacan' ),
'label_select_taxonomy_input_type' => __( 'Input type', 'tainacan' ), 'label_select_taxonomy_input_type' => __( 'Input type', 'tainacan' ),
'label_taxonomy_allow_new_terms' => __( 'Allow new terms', 'tainacan' ), 'label_taxonomy_allow_new_terms' => __( 'Allow new terms', 'tainacan' ),
@ -586,6 +585,9 @@ return apply_filters( 'tainacan-admin-i18n', [
'label_pan_selection' => __( 'Pan selection', 'tainacan'), 'label_pan_selection' => __( 'Pan selection', 'tainacan'),
'label_reset_zoom' => __( 'Reset zoom', 'tainacan'), 'label_reset_zoom' => __( 'Reset zoom', 'tainacan'),
'label_chart_export_options' => __( 'Chart export options', 'tainacan'), 'label_chart_export_options' => __( 'Chart export options', 'tainacan'),
'label_related_items' => __( 'Items related to this', 'tainacan'),
'label_view_all_%s_related_items' => __( 'View all %s related items', 'tainacan'),
'label_back_to_related_item' => __( 'Back to related item', 'tainacan'),
// Instructions. More complex sentences to guide user and placeholders // Instructions. More complex sentences to guide user and placeholders
'instruction_delete_selected_collections' => __( 'Delete selected collections', 'tainacan' ), 'instruction_delete_selected_collections' => __( 'Delete selected collections', 'tainacan' ),
@ -870,7 +872,8 @@ return apply_filters( 'tainacan-admin-i18n', [
'info_no_taxonomy_metadata_created' => __( 'No taxonomy metadata created yet', 'tainacan'), 'info_no_taxonomy_metadata_created' => __( 'No taxonomy metadata created yet', 'tainacan'),
'label_amount_of_metadata_of_type' => __( 'Amount of metadata of this type', 'tainacan'), 'label_amount_of_metadata_of_type' => __( 'Amount of metadata of this type', 'tainacan'),
'info_child_terms_chart' => __( 'Click on the term bar on the chart aside to see its child terms (if any) in this panel', 'tainacan' ), 'info_child_terms_chart' => __( 'Click on the term bar on the chart aside to see its child terms (if any) in this panel', 'tainacan' ),
'info_related_items' => __( 'These are items that are related to this item via their own relationship type metadata. You can edit such relation on their pages.', 'tainacan'),
/* Activity actions */ /* Activity actions */
'action_update-metadata-value' => __( 'Item Metadata Value Updates', 'tainacan'), 'action_update-metadata-value' => __( 'Item Metadata Value Updates', 'tainacan'),
'action_update' => __( 'General Updates', 'tainacan'), 'action_update' => __( 'General Updates', 'tainacan'),

View File

@ -0,0 +1,171 @@
<?php
namespace Tainacan\Tests;
/**
* Class TestCollections
*
* @package Test_Tainacan
*/
use Tainacan\Entities;
/**
* Sample test case.
*/
class RelationshipMetadatumTypes extends TAINACAN_UnitTestCase {
private $collection_author = null;
private $collection_book = null;
private $collection_article = null;
public function setUp() {
parent::setUp();
$this->collection_book = $this->tainacan_entity_factory->create_entity('collection', ['name' => 'Book', 'status' => 'publish'], true);
$this->collection_author = $this->tainacan_entity_factory->create_entity('collection', ['name' => 'Author', 'status' => 'publish'], true);
$this->collection_article = $this->tainacan_entity_factory->create_entity('collection', ['name' => 'Article','status' => 'publish'], true);
$this->metadatum_author_book = $this->tainacan_entity_factory->create_entity(
'metadatum',
array(
'name' => 'author',
'description' => 'author',
'collection' => $this->collection_book,
'metadata_type' => 'Tainacan\Metadata_Types\Relationship',
'status' => 'publish',
'metadata_type_options' => [
'display_in_related_items' => 'yes',
'collection_id' => $this->collection_author->get_id(),
'search' => ''
],
'multiple' => 'yes'
),
true
);
$this->metadatum_author_article = $this->tainacan_entity_factory->create_entity(
'metadatum',
array(
'name' => 'author',
'description' => 'author',
'collection' => $this->collection_article,
'metadata_type' => 'Tainacan\Metadata_Types\Relationship',
'status' => 'publish',
'metadata_type_options' => [
'display_in_related_items' => 'yes',
'collection_id' => $this->collection_author->get_id(),
'search' => ''
],
),
true
);
$this->metadatum_second_author_article = $this->tainacan_entity_factory->create_entity(
'metadatum',
array(
'name' => 'secound author',
'description' => 'secound author',
'collection' => $this->collection_article,
'metadata_type' => 'Tainacan\Metadata_Types\Relationship',
'status' => 'publish',
'metadata_type_options' => [
'display_in_related_items' => 'no',
'collection_id' => $this->collection_author->get_id(),
'search' => ''
]
),
true
);
}
function test_related_items() {
$a1 = $this->tainacan_entity_factory->create_entity(
'item',
array(
'title' => 'Lopes da silva',
'description' => 'autor fisico',
'collection' => $this->collection_author,
'status' => 'publish'
),
true
);
$a2 = $this->tainacan_entity_factory->create_entity(
'item',
array(
'title' => 'Siqueira da martins',
'description' => 'autor simuante',
'collection' => $this->collection_author,
'status' => 'publish'
),
true
);
$b1 = $this->tainacan_entity_factory->create_entity(
'item',
array(
'title' => 'O livro dos livros',
'description' => 'livro broxura',
'collection' => $this->collection_book,
'status' => 'publish'
),
true
);
$j1 = $this->tainacan_entity_factory->create_entity(
'item',
array(
'title' => 'O artigo da semana',
'description' => 'artigo mais que cientifico',
'collection' => $this->collection_article,
'status' => 'publish'
),
true
);
$j2 = $this->tainacan_entity_factory->create_entity(
'item',
array(
'title' => 'Não é magia é tecnologias',
'description' => 'So digo que não digo nada',
'collection' => $this->collection_article,
'status' => 'publish'
),
true
);
$this->tainacan_item_metadata_factory->create_item_metadata($j1, $this->metadatum_author_article, $a1->get_id());
$this->tainacan_item_metadata_factory->create_item_metadata($j1, $this->metadatum_second_author_article, $a2->get_id());
$this->tainacan_item_metadata_factory->create_item_metadata($j2, $this->metadatum_author_article, $a1->get_id());
$this->tainacan_item_metadata_factory->create_item_metadata($j2, $this->metadatum_second_author_article, $a2->get_id());
$this->tainacan_item_metadata_factory->create_item_metadata($b1, $this->metadatum_author_book, [$a1->get_id(), $a2->get_id()]);
$a1_related_items = $a1->get_related_items();
$a2_related_items = $a1->get_related_items();
$this->assertTrue(isset($a1_related_items[$this->metadatum_author_article->get_id()]));
$this->assertEquals(2, count($a1_related_items[$this->metadatum_author_article->get_id()]['items']));
$this->assertTrue(!isset($a2_related_items[$this->metadatum_second_author_article->get_id()])); //não existe esse
$this->assertTrue(isset($a1_related_items[$this->metadatum_author_book->get_id()]));
$this->assertTrue(isset($a2_related_items[$this->metadatum_author_book->get_id()]));
$order = array([
'id' => $this->metadatum_author_article->get_id(),
'enabled' => false,
]);
$this->collection_article->set_metadata_order($order);
$this->collection_article->validate();
\tainacan_collections()->insert($this->collection_article);
\tainacan_items()->fetch($a1->get_id());
$a1_related_items = $a1->get_related_items();
$this->assertTrue(!isset($a1_related_items[$this->metadatum_author_article->get_id()]));
}
}

View File

@ -28,7 +28,9 @@ module.exports = {
block_carousel_collections_list: './src/views/gutenberg-blocks/tainacan-collections/carousel-collections-list/index.js', block_carousel_collections_list: './src/views/gutenberg-blocks/tainacan-collections/carousel-collections-list/index.js',
block_carousel_collections_list_theme: './src/views/gutenberg-blocks/tainacan-collections/carousel-collections-list/carousel-collections-list-theme.js', block_carousel_collections_list_theme: './src/views/gutenberg-blocks/tainacan-collections/carousel-collections-list/carousel-collections-list-theme.js',
block_carousel_related_items: './src/views/gutenberg-blocks/tainacan-items/carousel-related-items/index.js',
block_facets_list: './src/views/gutenberg-blocks/tainacan-facets/facets-list/index.js', block_facets_list: './src/views/gutenberg-blocks/tainacan-facets/facets-list/index.js',
block_facets_list_theme: './src/views/gutenberg-blocks/tainacan-facets/facets-list/facets-list-theme.js', block_facets_list_theme: './src/views/gutenberg-blocks/tainacan-facets/facets-list/facets-list-theme.js',