supervisely 6.73.431__py3-none-any.whl → 6.73.433__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- supervisely/api/dataset_api.py +34 -21
- supervisely/api/project_api.py +104 -0
- supervisely/app/widgets/dataset_thumbnail/dataset_thumbnail.py +11 -2
- supervisely/app/widgets/dataset_thumbnail/template.html +3 -1
- supervisely/app/widgets/fast_table/fast_table.py +156 -21
- supervisely/app/widgets/fast_table/script.js +165 -36
- supervisely/app/widgets/fast_table/style.css +32 -2
- supervisely/app/widgets/fast_table/template.html +1 -1
- supervisely/versions.json +2 -1
- {supervisely-6.73.431.dist-info → supervisely-6.73.433.dist-info}/METADATA +1 -1
- {supervisely-6.73.431.dist-info → supervisely-6.73.433.dist-info}/RECORD +15 -15
- {supervisely-6.73.431.dist-info → supervisely-6.73.433.dist-info}/LICENSE +0 -0
- {supervisely-6.73.431.dist-info → supervisely-6.73.433.dist-info}/WHEEL +0 -0
- {supervisely-6.73.431.dist-info → supervisely-6.73.433.dist-info}/entry_points.txt +0 -0
- {supervisely-6.73.431.dist-info → supervisely-6.73.433.dist-info}/top_level.txt +0 -0
supervisely/api/dataset_api.py
CHANGED
|
@@ -185,6 +185,7 @@ class DatasetApi(UpdateableModule, RemoveableModuleApi):
|
|
|
185
185
|
filters: Optional[List[Dict[str, str]]] = None,
|
|
186
186
|
recursive: Optional[bool] = False,
|
|
187
187
|
parent_id: Optional[int] = None,
|
|
188
|
+
include_custom_data: Optional[bool] = False,
|
|
188
189
|
) -> List[DatasetInfo]:
|
|
189
190
|
"""
|
|
190
191
|
Returns list of dataset in the given project, or list of nested datasets
|
|
@@ -200,6 +201,9 @@ class DatasetApi(UpdateableModule, RemoveableModuleApi):
|
|
|
200
201
|
:type recursive: bool, optional
|
|
201
202
|
:param parent_id: Parent Dataset ID. If set to None, the search will be performed at the top level of the Project,
|
|
202
203
|
otherwise the search will be performed in the specified Dataset.
|
|
204
|
+
:type parent_id: Union[int, None], optional
|
|
205
|
+
:param include_custom_data: If True, the response will include the `custom_data` field for each Dataset.
|
|
206
|
+
:type include_custom_data: bool, optional
|
|
203
207
|
:return: List of all Datasets with information for the given Project. See :class:`info_sequence<info_sequence>`
|
|
204
208
|
:rtype: :class:`List[DatasetInfo]`
|
|
205
209
|
:Usage example:
|
|
@@ -246,14 +250,16 @@ class DatasetApi(UpdateableModule, RemoveableModuleApi):
|
|
|
246
250
|
filters.append({"field": ApiField.PARENT_ID, "operator": "=", "value": parent_id})
|
|
247
251
|
recursive = True
|
|
248
252
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
253
|
+
method = "datasets.list"
|
|
254
|
+
data = {
|
|
255
|
+
ApiField.PROJECT_ID: project_id,
|
|
256
|
+
ApiField.FILTER: filters,
|
|
257
|
+
ApiField.RECURSIVE: recursive,
|
|
258
|
+
}
|
|
259
|
+
if include_custom_data:
|
|
260
|
+
data[ApiField.EXTRA_FIELDS] = [ApiField.CUSTOM_DATA]
|
|
261
|
+
|
|
262
|
+
return self.get_list_all_pages(method, data)
|
|
257
263
|
|
|
258
264
|
def get_info_by_id(self, id: int, raise_error: Optional[bool] = False) -> DatasetInfo:
|
|
259
265
|
"""
|
|
@@ -304,6 +310,7 @@ class DatasetApi(UpdateableModule, RemoveableModuleApi):
|
|
|
304
310
|
description: Optional[str] = "",
|
|
305
311
|
change_name_if_conflict: Optional[bool] = False,
|
|
306
312
|
parent_id: Optional[int] = None,
|
|
313
|
+
custom_data: Optional[Dict[Any, Any]] = None,
|
|
307
314
|
) -> DatasetInfo:
|
|
308
315
|
"""
|
|
309
316
|
Create Dataset with given name in the given Project.
|
|
@@ -318,6 +325,9 @@ class DatasetApi(UpdateableModule, RemoveableModuleApi):
|
|
|
318
325
|
:type change_name_if_conflict: bool, optional
|
|
319
326
|
:param parent_id: Parent Dataset ID. If set to None, then the Dataset will be created at
|
|
320
327
|
the top level of the Project, otherwise the Dataset will be created in a specified Dataset.
|
|
328
|
+
:type parent_id: Union[int, None]
|
|
329
|
+
:param custom_data: Custom data to store in the Dataset.
|
|
330
|
+
:type custom_data: Dict[Any, Any], optional
|
|
321
331
|
:return: Information about Dataset. See :class:`info_sequence<info_sequence>`
|
|
322
332
|
:rtype: :class:`DatasetInfo`
|
|
323
333
|
:Usage example:
|
|
@@ -345,15 +355,16 @@ class DatasetApi(UpdateableModule, RemoveableModuleApi):
|
|
|
345
355
|
change_name_if_conflict=change_name_if_conflict,
|
|
346
356
|
parent_id=parent_id,
|
|
347
357
|
)
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
358
|
+
method = "datasets.add"
|
|
359
|
+
payload = {
|
|
360
|
+
ApiField.PROJECT_ID: project_id,
|
|
361
|
+
ApiField.NAME: effective_name,
|
|
362
|
+
ApiField.DESCRIPTION: description,
|
|
363
|
+
ApiField.PARENT_ID: parent_id,
|
|
364
|
+
}
|
|
365
|
+
if custom_data is not None:
|
|
366
|
+
payload[ApiField.CUSTOM_DATA] = custom_data
|
|
367
|
+
response = self._api.post(method, payload)
|
|
357
368
|
return self._convert_json_info(response.json())
|
|
358
369
|
|
|
359
370
|
def get_or_create(
|
|
@@ -564,6 +575,7 @@ class DatasetApi(UpdateableModule, RemoveableModuleApi):
|
|
|
564
575
|
new_dataset_name,
|
|
565
576
|
dataset.description,
|
|
566
577
|
change_name_if_conflict=change_name_if_conflict,
|
|
578
|
+
custom_data=dataset.custom_data,
|
|
567
579
|
)
|
|
568
580
|
items_api.copy_batch(
|
|
569
581
|
new_dataset.id, src_item_ids, change_name_if_conflict, with_annotations
|
|
@@ -797,6 +809,7 @@ class DatasetApi(UpdateableModule, RemoveableModuleApi):
|
|
|
797
809
|
sort_order: Optional[str] = None,
|
|
798
810
|
per_page: Optional[int] = None,
|
|
799
811
|
page: Union[int, Literal["all"]] = "all",
|
|
812
|
+
include_custom_data: Optional[bool] = False,
|
|
800
813
|
) -> dict:
|
|
801
814
|
"""
|
|
802
815
|
List all available datasets from all available teams for the user that match the specified filtering criteria.
|
|
@@ -807,22 +820,20 @@ class DatasetApi(UpdateableModule, RemoveableModuleApi):
|
|
|
807
820
|
- 'operator': Takes values '=', 'eq', '!=', 'not', 'in', '!in', '>', 'gt', '>=', 'gte', '<', 'lt', '<=', 'lte'
|
|
808
821
|
- 'value': Takes on values according to the meaning of 'field' or null
|
|
809
822
|
:type filters: List[Dict[str, str]], optional
|
|
810
|
-
|
|
811
823
|
:param sort: Specifies by which parameter to sort the project list.
|
|
812
824
|
Takes values 'id', 'name', 'size', 'createdAt', 'updatedAt'
|
|
813
825
|
:type sort: str, optional
|
|
814
|
-
|
|
815
826
|
:param sort_order: Determines which value to list from.
|
|
816
827
|
:type sort_order: str, optional
|
|
817
|
-
|
|
818
828
|
:param per_page: Number of first items found to be returned.
|
|
819
829
|
'None' will return the first page with a default size of 20000 datasets.
|
|
820
830
|
:type per_page: int, optional
|
|
821
|
-
|
|
822
831
|
:param page: Page number, used to retrieve the following items if the number of them found is more than per_page.
|
|
823
832
|
The default value is 'all', which retrieves all available datasets.
|
|
824
833
|
'None' will return the first page with datasets, the amount of which is set in param 'per_page'.
|
|
825
834
|
:type page: Union[int, Literal["all"]], optional
|
|
835
|
+
:param include_custom_data: If True, the response will include the `custom_data` field for each Dataset.
|
|
836
|
+
:type include_custom_data: bool, optional
|
|
826
837
|
|
|
827
838
|
:return: Search response information and 'DatasetInfo' of all datasets that are searched by a given criterion.
|
|
828
839
|
:rtype: dict
|
|
@@ -899,6 +910,8 @@ class DatasetApi(UpdateableModule, RemoveableModuleApi):
|
|
|
899
910
|
request_body[ApiField.PER_PAGE] = per_page
|
|
900
911
|
if page is not None and page != "all":
|
|
901
912
|
request_body[ApiField.PAGE] = page
|
|
913
|
+
if include_custom_data:
|
|
914
|
+
request_body[ApiField.EXTRA_FIELDS] = [ApiField.CUSTOM_DATA]
|
|
902
915
|
|
|
903
916
|
first_response = self._api.post(method, request_body).json()
|
|
904
917
|
|
supervisely/api/project_api.py
CHANGED
|
@@ -12,10 +12,12 @@ from typing import (
|
|
|
12
12
|
Any,
|
|
13
13
|
Callable,
|
|
14
14
|
Dict,
|
|
15
|
+
Generator,
|
|
15
16
|
List,
|
|
16
17
|
Literal,
|
|
17
18
|
NamedTuple,
|
|
18
19
|
Optional,
|
|
20
|
+
Tuple,
|
|
19
21
|
Union,
|
|
20
22
|
)
|
|
21
23
|
|
|
@@ -38,6 +40,7 @@ from supervisely.annotation.annotation import TagCollection
|
|
|
38
40
|
from supervisely.annotation.obj_class import ObjClass
|
|
39
41
|
from supervisely.annotation.obj_class_collection import ObjClassCollection
|
|
40
42
|
from supervisely.annotation.tag_meta import TagMeta, TagValueType
|
|
43
|
+
from supervisely.api.dataset_api import DatasetInfo
|
|
41
44
|
from supervisely.api.module_api import (
|
|
42
45
|
ApiField,
|
|
43
46
|
CloneableModuleApi,
|
|
@@ -2556,3 +2559,104 @@ class ProjectApi(CloneableModuleApi, UpdateableModule, RemoveableModuleApi):
|
|
|
2556
2559
|
api.project.calculate_embeddings(project_id)
|
|
2557
2560
|
"""
|
|
2558
2561
|
self._api.post("embeddings.calculate-project-embeddings", {ApiField.PROJECT_ID: id})
|
|
2562
|
+
|
|
2563
|
+
def recreate_structure_generator(
|
|
2564
|
+
self,
|
|
2565
|
+
src_project_id: int,
|
|
2566
|
+
dst_project_id: Optional[int] = None,
|
|
2567
|
+
dst_project_name: Optional[str] = None,
|
|
2568
|
+
) -> Generator[Tuple[DatasetInfo, DatasetInfo], None, None]:
|
|
2569
|
+
"""This method can be used to recreate a project with hierarchial datasets (without the data itself) and
|
|
2570
|
+
yields the tuple of source and destination DatasetInfo objects.
|
|
2571
|
+
|
|
2572
|
+
:param src_project_id: Source project ID
|
|
2573
|
+
:type src_project_id: int
|
|
2574
|
+
:param dst_project_id: Destination project ID
|
|
2575
|
+
:type dst_project_id: int, optional
|
|
2576
|
+
:param dst_project_name: Name of the destination project. If `dst_project_id` is None, a new project will be created with this name. If `dst_project_id` is provided, this parameter will be ignored.
|
|
2577
|
+
:type dst_project_name: str, optional
|
|
2578
|
+
|
|
2579
|
+
:return: Generator of tuples of source and destination DatasetInfo objects
|
|
2580
|
+
:rtype: Generator[Tuple[DatasetInfo, DatasetInfo], None, None]
|
|
2581
|
+
|
|
2582
|
+
:Usage example:
|
|
2583
|
+
|
|
2584
|
+
.. code-block:: python
|
|
2585
|
+
|
|
2586
|
+
import supervisely as sly
|
|
2587
|
+
|
|
2588
|
+
api = sly.Api.from_env()
|
|
2589
|
+
|
|
2590
|
+
src_project_id = 123
|
|
2591
|
+
dst_project_id = api.project.create("new_project", "images").id
|
|
2592
|
+
|
|
2593
|
+
for src_ds, dst_ds in api.project.recreate_structure_generator(src_project_id, dst_project_id):
|
|
2594
|
+
print(f"Recreated dataset {src_ds.id} -> {dst_ds.id}")
|
|
2595
|
+
# Implement your logic here to process the datasets.
|
|
2596
|
+
"""
|
|
2597
|
+
if dst_project_id is None:
|
|
2598
|
+
src_project_info = self._api.project.get_info_by_id(src_project_id)
|
|
2599
|
+
dst_project_info = self._api.project.create(
|
|
2600
|
+
src_project_info.workspace_id,
|
|
2601
|
+
dst_project_name or f"Recreation of {src_project_info.name}",
|
|
2602
|
+
src_project_info.type,
|
|
2603
|
+
src_project_info.description,
|
|
2604
|
+
change_name_if_conflict=True,
|
|
2605
|
+
)
|
|
2606
|
+
dst_project_id = dst_project_info.id
|
|
2607
|
+
|
|
2608
|
+
datasets = self._api.dataset.get_list(src_project_id, recursive=True, include_custom_data=True)
|
|
2609
|
+
src_to_dst_ids = {}
|
|
2610
|
+
|
|
2611
|
+
for src_dataset_info in datasets:
|
|
2612
|
+
dst_dataset_info = self._api.dataset.create(
|
|
2613
|
+
dst_project_id,
|
|
2614
|
+
src_dataset_info.name,
|
|
2615
|
+
description=src_dataset_info.description,
|
|
2616
|
+
parent_id=src_to_dst_ids.get(src_dataset_info.parent_id),
|
|
2617
|
+
custom_data=src_dataset_info.custom_data,
|
|
2618
|
+
)
|
|
2619
|
+
src_to_dst_ids[src_dataset_info.id] = dst_dataset_info.id
|
|
2620
|
+
|
|
2621
|
+
yield src_dataset_info, dst_dataset_info
|
|
2622
|
+
|
|
2623
|
+
def recreate_structure(
|
|
2624
|
+
self,
|
|
2625
|
+
src_project_id: int,
|
|
2626
|
+
dst_project_id: Optional[int] = None,
|
|
2627
|
+
dst_project_name: Optional[str] = None,
|
|
2628
|
+
) -> Tuple[List[DatasetInfo], List[DatasetInfo]]:
|
|
2629
|
+
"""This method can be used to recreate a project with hierarchial datasets (without the data itself).
|
|
2630
|
+
|
|
2631
|
+
:param src_project_id: Source project ID
|
|
2632
|
+
:type src_project_id: int
|
|
2633
|
+
:param dst_project_id: Destination project ID
|
|
2634
|
+
:type dst_project_id: int, optional
|
|
2635
|
+
:param dst_project_name: Name of the destination project. If `dst_project_id` is None, a new project will be created with this name. If `dst_project_id` is provided, this parameter will be ignored.
|
|
2636
|
+
:type dst_project_name: str, optional
|
|
2637
|
+
|
|
2638
|
+
:return: Destination project ID
|
|
2639
|
+
:rtype: int
|
|
2640
|
+
|
|
2641
|
+
:Usage example:
|
|
2642
|
+
|
|
2643
|
+
.. code-block:: python
|
|
2644
|
+
|
|
2645
|
+
import supervisely as sly
|
|
2646
|
+
|
|
2647
|
+
api = sly.Api.from_env()
|
|
2648
|
+
|
|
2649
|
+
src_project_id = 123
|
|
2650
|
+
dst_project_name = "New Project"
|
|
2651
|
+
|
|
2652
|
+
dst_project_id = api.project.recreate_structure(src_project_id, dst_project_name=dst_project_name)
|
|
2653
|
+
print(f"Recreated project {src_project_id} -> {dst_project_id}")
|
|
2654
|
+
"""
|
|
2655
|
+
infos = []
|
|
2656
|
+
for src_info, dst_info in self.recreate_structure_generator(
|
|
2657
|
+
src_project_id, dst_project_id, dst_project_name
|
|
2658
|
+
):
|
|
2659
|
+
infos.append((src_info, dst_info))
|
|
2660
|
+
|
|
2661
|
+
|
|
2662
|
+
return infos
|
|
@@ -19,6 +19,10 @@ class DatasetThumbnail(Widget):
|
|
|
19
19
|
:type dataset_info: Optional[DatasetInfo]
|
|
20
20
|
:param show_project_name: if True, project name will be shown
|
|
21
21
|
:type show_project_name: Optional[bool]
|
|
22
|
+
:param remove_margins: if True, removes margins around the widget
|
|
23
|
+
:type remove_margins: bool, optional
|
|
24
|
+
:param custom_name: custom name for the dataset (if None, actual dataset name will be used)
|
|
25
|
+
:type custom_name: str, optional
|
|
22
26
|
:param widget_id: An identifier of the widget.
|
|
23
27
|
:type widget_id: str, optional
|
|
24
28
|
|
|
@@ -38,18 +42,21 @@ class DatasetThumbnail(Widget):
|
|
|
38
42
|
project_info: Optional[ProjectInfo] = None,
|
|
39
43
|
dataset_info: Optional[DatasetInfo] = None,
|
|
40
44
|
show_project_name: Optional[bool] = True,
|
|
45
|
+
remove_margins: bool = False,
|
|
46
|
+
custom_name: str = None,
|
|
41
47
|
widget_id: Optional[str] = None,
|
|
42
48
|
):
|
|
43
49
|
self._project_info: ProjectInfo = None
|
|
44
50
|
self._dataset_info: DatasetInfo = None
|
|
45
51
|
self._id: int = None
|
|
46
|
-
self._name: str =
|
|
52
|
+
self._name: str = custom_name
|
|
47
53
|
self._description: str = None
|
|
48
54
|
self._url: str = None
|
|
49
55
|
self._image_preview_url: str = None
|
|
50
56
|
self._show_project_name: bool = show_project_name
|
|
51
57
|
self._project_name: str = None
|
|
52
58
|
self._project_url: str = None
|
|
59
|
+
self._remove_margins: bool = remove_margins
|
|
53
60
|
self._set_info(project_info, dataset_info, show_project_name)
|
|
54
61
|
|
|
55
62
|
super().__init__(widget_id=widget_id, file_path=__file__)
|
|
@@ -79,6 +86,7 @@ class DatasetThumbnail(Widget):
|
|
|
79
86
|
"show_project_name": self._show_project_name,
|
|
80
87
|
"project_name": self._project_name,
|
|
81
88
|
"project_url": self._project_url,
|
|
89
|
+
"removeMargins": self._remove_margins,
|
|
82
90
|
}
|
|
83
91
|
|
|
84
92
|
def get_json_state(self) -> None:
|
|
@@ -96,7 +104,8 @@ class DatasetThumbnail(Widget):
|
|
|
96
104
|
self._project_info = project_info
|
|
97
105
|
self._dataset_info = dataset_info
|
|
98
106
|
self._id = dataset_info.id
|
|
99
|
-
self._name
|
|
107
|
+
if self._name is None:
|
|
108
|
+
self._name = dataset_info.name
|
|
100
109
|
self._description = f"{self._dataset_info.items_count} {self._project_info.type} in dataset"
|
|
101
110
|
self._url = Dataset.get_url(project_id=project_info.id, dataset_id=dataset_info.id)
|
|
102
111
|
self._image_preview_url = dataset_info.image_preview_url
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
<
|
|
1
|
+
<link rel="stylesheet" href="./sly/css/app/widgets/project_thumbnail/style.css" />
|
|
2
|
+
|
|
3
|
+
<sly-field {% if widget._remove_margins %} class="remove-margins" {% endif %} title="" :description="data.{{{widget.widget_id}}}.description">
|
|
2
4
|
<span slot="title">
|
|
3
5
|
<span v-if="data.{{{widget.widget_id}}}.show_project_name">
|
|
4
6
|
<a target="_blank" :href="data.{{{widget.widget_id}}}.project_url">
|
|
@@ -46,6 +46,21 @@ class FastTable(Widget):
|
|
|
46
46
|
:type width: str, optional
|
|
47
47
|
:param widget_id: Unique widget identifier.
|
|
48
48
|
:type widget_id: str, optional
|
|
49
|
+
:param show_header: Whether to show table header
|
|
50
|
+
:type show_header: bool, optional
|
|
51
|
+
:param is_radio: Enable radio button selection mode (single row selection)
|
|
52
|
+
:type is_radio: bool, optional
|
|
53
|
+
:param is_selectable: Enable multiple row selection
|
|
54
|
+
:type is_selectable: bool, optional
|
|
55
|
+
:param header_left_content: Widget to display in the left side of the header
|
|
56
|
+
:type header_left_content: Widget, optional
|
|
57
|
+
:param header_right_content: Widget to display in the right side of the header
|
|
58
|
+
:type header_right_content: Widget, optional
|
|
59
|
+
:param max_selected_rows: Maximum number of rows that can be selected
|
|
60
|
+
:type max_selected_rows: int, optional
|
|
61
|
+
:param search_position: Position of the search input ("left" or "right")
|
|
62
|
+
:type search_position: Literal["left", "right"], optional
|
|
63
|
+
|
|
49
64
|
|
|
50
65
|
:Usage example:
|
|
51
66
|
.. code-block:: python
|
|
@@ -141,6 +156,8 @@ class FastTable(Widget):
|
|
|
141
156
|
is_selectable: bool = False,
|
|
142
157
|
header_left_content: Optional[Widget] = None,
|
|
143
158
|
header_right_content: Optional[Widget] = None,
|
|
159
|
+
max_selected_rows: Optional[int] = None,
|
|
160
|
+
search_position: Optional[Literal["left", "right"]] = None,
|
|
144
161
|
):
|
|
145
162
|
self._supported_types = tuple([pd.DataFrame, list, type(None)])
|
|
146
163
|
self._row_click_handled = False
|
|
@@ -170,8 +187,9 @@ class FastTable(Widget):
|
|
|
170
187
|
self._searched_data = None
|
|
171
188
|
self._active_page = 1
|
|
172
189
|
self._width = width
|
|
173
|
-
self._selected_rows =
|
|
190
|
+
self._selected_rows = []
|
|
174
191
|
self._selected_cell = None
|
|
192
|
+
self._clicked_row = None
|
|
175
193
|
self._is_row_clickable = False
|
|
176
194
|
self._is_cell_clickable = False
|
|
177
195
|
self._search_str = ""
|
|
@@ -179,6 +197,9 @@ class FastTable(Widget):
|
|
|
179
197
|
self._project_meta = self._unpack_project_meta(project_meta)
|
|
180
198
|
self._header_left_content = header_left_content
|
|
181
199
|
self._header_right_content = header_right_content
|
|
200
|
+
self._max_selected_rows = max_selected_rows
|
|
201
|
+
acceptable_search_positions = ["left", "right"]
|
|
202
|
+
self._search_position = search_position if search_position in acceptable_search_positions else "left"
|
|
182
203
|
|
|
183
204
|
# table_options
|
|
184
205
|
self._page_size = page_size
|
|
@@ -234,7 +255,9 @@ class FastTable(Widget):
|
|
|
234
255
|
self._searched_data = self._search(search_value)
|
|
235
256
|
self._rows_total = len(self._searched_data)
|
|
236
257
|
|
|
237
|
-
if
|
|
258
|
+
# if active page is greater than the number of pages (e.g. after filtering)
|
|
259
|
+
max_page = (self._rows_total - 1) // self._page_size + 1
|
|
260
|
+
if (self._rows_total > 0 and self._active_page == 0) or self._active_page > max_page:
|
|
238
261
|
self._active_page = 1
|
|
239
262
|
StateJson()[self.widget_id]["page"] = self._active_page
|
|
240
263
|
|
|
@@ -260,7 +283,13 @@ class FastTable(Widget):
|
|
|
260
283
|
- isRowClickable: whether rows are clickable
|
|
261
284
|
- isCellClickable: whether cells are clickable
|
|
262
285
|
- fixColumns: number of fixed columns
|
|
286
|
+
- isRadio: whether radio button selection mode is enabled
|
|
287
|
+
- isRowSelectable: whether multiple row selection is enabled
|
|
288
|
+
- maxSelectedRows: maximum number of rows that can be selected
|
|
289
|
+
- searchPosition: position of the search input ("left" or "right")
|
|
263
290
|
- pageSize: number of rows per page
|
|
291
|
+
- showHeader: whether to show table header
|
|
292
|
+
- selectionChangedHandled: whether selection changed event listener is set
|
|
264
293
|
|
|
265
294
|
:return: Dictionary with widget data
|
|
266
295
|
:rtype: Dict[str, Any]
|
|
@@ -276,6 +305,9 @@ class FastTable(Widget):
|
|
|
276
305
|
"isCellClickable": self._is_cell_clickable,
|
|
277
306
|
"fixColumns": self._fix_columns,
|
|
278
307
|
"isRadio": self._is_radio,
|
|
308
|
+
"isRowSelectable": self._is_selectable,
|
|
309
|
+
"maxSelectedRows": self._max_selected_rows,
|
|
310
|
+
"searchPosition": self._search_position
|
|
279
311
|
},
|
|
280
312
|
"pageSize": self._page_size,
|
|
281
313
|
"showHeader": self._show_header,
|
|
@@ -286,8 +318,9 @@ class FastTable(Widget):
|
|
|
286
318
|
"""Returns dictionary with widget state.
|
|
287
319
|
Dictionary contains the following fields:
|
|
288
320
|
- search: search string
|
|
289
|
-
-
|
|
321
|
+
- selectedRows: selected rows
|
|
290
322
|
- selectedCell: selected cell
|
|
323
|
+
- clickedRow: clicked row
|
|
291
324
|
- page: active page
|
|
292
325
|
- sort: sorting options with the following fields:
|
|
293
326
|
- column: index of the column to sort by
|
|
@@ -300,6 +333,7 @@ class FastTable(Widget):
|
|
|
300
333
|
"search": self._search_str,
|
|
301
334
|
"selectedRows": self._selected_rows,
|
|
302
335
|
"selectedCell": self._selected_cell,
|
|
336
|
+
"clickedRow": self._clicked_row,
|
|
303
337
|
"page": self._active_page,
|
|
304
338
|
"sort": {
|
|
305
339
|
"column": self._sort_column_idx,
|
|
@@ -383,38 +417,95 @@ class FastTable(Widget):
|
|
|
383
417
|
filter_function = self._default_filter_function
|
|
384
418
|
self._filter_function = filter_function
|
|
385
419
|
|
|
386
|
-
def read_json(self, data: Dict, meta: Dict = None) -> None:
|
|
420
|
+
def read_json(self, data: Dict, meta: Dict = None, custom_columns: Optional[List[Union[str, tuple]]] = None) -> None:
|
|
387
421
|
"""Replace table data with options and project meta in the widget
|
|
388
422
|
|
|
389
|
-
:param data: Table data with options
|
|
423
|
+
:param data: Table data with options:
|
|
424
|
+
- data: table data
|
|
425
|
+
- columns: list of column names
|
|
426
|
+
- projectMeta: project meta information - if provided
|
|
427
|
+
- columnsOptions: list of dicts with options for each column
|
|
428
|
+
- total: total number of rows
|
|
429
|
+
- options: table options with the following fields:
|
|
430
|
+
- isRowClickable: whether rows are clickable
|
|
431
|
+
- isCellClickable: whether cells are clickable
|
|
432
|
+
- fixColumns: number of fixed columns
|
|
433
|
+
- isRadio: whether radio button selection mode is enabled
|
|
434
|
+
- isRowSelectable: whether multiple row selection is enabled
|
|
435
|
+
- maxSelectedRows: maximum number of rows that can be selected
|
|
436
|
+
- searchPosition: position of the search input ("left" or "right")
|
|
437
|
+
- pageSize: number of rows per page
|
|
438
|
+
- showHeader: whether to show table header
|
|
439
|
+
- selectionChangedHandled: whether selection changed event listener is set
|
|
440
|
+
|
|
390
441
|
:type data: dict
|
|
391
442
|
:param meta: Project meta information
|
|
392
443
|
:type meta: dict
|
|
444
|
+
:param custom_columns: List of column names. Can include widgets as tuples (column_name, widget)
|
|
445
|
+
:type custom_columns: List[Union[str, tuple]], optional
|
|
446
|
+
|
|
447
|
+
Example of data dict:
|
|
448
|
+
.. code-block:: python
|
|
449
|
+
|
|
450
|
+
data = {
|
|
451
|
+
"data": [["apple", "21"], ["banana", "15"]],
|
|
452
|
+
"columns": ["Class", "Items"],
|
|
453
|
+
"columnsOptions": [
|
|
454
|
+
{ "type": "class"},
|
|
455
|
+
{ "maxValue": 21, "postfix": "pcs", "tooltip": "description text", "subtitle": "boxes" }
|
|
456
|
+
],
|
|
457
|
+
"options": {
|
|
458
|
+
"isRowClickable": True,
|
|
459
|
+
"isCellClickable": True,
|
|
460
|
+
"fixColumns": 1,
|
|
461
|
+
"isRadio": False,
|
|
462
|
+
"isRowSelectable": True,
|
|
463
|
+
"maxSelectedRows": 5,
|
|
464
|
+
"searchPosition": "right",
|
|
465
|
+
"sort": {"column": 0, "order": "asc"},
|
|
466
|
+
},
|
|
467
|
+
}
|
|
393
468
|
"""
|
|
394
|
-
self._columns_first_idx = self._prepare_json_data(data, "columns")
|
|
395
469
|
self._columns_options = self._prepare_json_data(data, "columnsOptions")
|
|
470
|
+
self._read_custom_columns(custom_columns)
|
|
471
|
+
if not self._columns_first_idx:
|
|
472
|
+
self._columns_first_idx = self._prepare_json_data(data, "columns")
|
|
396
473
|
self._table_options = self._prepare_json_data(data, "options")
|
|
397
474
|
self._project_meta = self._unpack_project_meta(meta)
|
|
398
|
-
|
|
399
|
-
self.
|
|
400
|
-
self.
|
|
401
|
-
|
|
475
|
+
table_data = data.get("data", None)
|
|
476
|
+
self._validate_input_data(table_data)
|
|
477
|
+
self._source_data = self._prepare_input_data(table_data)
|
|
478
|
+
(
|
|
479
|
+
self._parsed_source_data,
|
|
480
|
+
self._sliced_data,
|
|
481
|
+
self._parsed_active_data,
|
|
482
|
+
) = self._prepare_working_data()
|
|
402
483
|
self._rows_total = len(self._parsed_source_data["data"])
|
|
403
484
|
init_options = DataJson()[self.widget_id]["options"]
|
|
404
485
|
init_options.update(self._table_options)
|
|
405
486
|
sort = init_options.pop("sort", {"column": None, "order": None})
|
|
406
|
-
|
|
487
|
+
self._active_page = 1
|
|
488
|
+
self._sort_column_idx = sort.get("column", None)
|
|
489
|
+
if self._sort_column_idx is not None and self._sort_column_idx > len(self._columns_first_idx) - 1:
|
|
490
|
+
self._sort_column_idx = None
|
|
491
|
+
self._sort_order = sort.get("order", None)
|
|
492
|
+
self._page_size = init_options.pop("pageSize", 10)
|
|
407
493
|
DataJson()[self.widget_id]["data"] = self._parsed_active_data["data"]
|
|
408
494
|
DataJson()[self.widget_id]["columns"] = self._parsed_active_data["columns"]
|
|
409
495
|
DataJson()[self.widget_id]["columnsOptions"] = self._columns_options
|
|
410
496
|
DataJson()[self.widget_id]["options"] = init_options
|
|
411
497
|
DataJson()[self.widget_id]["total"] = len(self._source_data)
|
|
412
|
-
DataJson()[self.widget_id]["pageSize"] =
|
|
498
|
+
DataJson()[self.widget_id]["pageSize"] = self._page_size
|
|
413
499
|
DataJson()[self.widget_id]["projectMeta"] = self._project_meta
|
|
414
|
-
StateJson()[self.widget_id]["sort"] =
|
|
500
|
+
StateJson()[self.widget_id]["sort"]["column"] = self._sort_column_idx
|
|
501
|
+
StateJson()[self.widget_id]["sort"]["order"] = self._sort_order
|
|
502
|
+
StateJson()[self.widget_id]["page"] = self._active_page
|
|
503
|
+
StateJson()[self.widget_id]["selectedRows"] = []
|
|
504
|
+
StateJson()[self.widget_id]["selectedCell"] = None
|
|
505
|
+
self._maybe_update_selected_row()
|
|
506
|
+
self._validate_sort_attrs()
|
|
415
507
|
DataJson().send_changes()
|
|
416
508
|
StateJson().send_changes()
|
|
417
|
-
self.clear_selection()
|
|
418
509
|
|
|
419
510
|
def read_pandas(self, data: pd.DataFrame) -> None:
|
|
420
511
|
"""Replace table data (rows and columns) in the widget.
|
|
@@ -440,6 +531,24 @@ class FastTable(Widget):
|
|
|
440
531
|
def to_json(self, active_page: Optional[bool] = False) -> Dict[str, Any]:
|
|
441
532
|
"""Export table data with current options as dict.
|
|
442
533
|
|
|
534
|
+
Dictionary contains the following fields:
|
|
535
|
+
- data: table data
|
|
536
|
+
- columns: list of column names
|
|
537
|
+
- projectMeta: project meta information - if provided
|
|
538
|
+
- columnsOptions: list of dicts with options for each column
|
|
539
|
+
- total: total number of rows
|
|
540
|
+
- options: table options with the following fields:
|
|
541
|
+
- isRowClickable: whether rows are clickable
|
|
542
|
+
- isCellClickable: whether cells are clickable
|
|
543
|
+
- fixColumns: number of fixed columns
|
|
544
|
+
- isRadio: whether radio button selection mode is enabled
|
|
545
|
+
- isRowSelectable: whether multiple row selection is enabled
|
|
546
|
+
- maxSelectedRows: maximum number of rows that can be selected
|
|
547
|
+
- searchPosition: position of the search input ("left" or "right")
|
|
548
|
+
- pageSize: number of rows per page
|
|
549
|
+
- showHeader: whether to show table header
|
|
550
|
+
- selectionChangedHandled: whether selection changed event listener is set
|
|
551
|
+
|
|
443
552
|
:param active_page: Specifies the size of the data to be exported. If True - returns only the active page of the table
|
|
444
553
|
:type active_page: Optional[bool]
|
|
445
554
|
:return: Table data with current options
|
|
@@ -477,7 +586,7 @@ class FastTable(Widget):
|
|
|
477
586
|
|
|
478
587
|
def clear_selection(self) -> None:
|
|
479
588
|
"""Clears the selection of the table."""
|
|
480
|
-
StateJson()[self.widget_id]["selectedRows"] =
|
|
589
|
+
StateJson()[self.widget_id]["selectedRows"] = []
|
|
481
590
|
StateJson()[self.widget_id]["selectedCell"] = None
|
|
482
591
|
StateJson().send_changes()
|
|
483
592
|
self._maybe_update_selected_row()
|
|
@@ -558,15 +667,15 @@ class FastTable(Widget):
|
|
|
558
667
|
if self._rows_total != 0:
|
|
559
668
|
self.select_row(0)
|
|
560
669
|
else:
|
|
561
|
-
self._selected_rows =
|
|
562
|
-
StateJson()[self.widget_id]["selectedRows"] =
|
|
670
|
+
self._selected_rows = []
|
|
671
|
+
StateJson()[self.widget_id]["selectedRows"] = self._selected_rows
|
|
563
672
|
StateJson().send_changes()
|
|
564
673
|
return
|
|
565
674
|
if not self._selected_rows:
|
|
566
675
|
return
|
|
567
676
|
if self._rows_total == 0:
|
|
568
|
-
self._selected_rows =
|
|
569
|
-
StateJson()[self.widget_id]["selectedRows"] =
|
|
677
|
+
self._selected_rows = []
|
|
678
|
+
StateJson()[self.widget_id]["selectedRows"] = self._selected_rows
|
|
570
679
|
StateJson().send_changes()
|
|
571
680
|
return
|
|
572
681
|
if self._is_selectable:
|
|
@@ -1115,8 +1224,12 @@ class FastTable(Widget):
|
|
|
1115
1224
|
|
|
1116
1225
|
@server.post(selection_changed_route_path)
|
|
1117
1226
|
def _selection_changed():
|
|
1118
|
-
|
|
1119
|
-
|
|
1227
|
+
if self._is_radio:
|
|
1228
|
+
selected_row = self.get_selected_row()
|
|
1229
|
+
func(selected_row)
|
|
1230
|
+
elif self._is_selectable:
|
|
1231
|
+
selected_rows = self.get_selected_rows()
|
|
1232
|
+
func(selected_rows)
|
|
1120
1233
|
|
|
1121
1234
|
self._selection_changed_handled = True
|
|
1122
1235
|
DataJson()[self.widget_id]["selectionChangedHandled"] = True
|
|
@@ -1201,3 +1314,25 @@ class FastTable(Widget):
|
|
|
1201
1314
|
|
|
1202
1315
|
idxs = self._source_data[self._source_data[column].isin(values)].index.tolist()
|
|
1203
1316
|
self.select_rows(idxs)
|
|
1317
|
+
|
|
1318
|
+
def _read_custom_columns(self, columns: List[Union[str, tuple]]) -> None:
|
|
1319
|
+
if not columns:
|
|
1320
|
+
return
|
|
1321
|
+
self._columns = columns
|
|
1322
|
+
self._columns_options = self._columns_options or [{} for _ in columns]
|
|
1323
|
+
self._columns_data = []
|
|
1324
|
+
self._columns_first_idx = []
|
|
1325
|
+
for i, col in enumerate(columns):
|
|
1326
|
+
if isinstance(col, str):
|
|
1327
|
+
self._columns_first_idx.append(col)
|
|
1328
|
+
self._columns_data.append(self.ColumnData(name=col))
|
|
1329
|
+
elif isinstance(col, tuple):
|
|
1330
|
+
self._columns_first_idx.append(col[0])
|
|
1331
|
+
self._columns_data.append(
|
|
1332
|
+
self.ColumnData(name=col[0], is_widget=True, widget=col[1])
|
|
1333
|
+
)
|
|
1334
|
+
self._columns_options[i]["customCell"] = True
|
|
1335
|
+
else:
|
|
1336
|
+
raise TypeError(f"Column name must be a string or a tuple, got {type(col)}")
|
|
1337
|
+
|
|
1338
|
+
self._validate_sort_attrs()
|
|
@@ -17,7 +17,7 @@ Vue.component('fast-table', {
|
|
|
17
17
|
style="flex-grow: 1"
|
|
18
18
|
>
|
|
19
19
|
<slot name="header-left-side-start" />
|
|
20
|
-
<div class="relative w-full md:max-w-[14rem]">
|
|
20
|
+
<div v-if="settings.searchPosition !== 'right'" class="relative w-full md:max-w-[14rem]">
|
|
21
21
|
<i class="zmdi zmdi-search h-4 absolute top-2 left-2.5 opacity-50" />
|
|
22
22
|
<i
|
|
23
23
|
v-if="search"
|
|
@@ -35,6 +35,22 @@ Vue.component('fast-table', {
|
|
|
35
35
|
</div>
|
|
36
36
|
<slot name="header-left-side-end" />
|
|
37
37
|
</div>
|
|
38
|
+
<div v-if="settings.searchPosition === 'right'" class="relative w-full md:max-w-[14rem]">
|
|
39
|
+
<i class="zmdi zmdi-search h-4 absolute top-2 left-2.5 opacity-50" />
|
|
40
|
+
<i
|
|
41
|
+
v-if="search"
|
|
42
|
+
class="zmdi zmdi-close h-4 absolute top-2.5 right-3 opacity-50 cursor-pointer"
|
|
43
|
+
@click="searchChanged('')"
|
|
44
|
+
/>
|
|
45
|
+
<input
|
|
46
|
+
:value="search"
|
|
47
|
+
type="text"
|
|
48
|
+
:placeholder="\`\${name ? \`Search for \${name}...\` : 'Search'}\`"
|
|
49
|
+
class="text-sm rounded-md px-3 py-1.5 text-gray-900 shadow-sm placeholder:text-gray-400 border border-slate-200 w-full pl-8 bg-slate-50"
|
|
50
|
+
@input="searchChanged($event.target.value)"
|
|
51
|
+
@keydown.esc="searchChanged('')"
|
|
52
|
+
>
|
|
53
|
+
</div>
|
|
38
54
|
<div
|
|
39
55
|
v-if="data && data.length"
|
|
40
56
|
class="text-[.9rem] text-slate-500 flex items-center gap-0 w-full md:w-auto whitespace-nowrap"
|
|
@@ -73,6 +89,7 @@ Vue.component('fast-table', {
|
|
|
73
89
|
<table
|
|
74
90
|
ref="tableBox"
|
|
75
91
|
class="w-full text-[.8rem] md:text-[.9rem] mb-1"
|
|
92
|
+
:key="'table-' + page"
|
|
76
93
|
>
|
|
77
94
|
<thead>
|
|
78
95
|
<tr>
|
|
@@ -84,17 +101,25 @@ Vue.component('fast-table', {
|
|
|
84
101
|
>
|
|
85
102
|
</th>
|
|
86
103
|
<th
|
|
87
|
-
v-
|
|
104
|
+
v-if="settings.isRowSelectable && (!settings.maxSelectedRows || settings.maxSelectedRows > 1)"
|
|
88
105
|
class="px-2 md:px-3 py-2.5 whitespace-nowrap first:pl-3 last:pr-3 md:last:pr-6 first:text-left cursor-pointer sticky top-0 bg-slate-50 box-content shadow-[inset_0_-2px_0_#dfe6ec] group"
|
|
89
106
|
:class="{ 'first:sticky first:left-0 first:z-20 first:shadow-[inset_-2px_-2px_0_#dfe6ec]': fixColumns }"
|
|
90
107
|
>
|
|
91
108
|
<el-checkbox
|
|
92
|
-
v-if="settings.isRowSelectable"
|
|
109
|
+
v-if="settings.isRowSelectable && (!settings.maxSelectedRows || settings.maxSelectedRows > 1)"
|
|
93
110
|
size="small"
|
|
94
|
-
|
|
95
|
-
|
|
111
|
+
v-model="headerCheckboxModel"
|
|
112
|
+
:indeterminate="isSomeOnPageSelected && !isAllOnPageSelected"
|
|
113
|
+
:key="'headercb-' + page + '-' + (data && data.map(r => r.idx).join(',')) + '-' + (selectedRows && selectedRows.map(r => r.idx).join(',')) + '-' + (isAllOnPageSelected?1:0) + '-' + (isSomeOnPageSelected?1:0)"
|
|
114
|
+
style="transform: scale(1.2);"
|
|
96
115
|
/>
|
|
97
116
|
</th>
|
|
117
|
+
<th
|
|
118
|
+
v-if="settings.isRowSelectable && settings.maxSelectedRows === 1"
|
|
119
|
+
class="px-2 md:px-3 py-2.5 whitespace-nowrap first:pl-3 last:pr-3 md:last:pr-6 first:text-left cursor-pointer sticky top-0 bg-slate-50 box-content shadow-[inset_0_-2px_0_#dfe6ec] group"
|
|
120
|
+
:class="{ 'first:sticky first:left-0 first:z-20 first:shadow-[inset_-2px_-2px_0_#dfe6ec]': fixColumns }"
|
|
121
|
+
>
|
|
122
|
+
</th>
|
|
98
123
|
<th
|
|
99
124
|
v-for="(c,idx) in columns.slice(0, columnNumberLimit)"
|
|
100
125
|
class="px-2 md:px-3 py-2.5 whitespace-nowrap first:pl-3 last:pr-3 md:first:pl-6 md:last:pr-6 first:text-left cursor-pointer sticky top-0 bg-slate-50 box-content shadow-[inset_0_-2px_0_#dfe6ec] group"
|
|
@@ -140,12 +165,13 @@ Vue.component('fast-table', {
|
|
|
140
165
|
</th>
|
|
141
166
|
</tr>
|
|
142
167
|
</thead>
|
|
143
|
-
<tbody>
|
|
168
|
+
<tbody :key="'page-' + page">
|
|
144
169
|
<tr
|
|
145
170
|
v-for="row in data"
|
|
171
|
+
:key="'row-' + rowKeyValue(row)"
|
|
146
172
|
class="border-b border-gray-200 last:border-0 group"
|
|
147
173
|
:class="{ 'cursor-pointer': settings.isRowClickable }"
|
|
148
|
-
@click="settings.isRowClickable && $emit('row-click', { idx: row
|
|
174
|
+
@click="settings.isRowClickable && $emit('row-click', { idx: rowKeyValue(row), row: row.items, columnsSettings })"
|
|
149
175
|
>
|
|
150
176
|
<td
|
|
151
177
|
v-if="settings.isRadio"
|
|
@@ -169,8 +195,11 @@ Vue.component('fast-table', {
|
|
|
169
195
|
>
|
|
170
196
|
<el-checkbox
|
|
171
197
|
size="small"
|
|
172
|
-
|
|
173
|
-
@
|
|
198
|
+
v-model="rowCheckboxModel[rowKeyValue(row)]"
|
|
199
|
+
@change="onRowCheckboxChange(row, rowCheckboxModel[rowKeyValue(row)])"
|
|
200
|
+
@click.stop
|
|
201
|
+
:key="'rowcb-' + page + '-' + rowKeyValue(row)"
|
|
202
|
+
style="transform: scale(1.2);"
|
|
174
203
|
/>
|
|
175
204
|
</td>
|
|
176
205
|
|
|
@@ -314,11 +343,16 @@ Vue.component('fast-table', {
|
|
|
314
343
|
type: Boolean,
|
|
315
344
|
default: false,
|
|
316
345
|
},
|
|
346
|
+
rowKey: {
|
|
347
|
+
type: String,
|
|
348
|
+
default: 'idx',
|
|
349
|
+
},
|
|
317
350
|
},
|
|
318
351
|
|
|
319
352
|
data() {
|
|
320
353
|
return {
|
|
321
354
|
columnNumberLimit: 50,
|
|
355
|
+
rowCheckboxModel: {},
|
|
322
356
|
};
|
|
323
357
|
},
|
|
324
358
|
|
|
@@ -342,6 +376,7 @@ Vue.component('fast-table', {
|
|
|
342
376
|
settings() {
|
|
343
377
|
const defaultSettings = {
|
|
344
378
|
showHeaderControls: true,
|
|
379
|
+
searchPosition: 'left',
|
|
345
380
|
};
|
|
346
381
|
return {
|
|
347
382
|
...defaultSettings,
|
|
@@ -353,10 +388,6 @@ Vue.component('fast-table', {
|
|
|
353
388
|
return this.pageSize || 50;
|
|
354
389
|
},
|
|
355
390
|
|
|
356
|
-
selectedRowsById() {
|
|
357
|
-
return _.keyBy(this.selectedRows, 'id');
|
|
358
|
-
},
|
|
359
|
-
|
|
360
391
|
classesMap() {
|
|
361
392
|
return this.projectMeta ? _.keyBy(this.projectMeta.classes, 'title') : {};
|
|
362
393
|
},
|
|
@@ -373,15 +404,53 @@ Vue.component('fast-table', {
|
|
|
373
404
|
return this.columnsSettings.find(i => i?.subtitle);
|
|
374
405
|
},
|
|
375
406
|
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
407
|
+
isAllOnPageSelected() {
|
|
408
|
+
const rows = this.data || [];
|
|
409
|
+
if (rows.length === 0) return false;
|
|
410
|
+
const selectedIdx = new Set((this.selectedRows || []).map(r => this.rowKeyValue(r)));
|
|
411
|
+
return rows.every(r => selectedIdx.has(this.rowKeyValue(r)));
|
|
412
|
+
},
|
|
413
|
+
|
|
414
|
+
isSomeOnPageSelected() {
|
|
415
|
+
const rows = this.data || [];
|
|
416
|
+
if (rows.length === 0) return false;
|
|
417
|
+
const selectedIdx = new Set((this.selectedRows || []).map(r => this.rowKeyValue(r)));
|
|
418
|
+
const count = rows.filter(r => selectedIdx.has(this.rowKeyValue(r))).length;
|
|
419
|
+
return count > 0 && count < rows.length;
|
|
420
|
+
},
|
|
421
|
+
|
|
422
|
+
selectedIdxSet() {
|
|
423
|
+
return new Set((this.selectedRows || []).map(r => this.rowKeyValue(r)));
|
|
379
424
|
},
|
|
425
|
+
|
|
426
|
+
headerCheckboxModel: {
|
|
427
|
+
get() {
|
|
428
|
+
return this.isAllOnPageSelected;
|
|
429
|
+
},
|
|
430
|
+
set(val) {
|
|
431
|
+
this.selectAllRows(val);
|
|
432
|
+
}
|
|
433
|
+
},
|
|
434
|
+
},
|
|
435
|
+
|
|
436
|
+
watch: {
|
|
437
|
+
data: {
|
|
438
|
+
immediate: true,
|
|
439
|
+
handler() {
|
|
440
|
+
this.syncRowCheckboxModel();
|
|
441
|
+
}
|
|
442
|
+
},
|
|
443
|
+
selectedRows: {
|
|
444
|
+
immediate: true,
|
|
445
|
+
handler() {
|
|
446
|
+
this.syncRowCheckboxModel();
|
|
447
|
+
}
|
|
448
|
+
}
|
|
380
449
|
},
|
|
381
450
|
|
|
382
451
|
methods: {
|
|
383
452
|
_updateData() {
|
|
384
|
-
this.$emit('filters-changed')
|
|
453
|
+
this.$emit('filters-changed')
|
|
385
454
|
},
|
|
386
455
|
|
|
387
456
|
searchChanged(val) {
|
|
@@ -409,31 +478,85 @@ Vue.component('fast-table', {
|
|
|
409
478
|
this.updateData();
|
|
410
479
|
},
|
|
411
480
|
|
|
412
|
-
selectAllRows() {
|
|
413
|
-
if (this.
|
|
414
|
-
|
|
415
|
-
|
|
481
|
+
selectAllRows(checked) {
|
|
482
|
+
if (!this.data || this.data.length === 0) return;
|
|
483
|
+
|
|
484
|
+
const current = Array.isArray(this.selectedRows) ? [...this.selectedRows] : [];
|
|
485
|
+
const max = this.settings && this.settings.maxSelectedRows ? this.settings.maxSelectedRows : 0;
|
|
486
|
+
|
|
487
|
+
if (checked) {
|
|
488
|
+
const selectedIdx = new Set(current.map(r => this.rowKeyValue(r)));
|
|
489
|
+
const result = [...current];
|
|
490
|
+
|
|
491
|
+
if (max && max > 0) {
|
|
492
|
+
let quota = Math.max(0, max - result.length);
|
|
493
|
+
for (const r of this.data) {
|
|
494
|
+
const key = this.rowKeyValue(r);
|
|
495
|
+
if (!selectedIdx.has(key)) {
|
|
496
|
+
if (quota <= 0) break;
|
|
497
|
+
result.push(_.cloneDeep(r));
|
|
498
|
+
selectedIdx.add(key);
|
|
499
|
+
quota -= 1;
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
this.$emit('update:selected-rows', result);
|
|
503
|
+
} else {
|
|
504
|
+
for (const r of this.data) {
|
|
505
|
+
const key = this.rowKeyValue(r);
|
|
506
|
+
if (!selectedIdx.has(key)) {
|
|
507
|
+
result.push(_.cloneDeep(r));
|
|
508
|
+
selectedIdx.add(key);
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
this.$emit('update:selected-rows', result);
|
|
512
|
+
}
|
|
416
513
|
} else {
|
|
417
|
-
const
|
|
418
|
-
|
|
419
|
-
this
|
|
420
|
-
if (this.selectedRowsById[row.id]) return;
|
|
421
|
-
|
|
422
|
-
selected.push(row);
|
|
423
|
-
});
|
|
424
|
-
|
|
425
|
-
this.$emit('update:selected-rows', selected);
|
|
514
|
+
const pageIdx = new Set(this.data.map(r => this.rowKeyValue(r)));
|
|
515
|
+
const result = current.filter(r => !pageIdx.has(this.rowKeyValue(r)));
|
|
516
|
+
this.$emit('update:selected-rows', result);
|
|
426
517
|
}
|
|
427
518
|
},
|
|
428
519
|
|
|
429
|
-
|
|
430
|
-
const
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
520
|
+
syncRowCheckboxModel() {
|
|
521
|
+
const map = {};
|
|
522
|
+
const selected = new Set((this.selectedRows || []).map(r => this.rowKeyValue(r)));
|
|
523
|
+
for (const r of (this.data || [])) {
|
|
524
|
+
const key = this.rowKeyValue(r);
|
|
525
|
+
map[key] = selected.has(key);
|
|
526
|
+
}
|
|
527
|
+
this.rowCheckboxModel = map;
|
|
528
|
+
},
|
|
529
|
+
|
|
530
|
+
onRowCheckboxChange(row, checked) {
|
|
531
|
+
this.updateSelectedRows(row, checked);
|
|
532
|
+
},
|
|
533
|
+
|
|
534
|
+
updateSelectedRows(row, checked) {
|
|
535
|
+
checked = typeof checked === 'boolean' ? checked : !!checked;
|
|
536
|
+
const current = Array.isArray(this.selectedRows) ? [...this.selectedRows] : [];
|
|
537
|
+
const key = this.rowKeyValue(row);
|
|
538
|
+
const exists = current.some(r => this.rowKeyValue(r) === key);
|
|
539
|
+
const max = this.settings && this.settings.maxSelectedRows ? this.settings.maxSelectedRows : 0;
|
|
540
|
+
|
|
541
|
+
let result = current;
|
|
542
|
+
if (checked) {
|
|
543
|
+
if (max && max > 0) {
|
|
544
|
+
if (max === 1) {
|
|
545
|
+
result = [_.cloneDeep(row)];
|
|
546
|
+
this.$emit('update:selected-rows', result);
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
549
|
+
if (!exists && current.length >= max) {
|
|
550
|
+
// revert checkbox state if over the limit
|
|
551
|
+
this.$nextTick(() => { this.$set(this.rowCheckboxModel, key, false); });
|
|
552
|
+
return;
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
if (!exists) result = [...current, _.cloneDeep(row)];
|
|
434
556
|
} else {
|
|
435
|
-
|
|
557
|
+
if (exists) result = current.filter(r => this.rowKeyValue(r) !== key);
|
|
436
558
|
}
|
|
559
|
+
this.$emit('update:selected-rows', result);
|
|
437
560
|
},
|
|
438
561
|
|
|
439
562
|
updateSelectedRadio(row) {
|
|
@@ -453,6 +576,12 @@ Vue.component('fast-table', {
|
|
|
453
576
|
if (!this.search) return text;
|
|
454
577
|
return text.toString().replace(new RegExp(this.search, 'gi'), match => '<span class="bg-yellow-400">'+match+'</span>');
|
|
455
578
|
},
|
|
579
|
+
|
|
580
|
+
rowKeyValue(row) {
|
|
581
|
+
if (!row) return undefined;
|
|
582
|
+
const keyName = this.rowKey || 'idx';
|
|
583
|
+
return row[keyName] != null ? row[keyName] : row.idx;
|
|
584
|
+
},
|
|
456
585
|
},
|
|
457
586
|
|
|
458
587
|
mounted() {
|
|
@@ -704,8 +704,38 @@
|
|
|
704
704
|
border-width: 1px;
|
|
705
705
|
}
|
|
706
706
|
|
|
707
|
-
.
|
|
708
|
-
|
|
707
|
+
.tailwind .el-checkbox__inner {
|
|
708
|
+
--tw-border-opacity: 1;
|
|
709
|
+
border-width: 1px;
|
|
710
|
+
border-color: rgb(226 232 240 / var(--tw-border-opacity));
|
|
711
|
+
}
|
|
712
|
+
.tailwind .el-checkbox__input.is-checked .el-checkbox__inner,
|
|
713
|
+
.tailwind .el-checkbox__input.is-indeterminate .el-checkbox__inner {
|
|
714
|
+
--tw-bg-opacity: 1;
|
|
715
|
+
background-color: rgb(14 165 233 / var(--tw-bg-opacity));
|
|
716
|
+
border-color: rgb(14 165 233 / var(--tw-bg-opacity));
|
|
717
|
+
}
|
|
718
|
+
/* refine the checkmark so it's thinner and not a full box */
|
|
719
|
+
.tailwind .el-checkbox__inner::after {
|
|
720
|
+
border-left: 0 !important;
|
|
721
|
+
border-top: 0 !important;
|
|
722
|
+
}
|
|
723
|
+
.tailwind .el-checkbox__input.is-checked .el-checkbox__inner::after {
|
|
724
|
+
border-right: 1.5px solid #fff;
|
|
725
|
+
border-bottom: 1.5px solid #fff;
|
|
726
|
+
border-left: 0 !important;
|
|
727
|
+
border-top: 0 !important;
|
|
728
|
+
}
|
|
729
|
+
.tailwind .el-checkbox__input.is-indeterminate .el-checkbox__inner::before {
|
|
730
|
+
background-color: #fff;
|
|
731
|
+
height: 2px;
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
.tailwind .el-checkbox__inner::after {
|
|
735
|
+
left: 4px;
|
|
736
|
+
top: 1px;
|
|
737
|
+
width: 4px;
|
|
738
|
+
height: 8px;
|
|
709
739
|
}
|
|
710
740
|
|
|
711
741
|
.sly-fast-table-disable-overlay {
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
if
|
|
25
25
|
widget._row_click_handled
|
|
26
26
|
%}
|
|
27
|
-
@row-click="state.{{{widget.widget_id}}}.
|
|
27
|
+
@row-click="state.{{{widget.widget_id}}}.clickedRow = $event; post('/{{{widget.widget_id}}}/row_clicked_cb')"
|
|
28
28
|
{%
|
|
29
29
|
endif
|
|
30
30
|
%}
|
supervisely/versions.json
CHANGED
|
@@ -4,7 +4,7 @@ supervisely/_utils.py,sha256=59vOeNOnmVHODnlAvULT8jdwvHHVTs2FOtAFw8mvaqE,20643
|
|
|
4
4
|
supervisely/function_wrapper.py,sha256=R5YajTQ0GnRp2vtjwfC9hINkzQc0JiyGsu8TER373xY,1912
|
|
5
5
|
supervisely/sly_logger.py,sha256=z92Vu5hmC0GgTIJO1n6kPDayRW9__8ix8hL6poDZj-Y,6274
|
|
6
6
|
supervisely/tiny_timer.py,sha256=hkpe_7FE6bsKL79blSs7WBaktuPavEVu67IpEPrfmjE,183
|
|
7
|
-
supervisely/versions.json,sha256=
|
|
7
|
+
supervisely/versions.json,sha256=J7V7XPpDno0fvmNaAXXcxNksXqpRFaYRrG4POC0UDKQ,608
|
|
8
8
|
supervisely/annotation/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
9
9
|
supervisely/annotation/annotation.py,sha256=th4gsIU-LNGcMRohHrtupmjxDwdzx1g4_0xIAa6NyJU,114717
|
|
10
10
|
supervisely/annotation/annotation_transforms.py,sha256=TlVy_gUbM-XH6GbLpZPrAi6pMIGTr7Ow02iSKOSTa-I,9582
|
|
@@ -26,7 +26,7 @@ supervisely/api/annotation_api.py,sha256=JdcCKuy_7PvtNcHsRhi4ANhn3aynLY44rtbqau2
|
|
|
26
26
|
supervisely/api/api.py,sha256=gRItQzO6Xj7k_pJIpVUS2dV1vkTTHV25_1Uia6xxZSc,67930
|
|
27
27
|
supervisely/api/app_api.py,sha256=Q6XxLxp3D_Vc3PIVyBmP7wJtTLbgYCPNOLND5UvJhMw,79010
|
|
28
28
|
supervisely/api/constants.py,sha256=WfqIcEpRnU4Mcfb6q0njeRs2VVSoTAJaIyrqBkBjP8I,253
|
|
29
|
-
supervisely/api/dataset_api.py,sha256=
|
|
29
|
+
supervisely/api/dataset_api.py,sha256=BD6kG2lj826ajWjHxmiKEsyWb2Ov6CyTQlItzAxADbo,48955
|
|
30
30
|
supervisely/api/entities_collection_api.py,sha256=Be13HsfMFLmq9XpiOfQog0Y569kbUn52hXv6x5vX3Vg,22624
|
|
31
31
|
supervisely/api/file_api.py,sha256=gNXNsikocSYRojoZrVmXIqXycqXm0e320piAwaLN6JI,92978
|
|
32
32
|
supervisely/api/github_api.py,sha256=NIexNjEer9H5rf5sw2LEZd7C1WR-tK4t6IZzsgeAAwQ,623
|
|
@@ -39,7 +39,7 @@ supervisely/api/labeling_queue_api.py,sha256=ilNjAL1d9NSa9yabQn6E-W26YdtooT3ZGXI
|
|
|
39
39
|
supervisely/api/module_api.py,sha256=8Asdr3llhC8XQ98xGdR03hpe2GYznJONzNfgN-mQYv8,46458
|
|
40
40
|
supervisely/api/object_class_api.py,sha256=7-npNFMYjWNtSXYZg6syc6bX56_oCzDU2kFRPGQWCwA,10399
|
|
41
41
|
supervisely/api/plugin_api.py,sha256=SFm0IlTTOjuHBLUMgG4d4k6U3cWJocE-SVb-f08fwMQ,5286
|
|
42
|
-
supervisely/api/project_api.py,sha256=
|
|
42
|
+
supervisely/api/project_api.py,sha256=R4KjWUiO1pLQhsd6hDTgRWjaSdEiSE_bFI8UZ9Lo3pQ,101688
|
|
43
43
|
supervisely/api/project_class_api.py,sha256=5cyjdGPPb2tpttu5WmYoOxUNiDxqiojschkhZumF0KM,1426
|
|
44
44
|
supervisely/api/remote_storage_api.py,sha256=1O4rTIwW8s9gxC00yvFuKbEMGNsa7YSRlZ8j494ARwY,17793
|
|
45
45
|
supervisely/api/report_api.py,sha256=Om7CGulUbQ4BuJ16eDtz7luLe0JQNqab-LoLpUXu7YE,7123
|
|
@@ -216,8 +216,8 @@ supervisely/app/widgets/custom_models_selector/custom_models_selector.py,sha256=
|
|
|
216
216
|
supervisely/app/widgets/custom_models_selector/style.css,sha256=-zPPXHnJvatYj_xVVAb7T8uoSsUTyhm5xCKWkkFQ78E,548
|
|
217
217
|
supervisely/app/widgets/custom_models_selector/template.html,sha256=Ot7dluekJEUtBWYezAVmcg6PbcEtb2_MRaTBhEnvRjk,2888
|
|
218
218
|
supervisely/app/widgets/dataset_thumbnail/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
219
|
-
supervisely/app/widgets/dataset_thumbnail/dataset_thumbnail.py,sha256=
|
|
220
|
-
supervisely/app/widgets/dataset_thumbnail/template.html,sha256=
|
|
219
|
+
supervisely/app/widgets/dataset_thumbnail/dataset_thumbnail.py,sha256=C-PaA9TYTs_AEC-rZr789ww6NMNkBOgZOSKkXHqgs1k,5106
|
|
220
|
+
supervisely/app/widgets/dataset_thumbnail/template.html,sha256=zQZrSLeqbUGR3W0XxuztvEgS1ndnzxvhAhC1JbgmVtk,828
|
|
221
221
|
supervisely/app/widgets/date_picker/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
222
222
|
supervisely/app/widgets/date_picker/date_picker.py,sha256=76tXh9OamTk7lz7aQJvKli7T-vbadULs8DpkndlvbeM,8213
|
|
223
223
|
supervisely/app/widgets/date_picker/template.html,sha256=bQ8y6W7ZIXxQ3xRH6SlARBSAhHMOjDZqMddbdOZ69YM,699
|
|
@@ -266,10 +266,10 @@ supervisely/app/widgets/empty/template.html,sha256=aDBKkin5aLuqByzNN517-rTYCGIg5
|
|
|
266
266
|
supervisely/app/widgets/experiment_selector/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
267
267
|
supervisely/app/widgets/experiment_selector/experiment_selector.py,sha256=9UseS99hb0ln4QLSeSIfXM7-pIpqLyWsyWoYQsELlnM,28402
|
|
268
268
|
supervisely/app/widgets/fast_table/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
269
|
-
supervisely/app/widgets/fast_table/fast_table.py,sha256=
|
|
270
|
-
supervisely/app/widgets/fast_table/script.js,sha256=
|
|
271
|
-
supervisely/app/widgets/fast_table/style.css,sha256=
|
|
272
|
-
supervisely/app/widgets/fast_table/template.html,sha256=
|
|
269
|
+
supervisely/app/widgets/fast_table/fast_table.py,sha256=OlClGdTcERueYBNR8uITYQa-8mwNT53NeIlJokebRN0,56293
|
|
270
|
+
supervisely/app/widgets/fast_table/script.js,sha256=V0l3hlRjejsVM6lSYDF6b79NZgegSdLKH0RBrhumnPU,22728
|
|
271
|
+
supervisely/app/widgets/fast_table/style.css,sha256=DwVGIX4Hap_rG7D570pfyFRAUSMtfHUZEQYs9C6JrAY,16685
|
|
272
|
+
supervisely/app/widgets/fast_table/template.html,sha256=KTeZs6F8bUYOVZ58D7C4yrDTXKB9wvqEVT4RgRZr4sk,2787
|
|
273
273
|
supervisely/app/widgets/field/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
274
274
|
supervisely/app/widgets/field/field.py,sha256=FqBqfIEcUK7R3va30YS_zijWTUSD5QT3-kpLKJNPq7o,6258
|
|
275
275
|
supervisely/app/widgets/field/style.css,sha256=sLMbqxS4EDPykcQZNMXeYdR4v1y1OP6iqcX0tRz7C68,56
|
|
@@ -1127,9 +1127,9 @@ supervisely/worker_proto/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZ
|
|
|
1127
1127
|
supervisely/worker_proto/worker_api_pb2.py,sha256=VQfi5JRBHs2pFCK1snec3JECgGnua3Xjqw_-b3aFxuM,59142
|
|
1128
1128
|
supervisely/worker_proto/worker_api_pb2_grpc.py,sha256=3BwQXOaP9qpdi0Dt9EKG--Lm8KGN0C5AgmUfRv77_Jk,28940
|
|
1129
1129
|
supervisely_lib/__init__.py,sha256=7-3QnN8Zf0wj8NCr2oJmqoQWMKKPKTECvjH9pd2S5vY,159
|
|
1130
|
-
supervisely-6.73.
|
|
1131
|
-
supervisely-6.73.
|
|
1132
|
-
supervisely-6.73.
|
|
1133
|
-
supervisely-6.73.
|
|
1134
|
-
supervisely-6.73.
|
|
1135
|
-
supervisely-6.73.
|
|
1130
|
+
supervisely-6.73.433.dist-info/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
|
|
1131
|
+
supervisely-6.73.433.dist-info/METADATA,sha256=eJ_LTlSVbr_NSUaG-xiuO2hpxMwnqKAtuuQbyu4sg64,35433
|
|
1132
|
+
supervisely-6.73.433.dist-info/WHEEL,sha256=iAkIy5fosb7FzIOwONchHf19Qu7_1wCWyFNR5gu9nU0,91
|
|
1133
|
+
supervisely-6.73.433.dist-info/entry_points.txt,sha256=U96-5Hxrp2ApRjnCoUiUhWMqijqh8zLR03sEhWtAcms,102
|
|
1134
|
+
supervisely-6.73.433.dist-info/top_level.txt,sha256=kcFVwb7SXtfqZifrZaSE3owHExX4gcNYe7Q2uoby084,28
|
|
1135
|
+
supervisely-6.73.433.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|