django-fast-treenode 2.0.11__py3-none-any.whl → 2.1.1__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.
Files changed (57) hide show
  1. {django_fast_treenode-2.0.11.dist-info → django_fast_treenode-2.1.1.dist-info}/LICENSE +2 -2
  2. django_fast_treenode-2.1.1.dist-info/METADATA +158 -0
  3. django_fast_treenode-2.1.1.dist-info/RECORD +64 -0
  4. {django_fast_treenode-2.0.11.dist-info → django_fast_treenode-2.1.1.dist-info}/WHEEL +1 -1
  5. treenode/admin/__init__.py +9 -0
  6. treenode/admin/admin.py +295 -0
  7. treenode/admin/changelist.py +65 -0
  8. treenode/admin/mixins.py +302 -0
  9. treenode/apps.py +12 -1
  10. treenode/cache.py +2 -2
  11. treenode/forms.py +8 -10
  12. treenode/managers/__init__.py +21 -0
  13. treenode/managers/adjacency.py +203 -0
  14. treenode/managers/closure.py +278 -0
  15. treenode/models/__init__.py +2 -1
  16. treenode/models/adjacency.py +343 -0
  17. treenode/models/classproperty.py +3 -0
  18. treenode/models/closure.py +23 -24
  19. treenode/models/factory.py +12 -2
  20. treenode/models/mixins/__init__.py +23 -0
  21. treenode/models/mixins/ancestors.py +65 -0
  22. treenode/models/mixins/children.py +81 -0
  23. treenode/models/mixins/descendants.py +66 -0
  24. treenode/models/mixins/family.py +63 -0
  25. treenode/models/mixins/logical.py +68 -0
  26. treenode/models/mixins/node.py +210 -0
  27. treenode/models/mixins/properties.py +156 -0
  28. treenode/models/mixins/roots.py +96 -0
  29. treenode/models/mixins/siblings.py +99 -0
  30. treenode/models/mixins/tree.py +344 -0
  31. treenode/signals.py +26 -0
  32. treenode/static/treenode/css/tree_widget.css +201 -31
  33. treenode/static/treenode/css/treenode_admin.css +48 -41
  34. treenode/static/treenode/js/tree_widget.js +269 -131
  35. treenode/static/treenode/js/treenode_admin.js +131 -171
  36. treenode/templates/admin/tree_node_changelist.html +6 -0
  37. treenode/templates/admin/treenode_ajax_rows.html +7 -0
  38. treenode/tests/tests.py +488 -0
  39. treenode/urls.py +10 -6
  40. treenode/utils/__init__.py +2 -0
  41. treenode/utils/aid.py +46 -0
  42. treenode/utils/base16.py +38 -0
  43. treenode/utils/base36.py +3 -1
  44. treenode/utils/db.py +116 -0
  45. treenode/utils/exporter.py +2 -0
  46. treenode/utils/importer.py +0 -1
  47. treenode/utils/radix.py +61 -0
  48. treenode/version.py +2 -2
  49. treenode/views.py +118 -43
  50. treenode/widgets.py +91 -43
  51. django_fast_treenode-2.0.11.dist-info/METADATA +0 -698
  52. django_fast_treenode-2.0.11.dist-info/RECORD +0 -42
  53. treenode/admin.py +0 -439
  54. treenode/docs/Documentation +0 -636
  55. treenode/managers.py +0 -419
  56. treenode/models/proxy.py +0 -669
  57. {django_fast_treenode-2.0.11.dist-info → django_fast_treenode-2.1.1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,344 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ TreeNode Tree Mixin
4
+
5
+ Version: 2.1.0
6
+ Author: Timur Kady
7
+ Email: timurkady@yandex.com
8
+ """
9
+
10
+ import json
11
+ from django.db import models, transaction
12
+ from collections import OrderedDict
13
+ from django.core.serializers.json import DjangoJSONEncoder
14
+
15
+ from ...cache import cached_method
16
+
17
+
18
+ class TreeNodeTreeMixin(models.Model):
19
+ """TreeNode Tree Mixin."""
20
+
21
+ class Meta:
22
+ """Moxin Meta Class."""
23
+
24
+ abstract = True
25
+
26
+ @classmethod
27
+ def dump_tree(cls, instance=None):
28
+ """
29
+ Return an n-dimensional dictionary representing the model tree.
30
+
31
+ Introduced for compatibility with other packages.
32
+ """
33
+ return cls.get_tree(cls, instance)
34
+
35
+ @classmethod
36
+ @cached_method
37
+ def get_tree(cls, instance=None):
38
+ """
39
+ Return an n-dimensional dictionary representing the model tree.
40
+
41
+ If instance is passed, returns a subtree rooted at instance (using
42
+ get_descendants_queryset), if not passed, builds a tree for all nodes
43
+ (loads all records in one query).
44
+ """
45
+ # If instance is passed, we get all its descendants (including itself)
46
+ if instance:
47
+ queryset = instance.get_descendants_queryset(include_self=True)\
48
+ .annotate(depth=models.Max("parents_set__depth"))
49
+ else:
50
+ # Load all records at once
51
+ queryset = cls.objects.all()
52
+
53
+ # Dictionary for quick access to nodes by id and list for iteration
54
+ nodes_by_id = {}
55
+ nodes_list = []
56
+
57
+ # Loop through all nodes using an iterator
58
+ for node in queryset.iterator(chunk_size=1000):
59
+ # Create a dictionary for the node.
60
+ # In Python 3.7+, the standard dict preserves insertion order.
61
+ # We'll stick to the order:
62
+ # id, 'tn_parent', 'tn_priority', 'level', then the rest of
63
+ # the fields. Сlose the dictionary with the fields 'level', 'path',
64
+ # 'children'
65
+ node_dict = OrderedDict()
66
+ node_dict['id'] = node.id
67
+ node_dict['tn_parent'] = node.tn_parent_id
68
+ node_dict['tn_priority'] = node.tn_priority
69
+ node_dict['level'] = node.get_depth()
70
+ node_dict['path'] = node.get_breadcrumbs('tn_priority')
71
+
72
+ # Add the rest of the model fields.
73
+ # Iterate over all the fields obtained via _meta.get_fields()
74
+ for field in node._meta.get_fields():
75
+ # Skipping fields that are already added or not required
76
+ # (e.g. tn_closure or virtual links)
77
+ if field.name in [
78
+ 'id', 'tn_parent', 'tn_priority', 'tn_closure',
79
+ 'children']:
80
+ continue
81
+
82
+ try:
83
+ value = getattr(node, field.name)
84
+ except Exception:
85
+ value = None
86
+
87
+ # If the field is many-to-many, we get a list of IDs of
88
+ # related objects
89
+ if hasattr(value, 'all'):
90
+ value = list(value.all().values_list('id', flat=True))
91
+
92
+ node_dict[field.name] = value
93
+
94
+ # Adding a materialized path
95
+ node_dict['path'] = None
96
+ # Adding a nesting level
97
+ node_dict['level'] = None
98
+ # We initialize the list of children, which we will then fill
99
+ # when assembling the tree
100
+ node_dict['children'] = []
101
+
102
+ # Save the node both in the list and in the dictionary by id
103
+ # for quick access
104
+ nodes_by_id[node.id] = node_dict
105
+ nodes_list.append(node_dict)
106
+
107
+ # Build a tree: assign each node a list of its children
108
+ tree = []
109
+ for node_dict in nodes_list:
110
+ parent_id = node_dict['tn_parent']
111
+ # If there is a parent and it is present in nodes_by_id, then
112
+ # add the current node to the list of its children
113
+ if parent_id and parent_id in nodes_by_id:
114
+ parent_node = nodes_by_id[parent_id]
115
+ parent_node['children'].append(node_dict)
116
+ else:
117
+ # If there is no parent, this is the root node of the tree
118
+ tree.append(node_dict)
119
+
120
+ return tree
121
+
122
+ @classmethod
123
+ def get_tree_json(cls, instance=None):
124
+ """Represent the tree as a JSON-compatible string."""
125
+ tree = cls.dump_tree(instance)
126
+ return DjangoJSONEncoder().encode(tree)
127
+
128
+ @classmethod
129
+ def load_tree(cls, tree_data):
130
+ """
131
+ Load a tree from a list of dictionaries.
132
+
133
+ Loaded nodes are synchronized with the database: existing records
134
+ are updated, new ones are created.
135
+ Each dictionary must contain the 'id' key to identify the record.
136
+ """
137
+
138
+ def flatten_tree(nodes, model_fields):
139
+ """
140
+ Recursively traverse the tree and generate lists of nodes.
141
+
142
+ Each node in the list is a copy of the dictionary without
143
+ the service keys 'children', 'level', 'path'.
144
+ """
145
+ flat = []
146
+ for node in nodes:
147
+ # Create a copy of the dictionary so as not to affect
148
+ # the original tree
149
+ node_copy = node.copy()
150
+ children = node_copy.pop('children', [])
151
+ # Remove temporary/service fields that are not related to
152
+ # the model
153
+ for key in list(node_copy.keys() - model_fields):
154
+ del node_copy[key]
155
+ flat.append(node_copy)
156
+ # Recursively add all children
157
+ flat.extend(flatten_tree(children, model_fields))
158
+ return flat
159
+
160
+ # Get a flat list of nodes (from root to leaf)
161
+ model_fields = [field.name for field in cls._meta.get_fields()]
162
+ flat_nodes = flatten_tree(tree_data, model_fields)
163
+
164
+ # Load all ids for a given model from the database to minimize
165
+ # the number of database requests
166
+ existing_ids = set(cls.objects.values_list('id', flat=True))
167
+
168
+ # Lists for nodes to update and create
169
+ nodes_to_update = []
170
+ nodes_to_create = []
171
+
172
+ # Determine which model fields should be updated (excluding 'id')
173
+ # This assumes that all model fields are used in serialization
174
+ # (excluding service ones, like children)
175
+ field_names = model_fields.remove('id')
176
+
177
+ # Iterate over each node from the flat list
178
+ for node_data in flat_nodes:
179
+ # Collect data for creation/update.
180
+ # There is already an 'id' in node_data, so we can distinguish
181
+ # an existing entry from a new one.
182
+ data = {k: v for k, v in node_data.items()
183
+ if k in field_names or k == 'id'}
184
+
185
+ # Handle the foreign key tn_parent.
186
+ # If the value is None, then there is no parent, otherwise
187
+ # the parent id is expected.
188
+ # Additional checks can be added here if needed.
189
+ if 'tn_parent' in data and data['tn_parent'] is None:
190
+ data['tn_parent'] = None
191
+
192
+ # Если id уже есть в базе, то будем обновлять запись,
193
+ # иначе создаем новую.
194
+ if data['id'] in existing_ids:
195
+ nodes_to_update.append(cls(**data))
196
+ else:
197
+ nodes_to_create.append(cls(**data))
198
+
199
+ # Perform operations in a transaction to ensure data integrity.
200
+ with transaction.atomic():
201
+ # bulk_create – creating new nodes
202
+ if nodes_to_create:
203
+ cls.objects.bulk_create(nodes_to_create, batch_size=1000)
204
+ # bulk_update – updating existing nodes.
205
+ # When bulk_update, we must specify a list of fields to update.
206
+ if nodes_to_update:
207
+ cls.objects.bulk_update(
208
+ nodes_to_update,
209
+ fields=field_names,
210
+ batch_size=1000
211
+ )
212
+ cls.clear_cache()
213
+
214
+ @classmethod
215
+ def load_tree_json(cls, json_str):
216
+ """
217
+ Decode a JSON string into a dictionary.
218
+
219
+ Takes a JSON-compatible string and decodes it into a tree structure.
220
+ """
221
+ try:
222
+ tree_data = json.loads(json_str)
223
+ except json.JSONDecodeError as e:
224
+ raise ValueError(f"Error decoding JSON: {e}")
225
+
226
+ cls.load_tree(tree_data)
227
+
228
+ @classmethod
229
+ @cached_method
230
+ def get_tree_display(cls, instance=None, symbol="—"):
231
+ """Get a multiline string representing the model tree."""
232
+ # If instance is passed, we get all its descendants (including itself)
233
+ if instance:
234
+ queryset = instance.get_descendants_queryset(include_self=True)\
235
+ .prefetch_related("tn_children")\
236
+ .annotate(depth=models.Max("parents_set__depth"))\
237
+ .order_by("depth", "tn_parent", "tn_priority")
238
+ else:
239
+ queryset = cls.objects.all()\
240
+ .prefetch_related("tn_children")\
241
+ .annotate(depth=models.Max("parents_set__depth"))\
242
+ .order_by("depth", "tn_parent", "tn_priority")
243
+ # Convert queryset to list for indexed access
244
+ nodes = list(queryset)
245
+ sorted_nodes = cls._sort_node_list(nodes)
246
+ result = []
247
+ for node in sorted_nodes:
248
+ # Insert an indent proportional to the depth of the node
249
+ indent = symbol * node.depth
250
+ result.append(indent + str(node))
251
+ return result
252
+
253
+ @classmethod
254
+ @cached_method
255
+ def get_tree_annotated(cls):
256
+ """
257
+ Get an annotated list from a tree branch.
258
+
259
+ Something like this will be returned:
260
+ [
261
+ (a, {'open':True, 'close':[], 'level': 0})
262
+ (ab, {'open':True, 'close':[], 'level': 1})
263
+ (aba, {'open':True, 'close':[], 'level': 2})
264
+ (abb, {'open':False, 'close':[], 'level': 2})
265
+ (abc, {'open':False, 'close':[0,1], 'level': 2})
266
+ (ac, {'open':False, 'close':[0], 'level': 1})
267
+ ]
268
+
269
+ All nodes are ordered by materialized path.
270
+ This can be used with a template like this:
271
+
272
+ {% for item, info in annotated_list %}
273
+ {% if info.open %}
274
+ <ul><li>
275
+ {% else %}
276
+ </li><li>
277
+ {% endif %}
278
+
279
+ {{ item }}
280
+
281
+ {% for close in info.close %}
282
+ </li></ul>
283
+ {% endfor %}
284
+ {% endfor %}
285
+
286
+ """
287
+ # Load the tree with the required preloads and depth annotation.
288
+ queryset = cls.objects.all()\
289
+ .prefetch_related("tn_children")\
290
+ .annotate(depth=models.Max("parents_set__depth"))\
291
+ .order_by("depth", "tn_parent", "tn_priority")
292
+ # Convert queryset to list for indexed access
293
+ nodes = list(queryset)
294
+ total_nodes = len(nodes)
295
+ sorted_nodes = cls._sort_node_list(nodes)
296
+
297
+ result = []
298
+
299
+ for i, node in enumerate(sorted_nodes):
300
+ # Get the value to display the node
301
+ value = str(node)
302
+ # Determine if there are descendants (use prefetch_related to avoid
303
+ # additional queries)
304
+ value_open = len(node.tn_children.all()) > 0
305
+ level = node.depth
306
+
307
+ # Calculate the "close" field
308
+ if i + 1 < total_nodes:
309
+ next_node = nodes[i + 1]
310
+ depth_diff = level - next_node.depth
311
+ # If the next node is at a lower level, then some open
312
+ # levels need to be closed
313
+ value_close = list(range(next_node.depth, level)
314
+ ) if depth_diff > 0 else []
315
+ else:
316
+ # For the last node, close all open levels
317
+ value_close = list(range(0, level + 1))
318
+
319
+ result.append(
320
+ (value, {
321
+ "open": value_open,
322
+ "close": value_close,
323
+ "level": level
324
+ })
325
+ )
326
+ return result
327
+
328
+ @classmethod
329
+ @transaction.atomic
330
+ def update_tree(cls):
331
+ """Rebuilds the closure table."""
332
+ # Clear cache
333
+ cls.clear_cache()
334
+ cls.closure_model.delete_all()
335
+ objs = list(cls.objects.all())
336
+ cls.closure_model.objects.bulk_create(objs, batch_size=1000)
337
+
338
+ @classmethod
339
+ def delete_tree(cls):
340
+ """Delete the whole tree for the current node class."""
341
+ cls.clear_cache()
342
+ cls.objects.all().delete()
343
+
344
+ # The end
treenode/signals.py ADDED
@@ -0,0 +1,26 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ TreeNode Signals Module
4
+
5
+ Version: 2.1.0
6
+ Author: Timur Kady
7
+ Email: timurkady@yandex.com
8
+ """
9
+
10
+ from contextlib import contextmanager
11
+
12
+
13
+ @contextmanager
14
+ def disable_signals(signal, sender):
15
+ """Temporarily disable execution of signal generation."""
16
+ # Save current signal handlers
17
+ old_receivers = signal.receivers[:]
18
+ signal.receivers = []
19
+ try:
20
+ yield
21
+ finally:
22
+ # Restore handlers
23
+ signal.receivers = old_receivers
24
+
25
+
26
+ # Tne End
@@ -11,52 +11,222 @@ Features:
11
11
  - Custom styling for search fields and selection indicators.
12
12
  - Enhances usability in tree-based data selection.
13
13
 
14
+ Main styles:
15
+ .tree-widget-display: styles the display area of ​​the selected item, adding
16
+ padding, borders, and background.
17
+ .tree-dropdown-arrow: styles the dropdown arrow.
18
+ .tree-widget-dropdown: defines the style of the dropdown, including positioning,
19
+ size, and shadows.
20
+ .tree-search-wrapper: decorates the search area inside the dropdown.
21
+ .tree-search: styles the search input.
22
+ .tree-clear-button: button to clear the search input.
23
+ .tree-list and .tree-node: styles for the list and its items.
24
+ .expand-button: button to expand child elements.
25
+
26
+ Dark theme:
27
+ Uses the .dark-theme class to apply dark styles. Changes the background,
28
+ borders, and text color for the corresponding elements. Ensures comfortable use
29
+ of the widget in dark mode.
30
+
14
31
  Version: 2.0.0
15
32
  Author: Timur Kady
16
33
  Email: timurkady@yandex.com
34
+
17
35
  */
18
36
 
19
- /* Select2 dropdown */
20
- .select2-dropdown.dark-theme {
21
- background-color: #2c2c2c !important;
22
- border: 1px solid #444 !important;
23
- color: #ddd !important;
37
+ .form-row.field-tn_parent {
38
+ position: relative;
39
+ overflow: visible !important;
40
+ z-index: auto;
41
+ }
42
+
43
+ .tree-widget {
44
+ position: relative;
45
+ width: 100%;
46
+ font-family: Arial, sans-serif;
47
+ }
48
+
49
+ .tree-widget-display {
50
+ display: flex;
51
+ align-items: center;
52
+ justify-content: space-between;
53
+ border: 1px solid #aaa;
54
+ border-radius: 4px;
55
+ background-color: #fff;
56
+ cursor: pointer;
57
+ transition: border-color 0.2s;
58
+ }
59
+
60
+ .tree-widget-display:hover {
61
+ border-color: #333;
62
+ }
63
+
64
+ .tree-dropdown-arrow {
65
+ font-size: 0.8em;
66
+ color: #888;
67
+ display: inline-block;
68
+ height: 100%;
69
+ background-color: lightgrey;
70
+ padding: 8px;
71
+ margin: 0;
72
+ border-radius: 0 4px 4px 0;
73
+ }
74
+
75
+ .tree-widget-dropdown {
76
+ display: none;
77
+ position: absolute;
78
+ top: 100%;
79
+ left: 0;
80
+ width: 100%;
81
+ max-height: 242px;
82
+ overflow-y: auto;
83
+ border: 1px solid #aaa;
84
+ border-top: none;
85
+ border-radius: 0 3px 3px 0;
86
+ background-color: #fff;
87
+ z-index: 1000;
88
+ box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
89
+ }
90
+
91
+ .tree-search-wrapper {
92
+ display: flex;
93
+ align-items: center;
94
+ padding: 6px;
95
+ border-bottom: 1px solid #ddd;
96
+ background-color: #f9f9f9;
97
+ }
98
+
99
+ .tree-search-icon {
100
+ margin-right: 6px;
101
+ font-size: 1.3em;
102
+ }
103
+
104
+ .tree-search {
105
+ flex-grow: 1;
106
+ padding: 6px;
107
+ border: 1px solid #ccc;
108
+ border-radius: 4px;
109
+ font-size: 1em;
110
+ }
111
+
112
+ .tree-search-clear{
113
+ border: none;
114
+ border-radius: 3px;
115
+ background: none;
116
+ font-size: 1.8em;
117
+ cursor: pointer;
118
+ }
119
+
120
+ .tree-clear-button {
121
+ background: none;
122
+ border: none;
123
+ font-size: 1.2em;
124
+ color: #888;
125
+ cursor: pointer;
126
+ margin-left: 6px;
127
+ }
128
+
129
+ .tree-list {
130
+ list-style: none;
131
+ margin: 0 !important;
132
+ padding: 0 !important;
133
+ }
134
+
135
+ .tree-node {
136
+ display: block !important;
137
+ padding: 6px 0px !important;
138
+ cursor: pointer;
139
+ transition: background-color 0.2s;
140
+ }
141
+
142
+ .tree-node:hover {
143
+ background-color: #f0f0f0;
144
+ }
145
+
146
+ .tree-node[data-level="1"] {
147
+ padding-left: 20px;
148
+ }
149
+
150
+ .tree-node[data-level="2"] {
151
+ padding-left: 40px;
152
+ }
153
+
154
+ .expand-button {
155
+ display: inline-block;
156
+ width: 18px;
157
+ height: 18px;
158
+ background: var(--button-bg);
159
+ color: var(--button-fg);
160
+ border-radius: 3px;
161
+ border: none;
162
+ margin: 0px 5px;
163
+ cursor: pointer;
164
+ font-size: 12px;
165
+ line-height: 18px;
166
+ padding: 0px;
167
+ opacity: 0.8;
168
+ }
169
+
170
+ .no-expand {
171
+ display: inline-block;
172
+ width: 18px;
173
+ height: 18px;
174
+ border: none;
175
+ margin: 0px 5px;
176
+ }
177
+
178
+ .selected-node {
179
+ margin: 6px 12px;
180
+ }
181
+
182
+ /* Тёмная тема */
183
+ .dark-theme .tree-widget-display {
184
+ background-color: #333;
185
+ border-color: #555;
186
+ color: #eee;
187
+ }
188
+
189
+ .dark-theme .tree-dropdown-arrow {
190
+ color: #ccc;
191
+ }
192
+
193
+ .dark-theme .tree-widget-dropdown {
194
+ background-color: #444;
195
+ border-color: #555;
196
+ box-shadow: 0 4px 8px rgba(0, 0, 0, 0.5);
197
+ }
198
+
199
+ .dark-theme .tree-search-wrapper {
200
+ background-color: #555;
201
+ border-bottom-color: #666;
202
+ }
203
+
204
+ .dark-theme .tree-search {
205
+ background-color: #666;
206
+ border-color: #777;
207
+ color: #eee;
24
208
  }
25
209
 
26
- /* List of options */
27
- .select2-dropdown.dark-theme .select2-results__option {
28
- background-color: transparent !important;
29
- color: #ddd !important;
210
+ .dark-theme .tree-search::placeholder {
211
+ color: #ccc;
30
212
  }
31
213
 
32
- /* Hover/selected option */
33
- .select2-dropdown.dark-theme .select2-results__option--highlighted,
34
- .select2-dropdown.dark-theme .select2-results__option--selected {
35
- background-color: #555 !important;
36
- color: #fff !important;
214
+ .dark-theme .tree-clear-button {
215
+ color: #ccc;
37
216
  }
38
217
 
39
- /* Search field */
40
- .select2-dropdown.dark-theme .select2-search.select2-search--dropdown .select2-search__field {
41
- background-color: #2c2c2c !important;
42
- color: #ddd !important;
43
- border: 1px solid #444 !important;
44
- outline: none !important;
218
+ .dark-theme .tree-node {
219
+ color: #eee;
45
220
  }
46
221
 
47
- /* Container of the selected element (if we want to darken it too) */
48
- .select2-container--default .select2-selection--single.dark-theme {
49
- background-color: #2c2c2c !important;
50
- border: 1px solid #444 !important;
51
- color: #ddd !important;
222
+ .dark-theme .tree-node:hover {
223
+ background-color: #555;
52
224
  }
53
225
 
54
- /* Arrow */
55
- .select2-container--default .select2-selection--single.dark-theme .select2-selection__arrow b {
56
- border-color: #ddd transparent transparent transparent !important;
226
+ .dark-theme .expand-button {
227
+ color: #ccc;
57
228
  }
58
229
 
59
- /* Text of the selected element */
60
- .select2-container--default .select2-selection--single.dark-theme .select2-selection__rendered {
61
- color: #ddd !important;
230
+ .dark-theme .selected-node {
231
+ color: #eee;
62
232
  }