From 9038a1f782bb60db38fdaad1e7bd470cee0390a4 Mon Sep 17 00:00:00 2001 From: sdarbinyan Date: Mon, 22 Jun 2026 01:44:17 +0400 Subject: [PATCH] toggle --- src/app/i18n/translations.ts | 12 +++ .../project-view/project-view.component.html | 36 ++++++++- .../project-view/project-view.component.scss | 36 ++++++++- .../project-view/project-view.component.ts | 21 ++++++ src/app/services/api.service.ts | 75 ++++++++++++++++++- 5 files changed, 171 insertions(+), 9 deletions(-) diff --git a/src/app/i18n/translations.ts b/src/app/i18n/translations.ts index be84e0d..1112007 100644 --- a/src/app/i18n/translations.ts +++ b/src/app/i18n/translations.ts @@ -112,6 +112,9 @@ export const TRANSLATIONS: Record> = { NO_MORE_ITEMS: 'No more items to load', SHOW: 'Show', HIDE: 'Hide', + SHOW_ALL: 'Show All', + HIDE_ALL: 'Hide All', + UPDATING_VISIBILITY: 'Updating visibility...', // --- Translations tab --- TRANSLATIONS_HINT: 'Fill in translations for marketplace localization.', @@ -164,7 +167,10 @@ export const TRANSLATIONS: Record> = { FAILED_DELETE_SUBCATEGORY: 'Failed to delete subcategory', FAILED_DELETE_ITEM: 'Failed to delete item', FAILED_UPDATE_ITEMS: 'Failed to update items', + FAILED_UPDATE_VISIBILITY: 'Failed to update visibility', FAILED_UPLOAD_IMAGE: 'Failed to upload image', + ALL_CONTENT_VISIBLE: 'All categories, subcategories, and items are now visible', + ALL_CONTENT_HIDDEN: 'All categories, subcategories, and items are now hidden', }, ru: { @@ -279,6 +285,9 @@ export const TRANSLATIONS: Record> = { NO_MORE_ITEMS: 'Все товары загружены', SHOW: 'Показать', HIDE: 'Скрыть', + SHOW_ALL: 'Показать все', + HIDE_ALL: 'Скрыть все', + UPDATING_VISIBILITY: 'Обновляем видимость...', // --- Translations tab --- TRANSLATIONS_HINT: 'Переводы для локализации маркетплейса.', @@ -331,6 +340,9 @@ export const TRANSLATIONS: Record> = { FAILED_DELETE_SUBCATEGORY: 'Не удалось удалить подкатегорию', FAILED_DELETE_ITEM: 'Не удалось удалить товар', FAILED_UPDATE_ITEMS: 'Не удалось обновить товары', + FAILED_UPDATE_VISIBILITY: 'Не удалось обновить видимость', FAILED_UPLOAD_IMAGE: 'Не удалось загрузить изображение', + ALL_CONTENT_VISIBLE: 'Все категории, подкатегории и товары теперь видимы', + ALL_CONTENT_HIDDEN: 'Все категории, подкатегории и товары теперь скрыты', }, }; diff --git a/src/app/pages/project-view/project-view.component.html b/src/app/pages/project-view/project-view.component.html index 9afb00f..e1b3ca6 100644 --- a/src/app/pages/project-view/project-view.component.html +++ b/src/app/pages/project-view/project-view.component.html @@ -14,10 +14,38 @@ @if (loading()) { diff --git a/src/app/pages/project-view/project-view.component.scss b/src/app/pages/project-view/project-view.component.scss index 5a49f1d..9f2bd20 100644 --- a/src/app/pages/project-view/project-view.component.scss +++ b/src/app/pages/project-view/project-view.component.scss @@ -46,6 +46,8 @@ .categories-sidebar { width: 420px; + display: flex; + flex-direction: column; border-right: 1px solid #e0e0e0; background-color: #fff; @@ -53,8 +55,35 @@ padding: 1rem; border-bottom: 1px solid #e0e0e0; display: flex; - align-items: center; - justify-content: space-between; + flex-direction: column; + gap: 0.75rem; + + .sidebar-title-row { + display: flex; + align-items: center; + justify-content: space-between; + } + + .sidebar-bulk-row { + display: flex; + flex-direction: column; + gap: 0.5rem; + } + + .bulk-visibility-actions { + display: flex; + gap: 0.5rem; + flex-wrap: wrap; + + button { + flex: 1 1 140px; + } + } + + .bulk-status { + font-size: 0.85rem; + color: #5f6368; + } h2 { margin: 0; @@ -73,7 +102,8 @@ .tree-container { overflow-y: auto; - height: calc(100% - 65px); + flex: 1; + min-height: 0; } } diff --git a/src/app/pages/project-view/project-view.component.ts b/src/app/pages/project-view/project-view.component.ts index 62bff64..0a83abe 100644 --- a/src/app/pages/project-view/project-view.component.ts +++ b/src/app/pages/project-view/project-view.component.ts @@ -60,6 +60,7 @@ export class ProjectViewComponent implements OnInit { project = signal(null); categories = signal([]); loading = signal(true); + bulkVisibilityUpdating = signal(false); treeData = signal([]); selectedNodeId = signal(null); @@ -201,6 +202,26 @@ export class ProjectViewComponent implements OnInit { this.router.navigate(['/']); } + setAllVisibility(visible: boolean) { + if (this.bulkVisibilityUpdating() || !this.categories().length) { + return; + } + + this.bulkVisibilityUpdating.set(true); + this.apiService.setProjectVisibility(this.categories(), visible).subscribe({ + next: () => { + this.loadCategories(); + this.bulkVisibilityUpdating.set(false); + this.toast.success(this.lang.t(visible ? 'ALL_CONTENT_VISIBLE' : 'ALL_CONTENT_HIDDEN')); + }, + error: (err) => { + console.error('Failed to update project visibility', err); + this.bulkVisibilityUpdating.set(false); + this.toast.error(err.message || this.lang.t('FAILED_UPDATE_VISIBILITY')); + } + }); + } + addCategory() { const dialogRef = this.dialog.open(CreateDialogComponent, { data: { diff --git a/src/app/services/api.service.ts b/src/app/services/api.service.ts index a2be6b6..d0df991 100644 --- a/src/app/services/api.service.ts +++ b/src/app/services/api.service.ts @@ -1,7 +1,18 @@ import { Injectable, inject, signal } from '@angular/core'; import { HttpClient, HttpParams } from '@angular/common/http'; -import { Observable, Subject, throwError } from 'rxjs'; -import { debounceTime, retry, catchError, map, groupBy, mergeMap } from 'rxjs/operators'; +import { EMPTY, Observable, Subject, from, of, throwError } from 'rxjs'; +import { + catchError, + concatMap, + debounceTime, + expand, + groupBy, + map, + mergeMap, + reduce, + retry, + toArray, +} from 'rxjs/operators'; import { Project, Category, Subcategory, Item, ItemsListResponse } from '../models'; import { ItemName } from '../models/item.model'; import { MockDataService } from './mock-data.service'; @@ -302,6 +313,25 @@ export class ApiService { ); } + setProjectVisibility(categories: Category[], visible: boolean): Observable { + const { categoryIds, subcategoryIds } = this.collectVisibilityTargets(categories); + const requests = [ + ...categoryIds.map(id => () => this.updateCategory(id, { visible })), + ...subcategoryIds.map(id => () => this.updateSubcategory(id, { visible })), + ...subcategoryIds.map(id => () => this.updateSubcategoryItemsVisibility(id, visible)), + ]; + + if (!requests.length) { + return of(void 0); + } + + return from(requests).pipe( + concatMap(request => request()), + toArray(), + map(() => void 0) + ); + } + // Image upload uploadImage(file: File): Observable<{ url: string }> { if (environment.useMockData) return this.mockService.uploadImage(file); @@ -349,6 +379,47 @@ export class ApiService { }); } + private updateSubcategoryItemsVisibility(subcategoryId: string, visible: boolean): Observable { + return this.getAllSubcategoryItemIds(subcategoryId).pipe( + concatMap(itemIds => itemIds.length ? this.bulkUpdateItems(itemIds, { visible }) : of(void 0)) + ); + } + + private getAllSubcategoryItemIds(subcategoryId: string): Observable { + const pageSize = 100; + + return this.getItems(subcategoryId, 1, pageSize).pipe( + expand(response => + response.hasMore ? this.getItems(subcategoryId, response.page + 1, pageSize) : EMPTY + ), + reduce((itemIds, response) => { + itemIds.push(...response.items.map(item => item.id)); + return itemIds; + }, [] as string[]) + ); + } + + private collectVisibilityTargets(categories: Category[]) { + const categoryIds = categories.map(category => category.id); + const subcategoryIds: string[] = []; + + const visitSubcategories = (subcategories: Subcategory[]) => { + for (const subcategory of subcategories) { + subcategoryIds.push(subcategory.id); + + if (subcategory.subcategories?.length) { + visitSubcategories(subcategory.subcategories); + } + } + }; + + for (const category of categories) { + visitSubcategories(category.subcategories || []); + } + + return { categoryIds, subcategoryIds }; + } + private handleError = (error: any): Observable => { let errorMessage = 'An unexpected error occurred';