diff --git a/.circleci/config.yml b/.circleci/config.yml index 497b17f72..f204e57c8 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -84,7 +84,7 @@ jobs: - run: name: Clone source on lightsail command: | - ssh lightsail 'export CIRCLE_BRANCH='"'$CIRCLE_BRANCH'"'; git clone -b $CIRCLE_BRANCH --recurse-submodules https://github.com/cvisionai/tator'; + ssh lightsail 'export CIRCLE_BRANCH='"'$CIRCLE_BRANCH'"'; git clone -b ${CIRCLE_BRANCH:-stable} --recurse-submodules https://github.com/cvisionai/tator'; - persist_to_workspace: root: ~/ paths: @@ -127,7 +127,7 @@ jobs: front-end-tests: machine: image: ubuntu-2004:202010-01 - resource_class: large + resource_class: xlarge steps: - attach_workspace: at: ~/ @@ -141,7 +141,7 @@ jobs: wget https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb; sudo -E apt-get -yq --no-install-suggests --no-install-recommends install ./google-chrome-stable_current_amd64.deb; sudo -E apt-get update && sudo -E apt-get -yq --no-install-suggests --no-install-recommends install tesseract-ocr; - pip3 install playwright==1.21.0 pytest-playwright==0.1.2 pytesseract==0.3.9 opencv-python; + pip3 install playwright==1.17.2 pytest-playwright==0.1.2 pytesseract==0.3.9 opencv-python; export PATH=$PATH:$HOME/.local/bin:/snap/bin; playwright install; scp -r lightsail:/tmp/tator_py_whl/tator*.whl /tmp; @@ -217,23 +217,23 @@ workflows: filters: tags: only: /.*/ - - rest-tests: + - front-end-tests: requires: - - install-tator + - install-tator context: cvisionai filters: tags: only: /.*/ - - front-end-tests: + - rest-tests: requires: - - install-tator + - front-end-tests context: cvisionai filters: tags: only: /.*/ - tator-py-tests: requires: - - install-tator + - front-end-tests context: cvisionai filters: tags: diff --git a/main/backup.py b/main/backup.py index b73f394e9..a80517e2d 100644 --- a/main/backup.py +++ b/main/backup.py @@ -4,7 +4,7 @@ import json import os from uuid import uuid4 -from typing import Dict, Tuple +from typing import Generator from django.db import transaction @@ -240,30 +240,32 @@ def get_store_info(self, project) -> bool: return success, store_info - def backup_resources(self, resource_qs) -> Tuple[int, Dict[int, set]]: + def backup_resources(self, resource_qs) -> Generator[tuple, None, None]: """ - Copies the resources in the given queryset from the live store to the backup store for their - respective projects. Returns a tuple where the first element is the number of resources - that were successfully backed up and the second is a dict that maps project ids to lists of - media ids with at least one resource that failed to back up properly. + Creates a generator that copies the resources in the given queryset from the live store to + the backup store for their respective projects. Yields a tuple with the first element being + the success of the backup operation for the current resource and the second element being + the resource in question, so the calling function can iterate over the queryset and keep + track of its progress. If there is no backup bucket for the given project (or a site-wide default), this will - return `False`. + yield `(False, resource)`. :param resource_qs: The resources to back up :type resource_qs: Queryset - :rtype: Tuple[int, Dict[int, set]] + :rtype: Generator[tuple, None, None] """ successful_backups = set() for resource in resource_qs.iterator(): project = self.project_from_resource(resource) path = resource.path success, store_info = self.get_store_info(project) + success = success and "backup" in store_info - if success and "backup" in store_info: + if success: if store_info["backup"]["store"].check_key(path): logger.info(f"Resource {path} already backed up") - return True + continue # Get presigned url from the live bucket, set to expire in 1h download_url = store_info["live"]["store"].get_download_url(path, 3600) diff --git a/main/models.py b/main/models.py index 78c843676..450efa033 100644 --- a/main/models.py +++ b/main/models.py @@ -1473,7 +1473,7 @@ class Leaf(Model, ModelDiffMixin): modified_by = ForeignKey(User, on_delete=SET_NULL, null=True, blank=True, related_name='leaf_modified_by', db_column='modified_by') parent=ForeignKey('self', on_delete=SET_NULL, blank=True, null=True, db_column='parent') - path=PathField(unique=True) + path=PathField() name = CharField(max_length=255) deleted = BooleanField(default=False) diff --git a/main/rest/_attribute_query.py b/main/rest/_attribute_query.py index 3f85b2343..c936f0c0d 100644 --- a/main/rest/_attribute_query.py +++ b/main/rest/_attribute_query.py @@ -15,6 +15,17 @@ logger = logging.getLogger(__name__) + +def format_query_string(query_str: str) -> str: + """ + Preformatting before passing the query to ElasticSearch. + + :param query_str: The raw query string + :type query_str: str + """ + return query_str.replace("/", "\\/") + + def get_attribute_es_query(query_params, query, bools, project, is_media=True, annotation_bools=None, modified=None): """ TODO: add documentation for this """ @@ -93,10 +104,10 @@ def get_attribute_es_query(query_params, query, bools, project, if section_object.lucene_search: attr_query['media']['filter'].append({'bool': { 'should': [ - {'query_string': {'query': section_object.lucene_search}}, + {'query_string': {'query': format_query_string(section_object.lucene_search)}}, {'has_child': { 'type': 'annotation', - 'query': {'query_string': {'query': section_object.lucene_search}}, + 'query': {'query_string': {'query': format_query_string(section_object.lucene_search)}}, }, }, ], @@ -131,13 +142,13 @@ def get_attribute_es_query(query_params, query, bools, project, search = query_params.get('search') if search is not None: - search_query = {'query_string': {'query': search}} + search_query = {'query_string': {'query': format_query_string(search)}} query['query']['bool']['filter'].append(search_query) annotation_search = query_params.get('annotation_search') if annotation_search is not None: annotation_search_query = {'has_child': {'type': 'annotation', - 'query': {'query_string': {'query': annotation_search}}}} + 'query': {'query_string': {'query': format_query_string(annotation_search)}}}} query['query']['bool']['filter'].append(annotation_search_query) else: @@ -172,13 +183,13 @@ def get_attribute_es_query(query_params, query, bools, project, search = query_params.get('search', None) if search is not None: - search_query = {'query_string': {'query': search}} + search_query = {'query_string': {'query': format_query_string(search)}} query['query']['bool']['filter'].append(search_query) media_search = query_params.get('media_search') if media_search is not None: media_search_query = {'has_parent': {'parent_type': 'media', - 'query': {'query_string': {'query': media_search}}}} + 'query': {'query_string': {'query': format_query_string(media_search)}}}} query['query']['bool']['filter'].append(media_search_query) if modified is not None: diff --git a/main/rest/_util.py b/main/rest/_util.py index 7366ec5a4..968586887 100644 --- a/main/rest/_util.py +++ b/main/rest/_util.py @@ -3,15 +3,16 @@ import logging from urllib.parse import urlparse +from django.contrib.contenttypes.models import ContentType from django.utils.http import urlencode from django.db.models.expressions import Subquery from rest_framework.reverse import reverse from rest_framework.exceptions import APIException from rest_framework.exceptions import PermissionDenied -from ..models import type_to_obj +from ..models import type_to_obj, ChangeLog, ChangeToObject, Project -from ._attributes import convert_attribute +from ._attributes import bulk_patch_attributes, convert_attribute logger = logging.getLogger(__name__) @@ -174,3 +175,146 @@ def url_to_key(url, project_obj): path = '/'.join(parsed.path.split('/')[-num_tokens:]) return path, bucket, upload + +def bulk_update_and_log_changes(queryset, project, user, update_kwargs=None, new_attributes=None): + """ + Performs a bulk update and creates a single changelog referenced by all changed objects + + :param queryset: The queryset to update + :param project: The project the request originates from + :param user: The user making the requests + :param update_kwargs: The dictionary of arguments for queryset.update(), will be used like this: + `queryset.update(**update_kwargs)` + :param new_attributes: The validated attributes returned by `validate_attributes`, if any, will + be used like this: `bulk_patch_attributes(new_attributes, queryset)` + """ + if not queryset.exists(): + logger.info("Queryset empty, not performing any updates") + return + + if update_kwargs is None and new_attributes is None: + raise ValueError( + "Must specify at least one of the following arguments: update_kwargs, new_attributes" + ) + + if type(project) != Project: + project = Project.objects.get(pk=project) + + # Get prior state data for ChangeLog creation + updated_ids = list(queryset.values_list("id", flat=True)) + first_obj = queryset.first() + ref_table = ContentType.objects.get_for_model(first_obj) + model_dict = first_obj.model_dict + + # Perform queryset update + if update_kwargs is not None: + queryset.update(**update_kwargs) + if new_attributes is not None: + bulk_patch_attributes(new_attributes, queryset) + + # Create ChangeLog + first_obj = type(first_obj).objects.get(pk=first_obj.id) + cl = ChangeLog( + project=project, + user=user, + description_of_change=first_obj.change_dict(model_dict), + ) + cl.save() + objs = ( + ChangeToObject(ref_table=ref_table, ref_id=obj_id, change_id=cl) for obj_id in updated_ids + ) + bulk_create_from_generator(objs, ChangeToObject) + + +def bulk_delete_and_log_changes(queryset, project, user): + """ + Performs a bulk delete and creates a changelog for it. + + :param queryset: The queryset to mark for deletion + :param project: The project the request originates from + :param user: The user making the requests + """ + delete_kwargs = { + "deleted": True, + "modified_datetime": datetime.datetime.now(datetime.timezone.utc), + "modified_by": user, + } + bulk_update_and_log_changes(queryset, project, user, update_kwargs=delete_kwargs) + + +def log_changes(obj, model_dict, project, user): + """ + Creates a changelog for a single updated object. + + :param obj: The object to compare and create a change log for. + :param model_dict: The state retrieved from `obj.model_dict` **before updating**. + :param project: The project the request originates from + :param user: The user making the requests + """ + if type(project) != Project: + project = Project.objects.get(pk=project) + + ref_table = ContentType.objects.get_for_model(obj) + cl = ChangeLog(project=project, user=user, description_of_change=obj.change_dict(model_dict)) + cl.save() + ChangeToObject(ref_table=ref_table, ref_id=obj.id, change_id=cl).save() + + +def delete_and_log_changes(obj, project, user): + """ + Deletes a single object and creates a changelog for it. + + :param obj: The object to delete and create a change log for. + :param project: The project the request originates from + :param user: The user making the requests + """ + model_dict = obj.model_dict + obj.deleted = True + obj.modified_datetime = datetime.datetime.now(datetime.timezone.utc) + obj.modified_by = user + obj.save() + + log_changes(obj, model_dict, project, user) + + +def log_creation(obj, project, user): + """ + Creates changelogs for a new object. + + :param obj: The new object to create a change log for. + :param project: The project the request originates from + :param user: The user making the requests + """ + if type(project) != Project: + project = Project.objects.get(pk=project) + + ref_table = ContentType.objects.get_for_model(obj) + cl = ChangeLog(project=project, user=user, description_of_change=obj.create_dict) + cl.save() + ChangeToObject(ref_table=ref_table, ref_id=obj.id, change_id=cl).save() + + +def bulk_log_creation(objects, project, user): + """ + Creates changelogs for multiple new objects. + + :param obj: The new object to create a change log for. + :param project: The project the request originates from + :param user: The user making the requests + """ + # Create ChangeLogs + objs = ( + ChangeLog(project=project, user=user, description_of_change=obj.create_dict) + for obj in objects + ) + change_logs = bulk_create_from_generator(objs, ChangeLog) + + # Associate ChangeLogs with created objects + ref_table = ContentType.objects.get_for_model(objects[0]) + ids = [obj.id for obj in objects] + objs = ( + ChangeToObject(ref_table=ref_table, ref_id=ref_id, change_id=cl) + for ref_id, cl in zip(ids, change_logs) + ) + bulk_create_from_generator(objs, ChangeToObject) + return ids diff --git a/main/rest/change_log.py b/main/rest/change_log.py index 7c4f2423a..b90d5586b 100644 --- a/main/rest/change_log.py +++ b/main/rest/change_log.py @@ -10,7 +10,7 @@ logger = logging.getLogger(__name__) -CHANGE_LOG_PROPERTIES = ["id", "project", "user", "description_of_change"] +CHANGE_LOG_PROPERTIES = ["id", "project", "user", "description_of_change", "modified_datetime"] class ChangeLogListAPI(BaseListView): diff --git a/main/rest/download_info.py b/main/rest/download_info.py index a558ec4fb..24d15cd7a 100644 --- a/main/rest/download_info.py +++ b/main/rest/download_info.py @@ -44,7 +44,8 @@ def _post(self, params): if url is None: upload = key.startswith('_uploads') bucket = project_obj.get_bucket(upload=upload) - store_default = get_tator_store(bucket, upload=upload) + use_upload_bucket = upload and not bucket + store_default = get_tator_store(bucket, upload=use_upload_bucket) tator_store = store_lookup.get(key, store_default) # Make sure the key corresponds to the correct project. diff --git a/main/rest/leaf.py b/main/rest/leaf.py index 341cf9498..7e3756c3a 100644 --- a/main/rest/leaf.py +++ b/main/rest/leaf.py @@ -6,8 +6,6 @@ from django.db import transaction from django.http import Http404 -from ..models import ChangeLog -from ..models import ChangeToObject from ..models import Leaf from ..models import LeafType from ..models import Project @@ -26,9 +24,16 @@ from ._attributes import patch_attributes from ._attributes import bulk_patch_attributes from ._attributes import validate_attributes -from ._util import bulk_create_from_generator -from ._util import computeRequiredFields -from ._util import check_required_fields +from ._util import ( + bulk_create_from_generator, + bulk_delete_and_log_changes, + bulk_log_creation, + bulk_update_and_log_changes, + computeRequiredFields, + check_required_fields, + delete_and_log_changes, + log_changes, +) from ._permissions import ProjectViewOnlyPermission from ._permissions import ProjectFullControlPermission @@ -169,23 +174,7 @@ def _post(self, params): documents = [] ts.bulk_add_documents(documents) - # Create ChangeLogs - objs = ( - ChangeLog( - project=project, user=self.request.user, description_of_change=leaf.create_dict - ) - for leaf in leaves - ) - change_logs = bulk_create_from_generator(objs, ChangeLog) - - # Associate ChangeLogs with created objects - ref_table = ContentType.objects.get_for_model(leaves[0]) - ids = [leaf.id for leaf in leaves] - objs = ( - ChangeToObject(ref_table=ref_table, ref_id=ref_id, change_id=cl) - for ref_id, cl in zip(ids, change_logs) - ) - bulk_create_from_generator(objs, ChangeToObject) + ids = bulk_log_creation(leaves, project, self.request.user) # Return created IDs. return {'message': f'Successfully created {len(ids)} leaves!', 'id': ids} @@ -194,63 +183,25 @@ def _delete(self, params): qs = get_leaf_queryset(params['project'], params) count = qs.count() if count > 0: - # Get info to populate ChangeLog entry - first_obj = qs.first() - project = first_obj.project - ref_table = ContentType.objects.get_for_model(first_obj) - delete_dicts = [] - ref_ids = [] - for obj in qs: - delete_dicts.append(obj.delete_dict) - ref_ids.append(obj.id) - - qs.update(deleted=True, - modified_datetime=datetime.datetime.now(datetime.timezone.utc), - modified_by=self.request.user) + bulk_delete_and_log_changes(qs, params["project"], self.request.user) query = get_leaf_es_query(params) TatorSearch().delete(self.kwargs['project'], query) - # Create ChangeLogs - objs = ( - ChangeLog(project=project, user=self.request.user, description_of_change=dd) - for dd in delete_dicts - ) - change_logs = bulk_create_from_generator(objs, ChangeLog) - - # Associate ChangeLogs with deleted objects - objs = ( - ChangeToObject(ref_table=ref_table, ref_id=ref_id, change_id=cl) - for ref_id, cl in zip(ref_ids, change_logs) - ) - bulk_create_from_generator(objs, ChangeToObject) - return {'message': f'Successfully deleted {count} leaves!'} def _patch(self, params): qs = get_leaf_queryset(params['project'], params) count = qs.count() if count > 0: - # Get the current representation of the object for comparison - original_dict = qs.first().model_dict new_attrs = validate_attributes(params, qs[0]) + bulk_update_and_log_changes( + qs, params["project"], self.request.user, new_attributes=new_attrs + ) bulk_patch_attributes(new_attrs, qs) - # Get one object from the queryset to create the change log - obj = qs.first() - change_dict = obj.change_dict(original_dict) - ref_table = ContentType.objects.get_for_model(obj) - query = get_leaf_es_query(params) TatorSearch().update(self.kwargs['project'], qs[0].meta, query, new_attrs) - # Create the ChangeLog entry and associate it with all objects in the queryset - cl = ChangeLog( - project=obj.project, user=self.request.user, description_of_change=change_dict - ) - cl.save() - objs = (ChangeToObject(ref_table=ref_table, ref_id=o.id, change_id=cl) for o in qs) - bulk_create_from_generator(objs, ChangeToObject) - return {'message': f'Successfully updated {count} leaves!'} def _put(self, params): @@ -277,7 +228,7 @@ def _get(self, params): @transaction.atomic def _patch(self, params): obj = Leaf.objects.get(pk=params['id'], deleted=False) - original_dict = obj.model_dict + model_dict = obj.model_dict # Patch common attributes. if 'name' in params: @@ -285,38 +236,17 @@ def _patch(self, params): obj.save() new_attrs = validate_attributes(params, obj) obj = patch_attributes(new_attrs, obj) - obj.save() - cl = ChangeLog( - project=obj.project, - user=self.request.user, - description_of_change=obj.change_dict(original_dict), - ) - cl.save() - ChangeToObject( - ref_table=ContentType.objects.get_for_model(obj), - ref_id=obj.id, - change_id=cl, - ).save() - + log_changes(obj, model_dict, obj.project, self.request.user) return {'message': 'Leaf {params["id"]} successfully updated!'} def _delete(self, params): leaf = Leaf.objects.get(pk=params['id'], deleted=False) project = leaf.project - delete_dict = leaf.delete_dict - ref_table = ContentType.objects.get_for_model(leaf) - ref_id = leaf.id - leaf.deleted = True - leaf.modified_datetime = datetime.datetime.now(datetime.timezone.utc) - leaf.modified_by = self.request.user - leaf.save() + model_dict = leaf.model_dict + delete_and_log_changes(leaf, project, self.request.user) TatorSearch().delete_document(leaf) - cl = ChangeLog(project=project, user=self.request.user, description_of_change=delete_dict) - cl.save() - ChangeToObject(ref_table=ref_table, ref_id=ref_id, change_id=cl).save() return {'message': 'Leaf {params["id"]} successfully deleted!'} def get_queryset(self): return Leaf.objects.all() - diff --git a/main/rest/localization.py b/main/rest/localization.py index 549e872c7..e7c60839f 100644 --- a/main/rest/localization.py +++ b/main/rest/localization.py @@ -5,8 +5,6 @@ from django.contrib.contenttypes.models import ContentType from django.http import Http404 -from ..models import ChangeLog -from ..models import ChangeToObject from ..models import Localization from ..models import LocalizationType from ..models import Media @@ -31,9 +29,16 @@ from ._attributes import patch_attributes from ._attributes import bulk_patch_attributes from ._attributes import validate_attributes -from ._util import bulk_create_from_generator -from ._util import computeRequiredFields -from ._util import check_required_fields +from ._util import ( + bulk_create_from_generator, + bulk_delete_and_log_changes, + bulk_log_creation, + bulk_update_and_log_changes, + computeRequiredFields, + check_required_fields, + delete_and_log_changes, + log_changes, +) from ._permissions import ProjectEditPermission logger = logging.getLogger(__name__) @@ -198,23 +203,7 @@ def _post(self, params): documents = [] ts.bulk_add_documents(documents) - # Create ChangeLogs - objs = ( - ChangeLog( - project=project, user=self.request.user, description_of_change=loc.create_dict - ) - for loc in localizations - ) - change_logs = bulk_create_from_generator(objs, ChangeLog) - - # Associate ChangeLogs with created objects - ref_table = ContentType.objects.get_for_model(localizations[0]) - ids = [loc.id for loc in localizations] - objs = ( - ChangeToObject(ref_table=ref_table, ref_id=ref_id, change_id=cl) - for ref_id, cl in zip(ids, change_logs) - ) - bulk_create_from_generator(objs, ChangeToObject) + ids = bulk_log_creation(localizations, project, self.request.user) # Return created IDs. return {'message': f'Successfully created {len(ids)} localizations!', 'id': ids} @@ -223,37 +212,11 @@ def _delete(self, params): qs = get_annotation_queryset(params['project'], params, 'localization') count = qs.count() if count > 0: - # Get info to populate ChangeLog entry - first_obj = qs.first() - project = first_obj.project - ref_table = ContentType.objects.get_for_model(first_obj) - delete_dicts = [] - ref_ids = [] - for obj in qs: - delete_dicts.append(obj.delete_dict) - ref_ids.append(obj.id) - # Delete the localizations. - qs.update(deleted=True, - modified_datetime=datetime.datetime.now(datetime.timezone.utc), - modified_by=self.request.user) + bulk_delete_and_log_changes(qs, params["project"], self.request.user) query = get_annotation_es_query(params['project'], params, 'localization') TatorSearch().delete(self.kwargs['project'], query) - # Create ChangeLogs - objs = ( - ChangeLog(project=project, user=self.request.user, description_of_change=dd) - for dd in delete_dicts - ) - change_logs = bulk_create_from_generator(objs, ChangeLog) - - # Associate ChangeLogs with deleted objects - objs = ( - ChangeToObject(ref_table=ref_table, ref_id=ref_id, change_id=cl) - for ref_id, cl in zip(ref_ids, change_logs) - ) - bulk_create_from_generator(objs, ChangeToObject) - return {'message': f'Successfully deleted {count} localizations!'} def _patch(self, params): @@ -263,31 +226,24 @@ def _patch(self, params): if count > 0: # Get the current representation of the object for comparison obj = qs.first() - original_dict = obj.model_dict first_id = obj.id entity_type = obj.meta new_attrs = validate_attributes(params, qs[0]) - bulk_patch_attributes(new_attrs, qs) + update_kwargs = {"modified_by": self.request.user} if patched_version is not None: - qs.update(version=patched_version) - qs.update(modified_by=self.request.user) - - # Get one object from the queryset to create the change log - obj = Localization.objects.get(pk=first_id) - change_dict = obj.change_dict(original_dict) - ref_table = ContentType.objects.get_for_model(obj) + update_kwargs["version"] = patched_version + + bulk_update_and_log_changes( + qs, + params["project"], + self.request.user, + update_kwargs=update_kwargs, + new_attributes=new_attrs, + ) query = get_annotation_es_query(params['project'], params, 'localization') TatorSearch().update(self.kwargs['project'], entity_type, query, new_attrs) - # Create the ChangeLog entry and associate it with all objects in the queryset - cl = ChangeLog( - project=obj.project, user=self.request.user, description_of_change=change_dict - ) - cl.save() - objs = (ChangeToObject(ref_table=ref_table, ref_id=o.id, change_id=cl) for o in qs) - bulk_create_from_generator(objs, ChangeToObject) - return {'message': f'Successfully updated {count} localizations!'} def _put(self, params): @@ -316,7 +272,7 @@ def _get(self, params): @transaction.atomic def _patch(self, params): obj = Localization.objects.get(pk=params['id'], deleted=False) - original_dict = obj.model_dict + model_dict = obj.model_dict # Patch common attributes. frame = params.get("frame", None) @@ -392,17 +348,7 @@ def _patch(self, params): obj.thumbnail_image.save() obj.save() - cl = ChangeLog( - project=obj.project, - user=self.request.user, - description_of_change=obj.change_dict(original_dict), - ) - cl.save() - ChangeToObject( - ref_table=ContentType.objects.get_for_model(obj), - ref_id=obj.id, - change_id=cl, - ).save() + log_changes(obj, model_dict, obj.project, self.request.user) return {'message': f'Localization {params["id"]} successfully updated!'} @@ -411,17 +357,8 @@ def _delete(self, params): if not qs.exists(): raise Http404 obj = qs[0] - project = obj.project - delete_dict = obj.delete_dict - ref_table = ContentType.objects.get_for_model(obj) - ref_id = obj.id + delete_and_log_changes(obj, obj.project, self.request.user) TatorSearch().delete_document(obj) - qs.update(deleted=True, - modified_datetime=datetime.datetime.now(datetime.timezone.utc), - modified_by=self.request.user) - cl = ChangeLog(project=project, user=self.request.user, description_of_change=delete_dict) - cl.save() - ChangeToObject(ref_table=ref_table, ref_id=ref_id, change_id=cl).save() return {'message': f'Localization {params["id"]} successfully deleted!'} def get_queryset(self): diff --git a/main/rest/media.py b/main/rest/media.py index 98bb67fb5..9632c2b18 100644 --- a/main/rest/media.py +++ b/main/rest/media.py @@ -1,5 +1,6 @@ import logging import datetime +from itertools import chain import os import shutil import mimetypes @@ -15,8 +16,6 @@ from PIL import Image from ..models import ( - ChangeLog, - ChangeToObject, Media, MediaType, Section, @@ -37,7 +36,15 @@ from ..cache import TatorCache from ._util import url_to_key -from ._util import bulk_create_from_generator, computeRequiredFields, check_required_fields +from ._util import ( + bulk_update_and_log_changes, + bulk_delete_and_log_changes, + delete_and_log_changes, + log_changes, + log_creation, + computeRequiredFields, + check_required_fields, +) from ._base_views import BaseListView, BaseDetailView from ._media_query import get_media_queryset, get_media_es_query from ._attributes import bulk_patch_attributes, patch_attributes, validate_attributes @@ -346,17 +353,11 @@ def _create_media(params, user): if url: path, bucket, upload = url_to_key(url, project_obj) if path is not None: - tator_store = get_tator_store(bucket, upload=upload) + use_upload_bucket = upload and not bucket + tator_store = get_tator_store(bucket, upload=use_upload_bucket) tator_store.put_media_id_tag(path, media_obj.id) - cl = ChangeLog( - project=media_obj.project, - user=user, - description_of_change=media_obj.create_dict, - ) - cl.save() - ref_table = ContentType.objects.get_for_model(media_obj) - ChangeToObject(ref_table=ref_table, ref_id=media_obj.id, change_id=cl).save() + log_creation(media_obj, media_obj.project, user) return media_obj, response @@ -413,20 +414,7 @@ def _delete(self, params): media_ids = list(qs.values_list('pk', flat=True).distinct()) count = qs.count() if count > 0: - # Get info to populate ChangeLog entry - first_obj = qs.first() - project = first_obj.project - ref_table = ContentType.objects.get_for_model(first_obj) - delete_dicts = [] - ref_ids = [] - for obj in qs: - delete_dicts.append(obj.delete_dict) - ref_ids.append(obj.id) - - # Mark media for deletion. - qs.update(deleted=True, - modified_datetime=datetime.datetime.now(datetime.timezone.utc), - modified_by=self.request.user) + bulk_delete_and_log_changes(qs, project, self.request.user) # Any states that are only associated to deleted media should also be marked # for deletion. @@ -436,15 +424,11 @@ def _delete(self, params): .values_list('id', flat=True) all_deleted = set(deleted) - set(not_deleted) state_qs = State.objects.filter(pk__in=all_deleted) - state_qs.update(deleted=True, - modified_datetime=datetime.datetime.now(datetime.timezone.utc), - modified_by=self.request.user) + bulk_delete_and_log_changes(state_qs, project, self.request.user) # Delete any localizations associated to this media loc_qs = Localization.objects.filter(project=project, media__in=media_ids) - loc_qs.update(deleted=True, - modified_datetime=datetime.datetime.now(datetime.timezone.utc), - modified_by=self.request.user) + bulk_delete_and_log_changes(loc_qs, project, self.request.user) # Clear elasticsearch entries for both media and its children. # Note that clearing children cannot be done using has_parent because it does @@ -467,19 +451,6 @@ def _delete(self, params): }, }) - # Create ChangeLogs - objs = ( - ChangeLog(project=project, user=self.request.user, description_of_change=dd) - for dd in delete_dicts - ) - change_logs = bulk_create_from_generator(objs, ChangeLog) - - # Associate ChangeLogs with deleted objects - objs = ( - ChangeToObject(ref_table=ref_table, ref_id=ref_id, change_id=cl) - for ref_id, cl in zip(ref_ids, change_logs) - ) - bulk_create_from_generator(objs, ChangeToObject) return {'message': f'Successfully deleted {count} medias!'} def _patch(self, params): @@ -506,21 +477,11 @@ def _patch(self, params): new_attrs = validate_attributes(params, obj) if new_attrs is not None: attr_count = len(ids_to_update) - ref_table = ContentType.objects.get_for_model(obj) - original_dict = obj.model_dict - bulk_patch_attributes(new_attrs, qs) + bulk_update_and_log_changes( + qs, params["project"], self.request.user, new_attributes=new_attrs + ) query = get_media_es_query(params["project"], params) ts.update(self.kwargs["project"], obj.meta, query, new_attrs) - qs = Media.objects.filter(pk__in=ids_to_update) - obj = Media.objects.get(id=original_dict["id"]) - change_dict = obj.change_dict(original_dict) - # Create the ChangeLog entry and associate it with all objects in the queryset - cl = ChangeLog( - project=obj.project, user=self.request.user, description_of_change=change_dict - ) - cl.save() - objs = (ChangeToObject(ref_table=ref_table, ref_id=o.id, change_id=cl) for o in qs) - bulk_create_from_generator(objs, ChangeToObject) count = max(count, attr_count) if desired_archive_state is not None: @@ -559,18 +520,23 @@ def _patch(self, params): # Get the original dict for creating the change log archive_objs = list(archive_qs) obj = archive_objs[0] - ref_table = ContentType.objects.get_for_model(obj) - original_dict = obj.model_dict + model_dict = obj.model_dict # Store the list of ids updated for this state and update them archive_ids_to_update = [o.id for o in archive_objs] archive_count += len(archive_ids_to_update) previously_updated += archive_ids_to_update dt_now = datetime.datetime.now(datetime.timezone.utc) - archive_qs.update( - archive_status_date=dt_now, - archive_state=next_archive_state, - modified_datetime=dt_now, + update_kwargs = { + "archive_status_date": dt_now, + "archive_state": next_archive_state, + "modified_datetime": dt_now, + } + bulk_update_and_log_changes( + archive_qs, + params["project"], + self.request.user, + update_kwargs=update_kwargs, ) archive_qs = Media.objects.filter(pk__in=archive_ids_to_update) @@ -580,19 +546,6 @@ def _patch(self, params): documents += ts.build_document(entity) ts.bulk_add_documents(documents) - # Create the ChangeLog entry and associate it with all updated objects - obj = Media.objects.get(id=original_dict["id"]) - cl = ChangeLog( - project=obj.project, - user=self.request.user, - description_of_change=obj.change_dict(original_dict), - ) - cl.save() - objs = ( - ChangeToObject(ref_table=ref_table, ref_id=obj_id, change_id=cl) - for obj_id in archive_ids_to_update - ) - bulk_create_from_generator(objs, ChangeToObject) count = max(count, archive_count) return {"message": f"Successfully patched {count} medias!"} @@ -636,9 +589,10 @@ def _patch(self, params): """ with transaction.atomic(): qs = Media.objects.select_for_update().filter(pk=params['id'], deleted=False) - original_dict = qs[0].model_dict + media = qs[0] + model_dict = media.model_dict if 'attributes' in params: - new_attrs = validate_attributes(params, qs[0]) + new_attrs = validate_attributes(params, media) bulk_patch_attributes(new_attrs, qs) if 'name' in params: @@ -669,7 +623,7 @@ def _patch(self, params): qs.update(summaryLevel=params['summaryLevel']) if 'multi' in params: - media_files = qs[0].media_files + media_files = media.media_files # If this object already contains non-multi media definitions, raise an exception. if media_files: for role in ['streaming', 'archival', 'image', 'live']: @@ -678,10 +632,10 @@ def _patch(self, params): raise ValueError(f"Cannot set a multi definition on a Media that contains " "individual media!") # Check values of IDs (that they exist and are part of the same project). - sub_media = Media.objects.filter(project=qs[0].project, pk__in=params['multi']['ids']) + sub_media = Media.objects.filter(project=media.project, pk__in=params['multi']['ids']) if len(params['multi']['ids']) != sub_media.count(): raise ValueError(f"One or more media IDs in multi definition is not part of " - "project {qs[0].project.pk} or does not exist!") + "project {media.project.pk} or does not exist!") if media_files is None: media_files = {} for key in ['ids', 'layout', 'quality']: @@ -690,7 +644,7 @@ def _patch(self, params): qs.update(media_files=media_files) if 'live' in params: - media_files = qs[0].media_files + media_files = media.media_files # If this object already contains non-live media definitions, raise an exception. if media_files: for role in ['streaming', 'archival', 'image', 'ids']: @@ -706,10 +660,14 @@ def _patch(self, params): if "archive_state" in params: next_archive_state = _get_next_archive_state( - params["archive_state"], qs[0].archive_state + params["archive_state"], media.archive_state ) if next_archive_state is not None: + project = media.project + user = self.request.user + + # Update the archive state of all videos if this is a multiview multi_constituent_ids = _single_ids_from_multi_qs( qs.filter(meta__dtype="multi") ) @@ -718,10 +676,14 @@ def _patch(self, params): pk__in=multi_constituent_ids ) dt_now = datetime.datetime.now(datetime.timezone.utc) - archive_state_qs.update( - archive_status_date=dt_now, - archive_state=next_archive_state, - modified_datetime=dt_now, + update_kwargs = { + "archive_status_date": dt_now, + "archive_state": next_archive_state, + "modified_datetime": dt_now, + "modified_by": user, + } + bulk_update_and_log_changes( + archive_state_qs, project, user, update_kwargs=update_kwargs ) obj = Media.objects.get(pk=params['id'], deleted=False) @@ -732,18 +694,7 @@ def _patch(self, params): localization = patch_attributes(new_attrs, localization) localization.save() - cl = ChangeLog( - project=obj.project, - user=self.request.user, - description_of_change=obj.change_dict(original_dict), - ) - cl.save() - ChangeToObject( - ref_table=ContentType.objects.get_for_model(obj), - ref_id=obj.id, - change_id=cl, - ).save() - + log_changes(obj, model_dict, obj.project, self.request.user) return {'message': f'Media {params["id"]} successfully updated!'} def _delete(self, params): @@ -754,17 +705,9 @@ def _delete(self, params): """ media = Media.objects.get(pk=params['id'], deleted=False) project = media.project - delete_dict = media.delete_dict - ref_table = ContentType.objects.get_for_model(media) - ref_id = media.id - media.deleted = True - media.modified_datetime = datetime.datetime.now(datetime.timezone.utc) - media.modified_by = self.request.user - media.save() + modified_datetime = datetime.datetime.now(datetime.timezone.utc) + delete_and_log_changes(media, project, self.request.user) TatorSearch().delete_document(media) - cl = ChangeLog(project=project, user=self.request.user, description_of_change=delete_dict) - cl.save() - ChangeToObject(ref_table=ref_table, ref_id=ref_id, change_id=cl).save() # Any states that are only associated to deleted media should also be marked # for deletion. @@ -774,22 +717,20 @@ def _delete(self, params): .values_list('id', flat=True) all_deleted = set(deleted) - set(not_deleted) state_qs = State.objects.filter(pk__in=all_deleted) - state_qs.update(deleted=True, - modified_datetime=datetime.datetime.now(datetime.timezone.utc), - modified_by=self.request.user) + bulk_delete_and_log_changes(state_qs, project, self.request.user) # Delete any localizations associated to this media loc_qs = Localization.objects.filter(project=project, media__in=[media.id]) - loc_qs.update(deleted=True, - modified_datetime=datetime.datetime.now(datetime.timezone.utc), - modified_by=self.request.user) + bulk_delete_and_log_changes(loc_qs, project, self.request.user) # Clear elasticsearch entries for both media and its children. # Note that clearing children cannot be done using has_parent because it does # not accept queries with size, and has_parent also does not accept ids queries. - loc_ids = [f'box_{val.id}' for val in loc_qs.iterator()] \ - + [f'line_{val.id}' for val in loc_qs.iterator()] \ - + [f'dot_{val.id}' for val in loc_qs.iterator()] + loc_types = ["box", "line", "dot"] + loc_id_iterator = chain( + *([f"{loc_type}_{val.id}" for loc_type in loc_types] for val in loc_qs.iterator()) + ) + loc_ids = list(loc_id_iterator) TatorSearch().delete(project.id, {'query': {'ids': {'values': loc_ids}}}) state_ids = [val.id for val in state_qs.iterator()] TatorSearch().delete(project.id, { diff --git a/main/rest/project.py b/main/rest/project.py index 3ca683d2d..59f9690e7 100644 --- a/main/rest/project.py +++ b/main/rest/project.py @@ -149,7 +149,9 @@ def _patch(self, params): fname = tokens[-1] # Set up S3 clients. - upload_store = get_tator_store(project.get_bucket(upload=True), upload=True) + project_upload_bucket = project.get_bucket(upload=True) + use_default_upload_bucket = project_upload_bucket is None + upload_store = get_tator_store(project_upload_bucket, upload=use_default_upload_bucket) generic_store = get_tator_store() # Check prefix. diff --git a/main/rest/state.py b/main/rest/state.py index a780bb37d..24b805cc7 100644 --- a/main/rest/state.py +++ b/main/rest/state.py @@ -8,8 +8,6 @@ from django.http import Http404 import numpy as np -from ..models import ChangeLog -from ..models import ChangeToObject from ..models import State from ..models import StateType from ..models import Media @@ -35,9 +33,16 @@ from ._attributes import patch_attributes from ._attributes import bulk_patch_attributes from ._attributes import validate_attributes -from ._util import bulk_create_from_generator -from ._util import computeRequiredFields -from ._util import check_required_fields +from ._util import ( + bulk_create_from_generator, + bulk_delete_and_log_changes, + bulk_log_creation, + bulk_update_and_log_changes, + computeRequiredFields, + check_required_fields, + delete_and_log_changes, + log_changes, +) from ._permissions import ProjectEditPermission logger = logging.getLogger(__name__) @@ -294,88 +299,33 @@ def _post(self, params): documents = [] ts.bulk_add_documents(documents) - # Create ChangeLogs - objs = ( - ChangeLog( - project=project, user=self.request.user, description_of_change=state.create_dict - ) - for state in states - ) - change_logs = bulk_create_from_generator(objs, ChangeLog) - - # Associate ChangeLogs with created objects - ref_table = ContentType.objects.get_for_model(states[0]) - ids = [state.id for state in states] - objs = ( - ChangeToObject(ref_table=ref_table, ref_id=ref_id, change_id=cl) - for ref_id, cl in zip(ids, change_logs) - ) - bulk_create_from_generator(objs, ChangeToObject) + ids = bulk_log_creation(states, project, self.request.user) - # Return created IDs. - ids = [state.id for state in states] return {'message': f'Successfully created {len(ids)} states!', 'id': ids} def _delete(self, params): qs = get_annotation_queryset(params['project'], params, 'state') count = qs.count() if count > 0: - # Get info to populate ChangeLog entry - obj = qs.first() - project = obj.project - delete_dicts = [obj.delete_dict for obj in qs] - ref_table = ContentType.objects.get_for_model(obj) - ref_ids = [o.id for o in qs] - # Delete states. - qs.update(deleted=True, - modified_datetime=datetime.datetime.now(datetime.timezone.utc), - modified_by=self.request.user) + bulk_delete_and_log_changes(qs, params["project"], self.request.user) query = get_annotation_es_query(params['project'], params, 'state') TatorSearch().delete(self.kwargs['project'], query) - # Create ChangeLogs - objs = ( - ChangeLog(project=project, user=self.request.user, description_of_change=dd) - for dd in delete_dicts - ) - change_logs = bulk_create_from_generator(objs, ChangeLog) - - # Associate ChangeLogs with deleted objects - objs = ( - ChangeToObject(ref_table=ref_table, ref_id=ref_id, change_id=cl) - for ref_id, cl in zip(ref_ids, change_logs) - ) - bulk_create_from_generator(objs, ChangeToObject) - return {'message': f'Successfully deleted {count} states!'} def _patch(self, params): qs = get_annotation_queryset(params['project'], params, 'state') count = qs.count() if count > 0: - # Get the current representation of the object for comparison - original_dict = qs.first().model_dict new_attrs = validate_attributes(params, qs[0]) - bulk_patch_attributes(new_attrs, qs) - qs.update(modified_by=self.request.user) - - # Get one object from the queryset to create the change log - obj = qs.first() - change_dict = obj.change_dict(original_dict) - ref_table = ContentType.objects.get_for_model(obj) + bulk_update_and_log_changes( + qs, params["project"], self.request.user, new_attributes=new_attrs + ) query = get_annotation_es_query(params['project'], params, 'state') TatorSearch().update(self.kwargs['project'], qs[0].meta, query, new_attrs) - # Create the ChangeLog entry and associate it with all objects in the queryset - cl = ChangeLog( - project=obj.project, user=self.request.user, description_of_change=change_dict - ) - cl.save() - objs = (ChangeToObject(ref_table=ref_table, ref_id=o.id, change_id=cl) for o in qs) - bulk_create_from_generator(objs, ChangeToObject) - return {'message': f'Successfully updated {count} states!'} def _put(self, params): @@ -415,7 +365,7 @@ def _get(self, params): @transaction.atomic def _patch(self, params): obj = State.objects.get(pk=params['id'], deleted=False) - original_dict = obj.model_dict + model_dict = obj.model_dict if 'frame' in params: obj.frame = params['frame'] @@ -462,26 +412,14 @@ def _patch(self, params): obj.modified_by = self.request.user obj.save() - cl = ChangeLog( - project=obj.project, - user=self.request.user, - description_of_change=obj.change_dict(original_dict), - ) - cl.save() - ChangeToObject( - ref_table=ContentType.objects.get_for_model(obj), - ref_id=obj.id, - change_id=cl, - ).save() + log_changes(obj, model_dict, obj.project, self.request.user) return {'message': f'State {params["id"]} successfully updated!'} def _delete(self, params): state = State.objects.get(pk=params['id'], deleted=False) project = state.project - delete_dict = state.delete_dict - ref_table = ContentType.objects.get_for_model(state) - ref_id = state.id + model_dict = state.model_dict delete_localizations = [] if state.meta.delete_child_localizations: @@ -493,19 +431,11 @@ def _delete(self, params): if not loc_qs.exists(): delete_localizations.append(loc.id) - state.deleted=True - state.modified_datetime=datetime.datetime.now(datetime.timezone.utc) - state.modified_by=self.request.user - state.save() + delete_and_log_changes(state, project, self.request.user) TatorSearch().delete_document(state) - cl = ChangeLog(project=project, user=self.request.user, description_of_change=delete_dict) - cl.save() - ChangeToObject(ref_table=ref_table, ref_id=ref_id, change_id=cl).save() qs = Localization.objects.filter(pk__in=delete_localizations) - qs.update(deleted=True, - modified_datetime=datetime.datetime.now(datetime.timezone.utc), - modified_by=self.request.user) + bulk_delete_and_log_changes(qs, project, self.request.user) for loc in qs.iterator(): TatorSearch().delete_document(loc) diff --git a/main/rest/transcode.py b/main/rest/transcode.py index 487938335..9a9996c07 100644 --- a/main/rest/transcode.py +++ b/main/rest/transcode.py @@ -60,7 +60,8 @@ def _post(self, params): path, bucket, upload = url_to_key(url, project_obj) if path is not None: logger.info(f"Attempting to retrieve size for object key {path}...") - tator_store = get_tator_store(bucket, upload=upload) + use_upload_bucket = upload and not bucket + tator_store = get_tator_store(bucket, upload=use_upload_bucket) upload_size = tator_store.get_size(path) # If we have the media ID, tag the object with the media ID. if media_id: diff --git a/main/rest/upload_completion.py b/main/rest/upload_completion.py index 479d58414..d6a7cd819 100644 --- a/main/rest/upload_completion.py +++ b/main/rest/upload_completion.py @@ -28,7 +28,8 @@ def _post(self, params): # Complete the upload. upload = key.startswith('_uploads') bucket = project_obj.get_bucket(upload=upload) - tator_store = get_tator_store(bucket, upload=upload) + use_upload_bucket = upload and not bucket + tator_store = get_tator_store(bucket, upload=use_upload_bucket) tator_store.complete_multipart_upload(key, parts, upload_id) return {'message': f"Upload completion for {key} successful!"} diff --git a/main/rest/upload_info.py b/main/rest/upload_info.py index 959b2ad51..a936e21ec 100644 --- a/main/rest/upload_info.py +++ b/main/rest/upload_info.py @@ -56,7 +56,9 @@ def _get(self, params): today = datetime.datetime.now().strftime('%Y-%m-%d') user = self.request.user.pk key = f"_uploads/{today}/{organization}/{project}/{user}/{name}" - tator_store = get_tator_store(project_obj.get_bucket(upload=True), upload=True) + upload_bucket = project_obj.get_bucket(upload=True) + use_upload = not upload_bucket + tator_store = get_tator_store(upload_bucket, upload=use_upload) elif media_id is not None and file_id is not None: raise ValueError(f"Both a file_id and media_id was provided!") elif media_id is not None: diff --git a/main/schema/components/change_log.py b/main/schema/components/change_log.py index b5f19e1c0..1f0877012 100644 --- a/main/schema/components/change_log.py +++ b/main/schema/components/change_log.py @@ -16,6 +16,11 @@ "type": "integer", "description": "Unique integer identifying project of this change log.", }, + "modified_datetime": { + 'type': 'string', + 'format': 'date-time', + 'description': 'Datetime this change occurred.', + }, "description_of_change": { "type": "object", "description": "The old and new values for the changed object", diff --git a/main/schema/section.py b/main/schema/section.py index 864877700..a9536a99e 100644 --- a/main/schema/section.py +++ b/main/schema/section.py @@ -26,9 +26,11 @@ def get_operation(self, path, method): def get_description(self, path, method): if method == 'GET': short_desc = "Get section list." + long_desc = "" elif method == 'POST': short_desc = "Create section." - return f"{short_desc}\n\n{boilerplate}" + long_desc = "Note: In order for a section to be interpreted properly, the tator_user_sections attribute of the SectionSpec cannot be None. The front end assigns a uuid1 string for this attribute, but it is not required to follow this pattern." + return f"{short_desc}\n\n{boilerplate}\n\n{long_desc}" def get_path_parameters(self, path, method): return [{ @@ -40,13 +42,16 @@ def get_path_parameters(self, path, method): }] def get_filter_parameters(self, path, method): - return [{ - 'name': 'name', - 'in': 'query', - 'required': False, - 'description': 'Name of the section.', - 'schema': {'type': 'string'}, - }] + params = [] + if method == 'GET': + params = [{ + 'name': 'name', + 'in': 'query', + 'required': False, + 'description': 'Name of the section.', + 'schema': {'type': 'string'}, + }] + return params def get_request_body(self, path, method): body = {} diff --git a/main/search.py b/main/search.py index 44049d836..1e9c86ea5 100644 --- a/main/search.py +++ b/main/search.py @@ -66,9 +66,10 @@ def _get_mapping_values(entity_type, attributes): value = attributes.get(name) if value is not None: mapping_values[name] = str(value).replace("\\", "\\\\") + mapping_types[name] = "text" if entity_type.attribute_types is None: - return mapping_values + return mapping_values, mapping_types for attribute_type in entity_type.attribute_types: name = attribute_type['name'] diff --git a/main/store.py b/main/store.py index a86920d58..bef5243f4 100644 --- a/main/store.py +++ b/main/store.py @@ -579,8 +579,10 @@ def get_tator_store( gcs_key_info = json.loads(bucket.gcs_key_info) gcs_project = gcs_key_info["project_id"] client = storage.Client(gcs_project, Credentials.from_service_account_info(gcs_key_info)) - # TODO get rclone_config_create_params for GCP storage - rclone_config_create_params = {} + rclone_config_create_params = { + "project_number": gcs_key_info["project_id"], + "service_account_credentials": bucket.gcs_key_info, + } return TatorStorage.get_tator_store( ObjectStore.GCP, bucket, client, bucket.name, rclone_config_create_params ) diff --git a/main/tests.py b/main/tests.py index 90ccd78f7..777bf6e02 100644 --- a/main/tests.py +++ b/main/tests.py @@ -2945,6 +2945,7 @@ def setUp(self): attribute_types=create_test_attribute_types(), ) self.store = get_tator_store() + self.backup_bucket = None def tearDown(self): self.project.delete() @@ -3380,19 +3381,20 @@ def test_backup_lifecycle(self): if success: n_successful_backups += 1 - self.assertEqual(n_successful_backups, len(all_keys)) + self.assertEqual(n_successful_backups, len(all_keys) if self.backup_bucket else 0) # Check the value of each resource's `backed_up` flag is `True` resource_qs = Resource.objects.filter(path__in=all_keys, backed_up=True) - self.assertEqual(resource_qs.count(), len(all_keys)) + self.assertEqual(resource_qs.count(), len(all_keys) if self.backup_bucket else 0) # Check that each resource was copied to the backup bucket - success, store_info = TatorBackupManager().get_store_info(self.project) - self.assertTrue(success) - success, store = TatorBackupManager.get_backup_store(store_info) - self.assertTrue(success) - for resource in resource_qs.iterator(): - self.assertTrue(store.check_key(resource.path)) + if self.backup_bucket: + success, store_info = TatorBackupManager().get_store_info(self.project) + self.assertTrue(success) + success, store = TatorBackupManager.get_backup_store(store_info) + self.assertTrue(success) + for resource in resource_qs.iterator(): + self.assertTrue(store.check_key(resource.path)) class ResourceWithBackupTestCase(ResourceTestCase): diff --git a/scripts/packages/tator-py b/scripts/packages/tator-py index 52ec0e3c8..85dc71f28 160000 --- a/scripts/packages/tator-py +++ b/scripts/packages/tator-py @@ -1 +1 @@ -Subproject commit 52ec0e3c81bc12ef358d73d41ea4dddf54a7bec3 +Subproject commit 85dc71f2801d4375dd04f23e8eb8af23e2d4c23c diff --git a/test/conftest.py b/test/conftest.py index 2a2bf060b..f2aa64df7 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -109,6 +109,7 @@ def token(request, page_factory): page.click('input[type="submit"]') page.wait_for_selector('text=Your API token is:') token = page.text_content('modal-notify .modal__main p') + page.close() assert(len(token) == 40) yield token @@ -139,6 +140,7 @@ def project(request, page_factory, launch_time, base_url, token): href = link.get_attribute('href') project_id = int(href.split('/')[-2]) break + page.close() yield project_id @pytest.fixture(scope='session') @@ -151,6 +153,7 @@ def video_section(request, page_factory, project): page.click('text="Save"') page.click('text="Videos"') section = int(page.url.split('=')[-1]) + page.close() yield section @pytest.fixture(scope='session') @@ -163,6 +166,7 @@ def slow_video_section(request, page_factory, project): page.click('text="Save"') page.click('text="Slow Videos"') section = int(page.url.split('=')[-1]) + page.close() yield section @pytest.fixture(scope='session') @@ -175,6 +179,7 @@ def video_section2(request, page_factory, project): page.click('text="Save"') page.click('text="Videos 2"') section = int(page.url.split('=')[-1]) + page.close() yield section @pytest.fixture(scope='session') @@ -187,6 +192,7 @@ def video_section3(request, page_factory, project): page.click('text="Save"') page.click('text="Videos 3"') section = int(page.url.split('=')[-1]) + page.close() yield section @pytest.fixture(scope='session') @@ -199,6 +205,7 @@ def image_section(request, page_factory, project): page.click('text="Save"') page.click('text="Images"') section = int(page.url.split('=')[-1]) + page.close() yield section @pytest.fixture(scope='session') @@ -241,8 +248,9 @@ def video(request, page_factory, project, video_section, video_file): page.set_input_files('section-upload input', video_file) page.query_selector('upload-dialog').query_selector('text=Close').click() while True: - page.click('reload-button') - cards = page.query_selector_all('media-card') + page.locator('.project__header reload-button').click() + page.wait_for_load_state('networkidle') + cards = page.query_selector_all('entity-card') if len(cards) == 0: continue href = cards[0].query_selector('a').get_attribute('href') @@ -250,6 +258,7 @@ def video(request, page_factory, project, video_section, video_file): print(f"Card href is {href}, media is ready...") break video = int(cards[0].get_attribute('media-id')) + page.close() yield video @pytest.fixture(scope='session') @@ -269,8 +278,9 @@ def slow_video(request, page_factory, project, slow_video_section, slow_video_fi page.set_input_files('section-upload input', slow_video_file) page.query_selector('upload-dialog').query_selector('text=Close').click() while True: - page.click('reload-button') - cards = page.query_selector_all('media-card') + page.locator('.project__header reload-button').click() + page.wait_for_load_state('networkidle') + cards = page.query_selector_all('entity-card') if len(cards) == 0: continue href = cards[0].query_selector('a').get_attribute('href') @@ -278,6 +288,7 @@ def slow_video(request, page_factory, project, slow_video_section, slow_video_fi print(f"Card href is {href}, media is ready...") break video = int(cards[0].get_attribute('media-id')) + page.close() yield video @pytest.fixture(scope='session') @@ -289,8 +300,9 @@ def video2(request, page_factory, project, video_section2, video_file): page.set_input_files('section-upload input', video_file) page.query_selector('upload-dialog').query_selector('text=Close').click() while True: - page.click('reload-button') - cards = page.query_selector_all('media-card') + page.locator('.project__header reload-button').click() + page.wait_for_load_state('networkidle') + cards = page.query_selector_all('entity-card') if len(cards) == 0: continue href = cards[0].query_selector('a').get_attribute('href') @@ -298,6 +310,7 @@ def video2(request, page_factory, project, video_section2, video_file): print(f"Card href is {href}, media is ready...") break video = int(cards[0].get_attribute('media-id')) + page.close() yield video @pytest.fixture(scope='session') @@ -309,8 +322,9 @@ def video3(request, page_factory, project, video_section3, video_file): page.set_input_files('section-upload input', video_file) page.query_selector('upload-dialog').query_selector('text=Close').click() while True: - page.click('reload-button') - cards = page.query_selector_all('media-card') + page.locator('.project__header reload-button').click() + page.wait_for_load_state('networkidle') + cards = page.query_selector_all('entity-card') if len(cards) == 0: continue href = cards[0].query_selector('a').get_attribute('href') @@ -318,6 +332,7 @@ def video3(request, page_factory, project, video_section3, video_file): print(f"Card href is {href}, media is ready...") break video = int(cards[0].get_attribute('media-id')) + page.close() yield video @pytest.fixture(scope='session') @@ -347,8 +362,9 @@ def image(request, page_factory, project, image_section, image_file): page.set_input_files('section-upload input', image_file) page.query_selector('upload-dialog').query_selector('text=Close').click() while True: - page.click('reload-button') - cards = page.query_selector_all('media-card') + page.locator('.project__header reload-button').click() + page.wait_for_load_state('networkidle') + cards = page.query_selector_all('entity-card') if len(cards) == 0: continue href = cards[0].query_selector('a').get_attribute('href') @@ -356,6 +372,7 @@ def image(request, page_factory, project, image_section, image_file): print(f"Card href is {href}, media is ready...") break image = int(cards[0].get_attribute('media-id')) + page.close() yield image @pytest.fixture(scope='session') diff --git a/test/test_annotation.py b/test/test_annotation.py index cb9d7f90f..96023dc4a 100644 --- a/test/test_annotation.py +++ b/test/test_annotation.py @@ -23,8 +23,11 @@ def common_annotation(page, canvas, bias=0): width = 100 height = 100 page.mouse.move(x, y, steps=50) + time.sleep(1) page.mouse.down() + time.sleep(1) page.mouse.move(x + width, y + height, steps=50) + time.sleep(1) page.mouse.up() page.wait_for_selector('save-dialog.is-open') save_dialog = page.query_selector('save-dialog.is-open') @@ -41,11 +44,15 @@ def common_annotation(page, canvas, bias=0): x += canvas_center_x y += canvas_center_y page.mouse.move(x+50, y+50, steps=50) + time.sleep(1) page.mouse.click(x+50, y+50) selector = page.query_selector('entity-selector:visible') selector.wait_for_selector(f'#current-index :text("{idx+1+bias}")') + time.sleep(1) page.mouse.down() + time.sleep(1) page.mouse.move(x, y, steps=50) + time.sleep(1) page.mouse.up() light = page.query_selector('#tator-success-light') light.wait_for_element_state('visible') @@ -57,11 +64,15 @@ def common_annotation(page, canvas, bias=0): x += canvas_center_x y += canvas_center_y page.mouse.move(x+45, y+45, steps=50) + time.sleep(1) page.mouse.click(x+45, y+45) selector = page.query_selector('entity-selector:visible') selector.wait_for_selector(f'#current-index :text("{idx+1+bias}")') + time.sleep(1) page.mouse.down() + time.sleep(1) page.mouse.move(x+95, y+95, steps=50) + time.sleep(1) page.mouse.up() light = page.query_selector('#tator-success-light') light.wait_for_element_state('visible') diff --git a/test/test_project-detail.py b/test/test_project-detail.py index 21f5e6e1e..60fedb916 100644 --- a/test/test_project-detail.py +++ b/test/test_project-detail.py @@ -1,8 +1,8 @@ import os import time import inspect +import requests import math -import subprocess from ._common import print_page_error @@ -10,19 +10,20 @@ # Change pagination to 10 # Search & create a saved search section # Optional- Go to media, bookmark it? or last visited? -def test_features(request, page_factory, project): - print("Project Detail Page Feature tests...") +def test_basic(request, page_factory, project): #video + print("Project Detail Page tests...") page = page_factory( f"{os.path.basename(__file__)}__{inspect.stack()[0][3]}") - page.goto(f"/{project}/project-detail", wait_until='networkidle') + page.goto(f"/{project}/project-detail") page.on("pageerror", print_page_error) + print("Start: Test Pagination and image upload") page.select_option('.pagination select.form-select', value="100") # page.wait_for_selector('text="Page 1 of 1"') - time.sleep(5) + page.wait_for_timeout(5000) # Initial card length - cards = page.query_selector_all('media-card[style="display: block;"]') + cards = page.query_selector_all('section-files entity-card[style="display: block;"]') initialCardLength = len(cards) newCardsLength = 15 totalCards = initialCardLength + newCardsLength @@ -30,28 +31,41 @@ def test_features(request, page_factory, project): nasa_space_photo_1 = '/tmp/hubble-sees-the-wings-of-a-butterfly.jpg' if not os.path.exists(nasa_space_photo_1): url = 'https://images-assets.nasa.gov/image/hubble-sees-the-wings-of-a-butterfly-the-twin-jet-nebula_20283986193_o/hubble-sees-the-wings-of-a-butterfly-the-twin-jet-nebula_20283986193_o~small.jpg' - subprocess.run(['wget', '-O', nasa_space_photo_1, url]) + with requests.get(url, stream=True) as r: + r.raise_for_status() + with open(nasa_space_photo_1, 'wb') as f: + for chunk in r.iter_content(chunk_size=8192): + if chunk: + f.write(chunk) nasa_space_photo_2 = '/tmp/layers-in-galle-crater.jpg' if not os.path.exists(nasa_space_photo_2): url = 'https://images-assets.nasa.gov/image/PIA21575/PIA21575~medium.jpg' - subprocess.run(['wget', '-O', nasa_space_photo_2, url]) + with requests.get(url, stream=True) as r: + r.raise_for_status() + with open(nasa_space_photo_2, 'wb') as f: + for chunk in r.iter_content(chunk_size=8192): + if chunk: + f.write(chunk) nasa_space_photo_3 = '/tmp/behemoth-black-hole.jpg' if not os.path.exists(nasa_space_photo_3): url = 'https://images-assets.nasa.gov/image/behemoth-black-hole-found-in-an-unlikely-place_26209716511_o/behemoth-black-hole-found-in-an-unlikely-place_26209716511_o~medium.jpg' - subprocess.run(['wget', '-O', nasa_space_photo_3, url]) + with requests.get(url, stream=True) as r: + r.raise_for_status() + with open(nasa_space_photo_3, 'wb') as f: + for chunk in r.iter_content(chunk_size=8192): + if chunk: + f.write(chunk) page.set_input_files('section-upload input', [nasa_space_photo_1,nasa_space_photo_2,nasa_space_photo_3,nasa_space_photo_2,nasa_space_photo_2,nasa_space_photo_3,nasa_space_photo_1,nasa_space_photo_1,nasa_space_photo_1,nasa_space_photo_1,nasa_space_photo_1,nasa_space_photo_1,nasa_space_photo_1,nasa_space_photo_1,nasa_space_photo_1]) page.query_selector('upload-dialog').query_selector('text=Close').click() - page.click('reload-button') - page.wait_for_selector('text=hubble') - page.wait_for_selector('text=galle') - page.wait_for_selector('text=behemoth') - time.sleep(5) + page.locator('.project__header reload-button').click() + page.wait_for_selector('section-files entity-card') + page.wait_for_timeout(5000) - cards = page.query_selector_all('media-card[style="display: block;"]') + cards = page.query_selector_all('section-files entity-card[style="display: block;"]') cardLength = len(cards) # existing + new cards print(f"Length of cards {cardLength} == should match totalCards {totalCards}") @@ -61,16 +75,16 @@ def test_features(request, page_factory, project): page.select_option('.pagination select.form-select', value="10") pages = int(math.ceil(totalCards / 10)) page.wait_for_selector(f'text="Page 1 of {str(pages)}"') - time.sleep(5) + page.wait_for_timeout(5000) - cardsHidden = page.query_selector_all('media-card[style="display: none;"]') + cardsHidden = page.query_selector_all('section-files entity-card[style="display: none;"]') cardsHiddenLength = len(cardsHidden) print(f"Length of cards hidden {cardsHiddenLength} == totalCards - 10 {totalCards - 10}") totalMinus = totalCards - 10 assert cardsHiddenLength == totalMinus - cards = page.query_selector_all('media-card[style="display: block;"]') + cards = page.query_selector_all('section-files entity-card[style="display: block;"]') cardLength = len(cards) print(f"Visible card length {cardLength} == 10") @@ -80,9 +94,9 @@ def test_features(request, page_factory, project): paginationLinks = page.query_selector_all('.pagination a') paginationLinks[2].click() page.wait_for_selector(f'text="Page 2 of {pages}"') - time.sleep(5) + page.wait_for_timeout(5000) - cards = page.query_selector_all('media-card[style="display: block;"]') + cards = page.query_selector_all('section-files entity-card[style="display: block;"]') cardLength = len(cards) totalOnSecond = totalCards - 10 if totalOnSecond > 10: @@ -99,20 +113,22 @@ def test_features(request, page_factory, project): cards[0].query_selector('a').click() page.wait_for_selector('.annotation__panel h3') page.go_back() - time.sleep(1) + page.wait_for_timeout(5000) - page.wait_for_selector('media-card') + page.wait_for_selector('section-files entity-card') print(f"Is pagination preserved?") - cards = page.query_selector_all('media-card[style="display: block;"]') + cards = page.query_selector_all('section-files entity-card[style="display: block;"]') cardLength = len(cards) totalOnSecond = totalCards - 10 if totalOnSecond > 10: totalOnSecond = 10 print(f"(refreshed) Second page length of cards {cardLength} == {totalOnSecond}") assert cardLength == totalOnSecond + print("Complete!") # Test filtering + print("Start: Test Filtering") page.click('text="Filter"') page.wait_for_selector('filter-condition-group button.btn.btn-outline.btn-small') page.click('filter-condition-group button.btn.btn-outline.btn-small') @@ -127,13 +143,15 @@ def test_features(request, page_factory, project): filterGroupButtons[0].click() page.wait_for_selector('text="Page 1 of 1"') - time.sleep(5) + # time.sleep(5) + page.wait_for_timeout(5000) - cards = page.query_selector_all('media-card[style="display: block;"]') + cards = page.query_selector_all('section-files entity-card[style="display: block;"]') cardLength = len(cards) print(f"Cards length after search {cardLength} == 2") assert cardLength == 2 + print("Start: Test save search as section") saveSearch = page.query_selector('text="Add current search"') saveSearch.click() @@ -142,8 +160,9 @@ def test_features(request, page_factory, project): page.fill('.modal__main input[placeholder="Give it a name..."]', newSectionFromSearch) saveButton = page.query_selector('text="Save"') saveButton.click() + - + page.wait_for_selector(f'text="{newSectionFromSearch}"') print(f'New section created named: {newSectionFromSearch}') @@ -151,18 +170,114 @@ def test_features(request, page_factory, project): clearSearch.click() page.wait_for_selector(f'text="{totalCards} Files"') - time.sleep(5) + page.wait_for_timeout(5000) - cards = page.query_selector_all('media-card[style="display: block;"]') + cards = page.query_selector_all('section-files entity-card[style="display: block;"]') cardLength = len(cards) print(f"After search cleared cardLength {cardLength} == 10") assert cardLength == 10 page.query_selector(f'text="{newSectionFromSearch}"').click() page.wait_for_selector('text="2 Files"') - time.sleep(5) + page.wait_for_timeout(5000) - cards = page.query_selector_all('media-card[style="display: block;"]') + cards = page.query_selector_all('section-files entity-card[style="display: block;"]') cardLength = len(cards) print(f"Cards in saved section {cardLength} == 2") assert cardLength == 2 + print("Complete!") + +# def test_bulk_edit(request, page_factory, project): #video + # print("Project Detail Page tests...") + # page = page_factory( + # f"{os.path.basename(__file__)}__{inspect.stack()[0][3]}") + # page.goto(f"/{project}/project-detail") + # page.on("pageerror", print_page_error) + + ## Test multiple edit..... + print("Start: Test media labels and mult edit") + # Show labels, and multiselect, close more menu + page.query_selector('#icon-more-horizontal').click() + page.wait_for_selector('text="Media Labels"') + page.query_selector('text="Media Labels"').click() + + # select image labels & video (some in the card list and some not) @todo add video to this project + test_string = page.query_selector_all('.entity-gallery-labels .entity-gallery-labels--checkbox-div checkbox-input[name="Test String"]') + print(f'Label panel is open: found {len(test_string)} string labels....') + test_string[0].click() # for images + test_string[2].click() # for video + + page.query_selector('.enitity-gallery__labels-div nav-close').click() + + page.query_selector('#icon-more-horizontal').click() + page.wait_for_selector('text="Edit media attributes"') + page.locator('text="Edit media attributes"').click() + + # Did label selection also preselect attributes? + ## There should be two input checked + selected = page.query_selector_all('.bulk-edit-attr-choices_bulk-edit input:checked') + print(f'Assert selected inputs {len(selected)} == Checked count 2') + assert len(selected) == 2 + + # Get a handle on shown cards + cards = page.query_selector_all('section-files entity-card[style="display: block;"]') + print(f"Selecting... {len(cards)} cards") + + # Test select all + page.keyboard.press("Control+A") + + ## There should be two cards selected (black hole filter still on) + editbutton = page.locator('.bulk-edit-submit-button .text-bold') + count = editbutton.all_inner_texts() + + print(f'Assert selected cards {str(count)} == shown count {str(len(cards))}') + assert str(count[0]) == str(len(cards)) + + # Test escape + page.keyboard.press("Escape") + + ## There should be 0 cards selected + count = editbutton.all_inner_texts() + + print(f'Assert selected cards {str(count)} == 0') + assert str(count[0]) == '0' + + # Test click selection of 1 card + cards[1].click() + + + ## There should be 1 card selected + count = editbutton.all_inner_texts() + + print(f'Assert selected cards {str(count)} == 1') + assert str(count[0]) == '1' + + ## Add some text + # TODO -- check the label says "not set", update it, check label is "updated" + # attributeShown = page.locator('section-files .entity-gallery-card__attribute span[display="block"]').innerHTML() + attributeShown = page.query_selector_all('.entity-gallery-card__attribute:not(.hidden)') + attributeShownText = attributeShown[1].text_content() + assert attributeShownText == '' + + # + page.fill('.annotation__panel-group_bulk-edit text-input:not([hidden=""]) input', 'updated') + + editbutton.click() + + page.locator('.save-confirmation').click() + time.sleep(2) + + responseText = page.locator('modal-dialog .modal__main').all_inner_texts() + print(f'responseText {responseText[0]}') + assert responseText[0] == 'Successfully patched 1 medias!\n\n' + + page.locator('text="OK"').click() + + attributeShown = page.query_selector_all('.entity-gallery-card__attribute:not(.hidden)') + attributeShownText = attributeShown[1].text_content() + assert attributeShownText == 'updated' + + print('Complete!') + + + diff --git a/ui/src/css/components/_bulk-edit.scss b/ui/src/css/components/_bulk-edit.scss index 79acb67f1..07657bcc4 100644 --- a/ui/src/css/components/_bulk-edit.scss +++ b/ui/src/css/components/_bulk-edit.scss @@ -142,7 +142,7 @@ entity-gallery-bulk-edit { } entity-card:focus { - border: 2px solid lime; + // border: 2px solid white; .entity-card.multi-select { border: 2px solid white; } diff --git a/ui/src/css/components/_entity-gallery.scss b/ui/src/css/components/_entity-gallery.scss index f3ab3d85f..abad44bb4 100644 --- a/ui/src/css/components/_entity-gallery.scss +++ b/ui/src/css/components/_entity-gallery.scss @@ -57,6 +57,7 @@ entity-gallery-aspect-ratio { linear-gradient(to bottom, $color-charcoal--light 1px, transparent 1px); background-size: 10px 10px; // transition: all 300ms linear; + object-fit: cover; } &.aspect-true { @@ -115,17 +116,6 @@ entity-gallery-aspect-ratio { top: 62px; bottom: 0; - // &::-webkit-scrollbar-thumb { - // background: $color-purple !important; - // } - // &.gray-panel { - // border-left: 5px solid $color-charcoal--light; - - // .top-bar-arrow { - // color: $color-charcoal--light; - // } - // } - &.slide-close { right: -23%; @@ -136,6 +126,7 @@ entity-gallery-aspect-ratio { .top-bar-arrow { transform: scaleX(-1); + } .entity-panel--container--top-bar { @@ -145,6 +136,70 @@ entity-gallery-aspect-ratio { } } +.entity-panel--container-left { + transition: 500ms ease left; + background: $color-charcoal--dark; + width: 380px; + // border-right: 5px solid $color-purple; + -webkit-mask-image: linear-gradient(to bottom, black 95%, transparent 100%); + mask-image: linear-gradient(to bottom, black 95%, transparent 100%); + padding-bottom: 50px; + overflow-y: scroll; + position: fixed; + left: 0; + top: 62px; + bottom: 0; + overflow-x: hidden; + + &.slide-close { + left: -330px; + + .top-bar-arrow { + left: 340px; + svg { + transform: scaleX(1); + } + } + + .entity-panel--container--top-bar { + width: 300px; + //overflow: hidden; + } + + @media (max-width: 330px){ + .top-bar-arrow { + right: 0 !important; + left: unset !important; + } + } + } + + + +} + +@media (max-width: 330px){ + .sections-wrap { + max-width: 100%; + } + .top-bar-arrow.left { + left: 0 !important; + } + + .entity-panel--container-left { + max-width: 100%; + } + + .entity-panel--container-left.slide-close { + left: -80%; + } + + .entity-panel--container-left.slide-close .top-bar-arrow.left { + right: 0 !important; + left: unset !important; + } +} + .entity-panel--container--top-bar { border-bottom: 1px solid $color-charcoal--light; display: flex; @@ -157,10 +212,24 @@ entity-gallery-aspect-ratio { //height: 100px; cursor: pointer; padding: 60px 0; + position: relative; + z-index: 100; &:hover { color: $color-charcoal--light; } + + &.left { + svg { + transform: scaleX(-1); + } + + float: right; + padding: 20px 0; + position: absolute; + left: 340px; + top: 0; + } } .entity-panel { diff --git a/ui/src/css/components/_more.scss b/ui/src/css/components/_more.scss index b9dad7d43..851ef0c02 100644 --- a/ui/src/css/components/_more.scss +++ b/ui/src/css/components/_more.scss @@ -3,7 +3,7 @@ border-radius: 6px; box-shadow: 0 2px 4px $color-charcoal--dark70; position: absolute; - z-index: 1; + z-index: 1000; .main__header & { right: 8px; diff --git a/ui/src/css/components/_project.scss b/ui/src/css/components/_project.scss index 084239397..083e0e3ec 100644 --- a/ui/src/css/components/_project.scss +++ b/ui/src/css/components/_project.scss @@ -6,6 +6,10 @@ margin-bottom: 10px; } +.project__section { + padding-bottom: 350px; +} + .projects__link { width: 60%; &:hover, @@ -275,3 +279,4 @@ -webkit-mask-image: linear-gradient(to bottom, black 95%, transparent 100%); mask-image: linear-gradient(to bottom, black 95%, transparent 100%); } + diff --git a/ui/src/css/components/_section.scss b/ui/src/css/components/_section.scss index 8ef136cdb..65f969318 100644 --- a/ui/src/css/components/_section.scss +++ b/ui/src/css/components/_section.scss @@ -15,6 +15,25 @@ } } +.is-active { + .section__name { + color: $color-white; + font-weight: $weight-semibold; + + &:before { + background-color: $color-purple; + border-bottom-right-radius: 6px; + border-top-right-radius: 6px; + content: ""; + height: 22px; + left: 0; + position: absolute; + width: 4px; + } + } + +} + .section { margin-left: -$spacing-2; diff --git a/ui/src/js/analytics/collections/gallery.js b/ui/src/js/analytics/collections/gallery.js index 9d4166d4c..3e4651c81 100644 --- a/ui/src/js/analytics/collections/gallery.js +++ b/ui/src/js/analytics/collections/gallery.js @@ -64,11 +64,11 @@ export class CollectionsGallery extends EntityCardSlideGallery { /** * CARD Label display options link for menu, and checkbox div */ - this._cardAtributeLabels = document.createElement("entity-gallery-labels"); - this._cardAtributeLabels.titleEntityTypeName = "entry"; - this._mainTop.appendChild(this._cardAtributeLabels); - this._cardAtributeLabels.menuLinkTextSpan.innerHTML = "Entry Labels"; - this._moreMenu._menu.appendChild(this._cardAtributeLabels.menuLink); + this._cardAttributeLabels = document.createElement("entity-gallery-labels"); + this._cardAttributeLabels.titleEntityTypeName = "entry"; + this._mainTop.appendChild(this._cardAttributeLabels); + this._cardAttributeLabels.menuLinkTextSpan.innerHTML = "Entry Labels"; + this._moreMenu._menu.appendChild(this._cardAttributeLabels.menuLink); /** * CARD Sort display options link for menu, and checkbox div @@ -137,7 +137,7 @@ export class CollectionsGallery extends EntityCardSlideGallery { this._cardAtributeSort.add({ typeData:locTypeData }); - this._cardAtributeLabels.add({ + this._cardAttributeLabels.add({ typeData: locTypeData, checkedFirst: true }); @@ -147,7 +147,7 @@ export class CollectionsGallery extends EntityCardSlideGallery { this._cardAtributeSort.add({ typeData: mediaTypeData }); - this._cardAtributeLabels.add({ + this._cardAttributeLabels.add({ typeData: mediaTypeData, checkedFirst: true }); @@ -305,7 +305,7 @@ export class CollectionsGallery extends EntityCardSlideGallery { const slider = document.createElement("entity-gallery-slider"); slider.setAttribute("id", state.id); slider.setAttribute("meta", state.meta); - slider._cardAtributeLabels = this._cardAtributeLabels; + slider._cardAttributeLabels = this._cardAttributeLabels; slider._cardAtributeSort = this._cardAtributeSort; slider._resizeCards = this._resizeCards; sliderList.appendChild(slider); diff --git a/ui/src/js/analytics/corrections/gallery.js b/ui/src/js/analytics/corrections/gallery.js index dd0e9b82a..c7cb30b98 100644 --- a/ui/src/js/analytics/corrections/gallery.js +++ b/ui/src/js/analytics/corrections/gallery.js @@ -68,12 +68,12 @@ export class AnnotationsCorrectionsGallery extends EntityCardGallery { /** * CARD Label display options link for menu, and checkbox div */ - this._cardAtributeLabels = document.createElement("entity-gallery-labels"); - this._cardAtributeLabels.titleEntityTypeName = "localization"; - this._cardAtributeLabels._titleText = document.createTextNode("Select localization labels to display."); - this._mainTop.appendChild(this._cardAtributeLabels); - this._cardAtributeLabels.menuLinkTextSpan.innerHTML = "Localization Labels"; - this._moreMenu._menu.appendChild(this._cardAtributeLabels.menuLink); + this._cardAttributeLabels = document.createElement("entity-gallery-labels"); + this._cardAttributeLabels.titleEntityTypeName = "localization"; + this._cardAttributeLabels._titleText = document.createTextNode("Select localization labels to display."); + this._mainTop.appendChild(this._cardAttributeLabels); + this._cardAttributeLabels.menuLinkTextSpan.innerHTML = "Localization Labels"; + this._moreMenu._menu.appendChild(this._cardAttributeLabels.menuLink); // Init aspect toggle this._aspectToggle.init(this); @@ -116,13 +116,12 @@ export class AnnotationsCorrectionsGallery extends EntityCardGallery { for (let locTypeData of this.modelData._localizationTypes) { //init card labels with localization entity type definitions - this._cardAtributeLabels.add({ + this._cardAttributeLabels.add({ typeData: locTypeData, checkedFirst: true }); //init panel with localization entity type definitions - console.log("ADDING LOC TYPE") this._bulkEdit._editPanel.addLocType(locTypeData); } @@ -212,7 +211,7 @@ export class AnnotationsCorrectionsGallery extends EntityCardGallery { /** * Card labels / attributes of localization or media type */ - this.cardLabelsChosenByType[entityTypeId] = this._cardAtributeLabels._getValue(entityTypeId); + this.cardLabelsChosenByType[entityTypeId] = this._cardAttributeLabels._getValue(entityTypeId); this._bulkEdit._updateShownAttributes({typeId: entityTypeId, values: this.cardLabelsChosenByType[entityTypeId]} ); if (newCard) { @@ -226,12 +225,12 @@ export class AnnotationsCorrectionsGallery extends EntityCardGallery { return card._img.style.height = `${130 * resizeValuePerc}px`; }); - this._cardAtributeLabels.addEventListener("labels-update", (evt) => { + this._cardAttributeLabels.addEventListener("labels-update", (evt) => { card._updateShownAttributes(evt); - this._bulkEdit._updateShownAttributes({ typeId: entityTypeId, values: evt.detail.value }); - + this._bulkEdit._updateShownAttributes({ typeId: evt.detail.typeId, values: evt.detail.value }); - this.cardLabelsChosenByType[entityTypeId] = evt.detail.value; + this.cardLabelsChosenByType[evt.detail.typeId] = evt.detail.value; + let msg = `Entry labels updated`; Utilities.showSuccessIcon(msg); }); @@ -249,24 +248,22 @@ export class AnnotationsCorrectionsGallery extends EntityCardGallery { // Notifiy bulk edit about multi-select controls card.addEventListener("ctrl-select", (e) => { - console.log("Opening edit mode"); + // console.log("Opening edit mode"); this._bulkEdit._openEditMode(e); // this.dispatchEvent(new CustomEvent("multi-select", { detail: { clickDetail: e } })); }); card.addEventListener("shift-select", (e) => { // this.dispatchEvent(new CustomEvent("multi-select", { detail: { clickDetail: e } })); - console.log("Opening edit mode"); + // console.log("Opening edit mode"); this._bulkEdit._openEditMode(e); }); this._bulkEdit.addEventListener("multi-enabled", () => { - card._multiSelectionToggle = true; - card._li.classList.add("multi-select"); + card.multiEnabled = true; }); this._bulkEdit.addEventListener("multi-disabled", () => { - card._multiSelectionToggle = false; - card._li.classList.remove("multi-select"); + card.multiEnabled = false; }); // Update view @@ -307,7 +304,7 @@ export class AnnotationsCorrectionsGallery extends EntityCardGallery { cardObj.attributeOrder = cardLabelOptions; // Initialize Card - console.log(cardObj); + // console.log(cardObj); card.init({ idx: index, obj: cardObj, @@ -315,6 +312,11 @@ export class AnnotationsCorrectionsGallery extends EntityCardGallery { cardLabelsChosen: this.cardLabelsChosenByType[entityTypeId] }); + const selectedArray = this._bulkEdit._currentMultiSelectionToId.get(entityType.id); + if (typeof selectedArray !== "undefined" && selectedArray.has(cardObj.id)) { + this._bulkEdit._addSelected({ element: card, id: cardObj.id, isSelected: true }) + } + this._currentCardIndexes[cardObj.id] = index; card.style.display = "block"; @@ -330,7 +332,6 @@ export class AnnotationsCorrectionsGallery extends EntityCardGallery { } // Replace card info so that shift select can get cards in between - console.log("New list of card elements......"); this._bulkEdit.elementList = this._cardElements; this._bulkEdit.elementIndexes = this._currentCardIndexes; this._bulkEdit.startEditMode(); @@ -440,10 +441,6 @@ export class AnnotationsCorrectionsGallery extends EntityCardGallery { // } // } - customContentHandler() { - console.log(this); - } - } customElements.define("annotations-corrections-gallery", AnnotationsCorrectionsGallery); diff --git a/ui/src/js/analytics/corrections/localizations.js b/ui/src/js/analytics/corrections/localizations.js index e7dd57c0d..f651c829a 100644 --- a/ui/src/js/analytics/corrections/localizations.js +++ b/ui/src/js/analytics/corrections/localizations.js @@ -98,8 +98,7 @@ export class AnalyticsLocalizationsCorrections extends TatorPage { this.modal.addEventListener("open", this.showDimmer.bind(this)); this.modal.addEventListener("close", this.hideDimmer.bind(this)); - // Init after modal is defined - this._bulkEdit.init(this); + } async _init() { @@ -107,6 +106,9 @@ export class AnalyticsLocalizationsCorrections extends TatorPage { this.projectId = Number(this.getAttribute("project-id")); this._modelData = new TatorData(this.projectId); + // Init after modal is defined + this._bulkEdit.init(this); + // Card Data export class collects raw model and parses into view-model format this.cardData = document.createElement("annotation-card-data"); await this.cardData.init(this._modelData); @@ -270,7 +272,7 @@ export class AnalyticsLocalizationsCorrections extends TatorPage { // Reset the pagination back to page 0 async _updateFilterResults(evt) { this._filterConditions = evt.detail.conditions; - console.log("UPDATE FILTER RESULTS"); + // console.log("UPDATE FILTER RESULTS"); this._bulkEdit.checkForFilters(this._filterConditions); var filterURIString = encodeURIComponent(JSON.stringify(this._filterConditions)); diff --git a/ui/src/js/analytics/localizations/gallery.js b/ui/src/js/analytics/localizations/gallery.js index 72a0bd181..ae1b389ce 100644 --- a/ui/src/js/analytics/localizations/gallery.js +++ b/ui/src/js/analytics/localizations/gallery.js @@ -61,12 +61,12 @@ export class AnnotationsGallery extends EntityCardGallery { /** * CARD Label display options link for menu, and checkbox div */ - this._cardAtributeLabels = document.createElement("entity-gallery-labels"); - this._cardAtributeLabels.titleEntityTypeName = "localization"; - this._cardAtributeLabels._titleText = document.createTextNode("Select localization labels to display."); - this._mainTop.appendChild(this._cardAtributeLabels); - this._cardAtributeLabels.menuLinkTextSpan.innerHTML = "Localization Labels"; - this._moreMenu._menu.appendChild(this._cardAtributeLabels.menuLink); + this._cardAttributeLabels = document.createElement("entity-gallery-labels"); + this._cardAttributeLabels.titleEntityTypeName = "localization"; + this._cardAttributeLabels._titleText = document.createTextNode("Select localization labels to display."); + this._mainTop.appendChild(this._cardAttributeLabels); + this._cardAttributeLabels.menuLinkTextSpan.innerHTML = "Localization Labels"; + this._moreMenu._menu.appendChild(this._cardAttributeLabels.menuLink); // Init aspect toggle this._aspectToggle.init(this); @@ -102,7 +102,7 @@ export class AnnotationsGallery extends EntityCardGallery { // Initialize labels selection for (let locTypeData of this.modelData._localizationTypes){ - this._cardAtributeLabels.add({ + this._cardAttributeLabels.add({ typeData: locTypeData, checkedFirst: true }); @@ -185,7 +185,7 @@ export class AnnotationsGallery extends EntityCardGallery { /** * Card labels / attributes of localization or media type */ - this.cardLabelsChosenByType[entityTypeId] = this._cardAtributeLabels._getValue(entityTypeId); + this.cardLabelsChosenByType[entityTypeId] = this._cardAttributeLabels._getValue(entityTypeId); if (newCard) { card = document.createElement("entity-card"); @@ -198,7 +198,7 @@ export class AnnotationsGallery extends EntityCardGallery { return card._img.style.height = `${130 * resizeValuePerc}px`; }); - this._cardAtributeLabels.addEventListener("labels-update", (evt) => { + this._cardAttributeLabels.addEventListener("labels-update", (evt) => { card._updateShownAttributes(evt); this.cardLabelsChosenByType[entityTypeId] = evt.detail.value; let msg = `Entry labels updated`; diff --git a/ui/src/js/annotation/entity-browser.js b/ui/src/js/annotation/entity-browser.js index 5c9de9695..d7e20f1f1 100644 --- a/ui/src/js/annotation/entity-browser.js +++ b/ui/src/js/annotation/entity-browser.js @@ -313,7 +313,7 @@ export class EntityBrowser extends TatorElement { selectEntity(obj) { let group; - if (this._identifier && this._group.getValue()) { + if (this._identifier && (this._group.getValue() && this._group.getValue() !== "Off")) { group = obj.attributes[this._identifier.name]; } else { group = "All " + this._title.textContent; diff --git a/ui/src/js/components/buttons/bulk-correct-button.js b/ui/src/js/components/buttons/bulk-correct-button.js index 6f83c97a7..c4e7f587c 100644 --- a/ui/src/js/components/buttons/bulk-correct-button.js +++ b/ui/src/js/components/buttons/bulk-correct-button.js @@ -5,9 +5,9 @@ export class BulkCorrectButton extends TatorElement { constructor() { super(); - const button = document.createElement("button"); - button.setAttribute("class", "btn-clear d-flex px-2 py-2 rounded-1 f2 text-gray hover-text-white annotation__setting"); - this._shadow.appendChild(button); + this._button = document.createElement("button"); + this._button.setAttribute("class", "btn-clear d-flex px-2 py-2 rounded-1 f2 text-gray hover-text-white annotation__setting"); + this._shadow.appendChild(this._button); const svg = document.createElementNS(svgNamespace, "svg"); svg.setAttribute("viewBox", "0 0 24 24"); @@ -18,7 +18,7 @@ export class BulkCorrectButton extends TatorElement { svg.setAttribute("stroke-linecap", "round"); svg.setAttribute("stroke-linejoin", "round"); svg.style.fill = "none"; - button.appendChild(svg); + this._button.appendChild(svg); this._title = document.createElementNS(svgNamespace, "title"); this._title.textContent = "Bulk edit"; @@ -33,10 +33,35 @@ export class BulkCorrectButton extends TatorElement { this._path2.setAttribute("d", "M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z"); svg.appendChild(this._path2); - button.addEventListener("click", () => { - button.classList.toggle("enabled"); + this._button.addEventListener("click", () => { + this._button.classList.toggle("enabled"); }); + + this._span = document.createElement("span"); + this._span.setAttribute("class", "px-2"); + this._span.hidden = true; + this._button.appendChild(this._span); } + + static get observedAttributes() { + return ["text", "url", "name", "request"]; + } + + attributeChangedCallback(name, oldValue, newValue) { + switch (name) { + case "text": + this._span.textContent = newValue; + this._span.hidden = false; + this._button.setAttribute("class", "btn-clear py-2 px-0 text-gray hover-text-white d-flex flex-items-center") + break; + case "url": + this._button.setAttribute("href", newValue); + break; + case "name": + this._button.setAttribute("download", newValue); + break; + } + } } customElements.define("bulk-correct-button", BulkCorrectButton); diff --git a/ui/src/js/components/entity-gallery/bulk-edit/edit-panel.js b/ui/src/js/components/entity-gallery/bulk-edit/edit-panel.js index 38800c83e..abb92d297 100644 --- a/ui/src/js/components/entity-gallery/bulk-edit/edit-panel.js +++ b/ui/src/js/components/entity-gallery/bulk-edit/edit-panel.js @@ -18,13 +18,37 @@ export class MultiAttributeEditPanel extends TatorElement { barLeftTop.setAttribute("class", "bulk-edit-bar--left col-4") this._bulkEditBar.appendChild(barLeftTop); + // Escape Bulk Edit + this.xClose = document.createElement("a"); + this.xClose.setAttribute("class", "hidden text-white btn-clear px-2 py-2 clickable text-underline position-absolute"); + this.xClose.setAttribute("style", "top:0;right:0;"); + this._shadow.appendChild(this.xClose); + + const svg = document.createElementNS(svgNamespace, "svg"); + svg.setAttribute("id", "icon-x"); + svg.setAttribute("viewBox", "0 0 24 24"); + svg.setAttribute("height", "1em"); + svg.setAttribute("width", "1em"); + this.xClose.appendChild(svg); + + const title = document.createElementNS(svgNamespace, "title"); + title.textContent = "Close"; + svg.appendChild(title); + + const path = document.createElementNS(svgNamespace, "path"); + path.setAttribute("d", "M5.293 6.707l5.293 5.293-5.293 5.293c-0.391 0.391-0.391 1.024 0 1.414s1.024 0.391 1.414 0l5.293-5.293 5.293 5.293c0.391 0.391 1.024 0.391 1.414 0s0.391-1.024 0-1.414l-5.293-5.293 5.293-5.293c0.391-0.391 0.391-1.024 0-1.414s-1.024-0.391-1.414 0l-5.293 5.293-5.293-5.293c-0.391-0.391-1.024-0.391-1.414 0s-0.391 1.024 0 1.414z"); + svg.appendChild(path); + + // const exitText = document.createTextNode("Exit bulk edit"); + // this.xClose.appendChild(exitText); + let barMiddleTop = document.createElement("div"); barMiddleTop.setAttribute("class", "bulk-edit-bar--middle col-4") this._bulkEditBar.appendChild(barMiddleTop); let barRightTop = document.createElement("div"); - barRightTop.setAttribute("class", "bulk-edit-bar--right_form col-3 d-flex") + barRightTop.setAttribute("class", "col-4 d-flex") this._bulkEditBar.appendChild(barRightTop); // let barLeft = document.createElement("div"); @@ -106,7 +130,7 @@ export class MultiAttributeEditPanel extends TatorElement { // // this._bulkEditForm = document.createElement("div"); - this._bulkEditForm.setAttribute("class", "bulk-edit-form__panel-group py-3 text-gray f2 px-6 rounded-2"); + this._bulkEditForm.setAttribute("class", "bulk-edit-form__panel-group mx-3 py-3 text-gray f2 px-6 rounded-2"); barMiddleTop.appendChild(this._bulkEditForm); @@ -120,16 +144,16 @@ export class MultiAttributeEditPanel extends TatorElement { // this._quickSelectAllDiv.appendChild(this._selectionSummary); this._selectionPreCountText = document.createElement("span"); - this._selectionPreCountText.textContent = "Bulk Edit "; + this._selectionPreCountText.textContent = "Bulk Edit"; this._selectionSummary.appendChild(this._selectionPreCountText); - + this._selectionCount = document.createElement("span"); this._selectionCount.setAttribute("class", "px-1 text-bold"); this._selectionCount.textContent = "0"; this._selectionSummary.appendChild(this._selectionCount); this._selectionCountText = document.createElement("span"); - this._selectionCountText.textContent = "Localizations"; + this._selectionCountText.textContent = "Localization(s)"; this._selectionSummary.appendChild(this._selectionCountText); this._compareButton = document.createElement("button"); @@ -138,7 +162,7 @@ export class MultiAttributeEditPanel extends TatorElement { // barLeft.appendChild(this._compareButton); this._editButton = document.createElement("button"); - this._editButton.setAttribute("class", "btn btn-clear py-2 px-2 disabled col-12"); + this._editButton.setAttribute("class", "bulk-edit-submit-button btn btn-clear py-2 px-2 disabled col-12"); // this._editButton.style.width = "250px"; this._editButton.disabled = true; this._editButton.appendChild(this._selectionSummary); @@ -245,46 +269,29 @@ export class MultiAttributeEditPanel extends TatorElement { } addLocType(typeData) { - // console.log(typeData); let typeName = typeData.name ? typeData.name : ""; - if (this._shownTypes.has(typeData.id)) { - // don't re-add this type... + + if (this._shownTypes.has(typeData.id) || typeData.visible == false || typeData.attribute_types.length == 0) { + // don't re-add this type, or don't add if visible=false... return false; } else { + console.log(`Adding typeName ${typeName}`); this._shownTypes.set(typeData.id, true); } // // Main labels box - // let labelsMain = document.createElement("div"); - // labelsMain.setAttribute("class", "entity-gallery-labels rounded-2 my-2 d-flex flex-row flex-justify-center flex-justify-between col-12"); - - // // if(!hideTypeName){ - // let _title = document.createElement("div"); - // _title.setAttribute("class", "entity-gallery-labels--title py-3 px-2 col-3"); - // _title.appendChild(document.createTextNode(`${typeName}`)); - - // if (typeof typeData.description !== "undefined" && typeData.description !== "") { - // let descriptionText = document.createElement("div"); - // descriptionText.setAttribute("class", "f3 py-1 text-gray"); - // descriptionText.textContent = `${typeData.description}`; - // _title.appendChild(descriptionText); - // } - - // let idText = document.createElement("text"); - // idText.setAttribute("class", "d-flex py-1 text-gray f3"); - // idText.textContent = `Type ID: ${typeData.id}`; - // _title.appendChild(idText); - // labelsMain.appendChild(_title); - // // } + let labelsMain = document.createElement("div"); + labelsMain.setAttribute("class", "entity-gallery-labels rounded-2 my-2 col-12"); //d-flex flex-row flex-justify-center flex-justify-between - // // Labels details with checkboxes - // let _labelDetails = document.createElement("div"); - // _labelDetails.setAttribute("class", "float-right col-10"); - // labelsMain.appendChild(_labelDetails); + let idText = document.createElement("div"); + idText.setAttribute("class", "text-gray f3 px-3 mt-3"); + idText.textContent = `${typeName} | Type ID: ${typeData.id}`; + labelsMain.appendChild(idText); // Style div for checkbox set let styleDiv = document.createElement("div"); - styleDiv.setAttribute("class", "entity-gallery-labels--checkbox-div px-3 py-1 rounded-2"); + styleDiv.setAttribute("class", "entity-gallery-labels--title entity-gallery-labels--checkbox-div px-3 py-1 rounded-2"); + labelsMain.appendChild(styleDiv); this._warningConfirmation = document.createElement("div"); // this._warningConfirmation.setAttribute("class", "pb-3"); @@ -304,7 +311,7 @@ export class MultiAttributeEditPanel extends TatorElement { this._prefetchBool.setValue(true); this._prefetchBool.hidden = true; // this._warningConfirmation.appendChild(this._prefetchBool); - + /** * Label Choice @@ -312,6 +319,8 @@ export class MultiAttributeEditPanel extends TatorElement { // If ok, create the checkbox list const checkboxList = this.makeListFrom(typeData); + // + const selectionBoxes = document.createElement("checkbox-set"); selectionBoxes._colSize = "py-1 pr-2"; selectionBoxes._inputDiv.setAttribute("class", "d-flex flex-row flex-wrap col-12"); @@ -328,13 +337,15 @@ export class MultiAttributeEditPanel extends TatorElement { this._boxValueChanged(selectionBoxes, typeData.id); }); - this.div.innerHTML = ""; - this.div.appendChild(styleDiv); + // when we relied on global attribute list (not by type) + // we cleared each time, because the last iteration would have all the types + // this.div.innerHTML = ""; - console.log(selectionBoxes); + // Add the selection boxes + this.div.appendChild(labelsMain); - // Now make the inputs - this._addInputs(this._attribute_types.entries()) + // Make and Add associated hidden inputs + this._addInputs(typeData.attribute_types, typeData.id); } _boxValueChanged(checkBoxSet, typeId) { @@ -343,32 +354,29 @@ export class MultiAttributeEditPanel extends TatorElement { // let inputs = this._bulkEditForm.querySelector(`#${typeId}`); let nameIsFilteredOn = false; - console.log("box value changed"); - console.log(attributeNames); - - if (this._bulkEditForm.children.length !== 0) { - for (let input of this._inputsOnly) { - let name = input.getAttribute("name"); - + // console.log("box value changed"); + // console.log(attributeNames); - if (attributeNames.includes(name)) { - input.hidden = false; - } else { - input.hidden = true; - } - - // //Update compare table via event - // this.dispatchEvent(new CustomEvent("attribute-changed", { detail: { name: name, added: !input.hidden, typeId } })); + let inputDiv = this._inputGroup.get(typeId); + for (let input of inputDiv.children) { + let name = input.getAttribute("name"); + if (attributeNames.includes(name)) { + input.hidden = false; + } else { + input.hidden = true; } + + // //Update compare table via event + // this.dispatchEvent(new CustomEvent("attribute-changed", { detail: { name: name, added: !input.hidden, typeId } })); } let filterNames = []; for (let name of attributeNames) { if (this.resultsFilter.attributes.includes(name)) { - console.log("Warning: filter contains attribute.") + console.warn("Warning: filter contains attribute.") nameIsFilteredOn = true; filterNames.push(name); - } + } } @@ -393,23 +401,28 @@ export class MultiAttributeEditPanel extends TatorElement { */ makeListFrom(typeData) { this.newList = [...this._attributeCheckBoxList]; + const typeCheckboxList = [] // Non-hidden attributes (ie order >= 0)) let nonHiddenAttrs = []; - for (let attr of typeData.attribute_types) { - if (attr.order >= 0) { - if (!this._attribute_types.has(attr.name)) nonHiddenAttrs.push(attr); - } - } - if (nonHiddenAttrs.length > 0) { + // This collapses attributes with the same name + // for (let attr of typeData.attribute_types) { + // console.log(attr); + // if (attr.order >= 0) { + // if (!this._attribute_types.has(attr.name)) nonHiddenAttrs.push(attr); + // } + // } + + if (typeData.attribute_types.length > 0) { // Show array by order, or alpha - const sorted = nonHiddenAttrs.sort((a, b) => { + const sorted = typeData.attribute_types.sort((a, b) => { return a.order - b.order || a.name - b.name; }); // Create an array for checkbox set el for (let attr of sorted) { + // console.log(attr); let checkboxData = { id: encodeURI(attr.name), name: attr.name, @@ -417,14 +430,17 @@ export class MultiAttributeEditPanel extends TatorElement { }; this._attribute_types.set(attr.name, attr); this.newList.push(checkboxData); - + typeCheckboxList.push(checkboxData); // reset checked - only check the first one // if(checked) checked = false; } } this._attributeCheckBoxList = this.newList; - return this.newList; + console.log(typeCheckboxList); + + // return this.newList; + return typeCheckboxList; } hideShowTypes(setOfMetaIds) { @@ -450,50 +466,57 @@ export class MultiAttributeEditPanel extends TatorElement { setSelectionBoxValue({ typeId, values }) { // sets checked -- from listeners to attribute label change / default shown on card - // - for (let selectionBoxes of Array.from(this._selectionValues)) { - for (let box of selectionBoxes[1]._inputs) { + let listForType = this._selectionValues.get(typeId); + + // Evaluate list for shown types + if (listForType) { + for (let box of listForType._inputs) { let boxName = box.getAttribute("name"); - console.log(`values.includes(boxName) ${values.includes(boxName)} .....${boxName}....`); - if (values.includes(boxName)) { + if (values.includes(boxName) == true) { box._checked = true; - console.log(box); + // console.log(box); } else { box._checked = false; } } - this._boxValueChanged(selectionBoxes[1], typeId); + this._boxValueChanged(listForType, typeId); } + } // Loop through and add hidden inputs for each data type - _addInputs(dataType) { - // console.log(dataType); + _addInputs(attributeTypes, dataTypeId) { + console.log("Creating div for inputs... type id " + dataTypeId) const div = document.createElement("div"); div.setAttribute("class", "annotation__panel-group_bulk-edit text-gray f2"); - div.setAttribute("id", dataType.id); - this._bulkEditForm.innerHTML = ""; + div.setAttribute("id", dataTypeId); + + // if (typeof this._inputGroup.get(dataTypeId) == "undefined") { + // this._bulkEditForm.innerHTML = ""; this._bulkEditForm.appendChild(div); - // this._inputGroup.set(dataType.id, div); + this._inputGroup.set(dataTypeId, div); + // } else { + // return true; + // } + // div.hidden = true; // let label = document.createElement("label"); // label.setAttribute("class", "bulk-edit-legend"); - // label.textContent = `Type ID: ${dataType.id}`; + // label.textContent = `Type ID: ${dataTypeId}`; // div.appendChild(label); // User defined attributes - let array = Array.from(dataType); - const sorted = array.sort((a, b) => { - return a[1].order - b[1].order || a[1].name - b[1].name; + const sorted = attributeTypes.sort((a, b) => { + return a.order - b.order || a.name - b.name; }); - for (let a of sorted) { + for (let attributeDef of sorted) { let widget; var ignorePermission = false; - let attributeDef = a[1]; + // let attributeDef = a; if (attributeDef.dtype == "bool") { widget = document.createElement("bool-input"); @@ -517,7 +540,7 @@ export class MultiAttributeEditPanel extends TatorElement { widget = document.createElement("datetime-input"); widget.setAttribute("name", attributeDef.name); } catch (e) { - console.log(e.description); + console.error("Error making datetime input", e); } if ((widget && widget._input && widget._input.type == "text") || !widget._input) { @@ -574,10 +597,11 @@ export class MultiAttributeEditPanel extends TatorElement { if (typeof this._permission !== "undefined" && !ignorePermission) { widget.permission = this._permission; } + widget.hidden = true; div.appendChild(widget); - this._inputs.set(`${attributeDef.name} type_${dataType.id}`, widget); + this._inputs.set(`${attributeDef.name} type_${dataTypeId}`, widget); this._inputsOnly.push(widget); widget.addEventListener("change", () => { @@ -593,6 +617,7 @@ export class MultiAttributeEditPanel extends TatorElement { // const value = []; + // Each group is related to a type ID for (const group of this._bulkEditForm.children) { if (!group.hidden) { let response = { typeId: group.id, values: {}, rejected: {} }; @@ -601,7 +626,7 @@ export class MultiAttributeEditPanel extends TatorElement { let name = widget.getAttribute("name"); let val = widget.getValue() - console.log(`Evaluating value of widget named ${name}. Value = ${val}`); + // console.log(`Evaluating value of widget named ${name}. Value = ${val}`); if (val !== null) { response.values[name] = val; @@ -619,13 +644,14 @@ export class MultiAttributeEditPanel extends TatorElement { shownAttrNames() { let values = new Map(); + + // Each group is related to a type ID for (const group of this._bulkEditForm.children) { if (!group.hidden) { let typeId = group.id; for (const widget of group.children) { - if (!widget.hidden && widget.tagName !== "LABEL") { - console.log(widget.getAttribute("name")); + // console.log(widget.getAttribute("name")); //${e.detail.name} type_${e.detail.typeId} values.set(`${widget.getAttribute("name")} type_${typeId}`, widget.getAttribute("name")); } diff --git a/ui/src/js/components/entity-gallery/bulk-edit/selection-panel.js b/ui/src/js/components/entity-gallery/bulk-edit/selection-panel.js index 361718cbd..8cc24f4df 100644 --- a/ui/src/js/components/entity-gallery/bulk-edit/selection-panel.js +++ b/ui/src/js/components/entity-gallery/bulk-edit/selection-panel.js @@ -1,18 +1,17 @@ -import { TatorElement } from "../../tator-element.js"; +import { TatorElement, svgNamespace } from "../../tator-element.js"; export class MultiSelectionPanel extends TatorElement { constructor() { super(); - - + // Bar under the filter for shortcuts, and links this._bulkEditBar = document.createElement("div"); this._bulkEditBar.setAttribute("class", "py-2 d-flex flex-wrap") this._shadow.appendChild(this._bulkEditBar); this._shortCuts = document.createElement("div"); this._shortCuts.setAttribute("class", "py-2 col-6") - this._shortCuts.innerHTML = `Shorcuts: Use Ctrl+A to select all localizations on the page, and Esc to deselect all.`; + this._shortCuts.innerHTML = `Shortcuts: Use Ctrl+A to select all on the page, and Esc to deselect all.`; this._bulkEditBar.appendChild(this._shortCuts) this._minimizeBar = document.createElement("div"); @@ -45,14 +44,36 @@ export class MultiSelectionPanel extends TatorElement { this._selectAllPage = document.createElement("a"); this._selectAllPage.setAttribute("class", "text-purple py-2 clickable float-left text-left"); - this._selectAllPage.innerHTML = "Select all on page"; + this._selectAllPage.innerHTML = "Select Page"; this._minimizeBar.appendChild(this._selectAllPage); this._clearSelection = document.createElement("a"); this._clearSelection.setAttribute("class", "text-gray py-2 px-6 clickable float-right text-right"); - this._clearSelection.innerHTML = "Clear all selected"; + this._clearSelection.innerHTML = "Deselect All"; this._minimizeBar.appendChild(this._clearSelection); + // Escape Bulk Edit + this.xClose = document.createElement("a"); + this.xClose.setAttribute("class", "hidden text-white btn-clear px-2 py-2 clickable text-underline"); + this._minimizeBar.appendChild(this.xClose); + + const svg = document.createElementNS(svgNamespace, "svg"); + svg.setAttribute("id", "icon-x"); + svg.setAttribute("viewBox", "0 0 24 24"); + svg.setAttribute("height", "1em"); + svg.setAttribute("width", "1em"); + this.xClose.appendChild(svg); + + const title = document.createElementNS(svgNamespace, "title"); + title.textContent = "Close"; + svg.appendChild(title); + + const path = document.createElementNS(svgNamespace, "path"); + path.setAttribute("d", "M5.293 6.707l5.293 5.293-5.293 5.293c-0.391 0.391-0.391 1.024 0 1.414s1.024 0.391 1.414 0l5.293-5.293 5.293 5.293c0.391 0.391 1.024 0.391 1.414 0s0.391-1.024 0-1.414l-5.293-5.293 5.293-5.293c0.391-0.391 0.391-1.024 0-1.414s-1.024-0.391-1.414 0l-5.293 5.293-5.293-5.293c-0.391-0.391-1.024-0.391-1.414 0s-0.391 1.024 0 1.414z"); + svg.appendChild(path); + + const exitText = document.createTextNode("Exit bulk edit"); + this.xClose.appendChild(exitText); // this._h2 = document.createElement("h2"); // this._h2.setAttribute("class", "py-2 px-2 f1 semi-bold"); // this._h2.innerHTML = `Selection Mode: Ctrl + A to select all. Esc to exit.`; diff --git a/ui/src/js/components/entity-gallery/entity-gallery-bulk-edit.js b/ui/src/js/components/entity-gallery/entity-gallery-bulk-edit.js index 8ada3144f..e8f68fce9 100644 --- a/ui/src/js/components/entity-gallery/entity-gallery-bulk-edit.js +++ b/ui/src/js/components/entity-gallery/entity-gallery-bulk-edit.js @@ -1,7 +1,7 @@ import { TatorElement } from "../tator-element.js"; import { getCookie } from "../../util/get-cookie.js"; import { FilterConditionData } from "../../util/filter-utilities.js"; -import { svgNamespace } from "../tator-element.js"; + import { SettingsBox } from "../../project-settings/settings-box-helpers.js"; @@ -22,33 +22,16 @@ export class GalleryBulkEdit extends TatorElement { */ // // Mesage bar top this._messageBar_top = document.createElement("div"); - this._messageBar_top.setAttribute("class", "px-6 py-2 bulk-edit-bar_top text-center hidden") - // this._shadow.appendChild(this._messageBar_top); + this._messageBar_top.setAttribute("class", "px-6 py-2 bulk-edit-bar_top text-center hidden"); + this._messageBar_top.hidden = true; + this._shadow.appendChild(this._messageBar_top); // this._h2 = document.createElement("h2"); // this._h2.setAttribute("class", "py-2 px-2 f1 semi-bold"); // this._h2.innerHTML = `Selection Mode: Ctrl + A to select all. Esc to exit.`; // this._messageBar_top.appendChild(this._h2); - // Escape Bulk Edit - this.xClose = document.createElement("button"); - this.xClose.setAttribute("class", "text-white bulk-edit--cancel btn-clear px-2 py-2 h2 text-white"); - this._messageBar_top.appendChild(this.xClose); - - const svg = document.createElementNS(svgNamespace, "svg"); - svg.setAttribute("id", "icon-x"); - svg.setAttribute("viewBox", "0 0 24 24"); - svg.setAttribute("height", "1em"); - svg.setAttribute("width", "1em"); - this.xClose.appendChild(svg); - - const title = document.createElementNS(svgNamespace, "title"); - title.textContent = "Close"; - svg.appendChild(title); - - const path = document.createElementNS(svgNamespace, "path"); - path.setAttribute("d", "M5.293 6.707l5.293 5.293-5.293 5.293c-0.391 0.391-0.391 1.024 0 1.414s1.024 0.391 1.414 0l5.293-5.293 5.293 5.293c0.391 0.391 1.024 0.391 1.414 0s0.391-1.024 0-1.414l-5.293-5.293 5.293-5.293c0.391-0.391 0.391-1.024 0-1.414s-1.024-0.391-1.414 0l-5.293 5.293-5.293-5.293c-0.391-0.391-1.024-0.391-1.414 0s-0.391 1.024 0 1.414z"); - svg.appendChild(path); + // Message Panel this._bulkEditBar = document.createElement("div"); @@ -72,6 +55,7 @@ export class GalleryBulkEdit extends TatorElement { this._editPanel.addEventListener("select-click", this._showSelectionPanel.bind(this)); // Back this._editPanel.addEventListener("save-edit-click", this._saveBulkEdit.bind(this)); this._editPanel.addEventListener("comparison-click", this._showComparisonPanel.bind(this)); + this._selectionPanel.addEventListener("clear-selection", this._clearSelection.bind(this)); this._editPanel.hidden = true; this._bulkEditBar.appendChild(this._editPanel); @@ -83,7 +67,6 @@ export class GalleryBulkEdit extends TatorElement { this._comparisonPanel.hidden = true; this._bulkEditBar.appendChild(this._comparisonPanel); - // this._bulkCorrect = document.createElement("bulk-correct-button"); // this._bulkCorrect.style.position = "absolute"; // this._bulkCorrect.style.top = "0"; @@ -102,9 +85,6 @@ export class GalleryBulkEdit extends TatorElement { // return this._bulkEditBar.classList.toggle("minimized"); // }); - - - /** * Initially selection panel is shown */ @@ -128,7 +108,8 @@ export class GalleryBulkEdit extends TatorElement { // Listen to escape or Close document.addEventListener("keydown", this._keyDownHandler.bind(this)); - this.xClose.addEventListener("click", this._escapeEditMode.bind(this)); + this._selectionPanel.xClose.addEventListener("click", this._escapeEditMode.bind(this)); + this._editPanel.xClose.addEventListener("click", this._escapeEditMode.bind(this)); this._editPanel._bulkEditModal.addEventListener("close", () => { if (this._page) { @@ -155,6 +136,8 @@ export class GalleryBulkEdit extends TatorElement { } set elementList(val) { + // console.log("this._elements updated"); + // console.log(this._elements); this._elements = val; } @@ -162,13 +145,25 @@ export class GalleryBulkEdit extends TatorElement { this._elementIndexes = val; } - init(page) { + init(page, gallery, type = "localization", projectId = null) { + // console.log("BULK EDIT INITIALIZED!"); this._page = page; + this._projectId = this._page.getAttribute("project-id"); + this._editType = type; + + if (type == "media") { + this._editPanel.xClose.classList.remove("hidden"); + this._selectionPanel.xClose.classList.remove("hidden"); + this._editPanel._selectionCountText.textContent = "Media(s)"; + } - // todo- generalize this - this._page._filterResults.addEventListener("multi-select", this._openEditMode.bind(this)) - this.boxHelper = new SettingsBox( this._page.modal ); - // this._page._filterResults.before(this._selectionPanel); + this.boxHelper = new SettingsBox(this._page.modal); + + if (gallery == null) { + this._gallery = this._page._filterResults + } else { + this._gallery = gallery; + } } _keyUpHandler(e) { @@ -185,22 +180,21 @@ export class GalleryBulkEdit extends TatorElement { // console.log(`Code: ${e.key}`); if (e.key == "Escape") { - console.log(`Escape!`) + // console.log(`Escape!`) // this._escapeEditMode(); this._clearSelection(); } if (e.code == "Control") { if (e.code == "a" || e.code == "A") { - console.log("asdasdasdasdasd+A"); this.selectAllOnPage(); - } + } } if (e.ctrlKey && (e.key === 'a' || e.key === 'A')) { e.preventDefault(); - console.log("Control+A"); - this.selectAllOnPage(); + // console.log("Control+A"); + this.selectAllOnPage(); } // if (e.code == "Enter") { @@ -222,7 +216,7 @@ export class GalleryBulkEdit extends TatorElement { } shiftSelect({ element, id, isSelected }) { - console.log("Shift select"); + // console.log("Shift select"); // if (!this._editMode) this.startEditMode(); // // clicked element @@ -266,13 +260,14 @@ export class GalleryBulkEdit extends TatorElement { if (el.card._li.classList.contains("is-selected")) { //this._removeSelected({ element:el.card, id, isSelected: el.card._li.classList.contains("is-selected") }); } else { - this._addSelected({ element:el.card, id, isSelected: el.card._li.classList.contains("is-selected") }); + this._addSelected({ element: el.card, id, isSelected: el.card._li.classList.contains("is-selected") }); } } } } _addSelected({ element, id, isSelected }) { + // console.log("Add selected"); if (!this._editMode) this.startEditMode(); element._li.classList.add("is-selected"); @@ -284,7 +279,7 @@ export class GalleryBulkEdit extends TatorElement { this._updatePanelCount(this._currentMultiSelection.size); this._editPanel.hideShowTypes(this.setOfSelectedMetaIds); let entityId = element.cardObj.entityType.id; - + // list is a set to ensure uniqueness of additions in list let list = typeof this._currentMultiSelectionToId.get(entityId) !== "undefined" ? this._currentMultiSelectionToId.get(entityId) : new Set(); list.add(id); @@ -293,7 +288,7 @@ export class GalleryBulkEdit extends TatorElement { } _removeSelected({ element, id, isSelected }) { - console.log("remove selected"); + // console.log("remove selected"); if (isSelected) { element._li.classList.remove("is-selected"); } @@ -306,12 +301,12 @@ export class GalleryBulkEdit extends TatorElement { let entityId = element.cardObj.entityType.id; let idsInType = this._currentMultiSelectionToId.get(entityId); - + if (idsInType.length == 1) { // if the only id selected for this type is this one, then clean it out and update view this._currentMultiSelectionToId.delete(entityId); this.setOfSelectedMetaIds.delete(entityId); - this._editPanel.hideShowTypes(this.setOfSelectedMetaIds); + this._editPanel.hideShowTypes(this.setOfSelectedMetaIds); } else { // just remove it from the selection list idsInType.delete(id); @@ -334,33 +329,44 @@ export class GalleryBulkEdit extends TatorElement { _openEditMode(e) { + // console.log("Bulk edit is running open edit mode with this event....... detail"); + // console.log(e.detail); // let clickType = typeof e.detail.clickDetail == "undefined" ? e.type : e.detail.clickDetail.type; // if (clickType == "shift-select") { // this.shiftSelect(e.detail); // } else { - if (e.detail.isSelected) { - this._removeSelected(e.detail); - } else { - this._addSelected(e.detail); - } + if (e.detail.isSelected) { + this._removeSelected(e.detail); + } else { + this._addSelected(e.detail); + } // } this._updatePanelCount(this._currentMultiSelection.size); } + // Used on pagination, and in clear selection + clearAllCheckboxes() { + if (this._elements && this._elements.length > 0) { + for (let el of this._elements) { + el.card._li.classList.remove("is-selected"); + el.card._multiSelectionToggle = false; + } + } + + } + _clearSelection() { - console.log("CLEARING SELECTION!"); + // console.log("CLEARING SELECTION! (in _clearSelection) "); this._currentMultiSelection.clear(); this._currentSelectionObjects.clear(); + this._currentMultiSelectionToId.clear(); // this._currentSelectionObjects.clear(); this.setOfSelectedMetaIds.clear(); this._editPanel.hideShowTypes(this.setOfSelectedMetaIds); - for (let el of this._elements) { - el.card._li.classList.remove("is-selected"); - el.card._multiSelectionToggle = false; - } + this.clearAllCheckboxes(); this._updatePanelCount(0); } @@ -370,59 +376,75 @@ export class GalleryBulkEdit extends TatorElement { } startEditMode() { + // console.log("startEditMode"); this._editMode = true; for (let el of this._elements) { + // console.log(el); + el.card.multiEnabled = true; if (el.card._li.classList.contains("is-selected") && !this._currentMultiSelection.has(el.card.cardObj.id)) { - this._addSelected({element: el.card, id: el.card.cardObj.id, isSelected: el.card._li.classList.contains("is-selected")}) + this._addSelected({ element: el.card, id: el.card.cardObj.id, isSelected: el.card._li.classList.contains("is-selected") }) } } - + // show edit drawer and tools this._messageBar_top.classList.remove("hidden"); this._bulkEditBar.classList.remove("hidden"); - if (this._page.main.classList.contains("col-9")) { - this._editPanelWasOpen = true; - this._page.main.classList.remove("col-9"); - this._page.main.classList.add("col-12"); - } else { - this._editPanelWasOpen = false; + if (this._editType != "media") { + if (this._page.main.classList.contains("col-9")) { + // console.log("_editPanelWasOpen is being set to true"); + this._editPanelWasOpen = true; + this._page.main.classList.remove("col-9"); + this._page.main.classList.add("col-12"); + } else { + this._editPanelWasOpen = false; + } } + // // hide page elements // this._page._header.classList.add("hidden"); // this._page.aside.classList.add("hidden"); - + // // this._page.main.style.marginTop = "-100px"; // this._page.main.style.paddingBottom = "300px"; // this._page._filterView.classList.add("hidden"); - this._page._filterResults._ul.classList.add("multi-select-mode"); + // this._page._filterResults._ul.classList.add("multi-select-mode"); + this._gallery._ul.classList.add("multi-select-mode"); this.dispatchEvent(new Event("multi-enabled")); if (this.resultsFilter.containsAttributes == true) { this._editPanel.addEventListener("attribute-is-filtered-on", (e) => { - if (e.detail.names.length > 0 ){ - console.log("Setting this._requiresPrefetch = true"); + if (e.detail.names.length > 0) { + // console.log("Setting this._requiresPrefetch = true"); this._requiresPrefetch = true; } else { - console.log("Setting this._requiresPrefetch = false"); + // console.log("Setting this._requiresPrefetch = false"); this._requiresPrefetch = false; } }); } + + this._showEditPanel(true); } - _escapeEditMode() { + _escapeEditMode(e) { + e.preventDefault(); this._editMode = false; // hide edit drawer and tools this._messageBar_top.classList.add("hidden"); this._bulkEditBar.classList.add("hidden"); - if (this._editPanelWasOpen) { - this._page.main.classList.add("col-9"); + // In correction page this panel stays open, in media is it open / shut + if (this._editType == "media" && !this._selectionPanel.isHidden()) { + this._selectionPanel.show(false); + } + + if (this._editPanelWasOpen && this._editType != "media") { + this._page.main.classList.add("col-9"); this._page.main.classList.remove("col-12"); // reset this this._editPanelWasOpen = false; @@ -433,18 +455,19 @@ export class GalleryBulkEdit extends TatorElement { this._page.aside.classList.remove("hidden"); this._page.main.style.marginTop = "0"; // this._page._filterView.classList.remove("hidden"); - this._page._filterResults._ul.classList.remove("multi-select-mode"); + // this._page._filterResults._ul.classList.remove("multi-select-mode"); + this._gallery._ul.classList.remove("multi-select-mode"); this._clearSelection(); // this.resetElements(); this.dispatchEvent(new Event("multi-disabled")); this._editPanel.removeEventListener("attribute-is-filtered-on", (e) => { - if (e.detail.names.length > 0 ){ - console.log("Setting this._requiresPrefetch = true"); + if (e.detail.names.length > 0) { + // console.log("Setting this._requiresPrefetch = true"); this._requiresPrefetch = true; } else { - console.log("Setting this._requiresPrefetch = false"); + // console.log("Setting this._requiresPrefetch = false"); this._requiresPrefetch = false; } }); @@ -461,22 +484,26 @@ export class GalleryBulkEdit extends TatorElement { // // this._page.hideDimmer(); // // }); // // } else { - // this._page.hideDimmer(); - this._selectionPanel.show(val); - if (!this._editMode) this.startEditMode(); + // this._page.hideDimmer(); + this._selectionPanel.show(val); + // console.log("this._editMode.......... "+this._editMode) + if (!this._editMode) this.startEditMode(); // } } _showEditPanel(val = true) { + // console.log("SHOW EDIT PANEL!"); + // console.log(this._page); // if (val) { - this._page.showDimmer(); + // this._page.showDimmer(); // this._comparisonPanel.show(false); - // this._selectionPanel.show(false); + this._selectionPanel.show(true); // } // this._editPanel.hideShowTypes(this.setOfSelectedMetaIds); - this._editPanel.show(val); + this._editPanel.show(true); //val // this._page._bulkEditModal.setAttribute("is-open", "true"); } + _showComparisonPanel(val = true) { if (val) { this._editPanel.show(false); @@ -487,64 +514,108 @@ export class GalleryBulkEdit extends TatorElement { let shownAttributes = this._editPanel.shownAttrNames(); this._editPanel.toggleAttribute("hide"); this._comparisonPanel.init({ columns: shownAttributes }); - + if (typeof this._currentSelectionObjects !== "undefined" || this._currentSelectionObjects !== null) { this._comparisonPanel._refreshTable(this._currentSelectionObjects); } this._comparisonPanel.show(val); } + _saveBulkEdit() { this._saveConfirmation(); } + + // This feature was to compare the values of selected attributes and cards in a table + // This never went live // triggers were hidden or disconnected _showMiniComparison(val = true) { this._editPanel.showComparison(val); } + _saveConfirmation() { let button = document.createElement("button"); - button.setAttribute("class", "btn f1 text-semibold"); + button.setAttribute("class", "save-confirmation btn f1 text-semibold"); let confirmText = document.createTextNode("Yes") button.appendChild(confirmText); - - let text = `

Edit ${this._currentMultiSelection.size} Localizations?

`; + let typeText = this._editType == "media" ? 'Media(s)' : 'Localization(s)'; + let text = `

Edit ${this._currentMultiSelection.size} ${typeText}?

`; let inputValueArray = this._editPanel.getValue(); - + let formData = []; for (let r of inputValueArray) { - // console.log(test); - // if(r.typeId !== "" && typeof this._currentMultiSelectionToId.get(Number(r.typeId)) !== "undefined" && this._currentMultiSelectionToId.get(Number(r.typeId)).size > 0){ - // if (inputValueArray.length > 1) { - // text += `

Updates to ${this._currentMultiSelectionToId.get(Number(r.typeId)).size} Localizations with Type ID: ${r.typeId}

` - // } - - if (r.values !== {}) { - for (let [name, value] of Object.entries(r.values)) { - text += `

- Updating attribute '${name}' to value: ${value}

` + // Inputs are associated to multiple types + // - If there are inputs for this types + // - currentMultiSelectionToId maps the selected IDs to the TypeId + // - There may be no selected items with that type + if (r.typeId !== "") { + // Are there any selected cards this MediaType? + // - Note: To handle if we put info in an input, but no media selected to apply it to + const mediaTypeInSelection = typeof this._currentMultiSelectionToId.get(Number(r.typeId)) !== "undefined" && this._currentMultiSelectionToId.get(Number(r.typeId)).size > 0; + console.log("Is this media type in the selection? ...... "+mediaTypeInSelection); + + // What are the inputes related to this type? + // - Note: To handle if we selected some media, but no input applies to it + const inputShownForSelectedType = Object.entries(r.values).length > 0; + // console.log("Are there input values? ...... "+inputShownForSelectedType); + + // We have inputs + if (inputShownForSelectedType) { + // and cards for this media + + if (mediaTypeInSelection) { + text += `

Update summary for ${this._currentMultiSelectionToId.get(Number(r.typeId)).size} ${typeText} with Type ID: ${r.typeId}

`; + } + + for (let [name, value] of Object.entries(r.values)) { + if(mediaTypeInSelection) { + text += `

- Change attribute '${name}' to value: ${value}

` + } else { + // inputs and no cards + text += `

No update for Type ID: ${r.typeId} `; + text += `

- No items selected to change '${name}' to value: ${value}

`; + } } - let formDataForType = { - attributes: r.values, - ids: Array.from(this._currentMultiSelection) + + if (mediaTypeInSelection) { + // console.log("Making form data......."); + let formDataForType = { + attributes: r.values, + ids: Array.from(this._currentMultiSelectionToId.get(Number(r.typeId))) //Array.from(this._currentMultiSelection) + } + + formData.push(formDataForType) } - // console.log(`Form Data For Type ${r.typeId} :::::::::::::::::::::::::::::::::::::::::::::::::::::::::`); - console.log(formDataForType); - formData.push(formDataForType) } else { - return text += `

- No valid values to update

` + // no attribute, but cards are selected + if (mediaTypeInSelection) { + text += `

Update summary for ${this._currentMultiSelectionToId.get(Number(r.typeId)).size} ${typeText} with Type ID: ${r.typeId}

` + text += `

- Attribute does not exist on this type

`; + } else { + // no attribute and no cards -- do nothing + } } - + + + + // } else { + // return text += `

- No valid values to update

` + // } + if (r.rejected !== {}) { for (let rej of Object.entries(r.rejected)) { text += `

- Will not update attribute '${rej[0]}' - value is invalid, or null.

` } } - // } + } } + // console.log(`formData.length = ${formData.length}`) + // Save button is disabled if there are 0 total selected, so there should be formData - otherwise there was a bug if (formData.length == 0) { - return this.boxHelper._modalError("Error with update.", "Error"); + return this.boxHelper._modalError("Error with update: No selection found.", "Error"); } let buttonContinue = document.createElement("button"); @@ -554,63 +625,12 @@ export class GalleryBulkEdit extends TatorElement { let buttonExit = document.createElement("button"); buttonExit.setAttribute("class", "btn btn-charcoal btn-clear f1 text-semibold"); + let confirmTextExit = document.createTextNode("Exit Select Mode") buttonExit.appendChild(confirmTextExit); - button.addEventListener("click", (e) => { - e.preventDefault(); - this.boxHelper.modal._closeCallback(); - this._page.showDimmer(); - this._page.loading.showSpinner(); - let promise = Promise.resolve(); - let text = ""; - let errorText = ""; - let respCode = 0; - - for (let jsonData of formData) { - console.log(jsonData); - promise = promise.then(() => this._patchLocalizations(jsonData)).then((resp) => { - respCode = resp.status; - console.log(respCode); - return resp.json(); - }).then((data) => { - console.log("Then reading message"); - if (respCode == "200" ) { - text += `${data.message}

`; - this.updateSelectionObjects(jsonData); - } else { - errorText += `${data.message}

`; - // this.updateSelectionObjects(jsonData); - } - - }); - } - - return promise.then(() => { - console.log("Then clean up"); - this._editPanel.resetWidgets() - this.dispatchEvent(new CustomEvent("bulk-attributes-edited", { detail: { editedIds: this._currentMultiSelection, editedObjs: this._currentSelectionObjects } })); - this._clearSelection(); - this._page.loading.hideSpinner(); - this._page.hideDimmer(); - - if (errorText === "" && text !== "") { - this.boxHelper._modalSuccess(text); - } else if (errorText !== "" && text === "") { - this.boxHelper._modalError(errorText, "Error"); - } else if (errorText !== "" && text !== "") { - this.boxHelper._modalWarn(text+errorText); - } - - // }); - }).catch(err => { - this._clearSelection(); - this._page.loading.hideSpinner(); - this._page.hideDimmer(); - return this.boxHelper._modalError("Error with update: "+err); - }); - + this.handleEdit(e, formData); }); buttonContinue.addEventListener("click", (e) => { @@ -624,15 +644,29 @@ export class GalleryBulkEdit extends TatorElement { }); this.boxHelper._modalConfirm({ - "titleText" : `Confirm`, - "mainText" : text, - "buttonSave" : button, - "scroll" : false + "titleText": `Confirm`, + "mainText": text, + "buttonSave": button, + "scroll": false + }); + } + + _patchMedia(formData) { + return fetch(`/rest/Medias/${this._projectId}`, { + method: "PATCH", + mode: "cors", + credentials: "include", + body: JSON.stringify(formData), + headers: { + "X-CSRFToken": getCookie("csrftoken"), + "Accept": "application/json", + "Content-Type": "application/json" + } }); } _patchLocalizations(formData) { - return fetch(`/rest/Localizations/${this._page.projectId}`, { + return fetch(`/rest/Localizations/${this._projectId}`, { method: "PATCH", mode: "cors", credentials: "include", @@ -642,11 +676,137 @@ export class GalleryBulkEdit extends TatorElement { "Accept": "application/json", "Content-Type": "application/json" } - }); + }); + } + + handleEdit(e, formData) { + if (this._editType == "media") { + this._editMedia(e, formData); + } else { + this._editLocalization(e, formData); + } + } + + _editMedia(e, formData) { + // button.addEventListener("click", (e) => { + e.preventDefault(); + this.boxHelper.modal._closeCallback(); + this._page.showDimmer(); + this._page.loading.showSpinner(); + let promise = Promise.resolve(); + let text = ""; + let errorText = ""; + let respCode = 0; + + for (let jsonData of formData) { + // console.log("jsonData-----------------------------------------------------------"); + // console.log(jsonData); + + if (jsonData.attributes !== {}) { + promise = promise.then(() => this._patchMedia(jsonData)).then((resp) => { + respCode = resp.status; + // console.log(respCode); + return resp.json(); + }).then((data) => { + // console.log("Then reading message"); + if (respCode == "200") { + text += `${data.message}

`; + this.updateSelectionObjects(jsonData); + } else { + errorText += `${data.message}

`; + // this.updateSelectionObjects(jsonData); + } + + }); + } + } + + return promise.then(() => { + console.log("Then clean up"); + this._editPanel.resetWidgets() + this.dispatchEvent(new CustomEvent("bulk-attributes-edited", { detail: { editedIds: this._currentMultiSelection, editedObjs: this._currentSelectionObjects } })); + this._clearSelection(); + this._page.loading.hideSpinner(); + this._page.hideDimmer(); + + if (errorText === "" && text !== "") { + this.boxHelper._modalSuccess(text); + } else if (errorText !== "" && text === "") { + this.boxHelper._modalError(errorText, "Error"); + } else if (errorText !== "" && text !== "") { + this.boxHelper._modalError(`

${text}

${errorText}

`); + } + + // }); + }).catch(err => { + this._clearSelection(); + this._page.loading.hideSpinner(); + this._page.hideDimmer(); + return this.boxHelper._modalError("Error with update: " + err); + }); + + // }); + } + + _editLocalization(e, formData) { + // button.addEventListener("click", (e) => { + e.preventDefault(); + this.boxHelper.modal._closeCallback(); + this._page.showDimmer(); + this._page.loading.showSpinner(); + let promise = Promise.resolve(); + let text = ""; + let errorText = ""; + let respCode = 0; + + for (let jsonData of formData) { + // console.log(jsonData); + promise = promise.then(() => this._patchLocalizations(jsonData)).then((resp) => { + respCode = resp.status; + // console.log(respCode); + return resp.json(); + }).then((data) => { + // console.log("Then reading message"); + if (respCode == "200") { + text += `${data.message}

`; + this.updateSelectionObjects(jsonData); + } else { + errorText += `${data.message}

`; + // this.updateSelectionObjects(jsonData); + } + + }); + } + + return promise.then(() => { + // console.log("Then clean up"); + this._editPanel.resetWidgets() + this.dispatchEvent(new CustomEvent("bulk-attributes-edited", { detail: { editedIds: this._currentMultiSelection, editedObjs: this._currentSelectionObjects } })); + this._clearSelection(); + this._page.loading.hideSpinner(); + this._page.hideDimmer(); + + if (errorText === "" && text !== "") { + this.boxHelper._modalSuccess(text); + } else if (errorText !== "" && text === "") { + this.boxHelper._modalError(errorText, "Error"); + } else if (errorText !== "" && text !== "") { + this.boxHelper._modalError(`

${text}

${errorText}

`); + } + + // }); + }).catch(err => { + this._clearSelection(); + this._page.loading.hideSpinner(); + this._page.hideDimmer(); + return this.boxHelper._modalError("Error with update: " + err); + }); + + // }); } _updateShownAttributes({ typeId, values }) { - console.log(values); + // console.log(values); this._editPanel.setSelectionBoxValue({ typeId, values }); // this._comparisonPanel.newColumns({ typeId, values }); } @@ -660,9 +820,13 @@ export class GalleryBulkEdit extends TatorElement { newCardData.attributes[a] = b; } } - console.log(newCardData); - this._page._filterResults.updateCardData(newCardData); - this._page.cardData.updateBulkCache(newCardData); + // console.log(newCardData); + this._gallery.updateCardData(newCardData); + if (this._page.cardData) { + this._page.cardData.updateBulkCache(newCardData); + } else if (this._page._mediaSection) { + this._page._mediaSection.reload() + } } else { console.warn("Possibly an error with save. Could not find ID in currentSelectionObjects.") } @@ -706,7 +870,7 @@ export class GalleryBulkEdit extends TatorElement { } async _prefetch() { - console.log("PREFETCH"); + // console.log("PREFETCH"); // let condition = new FilterConditionData("", "results", "==", "true", "CACHED"); // this._page._filterView.addCachedPill(condition); @@ -718,7 +882,7 @@ export class GalleryBulkEdit extends TatorElement { if (!this._editMode) this.startEditMode(); } - prefetchWarning() { + prefetchWarning() { // if (this._editMode) { // // this._editPanel._warningShow(); // let buttonContinue = document.createElement("button") @@ -748,6 +912,6 @@ export class GalleryBulkEdit extends TatorElement { // this.boxHelper._modalWarningConfirm(`Current search results are filtered on ${names} attribute. Editing may change pagination.`, buttonExit, buttonContinue); // } } - + } customElements.define("entity-gallery-bulk-edit", GalleryBulkEdit); diff --git a/ui/src/js/components/entity-gallery/entity-gallery-card.js b/ui/src/js/components/entity-gallery/entity-gallery-card.js index b5497e7a3..dedec37dd 100644 --- a/ui/src/js/components/entity-gallery/entity-gallery-card.js +++ b/ui/src/js/components/entity-gallery/entity-gallery-card.js @@ -1,388 +1,878 @@ import { TatorElement } from "../tator-element.js"; +import { svgNamespace } from "../tator-element.js"; +import { hasPermission } from "../../util/has-permission.js"; +import { getCookie } from "../../util/get-cookie.js"; +import { fetchRetry } from "../../util/fetch-retry.js"; import Spinner from "../../../images/spinner-transparent.svg"; +import LiveThumb from "../../../images/live-thumb.png"; export class EntityCard extends TatorElement { - constructor() { - super(); - - // Entity Card - - // - @this._name Title text - // - Card is list element; Parent can be UL element, or see: EntityCardGallery - // - Card links out to one destination - // Optional: - // - @this.descDiv Div can contain anything else, desc or other (HIDDEN) - // - @this._ext Detail text - // - @this._pos_text Pagination position text - // - @this._more Menu (HIDDEN) - // - @this.getAttribute('thumb-gif') Gif for hover effect - - // List element (card) - this._li = document.createElement("li"); - this._li.setAttribute("class", "entity-card rounded-2 clickable"); - this._shadow.appendChild(this._li); - - // Link - this._link = document.createElement("a"); - this._link.setAttribute("class", "entity-card__link file__link d-flex flex-items-center text-white"); - this._link.setAttribute("href", "#"); - this._li.appendChild(this._link); - - // Image, spinner until SRC set - this._img = document.createElement("img"); - this._img.setAttribute("src", Spinner); - this._img.setAttribute("class", "entity-card__image rounded-1"); - this._link.appendChild(this._img); - - // containing div for li element (styling) - this._styledDiv = document.createElement("div"); - this._styledDiv.setAttribute("class", "entity-card__title__container py-2 px-2 lh-default"); - this._li.appendChild(this._styledDiv); - - // Title Div - this.titleDiv = document.createElement("div"); - this.titleDiv.setAttribute("class", "entity-card__title py-1"); - this._styledDiv.appendChild(this.titleDiv); - this.titleDiv.hidden = true; - - // Text for Title Div - this._name = document.createElement("a"); - this._name.setAttribute("class", "text-semibold text-white css-truncate"); - this._name.setAttribute("href", "#"); - this.titleDiv.appendChild(this._name); - - // OPTIONAL Description Div - this.descDiv = document.createElement("div"); - this.descDiv.setAttribute("class", "entity-card__description py-1 f2"); - this._styledDiv.appendChild(this.descDiv); - this.descDiv.hidden = true; // HIDDEN default - - // "More" (three dots) menu (OPTIONAL) - this._more = document.createElement("media-more"); - this._more.setAttribute("class", "entity-card__more position-relative"); + constructor() { + super(); + + // List element (card) + this._li = document.createElement("li"); + this._li.setAttribute("class", "entity-card rounded-2 clickable"); + this._shadow.appendChild(this._li); + + // Link + this._link = document.createElement("a"); + this._link.setAttribute("class", "entity-card__link file__link d-flex flex-items-center text-white"); + this._link.setAttribute("href", "#"); + this._li.appendChild(this._link); + + // Image, spinner until SRC set + this._img = document.createElement("img"); + this._img.setAttribute("src", Spinner); + this._img.setAttribute("class", "entity-card__image rounded-1"); + this._link.appendChild(this._img); + + // containing div for li element (styling) + this._styledDiv = document.createElement("div"); + this._styledDiv.setAttribute("class", "entity-card__title__container py-2 px-2 lh-default"); + this._li.appendChild(this._styledDiv); + + // Title Div + this.titleDiv = document.createElement("div"); + this.titleDiv.setAttribute("class", "entity-card__title py-1 d-flex flex-justify-between"); + this._styledDiv.appendChild(this.titleDiv); + this.titleDiv.hidden = true; + + // Section title - h2 + this._title = document.createElement("h2"); + this._title.setAttribute("class", "section__name text-hover-white text-gray py-1 px-1 css-truncate"); + this._link.appendChild(this._title); + + // Text for Title Div + this._name = document.createElement("a"); + this._name.setAttribute("class", "text-semibold text-white css-truncate"); + // this._name.setAttribute("href", "#"); + this.titleDiv.appendChild(this._name); + + // OPTIONAL Description Div + this.descDiv = document.createElement("div"); + this.descDiv.setAttribute("class", "entity-card__description py-1 f2"); + this._styledDiv.appendChild(this.descDiv); + this.descDiv.hidden = true; // HIDDEN default + + // "More" (three dots) menu (OPTIONAL) + this._more = document.createElement("media-more"); + this._more.setAttribute("class", "entity-card__more text-right "); + this._more.style.opacity = 0; + this.titleDiv.appendChild(this._more); + this._more.hidden = true; // HIDDEN default + + + + + // Lower div start + const lowerDiv = document.createElement("div"); + lowerDiv.setAttribute("class", ""); + this._styledDiv.appendChild(lowerDiv); + + const durationDiv = document.createElement("div"); + durationDiv.setAttribute("class", "d-flex flex-items-center"); + lowerDiv.appendChild(durationDiv); + + this._duration = document.createElement("span"); + this._duration.setAttribute("class", "f3 text-gray duration"); + durationDiv.appendChild(this._duration); + + // OPTIONAL bottom (contains pagination + id display) + this._bottom = document.createElement("div"); + this._bottom.setAttribute("class", "f3 d-flex flex-justify-between"); + this._styledDiv.appendChild(this._bottom); + + // OPTIONAL Detail text (ie file extension) + this._ext = document.createElement("span"); + this._ext.setAttribute("class", "f3 text-gray"); + this._ext.hidden = true; + this._bottom.appendChild(this._ext); + + // OPTIONAL Pagination position + this._pos_text = document.createElement("span"); + this._pos_text.setAttribute("class", "f3 text-gray pr-2"); + this._bottom.appendChild(this._pos_text); + + // Emblem div + this._emblemDiv = document.createElement("div"); + this._emblemDiv.setAttribute("class", "d-flex flex-items-center"); + lowerDiv.appendChild(this._emblemDiv); + + // Attachment button & emblem code + this._attachmentButton = document.createElement("button"); + this._attachmentButton.setAttribute("class", "px-1 btn-clear h2 text-gray hover-text-white"); + this._attachmentButton.style.display = "none"; + this._emblemDiv.appendChild(this._attachmentButton); + + var svg = document.createElementNS(svgNamespace, "svg"); + svg.setAttribute("width", "14"); + svg.setAttribute("height", "14"); + svg.setAttribute("viewBox", "0 0 24 24"); + svg.setAttribute("stroke", "currentColor"); + svg.setAttribute("stroke-width", "2"); + svg.setAttribute("stroke-linecap", "round"); + svg.setAttribute("stroke-linejoin", "round"); + svg.style.fill = "none"; + this._attachmentButton.appendChild(svg); + + const path = document.createElementNS(svgNamespace, "path"); + path.setAttribute("d", "M21.44 11.05l-9.19 9.19a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66l-9.2 9.19a2 2 0 0 1-2.83-2.83l8.49-8.48"); + svg.appendChild(path); + + let archiveSvg = ``; + let archiveUpSvg = ``; + let archiveDownSvg = ``; + this._archiveEmblem = document.createElement("button"); + this._archiveEmblem.setAttribute("class", "px-1 btn-clear h2 text-gray hover-text-white d-flex"); + this._archiveEmblem.innerHTML = archiveSvg; + this._archiveEmblem.style.display = "none"; + this._emblemDiv.appendChild(this._archiveEmblem); + + this._archiveUpEmblem = document.createElement("button"); + this._archiveUpEmblem.setAttribute("class", "px-1 btn-clear h2 text-gray hover-text-white d-flex"); + this._archiveUpEmblem.innerHTML = archiveSvg + archiveUpSvg; + this._archiveUpEmblem.style.display = "none"; + this._emblemDiv.appendChild(this._archiveUpEmblem); + + this._archiveDownEmblem = document.createElement("button"); + this._archiveDownEmblem.setAttribute("class", "px-1 btn-clear h2 text-gray hover-text-white d-flex"); + this._archiveDownEmblem.innerHTML = archiveSvg + archiveDownSvg; + this._archiveDownEmblem.style.display = "none"; + this._emblemDiv.appendChild(this._archiveDownEmblem); + + // OPTIONAL ID data + this._id_text = document.createElement("span"); + this._id_text.setAttribute("class", "f3 text-gray px-2"); + this._bottom.appendChild(this._id_text); + + // More menu event listener (if included) + this.addEventListener("mouseenter", () => { + this._more.style.opacity = 1; + }); + + this.addEventListener("mouseleave", () => { this._more.style.opacity = 0; - this.titleDiv.appendChild(this._more); - this._more.hidden = true; // HIDDEN default - - // OPTIONAL pagination + id display - this._bottom = document.createElement("div"); - this._bottom.setAttribute("class", "f3 d-flex flex-justify-between"); - this._styledDiv.appendChild(this._bottom); - - // OPTIONAL Detail text (ie file extension) - this._ext = document.createElement("span"); - this._ext.setAttribute("class", "f3 text-gray"); - this._ext.hidden = true; - this._bottom.appendChild(this._ext); - - // OPTIONAL Pagination position - this._pos_text = document.createElement("span"); - this._pos_text.setAttribute("class", "f3 text-gray pr-2"); - - this._bottom.appendChild(this._pos_text); - - // OPTIONAL ID data - this._id_text = document.createElement("span"); - this._id_text.setAttribute("class", "f3 text-gray px-2"); - this._bottom.appendChild(this._id_text); - - // More menu styling (if included) - this.addEventListener("mouseenter", () => { - this._more.style.opacity = 1; - }); + }); + + + + this._more.addEventListener("algorithmMenu", evt => { + this.dispatchEvent( + new CustomEvent("runAlgorithm", + { + composed: true, + detail: { + algorithmName: evt.detail.algorithmName, + mediaIds: [Number(this.getAttribute("media-id"))], + projectId: this._more._project.id, + } + })); + }); + + this._more.addEventListener("annotations", evt => { + this.dispatchEvent(new CustomEvent("downloadAnnotations", { + detail: { + mediaIds: this.getAttribute("media-id"), + annotations: true + }, + composed: true + })); + }); - this.addEventListener("mouseleave", () => { - this._more.style.opacity = 0; + this._more.addEventListener("rename", evt => { + const input = document.createElement("input"); + input.setAttribute("class", "form-control input-sm1 f1"); + input.setAttribute("value", this._name.textContent); + titleDiv.replaceChild(input, this._name); + input.addEventListener("focus", evt => { + evt.target.select(); }); + input.addEventListener("keydown", evt => { + if (evt.keyCode == 13) { + evt.preventDefault(); + input.blur(); + } + }); + input.addEventListener("blur", evt => { + if (evt.target.value !== "") { + this._name.textContent = evt.target.value; + const full = evt.target.value + this._ext.textContent; + this._li.setAttribute("title", full); + } + fetch("/rest/Media/" + this.getAttribute("media-id"), { + method: "PATCH", + credentials: "same-origin", + headers: { + "X-CSRFToken": getCookie("csrftoken"), + "Accept": "application/json", + "Content-Type": "application/json" + }, + body: JSON.stringify({ + name: `${this._name.textContent}.${this._ext.textContent}`, + }), + }) + .catch(err => console.error("Failed to change name: " + err)); + titleDiv.replaceChild(this._name, evt.target); + }); + input.focus(); + }); - this.addEventListener("click", this.togglePanel.bind(this) ); + this._more.addEventListener("delete", evt => { + this.dispatchEvent(new CustomEvent("deleteFile", { + detail: { + mediaId: this.getAttribute("media-id"), + mediaName: this._name.textContent + }, + composed: true + })); + }); - // prep this var - this._tmpHidden = null; - this.attributeDivs = {}; - this._currentShownAttributes = ""; - this.multiEnabled = false; - this._multiSelectionToggle = false; - - /* Holds attributes for the card */ - this.attributesDiv = document.createElement('div'); - /* Sends events related to selection clicks */ - this.addEventListener('contextmenu', this.contextMenuHandler.bind(this)); - } - - static get observedAttributes() { - return ["thumb", "thumb-gif", "name", "processing", "pos-text"]; - } - - attributeChangedCallback(name, oldValue, newValue) { - switch (name) { - case "thumb": - if (this._thumb != newValue) { - this._img.setAttribute("src", newValue); - this._img.onload = () => {this.dispatchEvent(new Event("loaded"))}; - this._thumb = newValue; - } - break; - case "thumb-gif": - if (this._thumbGif != newValue) { - this._thumbGif = newValue; - this._li.addEventListener("mouseenter", () => { - if (this.hasAttribute("thumb-gif")) { - this._img.setAttribute("src", this.getAttribute("thumb-gif")); - } - }); - this._li.addEventListener("mouseleave", () => { - if (this.hasAttribute("thumb")) { - this._img.setAttribute("src", this.getAttribute("thumb")); - } - }); - } - break; - case "name": - const dot = Math.max(0, newValue.lastIndexOf(".") || Infinity); - const ext = newValue.slice(dot + 1); - this._ext.textContent = ext.toUpperCase(); - this._name.textContent = newValue.slice(0, dot); - this._li.setAttribute("title", newValue); - this._more.setAttribute("name", newValue); - break; - case "processing": - if (newValue === null) { - this._more.removeAttribute("processing"); - } else { - this._more.setAttribute("processing", ""); - } - break; - case "pos-text": - this._pos_text.textContent = newValue; + // Attachment button listener + this._attachmentButton.addEventListener("click", () => { + this.dispatchEvent(new CustomEvent("attachments", { + composed: true, + detail: this._attachments, + })); + }); + + + // Card click / List item click listener + this.addEventListener("click", this.togglePanel.bind(this)); + this._link.addEventListener("click", (e) => { + if (this._multiEnabled) { + e.preventDefault(); + } + }); + this._name.addEventListener("click", (e) => { + if (this._multiEnabled) { + e.preventDefault(); } + }); + + // prep this var + this._tmpHidden = null; + this.attributeDivs = {}; + this._currentShownAttributes = ""; + this.multiEnabled = false; + this._multiSelectionToggle = false; + + /* Holds attributes for the card */ + this.attributesDiv = document.createElement('div'); + + /* Sends events related to selection clicks */ + this.addEventListener('contextmenu', this.contextMenuHandler.bind(this)); + + // + this._sectionInit = false; + this._mediaInit = false; + } + + set multiEnabled(val) { + // console.log("multiEnabled set..."+val) + this._multiEnabled = val; + this._multiSelectionToggle = val; + + if (val) { + this._li.classList.add("multi-select"); + } else { + this._li.classList.remove("multi-select"); + } + } + + static get observedAttributes() { + return ["thumb", "thumb-gif", "name", "processing", "pos-text", "duration"]; + } + + attributeChangedCallback(name, oldValue, newValue) { + switch (name) { + case "thumb": + if (this._thumb != newValue) { + this._img.setAttribute("src", newValue); + this._img.onload = () => { this.dispatchEvent(new Event("loaded")) }; + this._thumb = newValue; + } + break; + case "thumb-gif": + if (this._thumbGif != newValue) { + this._thumbGif = newValue; + this._li.addEventListener("mouseenter", () => { + if (this.hasAttribute("thumb-gif")) { + this._img.setAttribute("src", this.getAttribute("thumb-gif")); + } + }); + this._li.addEventListener("mouseleave", () => { + if (this.hasAttribute("thumb")) { + this._img.setAttribute("src", this.getAttribute("thumb")); + } + }); + } + break; + case "name": + const dot = Math.max(0, newValue.lastIndexOf(".") || Infinity); + const ext = newValue.slice(dot + 1); + this._ext.textContent = ext.toUpperCase(); + this._name.textContent = newValue.slice(0, dot); + this._li.setAttribute("title", newValue); + this._more.setAttribute("name", newValue); + break; + case "processing": + if (newValue === null) { + this._more.removeAttribute("processing"); + } else { + this._more.setAttribute("processing", ""); + } + break; + case "duration": + if (newValue !== null && newValue !== 'null') { // + this._duration.textContent = newValue; + } else { + this._duration.textContent = ""; + } + case "pos-text": + this._pos_text.textContent = newValue; } - - + } + + - init({ obj, panelContainer, cardLabelsChosen, enableMultiselect = false, idx = null }) { + init({ obj, panelContainer = null, cardLabelsChosen = null, enableMultiselect = false, idx = null, mediaInit = false }) { // Give card access to panel this.panelContainer = panelContainer; this.cardObj = obj; this.multiEnabled = enableMultiselect; this._idx = idx; + this._mediaInit = mediaInit; if (this._idx !== null) { - console.log(`Tab index ${this._idx}`); - this._li.setAttribute("tabindex", this._idx ) + // console.log(`Tab index ${this._idx}`); + this._li.setAttribute("tabindex", this._idx) } - + + if (!mediaInit) { // ID is title this._id_text.innerHTML = `ID: ${this.cardObj.id}`; - - // Graphic - if(typeof this.cardObj.image !== "undefined" && this.cardObj.image !== null) { - //this.setAttribute("thumb", obj.image); - this.setImageStatic(obj.image); - } else if(typeof obj.graphic !== "undefined" && obj.graphic !== null) { - this.reader = new FileReader(); - this.reader.readAsDataURL(obj.graphic); // converts the blob to base64 - this.reader.addEventListener("load", this._setImgSrc.bind(this)); - } else { - //this.setAttribute("thumb", Spinner); - this.setImageStatic(Spinner); - } - + } + + + // Graphic + if (typeof this.cardObj.image !== "undefined" && this.cardObj.image !== null) { + //this.setAttribute("thumb", obj.image); + this.setImageStatic(obj.image); + } else if (typeof obj.graphic !== "undefined" && obj.graphic !== null) { + this.reader = new FileReader(); + this.reader.readAsDataURL(obj.graphic); // converts the blob to base64 + this.reader.addEventListener("load", this._setImgSrc.bind(this)); + } else if (!mediaInit) { + //this.setAttribute("thumb", Spinner); + this.setImageStatic(Spinner); + } + + if (obj.posText) { // Add position text related to pagination this.setAttribute("pos-text", obj.posText); - - - /** - * Attributes hidden on card are controlled by outer menu - */ + } + + + /** + * Attributes hidden on card are controlled by outer menu + */ if (obj.attributeOrder && obj.attributeOrder.length > 0) { - // Clear this in case of reuse / re-init - this.attributesDiv.innerHTML = ""; - for(const attr of obj.attributeOrder){ - let attrStyleDiv = document.createElement("div"); - attrStyleDiv.setAttribute("class", `entity-gallery-card__attribute`); - - let attrLabel = document.createElement("span"); - attrLabel.setAttribute("class", "f3 text-gray text-normal"); - attrStyleDiv.appendChild(attrLabel); - - let key = attr.name; - if(obj.attributes !== null && typeof obj.attributes[key] !== "undefined" && obj.attributes[key] !== null && obj.attributes[key] !== ""){ - attrLabel.appendChild( document.createTextNode(`${obj.attributes[key]}`) ); + // console.log("Setting up labels on card with this data:"); + // console.log(obj); + // Clear this in case of reuse / re-init + this.attributesDiv.innerHTML = ""; + for (const attr of obj.attributeOrder) { + let attrStyleDiv = document.createElement("div"); + attrStyleDiv.setAttribute("class", `entity-gallery-card__attribute`); + + let attrLabel = document.createElement("span"); + attrLabel.setAttribute("class", "f3 text-gray text-normal"); + attrStyleDiv.appendChild(attrLabel); + + let key = attr.name; + if (obj.attributes !== null && typeof obj.attributes[key] !== "undefined" && obj.attributes[key] !== null && obj.attributes[key] !== "") { + attrLabel.appendChild(document.createTextNode(`${obj.attributes[key]}`)); + } else { + attrLabel.innerHTML = `<not set>`; + } + + // add to the card & keep a list + this.attributeDivs[key] = {}; + this.attributeDivs[key].div = attrStyleDiv; + this.attributeDivs[key].value = attrLabel; + + if (cardLabelsChosen && Array.isArray(cardLabelsChosen) && cardLabelsChosen.length > 0) { + // If we have any preferences saved check against it + if (cardLabelsChosen.indexOf(key) > -1) { + // console.log("FOUND "+key+" at index "+cardLabelsChosen.indexOf(key)); } else { - attrLabel.innerHTML =`<not set>`; + attrStyleDiv.classList.add("hidden"); } - - // add to the card & keep a list - this.attributeDivs[key] = {}; - this.attributeDivs[key].div = attrStyleDiv; - this.attributeDivs[key].value = attrLabel; - - if(cardLabelsChosen && Array.isArray(cardLabelsChosen) && cardLabelsChosen.length > 0){ - // If we have any preferences saved check against it - if(cardLabelsChosen.indexOf(key) > -1) { - // console.log("FOUND "+key+" at index "+cardLabelsChosen.indexOf(key)); - } else { - attrStyleDiv.classList.add("hidden"); - } - } - - this.attributesDiv.appendChild(attrStyleDiv); + } else { + attrStyleDiv.classList.add("hidden"); } - - if(this.attributeDivs){ - // Show description div - this.descDiv.appendChild(this.attributesDiv); - this.descDiv.hidden = false; + + this.attributesDiv.appendChild(attrStyleDiv); + } + + if (this.attributeDivs) { + // Show description div + this.descDiv.appendChild(this.attributesDiv); + this.descDiv.hidden = false; + } + } + } + + /** + * Custom label display update + */ + _updateShownAttributes(evt) { + // console.log("_updateShownAttributes, evt.detail.value="); + // console.log(evt.detail); + // console.log(this.cardObj); + let labelValues = evt.detail.value; + + if (this.attributeDivs && evt.detail.typeId === this.cardObj.entityType.id) { + // show selected + for (let [key, value] of Object.entries(this.attributeDivs)) { + if (labelValues.includes(key)) { + value.div.classList.remove("hidden"); + } else { + value.div.classList.add("hidden"); } } } - - /** - * Custom label display update - */ - _updateShownAttributes(evt){ - let labelValues = evt.detail.value; - - if(this.attributeDivs){ - // show selected - for (let [key, value] of Object.entries(this.attributeDivs)) { - if(labelValues.includes(key)){ - value.div.classList.remove("hidden"); + } + + /** + * Update Attribute Values + * - If side panel is edited the card needs to update attributes + */ + _updateAttributeValues(data) { + if (data.entityType.id == this.cardObj.entityType.id) { + for (let [attr, value] of Object.entries(data.attributes)) { + if (typeof this.attributeDivs[attr] !== "undefined") { + if (this.attributeDivs[attr] != null) { + this.attributeDivs[attr].value.innerHTML = value; } else { - value.div.classList.add("hidden"); + this.attributeDivs[attr].value.innerHTML = `<not set>`; } - } + } } } - - /** - * Update Attribute Values - * - If side panel is edited the card needs to update attributes - */ - _updateAttributeValues(data) { - for (let [attr, value] of Object.entries(data.attributes)) { - if(this.attributeDivs[attr] != null){ - this.attributeDivs[attr].value.innerHTML = value; - } else { - attrLabel.innerHTML =`<not set>`; - } + } + + set posText(val) { + this.setAttribute("pos-text", val); + } + + set active(enabled) { + if (enabled) { + this._li.classList.add("is-active"); + } else { + this._li.classList.remove("is-active"); + } + } + + set project(val) { + if (!hasPermission(val.permission, "Can Edit")) { + this._more.style.display = "none"; + } + this._more.project = val; + } + + set algorithms(val) { + this._more.algorithms = val; + } + + set mediaParams(val) { + this._mediaParams = val; + } + + set media(val) { + this._media = val; + this._more.media = val; + let valid = false; + if (this._media.media_files) { + if ('streaming' in this._media.media_files || + 'layout' in this._media.media_files || + 'image' in this._media.media_files) { + valid = true; + } + if (!('thumbnail' in this._media.media_files) && 'live' in this._media.media_files) { + // Default to tator thumbnail + // TODO: Have some visual indication if stream is active. + this._img.setAttribute("src", LiveThumb); + } } - - set posText(val){ - this.setAttribute("pos-text", val); + if (valid == false) { + this._name.style.opacity = 0.35; + this._link.style.opacity = 0.35; + this._name.style.cursor = "not-allowed"; + this._link.style.cursor = "not-allowed"; } - - /** - * Set the card's main image thumbnail - * @param {image} image - */ - setImage(image) { - this.reader = new FileReader(); - this.reader.readAsDataURL(image); // converts the blob to base64 - this.reader.addEventListener("load", this._setImgSrcReader.bind(this)); + else { + let project = val.project; + if (typeof (val.project) == "undefined") { + project = val.project_id; + } + var uri = `/${project}/annotation/${val.id}?${this._mediaParams.toString()}`; + this._name.setAttribute("href", uri); + this._link.setAttribute("href", uri); + this._name.style.opacity = 1; + this._link.style.opacity = 1; + this._name.style.cursor = "pointer"; + this._link.style.cursor = "pointer"; } - - _setImgSrcReader() { - this._img.setAttribute("src", this.reader.result); - this._img.onload = () => {this.dispatchEvent(new Event("loaded"))}; + + if (this._media.archive_state == "to_archive") { + this._archiveDownEmblem.style.display = "flex"; + this._archiveDownEmblem.setAttribute("tooltip", "Pending Archival"); } - - setImageStatic(image) { - //this.setAttribute("thumb", image); - this._img.setAttribute("src", image); - this.cardObj.image = image; - this._img.onload = () => {this.dispatchEvent(new Event("loaded"))}; + else if (this._media.archive_state == "archived") { + this._archiveEmblem.style.display = "flex"; + this._archiveEmblem.setAttribute("tooltip", "Archived"); } - - togglePanel(e){ + else if (this._media.archive_state == "to_live") { + this._archiveUpEmblem.style.display = "flex"; + this._archiveUpEmblem.setAttribute("tooltip", "Pending Live"); + } + else { + this._archiveDownEmblem.style.display = "none"; + this._archiveUpEmblem.style.display = "none"; + this._archiveEmblem.style.display = "none"; + } + } + + get media() { + return this._media; + } + + set attachments(val) { + this._attachments = val; + if (val.length > 0) { + this._attachmentButton.style.display = "flex"; + } else { + this._attachmentButton.style.display = "none"; + } + } + + /** + * Set the card's main image thumbnail + * @param {image} image + */ + setImage(image) { + this.reader = new FileReader(); + this.reader.readAsDataURL(image); // converts the blob to base64 + this.reader.addEventListener("load", this._setImgSrcReader.bind(this)); + } + + _setImgSrcReader() { + this._img.setAttribute("src", this.reader.result); + this._img.onload = () => { this.dispatchEvent(new Event("loaded")) }; + } + + setImageStatic(image) { + //this.setAttribute("thumb", image); + this._img.setAttribute("src", image); + this.cardObj.image = image; + this._img.onload = () => { this.dispatchEvent(new Event("loaded")) }; + } + + togglePanel(e) { + // console.log("TOGGLE CARD ") + if (this._link.getAttribute("href") !== "#" && !this._multiEnabled) { + // follow the link... + // otherwise do some panel, or multi stuff + // console.log("clicked...."); + } else { e.preventDefault(); - if (this.multiEnabled) { + if (this._multiEnabled) { + this._multiSelectionToggle = true; + /* @ "card-click"*/ - if (e.shiftKey ) { - console.log("Shift click!"); - this._multiSelectionToggle = true; - this.dispatchEvent(new CustomEvent("shift-select", { detail: { element: this, id: this.cardObj.id, isSelected: this._li.classList.contains("is-selected") } })); //user is clicking specific cards - } + // if (e.shiftKey && !this._mediaInit) { + // // console.log("Shift click!"); + // // this._multiSelectionToggle = true; + // this.dispatchEvent(new CustomEvent("shift-select", { detail: { element: this, id: this.cardObj.id, isSelected: this._li.classList.contains("is-selected") } })); //user is clicking specific cards + // } else - if (e.code == "Enter" ) { - console.log("Enter click!... "+this._li.hasFocus()); + if (e.code == "Enter") { + // console.log("Enter click!... " + this._li.hasFocus()); if (this._li.hasFocus()) { // } - this._multiSelectionToggle = true; - this.dispatchEvent(new CustomEvent("shift-select", { detail: { element: this, id: this.cardObj.id, isSelected: this._li.classList.contains("is-selected") } })); //user is clicking specific cards - } - - if (e.ctrlKey || e.code == "Control") { + // this._multiSelectionToggle = true; + this.dispatchEvent(new CustomEvent("shift-select", { detail: { element: this, id: this.cardObj.id, isSelected: this._li.classList.contains("is-selected") } })); //user is clicking specific cards + } else if ((e.ctrlKey && !this._mediaInit) || (e.code == "Control" && !this._mediaInit)) { // usually context menu is hit, and not this keeping in case.... - this._multiSelectionToggle = true; - this.dispatchEvent(new CustomEvent("ctrl-select", { detail: { element : this, id: this.cardObj.id, isSelected: this._li.classList.contains("is-selected") } })); //user is clicking specific cards - } + // this._multiSelectionToggle = true; + this.dispatchEvent(new CustomEvent("ctrl-select", { detail: { element: this, id: this.cardObj.id, isSelected: this._li.classList.contains("is-selected") } })); //user is clicking specific cards + } else if (this._mediaInit) { + // console.log("this._li.classList.contains(is-selected .................................... "+this._li.classList.contains("is-selected")) + this.dispatchEvent(new CustomEvent("ctrl-select", { detail: { element: this, id: this.cardObj.id, isSelected: this._li.classList.contains("is-selected") } })); //user is clicking specific cards + } } - if(this._li.classList.contains("is-selected") && !this._multiSelectionToggle) { - this._deselectedCardAndPanel(); - } else if(!this._multiSelectionToggle){ + if (this._li.classList.contains("is-selected") && !this._multiSelectionToggle && !this._mediaInit && !this._sectionInit) { + this._deselectedCardAndPanel(); + } else if (!this._multiSelectionToggle && !this._mediaInit && !this._sectionInit) { this._selectedCardAndPanel(); } else { this.cardClickEvent(false); } } - - _unselectOpen() { - const cardId = this.panelContainer._panelTop._panel.getAttribute("selected-id"); - - // if it exists, close it! - if (!this._multiSelectionToggle) { - console.log("unselecting this cardId: "+cardId) - if(typeof cardId !== "undefined" && cardId !== null) { - let evt = new CustomEvent("unselected", { detail: { id: cardId } }); - this.panelContainer.dispatchEvent(evt); // this even unselected related card - } + } + + _unselectOpen() { + const cardId = this.panelContainer._panelTop._panel.getAttribute("selected-id"); + + // if it exists, close it! + if (!this._multiSelectionToggle) { + // console.log("unselecting this cardId: " + cardId) + if (typeof cardId !== "undefined" && cardId !== null) { + let evt = new CustomEvent("unselected", { detail: { id: cardId } }); + this.panelContainer.dispatchEvent(evt); // this even unselected related card } } - - _deselectedCardAndPanel(){ - this.cardClickEvent(false); - this._li.classList.remove("is-selected"); - this.annotationEvent("hide-annotation"); - } - + } + + _deselectedCardAndPanel() { + this.cardClickEvent(false); + this._li.classList.remove("is-selected"); + this.annotationEvent("hide-annotation"); + } + _selectedCardAndPanel() { // Hide open panels this._unselectOpen(); - this.cardClickEvent(true); - this.annotationEvent("open-annotation"); - this._li.classList.add("is-selected"); + this.cardClickEvent(true); + this.annotationEvent("open-annotation"); + this._li.classList.add("is-selected"); + } + + cardClickEvent(openFlag = false) { + // Send event to panel to hide the localization canvas & title + let cardClickEvent = new CustomEvent("card-click", { detail: { openFlag, cardObj: this.cardObj } }); + this.dispatchEvent(cardClickEvent); + } + + annotationEvent(evtName) { + // Send event to panel to hide the localization + let annotationEvent = new CustomEvent(evtName, { detail: { cardObj: this.cardObj } }); + this.panelContainer.dispatchEvent(annotationEvent); + } + + contextMenuHandler(e) { + if (e.ctrlKey && !this._mediaInit) { + // console.log("Card was clicked with ctrl"); + this._multiSelectionToggle = true; + e.preventDefault(); // stop contextmenu + // this.togglePanel(e); + this.dispatchEvent(new CustomEvent("ctrl-select", { detail: { element: this, id: this.cardObj.id, isSelected: this._li.classList.contains("is-selected") } })); //user is clicking specific cards + // this._li.classList.add("is-selected"); } - - cardClickEvent(openFlag = false){ - // Send event to panel to hide the localization canvas & title - let cardClickEvent = new CustomEvent("card-click", { detail : { openFlag, cardObj : this.cardObj } }); - this.dispatchEvent( cardClickEvent ); + } + + rename(name) { + this._title.textContent = name; + } + + sectionInit(section, sectionType) { + this._section = section; + this._sectionType = sectionType; + this._img.remove(); + this._styledDiv.remove(); + this._li.classList.add("section"); + this._li.classList.remove("entity-card"); + this._sectionInit = true; + + if (section === null) { + this._title.textContent = "All Media"; + } else { + this._title.textContent = section.name; } - - annotationEvent(evtName){ - // Send event to panel to hide the localization - let annotationEvent = new CustomEvent(evtName, { detail : { cardObj : this.cardObj } }); - this.panelContainer.dispatchEvent( annotationEvent ); + this._title.hidden = false; + + const svg = document.createElementNS(svgNamespace, "svg"); + svg.setAttribute("viewBox", "0 0 24 24"); + svg.setAttribute("height", "1em"); + svg.setAttribute("width", "1em"); + svg.setAttribute("fill", "none"); + svg.style.fill = "none"; + svg.setAttribute("stroke", "currentColor"); + svg.setAttribute("stroke-width", "2"); + svg.setAttribute("stroke-linecap", "round"); + svg.setAttribute("stroke-linejoin", "round"); + this._link.insertBefore(svg, this._title); + + // Null section means display all media. + if (section === null) { + const path = document.createElementNS(svgNamespace, "path"); + path.setAttribute("d", "M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"); + svg.appendChild(path); + + const poly = document.createElementNS(svgNamespace, "polyline"); + poly.setAttribute("points", "9 22 9 12 15 12 15 22"); + svg.appendChild(poly); } - - contextMenuHandler(e) { - if (e.ctrlKey) { - console.log("Card was clicked with ctrl"); - this._multiSelectionToggle = true; - e.preventDefault(); // stop contextmenu - // this.togglePanel(e); - this.dispatchEvent(new CustomEvent("ctrl-select", { detail: { element : this, id: this.cardObj.id, isSelected: this._li.classList.contains("is-selected") } })); //user is clicking specific cards - // this._li.classList.add("is-selected"); + if (sectionType == "folder") { + const path = document.createElementNS(svgNamespace, "path"); + path.setAttribute("d", "M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"); + svg.appendChild(path); + + const context = document.createElement("div"); + context.setAttribute("class", "more d-flex flex-column f2 px-3 py-2 lh-condensed"); + context.style.display = "none"; + this._shadow.appendChild(context); + + const toggle = document.createElement("toggle-button"); + if (this._section.visible) { + toggle.setAttribute("text", "Archive folder"); + } else { + toggle.setAttribute("text", "Restore folder"); } - } - + context.appendChild(toggle); + + toggle.addEventListener("click", evt => { + this._section.visible = !this._section.visible; + const sectionId = Number(); + fetch(`/rest/Section/${this._section.id}`, { + method: "PATCH", + credentials: "same-origin", + headers: { + "X-CSRFToken": getCookie("csrftoken"), + "Accept": "application/json", + "Content-Type": "application/json" + }, + body: JSON.stringify({ + "visible": this._section.visible, + }) + }) + .then(response => { + if (this._section.visible) { + toggle.setAttribute("text", "Archive folder"); + } else { + toggle.setAttribute("text", "Restore folder"); + } + this.dispatchEvent(new CustomEvent("visibilityChange", { + detail: { section: this._section } + })); + }); + }); + + this.addEventListener("contextmenu", evt => { + evt.preventDefault(); + context.style.display = "block"; + }); + + window.addEventListener("click", evt => { + context.style.display = "none"; + }); + } else if (sectionType == "savedSearch") { + const circle = document.createElementNS(svgNamespace, "circle"); + circle.setAttribute("cx", "11"); + circle.setAttribute("cy", "11"); + circle.setAttribute("r", "8"); + svg.appendChild(circle); + + const line = document.createElementNS(svgNamespace, "line"); + line.setAttribute("x1", "21"); + line.setAttribute("y1", "21"); + line.setAttribute("x2", "16.65"); + line.setAttribute("y2", "16.65"); + svg.appendChild(line); + } else if (sectionType == "bookmark") { + const path = document.createElementNS(svgNamespace, "path"); + path.setAttribute("d", "M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z"); + svg.appendChild(path); + this._link.setAttribute("href", section.uri); + + // Set up bookmark management controls. + const input = document.createElement("input"); + input.setAttribute("class", "form-control input-sm f1"); + input.style.display = "none"; + this._link.appendChild(input); + + const context = document.createElement("div"); + context.setAttribute("class", "more d-flex flex-column f2 px-3 py-2 lh-condensed"); + context.style.display = "none"; + this._shadow.appendChild(context); + + const rename = document.createElement("rename-button"); + rename.setAttribute("text", "Rename"); + context.appendChild(rename); + + const remove = document.createElement("delete-button"); + remove.init("Delete"); + context.appendChild(remove); + + this._link.addEventListener("contextmenu", evt => { + evt.preventDefault(); + context.style.display = "block"; + }); + + rename.addEventListener("click", () => { + // console.log("Rename event......"); + input.style.display = "block"; + this._link.style.pointerEvents = "none"; + this._title.style.display = "none"; + input.setAttribute("value", this._title.textContent); + input.focus(); + }); + + input.addEventListener("focus", evt => { + evt.target.select(); + }); + + input.addEventListener("keydown", evt => { + if (evt.keyCode == 13) { + evt.preventDefault(); + input.blur(); + } + }); + + input.addEventListener("blur", evt => { + if (evt.target.value !== "") { + this._title.textContent = evt.target.value; + this._link.style.pointerEvents = ""; + this._section.name = evt.target.value; + fetchRetry("/rest/Bookmark/" + this._section.id, { + method: "PATCH", + headers: { + "X-CSRFToken": getCookie("csrftoken"), + "Accept": "application/json", + "Content-Type": "application/json" + }, + body: JSON.stringify({ "name": evt.target.value }), + }); + } + input.style.display = "none"; + this._title.style.display = "block"; + }); + + remove.addEventListener("click", () => { + this.parentNode.removeChild(this); + fetchRetry("/rest/Bookmark/" + this._section.id, { + method: "DELETE", + headers: { + "X-CSRFToken": getCookie("csrftoken"), + "Accept": "application/json", + "Content-Type": "application/json" + }, + }); + }); + + window.addEventListener("click", evt => { + context.style.display = "none"; + }); + } } - - customElements.define("entity-card", EntityCard); + +} + +customElements.define("entity-card", EntityCard); diff --git a/ui/src/js/components/entity-gallery/entity-gallery-labels.js b/ui/src/js/components/entity-gallery/entity-gallery-labels.js index 4b260b3d0..db7fb35ba 100644 --- a/ui/src/js/components/entity-gallery/entity-gallery-labels.js +++ b/ui/src/js/components/entity-gallery/entity-gallery-labels.js @@ -67,8 +67,8 @@ export class EntityGalleryLabels extends TatorElement { async add({ typeData, hideTypeName = false, checkedFirst = null }) { // console.log(typeData); let typeName = typeData.name ? typeData.name : ""; - if(this._shownTypes[typeData.id]) { - // don't re-add this type... + if(this._shownTypes[typeData.id] || typeData.visible == false || typeData.attribute_types.length == 0) { + // don't re-add this type, or don't add if visible=false... return false; } else { this._shownTypes[typeData.id] = true; @@ -138,14 +138,20 @@ export class EntityGalleryLabels extends TatorElement { return labelsMain; } - _getValue(typeId){ - return this._selectionValues[typeId].getValue(); + _getValue(typeId) { + if (this._selectionValues[typeId]) { + return this._selectionValues[typeId].getValue(); + } else { + return []; + } } _setValue({ typeId, values }){ // # assumes values are in the accepted format for checkbox set // let valuesList = this._getValue(typeId); + console.log("valuesList"); + console.log(valuesList); for(let box in valuesList){ if(values.contains(box.name)){ box.checked = true; @@ -187,16 +193,17 @@ export class EntityGalleryLabels extends TatorElement { sorted.push(...hiddenAttrs); // Create an array for checkbox set el - let checked = checkedFirst == null ? false : checkedFirst; + // console.log("checkedFirst "+checkedFirst) + let checkedValue = checkedFirst == null ? false : checkedFirst; for (let attr of sorted) { this.newList.push({ id: encodeURI(attr.name), name: attr.name, - checked + checked: checkedValue }); // reset checked - only check the first one - if(checked) checked = false; + checkedValue = false; } return this.newList; } diff --git a/ui/src/js/components/entity-gallery/entity-gallery-slider.js b/ui/src/js/components/entity-gallery/entity-gallery-slider.js index 9bc21717e..dd7e1ef75 100644 --- a/ui/src/js/components/entity-gallery/entity-gallery-slider.js +++ b/ui/src/js/components/entity-gallery/entity-gallery-slider.js @@ -336,12 +336,12 @@ export class EntityGallerySlider extends TatorElement { */ this.entityType = (this.association == "Localization") ? cardObj.entityType : cardObj.mediaInfo.entityType; this.entityTypeId = this.entityType.id; - // this._cardAtributeLabels.add({ + // this._cardAttributeLabels.add({ // typeData: this.entityType, // checkedFirst: true // }); - this.gallery.cardLabelsChosenByType[this.entityTypeId] = this._cardAtributeLabels._getValue(this.entityTypeId); + this.gallery.cardLabelsChosenByType[this.entityTypeId] = this._cardAttributeLabels._getValue(this.entityTypeId); this._cardLabelOptions = this.entityType.attribute_types; this._cardLabelOptions.sort((a, b) => { @@ -367,7 +367,7 @@ export class EntityGallerySlider extends TatorElement { this._resizeCards._rangeHandler(resizeValue, this._ul); }); - this._cardAtributeLabels.addEventListener("labels-update", (evt) => { + this._cardAttributeLabels.addEventListener("labels-update", (evt) => { card._updateShownAttributes(evt); this.gallery.cardLabelsChosenByType[this.entityTypeId] = evt.detail.value; let msg = `Entry labels updated`; diff --git a/ui/src/js/components/entity-panel/entity-panel-container.js b/ui/src/js/components/entity-panel/entity-panel-container.js index a7ff4f6f7..d4929990b 100644 --- a/ui/src/js/components/entity-panel/entity-panel-container.js +++ b/ui/src/js/components/entity-panel/entity-panel-container.js @@ -15,19 +15,37 @@ export class EntityPanelContainer extends TatorElement { this.el = null; } - init({ main, aside, pageModal, modelData, gallery }) { + // + init({ position = "right", main, isMediaSection = false, aside, pageModal, modelData, gallery, contents }) { this.lside = main; this.rside = aside; this.gallery = gallery; + this.position = position; + this.contents = contents; // listener to close panelContainer - if (this.gallery._customContent) { + if (isMediaSection) { + //then we have the media section.... + // this._panelTop.init({ pageModal, modelData, panelContainer: this }); + this.open = true; + this._panelTop.init({ + panelContainer: this, + customContentHandler: this.gallery.customContentHandler, + isMediaSection: true, + contents: this.contents + }) + } else if (this.gallery._customContent) { this._panelTop.init({ pageModal, modelData, panelContainer: this, customContentHandler: this.gallery.customContentHandler}); } else { this._panelTop.init({ pageModal, modelData, panelContainer: this }); } - this._panelTop._topBarArrow.addEventListener("click", this._toggleRightOnClick.bind(this)); + // if (position == "right") { + // this._panelTop._topBarArrow.addEventListener("click", this._toggleLeftOnClick.bind(this)); + // } else { + this._panelTop._topBarArrow.addEventListener("click", this._toggleRightOnClick.bind(this)); + // } + // Check and set current permission level on annotationPanel if (this.hasAttribute("permissionValue")) { @@ -70,9 +88,17 @@ export class EntityPanelContainer extends TatorElement { this.lside.classList.add("col-9"); this.lside.classList.remove("col-12"); this.lside.style.marginRight = "0"; + + if (this.position == "left") { + this.lside.style.paddingLeft = "390px"; + this.gallery._main.classList.remove("ml-6"); + this.gallery._main.classList.add("ml-3"); + } else { + this.gallery._main.classList.remove("mr-6"); + this.gallery._main.classList.add("mr-3"); + } + - this.gallery._main.classList.remove("mr-6"); - this.gallery._main.classList.add("mr-3"); this._panelTop._topBarArrow.style.transform = "scaleX(1)"; this.open = true; @@ -88,8 +114,17 @@ export class EntityPanelContainer extends TatorElement { this.lside.classList.remove("col-9"); this.lside.style.marginRight = "2%"; - this.gallery._main.classList.add("mr-6"); - this.gallery._main.classList.remove("mr-3"); + if (this.position == "left") { + this.lside.style.paddingLeft = "2%"; + this.lside.style.marginRight = "0"; + this.gallery._main.classList.add("ml-6"); + this.gallery._main.classList.remove("ml-3"); + } else { + this.gallery._main.classList.add("mr-6"); + this.gallery._main.classList.remove("mr-3"); + } + + this.open = false; this._panelTop._topBarArrow.style.transform = "scaleX(-1)"; diff --git a/ui/src/js/components/entity-panel/entity-panel-top.js b/ui/src/js/components/entity-panel/entity-panel-top.js index 03dee22bc..bc21367b2 100644 --- a/ui/src/js/components/entity-panel/entity-panel-top.js +++ b/ui/src/js/components/entity-panel/entity-panel-top.js @@ -80,8 +80,15 @@ export class EntityGalleryPanelTop extends TatorElement { } } - init({ pageModal, modelData, panelContainer, customContentHandler = false }) { - if (!customContentHandler) { + init({ pageModal, modelData, panelContainer, customContentHandler = false, isMediaSection = false, contents }) { + if (isMediaSection) { + // no heading and no data handling within panel top + this._headingText.innerHTML = ""; + this._staticImage.innerHTML = ""; + this._topBarArrow.classList.add("left"); + this.openHandler = customContentHandler; + this._box.appendChild(contents); + } else if (!customContentHandler) { if (this._locImage == undefined) { this._locImage = document.createElement("entity-panel-localization"); this._box.insertBefore(this._locImage, this._staticImage); diff --git a/ui/src/js/project-detail/media-card.js b/ui/src/js/project-detail/__bak_media-card.js similarity index 79% rename from ui/src/js/project-detail/media-card.js rename to ui/src/js/project-detail/__bak_media-card.js index 06f8277db..b3a818035 100644 --- a/ui/src/js/project-detail/media-card.js +++ b/ui/src/js/project-detail/__bak_media-card.js @@ -13,7 +13,7 @@ export class MediaCard extends TatorElement { this._li.setAttribute("class", "project__file rounded-2"); this._shadow.appendChild(this._li); - this._link = document.createElement("a"); + this._link = document.createElement("div"); this._link.setAttribute("class", "file__link d-flex flex-items-center text-white"); this._link.setAttribute("href", "#"); this._li.appendChild(this._link); @@ -246,94 +246,7 @@ export class MediaCard extends TatorElement { } } - set project(val) { - if (!hasPermission(val.permission, "Can Edit")) { - this._more.style.display = "none"; - } - this._more.project = val; - } - - set algorithms(val) { - this._more.algorithms = val; - } - - set mediaParams(val) { - this._mediaParams = val; - } - - set media(val) { - this._media = val; - this._more.media = val; - let valid = false; - if (this._media.media_files) - { - if ('streaming' in this._media.media_files || - 'layout' in this._media.media_files || - 'image' in this._media.media_files) - { - valid = true; - } - if (!('thumbnail' in this._media.media_files) && 'live' in this._media.media_files) - { - // Default to tator thumbnail - // TODO: Have some visual indication if stream is active. - this._img.setAttribute("src", LiveThumb); - - } - } - if (valid == false) - { - this._name.style.opacity = 0.35; - this._link.style.opacity = 0.35; - this._name.style.cursor = "not-allowed"; - this._link.style.cursor = "not-allowed"; - } - else - { - let project = val.project; - if(typeof(val.project) == "undefined") { - project = val.project_id; - } - var uri = `/${project}/annotation/${val.id}?${this._mediaParams.toString()}`; - this._name.setAttribute("href", uri); - this._link.setAttribute("href", uri); - this._name.style.opacity = 1; - this._link.style.opacity = 1; - this._name.style.cursor = "pointer"; - this._link.style.cursor = "pointer"; - } - - if (this._media.archive_state == "to_archive") { - this._archiveDownEmblem.style.display = "flex"; - this._archiveDownEmblem.setAttribute("tooltip", "Pending Archival"); - } - else if (this._media.archive_state == "archived") { - this._archiveEmblem.style.display = "flex"; - this._archiveEmblem.setAttribute("tooltip", "Archived"); - } - else if (this._media.archive_state == "to_live") { - this._archiveUpEmblem.style.display = "flex"; - this._archiveUpEmblem.setAttribute("tooltip", "Pending Live"); - } - else { - this._archiveDownEmblem.style.display = "none"; - this._archiveUpEmblem.style.display = "none"; - this._archiveEmblem.style.display = "none"; - } - } - - get media() { - return this._media; - } - - set attachments(val) { - this._attachments = val; - if (val.length > 0) { - this._attachmentButton.style.display = "flex"; - } else { - this._attachmentButton.style.display = "none"; - } - } + } customElements.define("media-card", MediaCard); diff --git a/ui/src/js/project-detail/__bak_section-card.js b/ui/src/js/project-detail/__bak_section-card.js new file mode 100644 index 000000000..7254583d0 --- /dev/null +++ b/ui/src/js/project-detail/__bak_section-card.js @@ -0,0 +1,40 @@ +import { TatorElement } from "../components/tator-element.js"; +import { getCookie } from "../util/get-cookie.js"; +import { fetchRetry } from "../util/fetch-retry.js"; +import { svgNamespace } from "../components/tator-element.js"; + +export class SectionCard extends TatorElement { + constructor() { + super(); + + this._li = document.createElement("li"); + this._li.style.cursor = "pointer"; + this._li.setAttribute("class", "section d-flex flex-items-center flex-justify-between px-2 rounded-1"); + this._shadow.appendChild(this._li); + + this._link = document.createElement("a"); + this._link.setAttribute("class", "section__link d-flex flex-items-center text-gray"); + this._li.appendChild(this._link); + + this._title = document.createElement("h2"); + this._title.setAttribute("class", "section__name py-1 px-1 css-truncate"); + this._link.appendChild(this._title); + + } + + + + rename(name) { + this._title.textContent = name; + } + + set active(enabled) { + if (enabled) { + this._li.classList.add("is-active"); + } else { + this._li.classList.remove("is-active"); + } + } +} + +customElements.define("section-card", SectionCard); diff --git a/ui/src/js/project-detail/media-section.js b/ui/src/js/project-detail/media-section.js index 55cc4c398..53013754b 100644 --- a/ui/src/js/project-detail/media-section.js +++ b/ui/src/js/project-detail/media-section.js @@ -55,6 +55,10 @@ export class MediaSection extends TatorElement { this._more.setAttribute("class", "px-2"); actions.appendChild(this._more); + + this._hiddenMediaLabel = document.createElement("div"); + section.appendChild(this._hiddenMediaLabel); + this._defaultPageSize = 25; this._maxPageSizeDefault = 100; @@ -71,6 +75,7 @@ export class MediaSection extends TatorElement { this._files = document.createElement("section-files"); this._files.setAttribute("class", "col-12"); this._files.mediaParams = this._sectionParams.bind(this); + div.appendChild(this._files); this._paginator_bottom = document.createElement("entity-gallery-paginator"); @@ -83,6 +88,10 @@ export class MediaSection extends TatorElement { this._numFilesCount = 0; this._searchString = ""; + this._more.addEventListener("bulk-edit", () => { + this.dispatchEvent(new Event("bulk-edit")); + }); + this._setCallbacks(); } @@ -98,6 +107,8 @@ export class MediaSection extends TatorElement { this._section = section; this._sectionName = this._sectionName; this._files.setAttribute("project-id", project); + + this._nameText.nodeValue = this._sectionName; this._upload.setAttribute("project-id", project); this._upload.setAttribute("username", username); @@ -107,10 +118,16 @@ export class MediaSection extends TatorElement { this._start = 0; this._stop = this._paginator_top._pageSize; this._after = new Map(); + return this.reload(); } + set mediaTypesMap(val) { + this._mediaTypesMap = val; + this._files.mediaTypesMap = val; + } + set project(val) { this._files.project = val; if (!hasPermission(val.permission, "Can Edit")) { @@ -169,7 +186,7 @@ export class MediaSection extends TatorElement { } removeMedia(mediaId) { - for (const mediaCard of this._files._main.children) { + for (const mediaCard of this._ul._main.children) { if (mediaCard.getAttribute("media-id") == mediaId) { mediaCard.parentNode.removeChild(mediaCard); const numFiles = Number(this._numFiles.textContent.split(' ')[0]) - 1; @@ -289,7 +306,7 @@ export class MediaSection extends TatorElement { } reload() { - console.log("Reload media..."); + console.log("Reload media section..."); this._reload.busy(); const sectionQuery = this._sectionParams(); @@ -732,6 +749,9 @@ export class MediaSection extends TatorElement { this._stop = evt.detail.stop; this._paginationState = evt.detail; + // clear any selected cards + this._bulkEdit.clearAllCheckboxes(); + otherPaginator.init(otherPaginator._numFiles, this._paginationState); this._updatePageArgs(); diff --git a/ui/src/js/project-detail/project-detail.js b/ui/src/js/project-detail/project-detail.js index 1fcc7e584..30744ce3c 100644 --- a/ui/src/js/project-detail/project-detail.js +++ b/ui/src/js/project-detail/project-detail.js @@ -14,13 +14,58 @@ export class ProjectDetail extends TatorPage { window._uploader = new Worker(new URL("../tasks/upload-worker.js", import.meta.url)); - const main = document.createElement("main"); - main.setAttribute("class", "d-flex"); - this._shadow.appendChild(main); - + // Success and warning Utility hooks + const utilitiesDiv = document.createElement("div"); + this._headerDiv = this._header._shadow.querySelector("header"); + utilitiesDiv.setAttribute("class", "annotation__header d-flex flex-items-center flex-justify-between px-6 f3"); + const user = this._header._shadow.querySelector("header-user"); + user.parentNode.insertBefore(utilitiesDiv, user); + + this._lightSpacer = document.createElement("span"); + this._lightSpacer.style.width = "32px"; + utilitiesDiv.appendChild(this._lightSpacer); + + this._success = document.createElement("success-light"); + this._lightSpacer.appendChild(this._success); + + this._warning = document.createElement("warning-light"); + this._lightSpacer.appendChild(this._warning); + + // Wrapper to allow r.side bar to slide into left + this.mainWrapper = document.createElement("div"); + this.mainWrapper.setAttribute("class", "analysis--main--wrapper col-12 d-flex"); + // this.mainWrapper.setAttribute("style", "padding-left: 25%;"); + this._shadow.appendChild(this.mainWrapper); + + // Original main element + this.main = document.createElement("main"); + this.main.setAttribute("class", "d-flex col-9"); + this.main.setAttribute("style", "padding-left: 390px;"); + this.mainWrapper.appendChild(this.main); + + // // Panel top bar + // const sectionContainer = document.createElement("entity-panel-container"); + // sectionContainer.setAttribute("class", ""); + // this.main.appendChild(sectionContainer); + + + // + /* LEFT*** Navigation Pane - Project Detail Viewer */ + this.aside = document.createElement("aside"); + this.aside.setAttribute("class", "entity-panel--container-left col-3"); //slide-close + this.aside.hidden = true; + this.mainWrapper.appendChild(this.aside); + + // Gallery navigation panel + this._panelContainer = document.createElement("entity-panel-container"); + this.aside.appendChild(this._panelContainer); + + // const section = document.createElement("section"); - section.setAttribute("class", "sections-wrap py-6 px-5 col-3 text-gray"); - main.appendChild(section); + section.setAttribute("class", "sections-wrap py-6 col-3 px-5 text-gray"); // + + // Content for panel is appended in panel code + // this._panelContainer._panelTop._shadow.appendChild(section); const folderHeader = document.createElement("div"); folderHeader.setAttribute("class", "d-flex flex-justify-between flex-items-center py-4"); @@ -101,13 +146,16 @@ export class ProjectDetail extends TatorPage { this._bookmarks.setAttribute("class", "sections"); section.appendChild(this._bookmarks); - const mainSection = document.createElement("section"); - mainSection.setAttribute("class", "project__main py-3 px-6 flex-grow"); - main.appendChild(mainSection); + this._mainSection = document.createElement("section"); + this._mainSection.setAttribute("class", "py-3 px-6 flex-grow"); //project__main + this.main.appendChild(this._mainSection); + + this.gallery = {}; + this.gallery._main = this._mainSection; const div = document.createElement("div"); div.setAttribute("class", "py-6"); - mainSection.appendChild(div); + this.gallery._main.appendChild(div); const header = document.createElement("div"); header.setAttribute("class", "main__header d-flex flex-justify-between"); @@ -155,15 +203,18 @@ export class ProjectDetail extends TatorPage { const subheader = document.createElement("div"); subheader.setAttribute("class", "d-flex flex-justify-right"); - mainSection.appendChild(subheader); + this._mainSection.appendChild(subheader); // Hidden search input this._search = document.createElement("project-search"); // subheader.appendChild(this._search); + + + const filterdiv = document.createElement("div"); filterdiv.setAttribute("class", "mt-3"); - mainSection.appendChild(filterdiv); + this._mainSection.appendChild(filterdiv); this._filterView = document.createElement("filter-interface"); this._filterView._algoButton.hidden = true; @@ -173,12 +224,50 @@ export class ProjectDetail extends TatorPage { subheader.appendChild(this._collaborators); this._projects = document.createElement("div"); - mainSection.appendChild(this._projects); + this._mainSection.appendChild(this._projects); + + // Part of Gallery: Communicates between card + page + this._bulkEdit = document.createElement("entity-gallery-bulk-edit"); + this._bulkEdit._messageBar_top.hidden = false; + this._bulkEdit._selectionPanel.hidden = true; + this._shadow.appendChild(this._bulkEdit); + filterdiv.appendChild(this._bulkEdit._selectionPanel); + // this._bulkEdit.addEventListener("multi-enabled", () => { + // // console.log("multi-enabled heard in project detail"); + // }); + + // Media section this._mediaSection = document.createElement("media-section"); this._projects.appendChild(this._mediaSection); this._mediaSection.addEventListener("runAlgorithm", this._openConfirmRunAlgoModal.bind(this)); + // Card attribute stuff related to mediaSection + /** + * CARD Label display options link for menu, and checkbox div + */ + this._cardAttributeLabels = document.createElement("entity-gallery-labels"); + this._cardAttributeLabels.titleEntityTypeName = "media"; + this._cardAttributeLabels._titleText = document.createTextNode("Select media labels to display."); + this._cardAttributeLabels.menuLinkTextSpan.innerHTML = "Media Labels"; + + this._mediaSection._hiddenMediaLabel.appendChild(this._cardAttributeLabels); + this._mediaSection._more._cardLink.appendChild(this._cardAttributeLabels.menuLink); + this._mediaSection._more.addEventListener("bulk-edit", this._openBulkEdit.bind(this)); + + this._cardAttributeLabels.addEventListener("labels-update", (evt) => { + // updates labels on cards + this._mediaSection._files.dispatchEvent(new CustomEvent("labels-update", evt.detail)); + this._bulkEdit._updateShownAttributes({ typeId: evt.detail.typeId, values: evt.detail.value }); + this._mediaSection._files.cardLabelsChosenByType[evt.detail.typeId] = evt.detail.value; + }); + + // references inner for card setup and pagination checkbox clear + this._mediaSection.bulkEdit = this._bulkEdit; + this._mediaSection._files.bulkEdit = this._bulkEdit; + + + // Confirm algorithm this._confirmRunAlgorithm = document.createElement("confirm-run-algorithm"); this._projects.appendChild(this._confirmRunAlgorithm); this._confirmRunAlgorithm.addEventListener("close", this._closeConfirmRunAlgoModal.bind(this)); @@ -189,8 +278,11 @@ export class ProjectDetail extends TatorPage { const deleteFile = document.createElement("delete-file-form"); this._projects.appendChild(deleteFile); - this._modalNotify = document.createElement("modal-notify"); - this._projects.appendChild(this._modalNotify); + this.modalNotify = document.createElement("modal-notify"); + this._projects.appendChild(this.modalNotify); + + this.modal = document.createElement("modal-dialog"); + this._projects.appendChild(this.modal); const cancelJob = document.createElement("cancel-confirm"); this._shadow.appendChild(cancelJob); @@ -205,7 +297,7 @@ export class ProjectDetail extends TatorPage { this._projects.appendChild(attachmentDialog); this._activityNav = document.createElement("activity-nav"); - main.appendChild(this._activityNav); + this.main.appendChild(this._activityNav); this._leaveConfirmOk = false; @@ -310,14 +402,14 @@ export class ProjectDetail extends TatorPage { }) .then(response => response.json()) .then(section => { - const card = document.createElement("section-card"); + const card = document.createElement("entity-card"); const sectionObj = { id: section.id, project: projectId, ...spec }; if (newSectionDialog._sectionType == "folder") { - card.init(sectionObj, "folder"); + card.sectionInit(sectionObj, "folder"); if (sectionObj.visible) { this._folders.appendChild(card); } else { @@ -335,8 +427,9 @@ export class ProjectDetail extends TatorPage { this._sectionVisibilityEL(evt) }); } else if (newSectionDialog._sectionType == "savedSearch") { - card.init(sectionObj, "savedSearch"); + card.sectionInit(sectionObj, "savedSearch"); this._savedSearches.appendChild(card); + card.addEventListener("click", () => { const clearPage = true; this._selectSection(sectionObj, projectId, clearPage); @@ -456,10 +549,6 @@ export class ProjectDetail extends TatorPage { this.removeAttribute("has-open-modal", ""); }); - this._modalNotify.addEventListener("close", evt => { - this.removeAttribute("has-open-modal", ""); - }); - this._newAlgorithmCallback = evt => { const newAlgorithm = document.createElement("new-algorithm-form"); this._projects.appendChild(newAlgorithm); @@ -483,12 +572,42 @@ export class ProjectDetail extends TatorPage { this._needScroll = true; this._lastQuery = null; + + + /* Init after modal is defined */ + // Init panel side behavior + this._panelContainer.init({ + main: this.main, + aside: this.aside, + pageModal: this.modal, + modelData: null, + gallery: this.gallery, + contents: section, + position: "left", + isMediaSection: true + }); + + + // + this.modalNotify.addEventListener("open", this.showDimmer.bind(this)); + this.modalNotify.addEventListener("close", this.hideDimmer.bind(this)); + this.modal.addEventListener("open", this.showDimmer.bind(this)); + this.modal.addEventListener("close", this.hideDimmer.bind(this)); + /* */ + + // State of chosen labels for gallery + this.cardLabelsChosenByType = {}; + this.mediaTypesMap = new Map(); } static get observedAttributes() { return ["project-id", "token"].concat(TatorPage.observedAttributes); } + _openBulkEdit() { + this._bulkEdit.startEditMode(); + } + _sectionVisibilityEL(evt) { const section = evt.detail.section; const id = section.id; @@ -505,8 +624,8 @@ export class ProjectDetail extends TatorPage { } // Create new section card and add to new list - const card = document.createElement("section-card"); - card.init(section, "folder"); + const card = document.createElement("entity-card"); + card.sectionInit(section, "folder"); if (visible) { this._folders.appendChild(card); } else { @@ -533,8 +652,8 @@ export class ProjectDetail extends TatorPage { } _notify(title, message, error_or_ok) { - this._modalNotify.init(title, message, error_or_ok); - this._modalNotify.setAttribute("is-open", ""); + this.modalNotify.init(title, message, error_or_ok); + this.modalNotify.setAttribute("is-open", ""); this.setAttribute("has-open-modal", ""); } @@ -589,26 +708,63 @@ export class ProjectDetail extends TatorPage { } }); + // Get MediaType data for attributes + const mediaTypePromise = fetch("/rest/MediaTypes/" + projectId, { + method: "GET", + credentials: "same-origin", + headers: { + "X-CSRFToken": getCookie("csrftoken"), + "Accept": "application/json", + "Content-Type": "application/json" + } + }); + // Run all above promises Promise.all([ projectPromise, sectionPromise, bookmarkPromise, algoPromise, + mediaTypePromise ]) - .then(([projectResponse, sectionResponse, bookmarkResponse, algoResponse]) => { + .then(([projectResponse, sectionResponse, bookmarkResponse, algoResponse, mediaTypeResponse]) => { const projectData = projectResponse.json(); const sectionData = sectionResponse.json(); const bookmarkData = bookmarkResponse.json(); const algoData = algoResponse.json(); + const mediaTypeData = mediaTypeResponse.json(); - Promise.all([projectData, sectionData, bookmarkData, algoData]) - .then(([project, sections, bookmarks, algos]) => { + Promise.all([projectData, sectionData, bookmarkData, algoData, mediaTypeData]) + .then(([project, sections, bookmarks, algos, mediaTypes]) => { // First hide algorithms if needed. These are not appropriate to be - // run at the project/section/media level. + // run at the project/this._section/media level. var hiddenAlgos = ['tator_extend_track', 'tator_fill_track_gaps']; const hiddenAlgoCategories = ['annotator-view']; + // + // Set up attributes for bulk edit + for (let mediaTypeData of mediaTypes) { + + //init card labels with localization entity type definitions + this._cardAttributeLabels.add({ + typeData: mediaTypeData, + checkedFirst: false + }); + + //init panel with localization entity type definitions + // console.log("ADDING MEDIA TYPE") + this._bulkEdit._editPanel.addLocType(mediaTypeData); + this.mediaTypesMap.set(mediaTypeData.id, mediaTypeData); + } + + this._mediaSection.mediaTypesMap = this.mediaTypesMap; + + // + this._mediaSection._files._cardAttributeLabels = this._cardAttributeLabels; + this._mediaSection._bulkEdit = this._bulkEdit; + this._bulkEdit.init(this, this._mediaSection._files, "media", projectId); + // this._bulkEdit._showEditPanel(); + const parsedAlgos = algos.filter(function (alg) { if (Array.isArray(alg.categories)) { for (const category of alg.categories) { @@ -632,8 +788,8 @@ export class ProjectDetail extends TatorPage { // this._search.autocomplete = project.filter_autocomplete; let projectParams = null; - const home = document.createElement("section-card"); - home.init(null, false); + const home = document.createElement("entity-card"); + home.sectionInit(null, false); home.addEventListener("click", () => { this._selectSection(null, projectId, true); for (const child of this._allSections()) { @@ -654,8 +810,8 @@ export class ProjectDetail extends TatorPage { } else { sectionType = "savedSearch"; } - const card = document.createElement("section-card"); - card.init(section, sectionType); + const card = document.createElement("entity-card"); + card.sectionInit(section, sectionType); if (sectionType == "folder") { if (section.visible) { this._folders.appendChild(card); @@ -663,7 +819,7 @@ export class ProjectDetail extends TatorPage { this._archivedFolders.appendChild(card); } card.addEventListener("visibilityChange", evt => { - this._sectionVisibilityEL(evt) + sectionVisibilityEL(evt) }); } else { this._savedSearches.appendChild(card); @@ -682,8 +838,8 @@ export class ProjectDetail extends TatorPage { const first = "Last visited"; bookmarks.sort((a, b) => { return a.name == first ? -1 : b.name == first ? 1 : 0; }); for (const bookmark of bookmarks) { - const card = document.createElement("section-card"); - card.init(bookmark, "bookmark"); + const card = document.createElement("entity-card"); + card.sectionInit(bookmark, "bookmark"); this._bookmarks.appendChild(card); } @@ -718,7 +874,7 @@ export class ProjectDetail extends TatorPage { // try { home.active = true; - this._selectSection(null, projectId).then( async () => { + this._selectSection(null, projectId).then(async () => { this.loading.hideSpinner(); this.hideDimmer(); }); @@ -748,15 +904,19 @@ export class ProjectDetail extends TatorPage { this._filterDataView.init(); this._filterView.dataView = this._filterDataView; + + // Set UI and results to any url param conditions that exist (from URL) this._mediaSection._filterConditions = this._mediaSection.getFilterConditionsObject(); + this._bulkEdit.checkForFilters(this._mediaSection._filterConditions); if (this._mediaSection._filterConditions.length > 0) { - this._updateFilterResults({ detail: { conditions: this._mediaSection._filterConditions }}); + this._updateFilterResults({ detail: { conditions: this._mediaSection._filterConditions } }); } // Listen for filter events this._filterView.addEventListener("filterParameters", this._updateFilterResults.bind(this)); + }); } catch (err) { @@ -765,15 +925,14 @@ export class ProjectDetail extends TatorPage { this.hideDimmer(); } - }).catch(err => { - console.log("Error setting up page with all promises", err); + console.error("Error setting up page with all promises", err); this.loading.hideSpinner(); this.hideDimmer(); }); }).catch(err => { - console.log("Error setting up page with all promises", err); + console.error("Error setting up page with all promises", err); this.loading.hideSpinner(); this.hideDimmer(); }); @@ -832,7 +991,7 @@ export class ProjectDetail extends TatorPage { const samePageSize = pageSize == this._mediaSection._defaultPageSize; const samePage = page == 1; - if (!samePageSize) { + if (!samePageSize) { this._mediaSection._paginator_bottom.pageSize = pageSize; this._mediaSection._paginator_top.pageSize = pageSize; } @@ -845,7 +1004,7 @@ export class ProjectDetail extends TatorPage { if (!samePageSize || !samePage) { this._mediaSection._paginator_top._emit(); this._mediaSection._paginator_bottom._emit(); - } + } } return true; @@ -925,6 +1084,7 @@ export class ProjectDetail extends TatorPage { async _updateFilterResults(evt) { this._filterConditions = evt.detail.conditions; this._filterView.setFilterConditions(this._filterConditions); + this._bulkEdit.checkForFilters(this._filterConditions); this.showDimmer(); this.loading.showSpinner(); @@ -949,14 +1109,14 @@ export class ProjectDetail extends TatorPage { this.hideDimmer(); } - // Modal for this page, and handlers - showDimmer() { - return this.setAttribute("has-open-modal", ""); - } + // Modal for this page, and handlers + showDimmer() { + return this.setAttribute("has-open-modal", ""); + } - hideDimmer() { - return this.removeAttribute("has-open-modal"); - } + hideDimmer() { + return this.removeAttribute("has-open-modal"); + } } diff --git a/ui/src/js/project-detail/section-card.js b/ui/src/js/project-detail/section-card.js deleted file mode 100644 index b5c02c165..000000000 --- a/ui/src/js/project-detail/section-card.js +++ /dev/null @@ -1,221 +0,0 @@ -import { TatorElement } from "../components/tator-element.js"; -import { getCookie } from "../util/get-cookie.js"; -import { fetchRetry } from "../util/fetch-retry.js"; -import { svgNamespace } from "../components/tator-element.js"; - -export class SectionCard extends TatorElement { - constructor() { - super(); - - this._li = document.createElement("li"); - this._li.style.cursor = "pointer"; - this._li.setAttribute("class", "section d-flex flex-items-center flex-justify-between px-2 rounded-1"); - this._shadow.appendChild(this._li); - - this._link = document.createElement("a"); - this._link.setAttribute("class", "section__link d-flex flex-items-center text-gray"); - this._li.appendChild(this._link); - - this._title = document.createElement("h2"); - this._title.setAttribute("class", "section__name py-1 px-1 css-truncate"); - this._link.appendChild(this._title); - - } - - init(section, sectionType) { - this._section = section; - this._sectionType = sectionType; - if (section === null) { - this._title.textContent = "All Media"; - } else { - this._title.textContent = section.name; - } - - const svg = document.createElementNS(svgNamespace, "svg"); - svg.setAttribute("viewBox", "0 0 24 24"); - svg.setAttribute("height", "1em"); - svg.setAttribute("width", "1em"); - svg.setAttribute("fill", "none"); - svg.style.fill = "none"; - svg.setAttribute("stroke", "currentColor"); - svg.setAttribute("stroke-width", "2"); - svg.setAttribute("stroke-linecap", "round"); - svg.setAttribute("stroke-linejoin", "round"); - this._link.insertBefore(svg, this._title); - - // Null section means display all media. - if (section === null) { - const path = document.createElementNS(svgNamespace, "path"); - path.setAttribute("d", "M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"); - svg.appendChild(path); - - const poly = document.createElementNS(svgNamespace, "polyline"); - poly.setAttribute("points", "9 22 9 12 15 12 15 22"); - svg.appendChild(poly); - } - if (sectionType == "folder") { - const path = document.createElementNS(svgNamespace, "path"); - path.setAttribute("d", "M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"); - svg.appendChild(path); - - const context = document.createElement("div"); - context.setAttribute("class", "more d-flex flex-column f2 px-3 py-2 lh-condensed"); - context.style.display = "none"; - this._shadow.appendChild(context); - - const toggle = document.createElement("toggle-button"); - if (this._section.visible) { - toggle.setAttribute("text", "Archive folder"); - } else { - toggle.setAttribute("text", "Restore folder"); - } - context.appendChild(toggle); - - toggle.addEventListener("click", evt => { - this._section.visible = !this._section.visible; - const sectionId = Number(); - fetch(`/rest/Section/${this._section.id}`, { - method: "PATCH", - credentials: "same-origin", - headers: { - "X-CSRFToken": getCookie("csrftoken"), - "Accept": "application/json", - "Content-Type": "application/json" - }, - body: JSON.stringify({ - "visible": this._section.visible, - }) - }) - .then(response => { - if (this._section.visible) { - toggle.setAttribute("text", "Archive folder"); - } else { - toggle.setAttribute("text", "Restore folder"); - } - this.dispatchEvent(new CustomEvent("visibilityChange", { - detail: {section: this._section} - })); - }); - }); - - this.addEventListener("contextmenu", evt => { - evt.preventDefault(); - context.style.display = "block"; - }); - - window.addEventListener("click", evt => { - context.style.display = "none"; - }); - } else if (sectionType == "savedSearch") { - const circle = document.createElementNS(svgNamespace, "circle"); - circle.setAttribute("cx", "11"); - circle.setAttribute("cy", "11"); - circle.setAttribute("r", "8"); - svg.appendChild(circle); - - const line = document.createElementNS(svgNamespace, "line"); - line.setAttribute("x1", "21"); - line.setAttribute("y1", "21"); - line.setAttribute("x2", "16.65"); - line.setAttribute("y2", "16.65"); - svg.appendChild(line); - } else if (sectionType == "bookmark") { - const path = document.createElementNS(svgNamespace, "path"); - path.setAttribute("d", "M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z"); - svg.appendChild(path); - this._link.setAttribute("href", section.uri); - - // Set up bookmark management controls. - const input = document.createElement("input"); - input.setAttribute("class", "form-control input-sm f1"); - input.style.display = "none"; - this._link.appendChild(input); - - const context = document.createElement("div"); - context.setAttribute("class", "more d-flex flex-column f2 px-3 py-2 lh-condensed"); - context.style.display = "none"; - this._shadow.appendChild(context); - - const rename = document.createElement("rename-button"); - rename.setAttribute("text", "Rename"); - context.appendChild(rename); - - const remove = document.createElement("delete-button"); - remove.init("Delete"); - context.appendChild(remove); - - this._link.addEventListener("contextmenu", evt => { - evt.preventDefault(); - context.style.display = "block"; - }); - - rename.addEventListener("click", () => { - input.style.display = "block"; - this._link.style.pointerEvents = "none"; - this._title.style.display = "none"; - input.setAttribute("value", this._title.textContent); - input.focus(); - }); - - input.addEventListener("focus", evt => { - evt.target.select(); - }); - - input.addEventListener("keydown", evt => { - if (evt.keyCode == 13) { - evt.preventDefault(); - input.blur(); - } - }); - - input.addEventListener("blur", evt => { - if (evt.target.value !== "") { - this._title.textContent = evt.target.value; - this._link.style.pointerEvents = ""; - this._section.name = evt.target.value; - fetchRetry("/rest/Bookmark/" + this._section.id, { - method: "PATCH", - headers: { - "X-CSRFToken": getCookie("csrftoken"), - "Accept": "application/json", - "Content-Type": "application/json" - }, - body: JSON.stringify({"name": evt.target.value}), - }); - } - input.style.display = "none"; - this._title.style.display = "block"; - }); - - remove.addEventListener("click", () => { - this.parentNode.removeChild(this); - fetchRetry("/rest/Bookmark/" + this._section.id, { - method: "DELETE", - headers: { - "X-CSRFToken": getCookie("csrftoken"), - "Accept": "application/json", - "Content-Type": "application/json" - }, - }); - }); - - window.addEventListener("click", evt => { - context.style.display = "none"; - }); - } - } - - rename(name) { - this._title.textContent = name; - } - - set active(enabled) { - if (enabled) { - this._li.classList.add("is-active"); - } else { - this._li.classList.remove("is-active"); - } - } -} - -customElements.define("section-card", SectionCard); diff --git a/ui/src/js/project-detail/section-files.js b/ui/src/js/project-detail/section-files.js index 46b115261..cab2a158a 100644 --- a/ui/src/js/project-detail/section-files.js +++ b/ui/src/js/project-detail/section-files.js @@ -1,19 +1,40 @@ import { TatorElement } from "../components/tator-element.js"; import Spinner from "../../images/spinner-transparent.svg"; +import { Utilities } from "../util/utilities.js"; export class SectionFiles extends TatorElement { constructor() { super(); - this._main = document.createElement("ul"); - this._main.setAttribute("class", "project__files d-flex"); - this._shadow.appendChild(this._main); + // this._main = document.createElement("ul"); + // this._main.setAttribute("class", "project__files d-flex"); + // this._shadow.appendChild(this._main); + + this._ul = document.createElement("ul"); + this._ul.setAttribute("class", "project__files d-flex"); + this._shadow.appendChild(this._ul); + + // State of chosen labels for gallery + this.cardLabelsChosenByType = {}; + this._cardElements = []; + this._currentCardIndexes = {}; + + // + this.multiEnabled = false; } static get observedAttributes() { return ["project-id", "username", "token", "section"]; } + set bulkEdit(val) { + this._bulkEdit = val; + } + + set cardAtributeLabels(val) { + this._cardAttributeLabels = val; + } + set project(val) { this._project = val; } @@ -35,7 +56,7 @@ export class SectionFiles extends TatorElement { } set mediaIds(val) { - this._media = val.map(id => {return {id: id}}); + this._media = val.map(id => { return { id: id } }); this._updateNumCards(); } @@ -43,6 +64,10 @@ export class SectionFiles extends TatorElement { this._algorithms = val; } + set mediaTypesMap(val) { + this._mediaTypesMap = val; + } + _updateCard(card, media, pos_text) { card.setAttribute("media-id", media.id); @@ -51,6 +76,8 @@ export class SectionFiles extends TatorElement { let seconds = Number(media.num_frames) / Number(media.fps); let duration = new Date(seconds * 1000).toISOString().substr(11, 8); card.setAttribute("duration", duration); + } else { + card.setAttribute("duration", null); } card.setAttribute("thumb", Spinner); @@ -72,9 +99,11 @@ export class SectionFiles extends TatorElement { card.attachments = []; } } + card.mediaParams = this._mediaParams(); card.media = media; card.style.display = "block"; + // card.rename(media.name); card.setAttribute("name", media.name); card.setAttribute("project-id", this.getAttribute("project-id")); card.setAttribute("pos-text", pos_text) @@ -84,21 +113,47 @@ export class SectionFiles extends TatorElement { const hasAlgorithms = typeof this._algorithms !== "undefined"; const hasProject = this.hasAttribute("project-id"); if (hasAlgorithms && hasProject) { - const children = this._main.children; + const children = this._ul.children; + // const cardList = []; + this._cardElements = []; + this._currentCardIndexes = {}; for (const [index, media] of cardInfo.entries()) { const newCard = index >= children.length; + const cardObj = { + id: media.id, + entityType: this._mediaTypesMap.get(media.meta), + media: media + } let card; + let entityType = cardObj.entityType; + let entityTypeId = entityType.id; + + /** + * Card labels / attributes of localization or media type + */ + this.cardLabelsChosenByType[entityTypeId] = this._cardAttributeLabels._getValue(entityTypeId); + // this._bulkEdit._updateShownAttributes({ typeId: entityTypeId, values: this.cardLabelsChosenByType[entityTypeId] }); + + if (newCard) { - card = document.createElement("media-card"); + card = document.createElement("entity-card"); + card.titleDiv.hidden = false; + card._more.hidden = false; + card._ext.hidden = false; + card._title.hidden = false; + card._id_text.hidden = true; card.project = this._project; card.algorithms = this._algorithms; + card._li.classList.add("dark-card"); + card.addEventListener("mouseenter", () => { if (card.hasAttribute("media-id")) { this.dispatchEvent(new CustomEvent("cardMouseover", { - detail: {media: card.media} + detail: { media: card.media } })); } }); + card.addEventListener("mouseleave", () => { if (card.hasAttribute("media-id")) { this.dispatchEvent(new Event("cardMouseexit")); @@ -108,18 +163,134 @@ export class SectionFiles extends TatorElement { card = children[index]; } + // make reference lists / object + // cardList.push(card); + let cardInfo = { + card: card + }; + + + this._cardElements.push(cardInfo); + this._currentCardIndexes[cardObj.id] = index; + + + // Non-hidden attributes (ie order >= 0)) + let nonHiddenAttrs = []; + let hiddenAttrs = []; + for (let attr of entityType.attribute_types) { + if (attr.order >= 0) { + nonHiddenAttrs.push(attr); + } + else { + hiddenAttrs.push(attr); + } + } + + // Show array by order, or alpha + var cardLabelOptions = nonHiddenAttrs.sort((a, b) => { + return a.order - b.order || a.name - b.name; + }); + + cardLabelOptions.push(...hiddenAttrs); + + cardObj.attributes = media.attributes; + cardObj.attributeOrder = cardLabelOptions; + // console.log("MEDIA CARD? ................ cardObj="); + // console.log(cardObj) + + // Notifiy bulk edit about multi-select controls + card.addEventListener("ctrl-select", (e) => { + // console.log("Opening edit mode"); + this._bulkEdit._openEditMode(e); + }); + + // card.addEventListener("shift-select", (e) => { + // // console.log("Opening edit mode"); + // this._bulkEdit._openEditMode(e); + // }); + + this._bulkEdit.addEventListener("multi-enabled", () => { + // console.log("multi-enabled heard in section files"); + card.multiEnabled = true; + this.multiEnabled = true; + }); + this._bulkEdit.addEventListener("multi-disabled", () => { + // console.log("multi-disabled heard in section files"); + card.multiEnabled = false; + this.multiEnabled = false; + }); + + // + this._cardAttributeLabels.addEventListener("labels-update", (evt) => { + // console.log(evt); + + if (entityTypeId == evt.detail.typeId) { + card._updateShownAttributes(evt); + // this._bulkEdit._updateShownAttributes({ typeId: evt.detail.typeId, values: evt.detail.value }); + + // this.cardLabelsChosenByType[evt.detail.typeId] = evt.detail.value; + // let msg = `Entry labels updated`; + // Utilities.showSuccessIcon(msg); + } + + }); + const pos_text = `(${this._startMediaIndex + index + 1} of ${this._numMedia})`; - this._updateCard(card, media, pos_text); + this._updateCard(card, media, pos_text); // todo init might do most of this and is required, maybe cut it out + if (newCard) { - this._main.appendChild(card); + this._ul.appendChild(card); } + // console.log('this.cardLabelsChosenByType[entityTypeId]') + // console.log(this.cardLabelsChosenByType[entityTypeId]); + // this is data used later by label chooser, and bulk edit + card.init({ + obj: cardObj, + idx: index, + mediaInit: true, + cardLabelsChosen: this.cardLabelsChosenByType[entityTypeId], + // enableMultiselect: this.multiEnabled + }); + + // If we're still in multiselect.. check if this card should be toggled... + if (this.multiEnabled) { + const selectedArray = this._bulkEdit._currentMultiSelectionToId.get(entityType.id); + if (typeof selectedArray !== "undefined" && Array.isArray(selectedArray) && selectedArray.includes(cardObj.id)) { + this._bulkEdit._addSelected({ element: card, id: cardObj.id, isSelected: true }) + } + } + + // + // console.log("Is this.multiEnabled??? "+this.multiEnabled) + card.multiEnabled = this.multiEnabled; } + + + // Replace card info so that shift select can get cards in between + this._bulkEdit.elementList = this._cardElements; + // this._bulkEdit.elementList = cardList; + this._bulkEdit.elementIndexes = this._currentCardIndexes; + + + if (children.length > cardInfo.length) { const len = children.length; for (let idx = len - 1; idx >= cardInfo.length; idx--) { children[idx].style.display = "none"; } } + + + } + } + updateCardData(newCardData) { + if (newCardData.id in this._currentCardIndexes) { + const index = this._currentCardIndexes[newCardData.id]; + const card = this._cardElements[index].card; + // this.cardData.updateLocalizationAttributes(card.cardObj).then(() => { + // //card.displayAttributes(); + // card._updateAttributeValues(card.cardObj) + // }); } } } diff --git a/ui/src/js/project-detail/section-more.js b/ui/src/js/project-detail/section-more.js index fb348c2ad..4db274d91 100644 --- a/ui/src/js/project-detail/section-more.js +++ b/ui/src/js/project-detail/section-more.js @@ -27,29 +27,52 @@ export class SectionMore extends TatorElement { this._algorithmMenu = document.createElement("algorithm-menu"); this._div.appendChild(this._algorithmMenu); - const otherButtons = document.createElement("div"); - otherButtons.setAttribute("class", "d-flex flex-column px-4 py-3 lh-condensed"); - this._div.appendChild(otherButtons); + this._otherButtons = document.createElement("div"); + this._otherButtons.setAttribute("class", "d-flex flex-column px-4 py-3 lh-condensed"); + this._div.appendChild(this._otherButtons); + + this._cardLink = document.createElement("div"); + this._otherButtons.appendChild(this._cardLink); + + const bulkDiv = document.createElement("div"); + bulkDiv.setAttribute("id", "bulkCorrectButtonDiv"); + this._otherButtons.appendChild(bulkDiv); + + this._bulkEditMedia = document.createElement("bulk-correct-button"); + // this._bulkEditMedia.setAttribute("id", "bulkCorrectButton"); + this._bulkEditMedia.setAttribute("text", "Edit media attributes"); + bulkDiv.appendChild(this._bulkEditMedia); this._download = document.createElement("download-button"); this._download.setAttribute("text", "Download files"); - otherButtons.appendChild(this._download); + this._otherButtons.appendChild(this._download); this._annotations = document.createElement("download-button"); this._annotations.setAttribute("text", "Download metadata"); - otherButtons.appendChild(this._annotations); + this._otherButtons.appendChild(this._annotations); this._rename = document.createElement("rename-button"); this._rename.setAttribute("text", "Rename section"); - otherButtons.appendChild(this._rename); + this._otherButtons.appendChild(this._rename); this._deleteSection = document.createElement("delete-button"); this._deleteSection.init("Delete section"); - otherButtons.appendChild(this._deleteSection); + this._otherButtons.appendChild(this._deleteSection); this._deleteMedia = document.createElement("delete-button"); this._deleteMedia.init("Delete media", "text-red"); - otherButtons.appendChild(this._deleteMedia); + this._otherButtons.appendChild(this._deleteMedia); + + this._bulkEditMedia.addEventListener("click", () => { + details.removeAttribute("open"); + console.log("dispatch bulk edit!") + this.dispatchEvent(new Event("bulk-edit")); + }); + + this._cardLink.addEventListener("click", () => { + details.removeAttribute("open"); + // this.dispatchEvent(new Event("bulk-edit")); + }); this._algorithmMenu.addEventListener("click", () => { details.removeAttribute("open"); @@ -111,6 +134,7 @@ export class SectionMore extends TatorElement { set algorithms(val) { this._algorithmMenu.algorithms = val; } + } customElements.define("section-more", SectionMore); diff --git a/ui/src/js/project-settings/settings-box-helpers.js b/ui/src/js/project-settings/settings-box-helpers.js index e4299dbee..54d7c4464 100644 --- a/ui/src/js/project-settings/settings-box-helpers.js +++ b/ui/src/js/project-settings/settings-box-helpers.js @@ -252,7 +252,8 @@ export class SettingsBox { } _modalCloseCallback(){ - return this.modal._closeCallback(); + this.modal._closeCallback(); + this._modalClear(); }