diff --git a/database/collections_manager.py b/database/collections_manager.py index 71a156249..cc7e89bb8 100644 --- a/database/collections_manager.py +++ b/database/collections_manager.py @@ -206,6 +206,8 @@ def _ensure_indexes(self) -> None: self.collections.create_indexes([ IndexModel([("user_id", ASCENDING), ("slug", ASCENDING)], name="user_slug_unique", unique=True), IndexModel([("user_id", ASCENDING), ("is_active", ASCENDING), ("updated_at", DESCENDING)], name="user_active_updated"), + # תומך בסינון הרשימה לפי ארכיון (is_archived) יחד עם מיון לפי updated_at + IndexModel([("user_id", ASCENDING), ("is_active", ASCENDING), ("is_archived", ASCENDING), ("updated_at", DESCENDING)], name="user_active_archived_updated"), # חיפוש מהיר לפי שם (למשל "שולחן עבודה") תחת user_id + is_active IndexModel([("user_id", ASCENDING), ("is_active", ASCENDING), ("name", ASCENDING)], name="user_active_name"), IndexModel([("user_id", ASCENDING), ("sort_order", ASCENDING)], name="user_sort_order"), @@ -239,6 +241,15 @@ def _ensure_indexes(self) -> None: ) except Exception: pass + # backfill — אוספים קיימים ללא is_archived יקבלו False (לניקיון וליעילות אינדקס; + # התקינות מובטחת גם בלעדיו בזכות $ne:True בשאילתת הרשימה) + try: + self.collections.update_many( + {"is_archived": {"$exists": False}}, + {"$set": {"is_archived": False}}, + ) + except Exception: + pass def ensure_default_collections(self, user_id: int) -> bool: """מאבטח יצירה של אוספים מובנים עבור משתמש חדש. @@ -687,6 +698,7 @@ def create_collection( "items_count": 0, "pinned_count": 0, "is_active": True, + "is_archived": False, "created_at": _now(), "updated_at": _now(), } @@ -745,6 +757,8 @@ def update_collection(self, user_id: int, collection_id: str, **fields: Any) -> updates["color"] = self._normalize_color(fields.get("color")) if "is_favorite" in fields: updates["is_favorite"] = bool(fields.get("is_favorite")) + if "is_archived" in fields: + updates["is_archived"] = bool(fields.get("is_archived")) if "sort_order" in fields and isinstance(fields.get("sort_order"), int): updates["sort_order"] = int(fields.get("sort_order")) if "mode" in fields: @@ -791,14 +805,21 @@ def delete_collection(self, user_id: int, collection_id: str) -> Dict[str, Any]: emit_event("collections_delete_error", severity="error", user_id=int(user_id), error=str(e)) return {"ok": False, "error": "שגיאה במחיקת האוסף"} - def list_collections(self, user_id: int, limit: int = 100, skip: int = 0) -> Dict[str, Any]: + def list_collections(self, user_id: int, limit: int = 100, skip: int = 0, *, archived_only: bool = False, include_archived: bool = False) -> Dict[str, Any]: try: eff_limit = max(1, min(int(limit or 100), 500)) eff_skip = max(0, int(skip or 0)) except Exception: eff_limit, eff_skip = 100, 0 - flt = {"user_id": user_id, "is_active": True} + # ברירת מחדל: רק אוספים פעילים שאינם בארכיון. תצוגת ארכיון: רק המאורכבים. + # include_archived=True (למשל גיבוי אישי): כל האוספים הפעילים, כולל בארכיון. + # ($ne:True מכסה גם אוספים ישנים שעדיין אין בהם את השדה is_archived) + flt: Dict[str, Any] = {"user_id": user_id, "is_active": True} + if archived_only: + flt["is_archived"] = True + elif not include_archived: + flt["is_archived"] = {"$ne": True} try: found = self.collections.find(flt) @@ -1760,6 +1781,7 @@ def _public_collection(self, d: Dict[str, Any]) -> Dict[str, Any]: "items_count": int(d.get("items_count") or 0), "pinned_count": int(d.get("pinned_count") or 0), "is_active": bool(d.get("is_active", True)), + "is_archived": bool(d.get("is_archived", False)), "created_at": (d.get("created_at").isoformat() if isinstance(d.get("created_at"), datetime) else None), "updated_at": (d.get("updated_at").isoformat() if isinstance(d.get("updated_at"), datetime) else None), "share": { diff --git a/services/personal_backup_service.py b/services/personal_backup_service.py index fe533f901..3f52348ca 100644 --- a/services/personal_backup_service.py +++ b/services/personal_backup_service.py @@ -286,7 +286,8 @@ def _export_collections(self, user_id: int) -> Dict[str, Any]: if raw_db is None: return {"collections": [], "items": []} mgr = CollectionsManager(raw_db) - result = mgr.list_collections(user_id, limit=500) + # כולל אוספים בארכיון — הגיבוי חייב לשמר גם אותם + result = mgr.list_collections(user_id, limit=500, include_archived=True) collections = result.get("collections", []) if result.get("ok") else [] all_items = [] @@ -920,7 +921,8 @@ def _restore_collections( # טען אוספים קיימים לבדיקת כפילות לפי שם existing_collections = {} try: - existing_result = mgr.list_collections(user_id, limit=500) + # כולל אוספים בארכיון — למניעת כפילות שם גם מול אוסף מאורכב קיים + existing_result = mgr.list_collections(user_id, limit=500, include_archived=True) if existing_result.get("ok"): for ec in existing_result.get("collections", []): name = (ec.get("name") or "").strip() diff --git a/tests/test_collections_archive.py b/tests/test_collections_archive.py new file mode 100644 index 000000000..ce936f2ff --- /dev/null +++ b/tests/test_collections_archive.py @@ -0,0 +1,169 @@ +"""טסט לארכיון אוספים: ארכוב/שחזור דרך update_collection + סינון ב-list_collections. + +מוודא ש: +- אוסף שאורכב (is_archived=True) נעלם מהתצוגה הרגילה ומופיע רק ב-archived_only=True. +- שחזור (is_archived=False) מחזיר אותו לתצוגה הרגילה. +- אוסף "ישן" ללא השדה is_archived עדיין מופיע בתצוגה הרגילה (בזכות $ne:True). + +ה-fakes כאן תומכים ב-$ne/$or/$in/$exists/$set ו-find_one_and_update (כמו +FakeCollection ב-test_collections_manager_unit), כדי לתרגל את המסלול המלא. +""" + +from database.collections_manager import CollectionsManager + + +def _match(doc, flt): + for k, v in flt.items(): + if k == "$or": + if not any(_match(doc, c) for c in v): + return False + elif isinstance(v, dict) and "$in" in v: + if doc.get(k) not in v["$in"]: + return False + elif isinstance(v, dict) and "$ne" in v: + # $ne מתקיים גם כששדה חסר (מיוצג כ-None) — חשוב לאוספים ישנים + if doc.get(k) == v["$ne"]: + return False + elif isinstance(v, dict) and "$exists" in v: + if bool(v["$exists"]) != (k in doc): + return False + else: + if str(doc.get(k)) != str(v): + return False + return True + + +def _apply_set(doc, upd): + if "$set" in upd: + for k, val in upd["$set"].items(): + doc[k] = val + + +class _Result: + def __init__(self, modified=0): + self.modified_count = modified + self.matched_count = modified + self.acknowledged = True + + +class _Coll: + def __init__(self): + self.docs = [] + + def create_indexes(self, *a, **k): + return None + + def create_index(self, *a, **k): + return None + + def insert_one(self, doc): + if not doc.get("_id"): + doc["_id"] = f"{len(self.docs) + 1:024x}" + self.docs.append(dict(doc)) + return _Result(1) + + def find(self, flt, projection=None): + return [dict(d) for d in self.docs if _match(d, flt)] + + def find_one(self, flt, projection=None): + for d in self.docs: + if _match(d, flt): + return dict(d) + return None + + def find_one_and_update(self, flt, upd, return_document=True): + for d in self.docs: + if _match(d, flt): + _apply_set(d, upd) + return dict(d) + return None + + def update_many(self, flt, upd, **k): + n = 0 + for d in self.docs: + if _match(d, flt): + _apply_set(d, upd) + n += 1 + return _Result(n) + + def count_documents(self, flt): + return sum(1 for d in self.docs if _match(d, flt)) + + def aggregate(self, *a, **k): + return [] + + +class _DB: + def __init__(self): + self.user_collections = _Coll() + self.collection_items = _Coll() + self.code_snippets = _Coll() + + +def _names(res): + return sorted(c["name"] for c in res["collections"]) + + +def test_archive_hides_from_default_and_shows_in_archive_view(): + mgr = CollectionsManager(_DB()) + uid = 7 + a = mgr.create_collection(uid, "אוסף א") + b = mgr.create_collection(uid, "אוסף ב") + assert a["ok"] and b["ok"] + cid_a = a["collection"]["id"] + + # לפני ארכוב: שניהם ברשימה הרגילה, הארכיון ריק + assert _names(mgr.list_collections(uid)) == ["אוסף א", "אוסף ב"] + assert mgr.list_collections(uid, archived_only=True)["collections"] == [] + + # ארכוב א' דרך update_collection (אותו מסלול כמו is_favorite) + upd = mgr.update_collection(uid, cid_a, is_archived=True) + assert upd["ok"] is True + assert upd["collection"]["is_archived"] is True + + # אחרי ארכוב: רק ב' ברגיל, רק א' בארכיון + assert _names(mgr.list_collections(uid)) == ["אוסף ב"] + assert _names(mgr.list_collections(uid, archived_only=True)) == ["אוסף א"] + + +def test_unarchive_restores_to_default(): + mgr = CollectionsManager(_DB()) + uid = 9 + c = mgr.create_collection(uid, "לשחזר") + cid = c["collection"]["id"] + mgr.update_collection(uid, cid, is_archived=True) + assert _names(mgr.list_collections(uid)) == [] + + upd = mgr.update_collection(uid, cid, is_archived=False) + assert upd["ok"] is True + assert upd["collection"]["is_archived"] is False + assert _names(mgr.list_collections(uid)) == ["לשחזר"] + assert mgr.list_collections(uid, archived_only=True)["collections"] == [] + + +def test_include_archived_returns_both(): + # include_archived=True (מסלול הגיבוי האישי): גם פעילים וגם מאורכבים + mgr = CollectionsManager(_DB()) + uid = 11 + mgr.create_collection(uid, "פעיל") + b = mgr.create_collection(uid, "מאורכב") + mgr.update_collection(uid, b["collection"]["id"], is_archived=True) + assert _names(mgr.list_collections(uid)) == ["פעיל"] + assert _names(mgr.list_collections(uid, include_archived=True)) == ["מאורכב", "פעיל"] + + +def test_legacy_collection_without_field_shows_in_default(): + # אוסף "ישן" שנשמר לפני הוספת is_archived (השדה חסר בכוונה) + db = _DB() + db.user_collections.docs.append( + { + "_id": "0" * 24, + "user_id": 5, + "name": "ישן", + "slug": "yashan", + "is_active": True, + } + ) + mgr = CollectionsManager(db) + assert _names(mgr.list_collections(5)) == ["ישן"] + assert mgr.list_collections(5, archived_only=True)["collections"] == [] diff --git a/webapp/FEATURE_SUGGESTIONS/MY_COLLECTIONS_IMPLEMENTATION_GUIDE.md b/webapp/FEATURE_SUGGESTIONS/MY_COLLECTIONS_IMPLEMENTATION_GUIDE.md index 1d1e1200d..0b40b3224 100644 --- a/webapp/FEATURE_SUGGESTIONS/MY_COLLECTIONS_IMPLEMENTATION_GUIDE.md +++ b/webapp/FEATURE_SUGGESTIONS/MY_COLLECTIONS_IMPLEMENTATION_GUIDE.md @@ -450,6 +450,7 @@ curl '/api/collections//items?page=1&per_page=20&include_computed=true' - בעתיד: ניתן להרחיב לסוגי פריטים נוספים (למשל Saved searches עצמאיים). ## 🗒️ Changelog +- 2026-07-20: ארכיון לאוספים — שדה `is_archived` (נפרד מ־`is_active`), פרמטרים `archived_only`/`include_archived` ל־`list_collections`, toggle 🗄️ "הצג ארכיון" בסיידבר, וכפתור ארכב/שחזר בכותרת האוסף. אוספים בארכיון נשמרים בגיבוי האישי (`include_archived=True`). בנוסף: אייקון התיאור בכרטיס עבר לצד שם הקובץ (עם "קפיצה" לשורת הכפתורים כשהשם ארוך). - 2025-10-23: מיזוג תוספות מהגיסט והפיכת המסמך לקאנוני: whitelist לאייקונים, פלטת צבעים, Feature flag ורול־אאוט, TTL לקאש, הוראות הטמעה תכל'ס, סכמות DB מעודכנות. ## 📎 נספח: גיסט רפרנס diff --git a/webapp/collections_api.py b/webapp/collections_api.py index a38b64a51..6a1cd4044 100644 --- a/webapp/collections_api.py +++ b/webapp/collections_api.py @@ -376,32 +376,37 @@ def list_collections(): skip = int(request.args.get('skip') or 0) except Exception: return jsonify({'ok': False, 'error': 'Invalid limit/skip'}), 400 + # תצוגת ארכיון: מציגה רק אוספים מאורכבים (?archived=1) + archived_only = str(request.args.get('archived') or '').strip().lower() in ('1', 'true', 'yes', 'on') mgr = get_manager() created_workspace = False - try: - created_workspace = mgr.ensure_default_collections(user_id) - except Exception: - created_workspace = False - result = mgr.list_collections(user_id, limit=limit, skip=skip) - # אם עדיין חסר אוסף "שולחן עבודה" – נסה ליצור ולשלוף מחדש (למשתמשים קיימים) - try: - collections = result.get('collections') if isinstance(result, dict) else None - except Exception: - collections = None - has_workspace = False - if isinstance(collections, list): + # לוגיקת ברירת-המחדל של "שולחן עבודה" רלוונטית רק לתצוגה הרגילה, לא לארכיון + if not archived_only: try: - has_workspace = any((c or {}).get('name') == 'שולחן עבודה' for c in collections) + created_workspace = mgr.ensure_default_collections(user_id) except Exception: - has_workspace = False - if not has_workspace: + created_workspace = False + result = mgr.list_collections(user_id, limit=limit, skip=skip, archived_only=archived_only) + # אם עדיין חסר אוסף "שולחן עבודה" – נסה ליצור ולשלוף מחדש (למשתמשים קיימים) + if not archived_only: try: - if mgr.ensure_default_collections(user_id): - created_workspace = True - result = mgr.list_collections(user_id, limit=limit, skip=skip) - collections = result.get('collections') if isinstance(result, dict) else None + collections = result.get('collections') if isinstance(result, dict) else None except Exception: - pass + collections = None + has_workspace = False + if isinstance(collections, list): + try: + has_workspace = any((c or {}).get('name') == 'שולחן עבודה' for c in collections) + except Exception: + has_workspace = False + if not has_workspace: + try: + if mgr.ensure_default_collections(user_id): + created_workspace = True + result = mgr.list_collections(user_id, limit=limit, skip=skip, archived_only=archived_only) + collections = result.get('collections') if isinstance(result, dict) else None + except Exception: + pass if result.get('ok'): collections = result.get('collections') or [] for col in collections: diff --git a/webapp/static/css/collections.css b/webapp/static/css/collections.css index 7469c399a..8cd1ba59c 100644 --- a/webapp/static/css/collections.css +++ b/webapp/static/css/collections.css @@ -346,6 +346,9 @@ .collections-sidebar{background:var(--glass);border:1px solid var(--glass-border);border-radius:14px;padding:0.75rem;backdrop-filter:blur(20px)} .sidebar-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:.5rem} .sidebar-header .title{font-weight:700} +.sidebar-header__actions{display:flex;align-items:center;gap:.35rem} +/* כפתור "הצג ארכיון" במצב פעיל (aria-pressed) — מודגש כדי לסמן שאנחנו בתצוגת ארכיון */ +#toggleArchivedBtn[aria-pressed="true"],#toggleArchivedBtn.is-active{background:rgba(255,255,255,.2);border-color:rgba(255,255,255,.45);box-shadow:0 0 0 1px rgba(255,255,255,.1) inset} .sidebar-search{margin:.5rem 0} .sidebar-search input{width:100%;padding:.5rem .6rem;border-radius:8px;border:1px solid var(--glass-border);background:rgba(255,255,255,.1);color:var(--text-primary)} .sidebar-list{display:flex;flex-direction:column;gap:.25rem;max-height:60vh;overflow:auto;scrollbar-width:thin} @@ -644,13 +647,22 @@ flex-direction:column; gap:.15rem } +.collection-card__title-row{ + display:flex; + align-items:center; + gap:.4rem; + min-width:0; + flex-wrap:nowrap +} .collection-card__name{ font-weight:600; color:var(--collections-dark-text); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; - max-width:100% + max-width:100%; + flex:0 1 auto; + min-width:0 } .collection-card__name.is-wrapped{ white-space:normal; @@ -689,9 +701,18 @@ line-height:1; cursor:pointer; opacity:.75; + flex:0 0 auto; transition:opacity .2s,transform .2s } .desc-info:hover,.desc-info:focus-visible{opacity:1;transform:scale(1.1)} +/* Slot 2: כשאין מקום ליד השם, האייקון עובר לשורת הכפתורים — משמאל להם, רווח ~2 תווים. + שומר על מראה עדין (שקוף) גם כאן, בניגוד לכפתורי הפעולה הממוסגרים. */ +.collection-card__actions .desc-info{ + background:transparent; + border:0; + margin-inline-start:.5rem; + align-self:center +} .collection-card__actions{ display:flex; gap:.35rem; diff --git a/webapp/static/js/collections.js b/webapp/static/js/collections.js index 398aa9dee..57edabf74 100644 --- a/webapp/static/js/collections.js +++ b/webapp/static/js/collections.js @@ -1,7 +1,7 @@ (function(){ const api = { - async listCollections() { - const r = await fetch('/api/collections'); + async listCollections(archivedOnly) { + const r = await fetch('/api/collections' + (archivedOnly ? '?archived=1' : '')); return r.json(); }, async createCollection(payload){ @@ -211,6 +211,8 @@ let currentCollectionId = ''; let isBulkMode = false; let initialCollectionIdConsumed = false; + // מצב תצוגת הארכיון בסיידבר (false = אוספים רגילים; true = רק מאורכבים) + let showArchived = false; const WORKSPACE_STATE_META = { todo: { label: 'לטיפול', description: 'משימות שטרם התחלת', shortcut: 'Shift+1' }, in_progress: { label: 'בתהליך', description: 'עבודה בתהליך', shortcut: 'Shift+2' }, @@ -1082,7 +1084,7 @@ if (!root) return; root.innerHTML = '
טוען…
'; try { - const data = await api.listCollections(); + const data = await api.listCollections(showArchived); if (!data || !data.ok) throw new Error(data && data.error || 'שגיאה'); const items = (data.collections || []).map((c) => { const iconChar = (ALLOWED_ICONS.includes(c.icon) ? c.icon : (ALLOWED_ICONS[0] || '📂')) || '📂'; @@ -1098,13 +1100,16 @@ }).join(''); root.innerHTML = ` - + `; wireSidebarHandlers(root); setupSidebarDropHandlers(root); @@ -1408,6 +1413,14 @@ openCreateCollectionDialog(); }); } + // מעבר בין תצוגת אוספים רגילים לתצוגת ארכיון (רענון הרשימה מהשרת) + const toggleArchivedBtn = root.querySelector('#toggleArchivedBtn'); + if (toggleArchivedBtn) { + toggleArchivedBtn.addEventListener('click', () => { + showArchived = !showArchived; + ensureCollectionsSidebar(); + }); + } if (search) { search.addEventListener('input', () => { @@ -1897,6 +1910,9 @@
${isWorkspace ? '' : ''} + ${isWorkspace ? '' : (col.is_archived + ? '' + : '')}
`; @@ -2036,8 +2052,8 @@ itemsContainer.querySelectorAll('.collection-cards').forEach(section => { wireDnd(section, collectionId); }); - // Auto fit text for both old and new card styles - autoFitText('#collectionItems .collection-card__name', { minPx: 12, maxPx: 16, allowWrap: true }); + // Auto fit text for both old and new card styles + מיקום אייקון התיאור (Slot 1 ליד השם / Slot 2 בשורת הכפתורים) + layoutDescIcons(); autoFitText('#collectionItems .file', { minPx: 12, maxPx: 16 }); const loadMoreBtn = container.querySelector('.load-more'); if (loadMoreBtn) { @@ -2068,6 +2084,24 @@ ensureCollectionsSidebar(); container.innerHTML = '
האוסף נמחק. הקבצים נשארים זמינים בבוט ובמסך הקבצים.
'; }); + // ארכוב/שחזור אוסף — מנצל את PUT הקיים דרך שדה is_archived (כמו is_favorite) + const archiveBtn = container.querySelector('.collection-header .archive'); + const unarchiveBtn = container.querySelector('.collection-header .unarchive'); + if (archiveBtn) archiveBtn.addEventListener('click', async () => { + if (!confirm('להעביר את האוסף לארכיון? הוא ייעלם מהרשימה אך יישמר, וניתן לשחזר אותו דרך 🗄️ "הצג ארכיון".')) return; + const res = await api.updateCollection(collectionId, { is_archived: true }); + if (!res || !res.ok) return alert((res && res.error) || 'שגיאה בארכוב האוסף'); + ensureCollectionsSidebar(); + container.innerHTML = '
האוסף הועבר לארכיון. אפשר לשחזר אותו דרך 🗄️ "הצג ארכיון".
'; + }); + if (unarchiveBtn) unarchiveBtn.addEventListener('click', async () => { + const res = await api.updateCollection(collectionId, { is_archived: false }); + if (!res || !res.ok) return alert((res && res.error) || 'שגיאה בשחזור מהארכיון'); + // חוזרים לתצוגת האוספים הרגילים ומציגים את האוסף המשוחזר + showArchived = false; + ensureCollectionsSidebar(); + await renderCollectionItems(collectionId); + }); if (!isWorkspace && itemsContainer) { itemsContainer.addEventListener('click', async (ev) => { @@ -2863,13 +2897,15 @@ ${selectBox} ⋮⋮
-
- ${escapeHtml(fileName)} +
+ + ${item.description ? `` : ''}
${tagsHtml} ${item.note ? `📝 ${escapeHtml(item.note)}` : ''} - ${item.description ? `` : ''}
@@ -3539,6 +3575,42 @@ } catch(_e){ /* ignore */ } } + // מיקום אייקון התיאור בכרטיס אוסף: ברירת מחדל ליד השם (Slot 1); ואם השם ארוך + // ונשבר לשתי שורות — אין מקום בשורת השם, אז האייקון "קופץ" לשורת 4 הכפתורים, + // משמאל להם (RTL ⇒ ילד אחרון), עם רווח קטן (Slot 2). מנצל את זיהוי ה-is-wrapped + // של autoFitText, ולכן מודד את השם *ליד* האייקון (sibling ב-flex). + function layoutDescIcons(){ + try { + const cards = document.querySelectorAll('#collectionItems .collection-card'); + if (!cards || !cards.length) return; + // שלב 1: reset — כל אייקון חוזר ל-Slot 1 (מיד אחרי השם, בתוך ה-title-row) + cards.forEach((card) => { + const icon = card.querySelector('.desc-info'); + const titleRow = card.querySelector('.collection-card__title-row'); + if (icon && titleRow && icon.parentElement !== titleRow) { + titleRow.appendChild(icon); + } + }); + // שלב 2: התאמת גודל השם — מודד את הרוחב שנשאר *ליד* האייקון (הוא sibling ב-flex) + autoFitText('#collectionItems .collection-card__name', { minPx: 12, maxPx: 16, allowWrap: true }); + // שלב 3: שם שנשבר לשתי שורות ⇒ אין מקום ⇒ האייקון עובר לשורת הכפתורים (ילד אחרון) + let moved = false; + cards.forEach((card) => { + const name = card.querySelector('.collection-card__name'); + const icon = card.querySelector('.desc-info'); + const actions = card.querySelector('.collection-card__actions'); + if (name && icon && actions && name.classList.contains('is-wrapped')) { + actions.appendChild(icon); + moved = true; + } + }); + // רה-פיט לשמות שהאייקון עזב (עכשיו ברוחב מלא) — רק אם באמת הזזנו משהו + if (moved) { + autoFitText('#collectionItems .collection-card__name', { minPx: 12, maxPx: 16, allowWrap: true }); + } + } catch (_e) { /* ignore */ } + } + // תזמון/חסימה של ארועי resize כדי לא להציף חישובים function debounce(fn, wait){ let t = null; @@ -3562,7 +3634,7 @@ // עדכון אוטומטי בהתאמת חלון const onResize = throttle(() => { autoFitText('#collectionItems .file', { minPx: 12, maxPx: 16 }); - autoFitText('#collectionItems .collection-card__name', { minPx: 12, maxPx: 16, allowWrap: true }); + layoutDescIcons(); autoFitText('#collectionsSidebar .sidebar-item .name', { minPx: 12, maxPx: 16 }); autoFitText('.workspace-card__name', { minPx: 12, maxPx: 16, allowWrap: true }); }, 150);