supervisely 6.73.431__py3-none-any.whl → 6.73.432__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.
@@ -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 = None
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 = dataset_info.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
- <sly-field title="" :description="data.{{{widget.widget_id}}}.description">
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 = None
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 self._rows_total > 0 and self._active_page == 0: # if previous filtered data was empty
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
- - selectedRow: selected row
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
- self._parsed_source_data = data.get("data", None)
399
- self._source_data = self._prepare_input_data(self._parsed_source_data)
400
- self._sliced_data = self._slice_table_data(self._source_data)
401
- self._parsed_active_data = self._unpack_pandas_table_data(self._sliced_data)
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
- page_size = init_options.pop("pageSize", 10)
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"] = page_size
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"] = 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"] = None
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 = None
562
- StateJson()[self.widget_id]["selectedRows"] = None
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 = None
569
- StateJson()[self.widget_id]["selectedRows"] = None
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
- selected_row = self.get_selected_row()
1119
- func(selected_row)
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-else-if="settings.isRowSelectable"
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
- :value="isAllRowsSelected"
95
- @input="selectAllRows"
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.idx, row: row.items, columnsSettings })"
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
- :value="!!selectedRowsById[row.id]"
173
- @input="updateSelectedRows(row)"
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
- isAllRowsSelected() {
377
- if (!this.selectedRows?.length) return false;
378
- return this.data.every(r => this.selectedRowsById[r.id]);
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.isAllRowsSelected) {
414
- const curRowsSet = new Set(this.data.map(r => r.id));
415
- this.$emit('update:selected-rows', [...this.selectedRows.filter(r => !curRowsSet.has(r.id))]);
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 selected = [...this.selectedRows];
418
-
419
- this.data.forEach((row) => {
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
- updateSelectedRows(row) {
430
- const isRowSelected = this.selectedRowsById[row.id];
431
-
432
- if (!isRowSelected) {
433
- this.$emit('update:selected-rows', [...this.selectedRows, row]);
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
- this.$emit('update:selected-rows', [...this.selectedRows.filter(p => p.id !== row.id)]);
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
- .header-left-side-start-cls {
708
- color: black;
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}}}.selectedRow = $event; post('/{{{widget.widget_id}}}/row_clicked_cb')"
27
+ @row-click="state.{{{widget.widget_id}}}.clickedRow = $event; post('/{{{widget.widget_id}}}/row_clicked_cb')"
28
28
  {%
29
29
  endif
30
30
  %}
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: supervisely
3
- Version: 6.73.431
3
+ Version: 6.73.432
4
4
  Summary: Supervisely Python SDK.
5
5
  Home-page: https://github.com/supervisely/supervisely
6
6
  Author: Supervisely
@@ -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=f7l7Te4jPRtvBcjTuUoo_AFRjpTujJHKHNYyt5EIx3Q,4646
220
- supervisely/app/widgets/dataset_thumbnail/template.html,sha256=yXlpgxkuQEklVY8iXLydKZ07vpG5RLHAmLCKb58l-ac,677
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=WEiQrWLBXx-USGhj5REn7AhlrLjA-5zQhQpedRfJF8E,49273
270
- supervisely/app/widgets/fast_table/script.js,sha256=cHjXm_tq5dFTgBxBKaS6lvT67wiaTjxnU2uWHF7hNoY,17327
271
- supervisely/app/widgets/fast_table/style.css,sha256=VwE8LHybH3vvZboaVFyfN7pE6MoIF_7fEdwNdjQMFUM,15763
272
- supervisely/app/widgets/fast_table/template.html,sha256=P0JVtohKUZcCI4aKDtnr6SxudHyYmWeGvjc8wm1VRs0,2788
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.431.dist-info/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
1131
- supervisely-6.73.431.dist-info/METADATA,sha256=Tdduz5-kfqxj39qOe4tcX_-58Hp904M_Ja2X3M6G_LA,35433
1132
- supervisely-6.73.431.dist-info/WHEEL,sha256=iAkIy5fosb7FzIOwONchHf19Qu7_1wCWyFNR5gu9nU0,91
1133
- supervisely-6.73.431.dist-info/entry_points.txt,sha256=U96-5Hxrp2ApRjnCoUiUhWMqijqh8zLR03sEhWtAcms,102
1134
- supervisely-6.73.431.dist-info/top_level.txt,sha256=kcFVwb7SXtfqZifrZaSE3owHExX4gcNYe7Q2uoby084,28
1135
- supervisely-6.73.431.dist-info/RECORD,,
1130
+ supervisely-6.73.432.dist-info/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
1131
+ supervisely-6.73.432.dist-info/METADATA,sha256=mJQpgFJG362uNb35ChhLMbHqErpjRc-bx5KHRkaut_c,35433
1132
+ supervisely-6.73.432.dist-info/WHEEL,sha256=iAkIy5fosb7FzIOwONchHf19Qu7_1wCWyFNR5gu9nU0,91
1133
+ supervisely-6.73.432.dist-info/entry_points.txt,sha256=U96-5Hxrp2ApRjnCoUiUhWMqijqh8zLR03sEhWtAcms,102
1134
+ supervisely-6.73.432.dist-info/top_level.txt,sha256=kcFVwb7SXtfqZifrZaSE3owHExX4gcNYe7Q2uoby084,28
1135
+ supervisely-6.73.432.dist-info/RECORD,,