This commit is contained in:
sdarbinyan
2026-06-22 01:44:17 +04:00
parent fb570a32f5
commit 9038a1f782
5 changed files with 171 additions and 9 deletions

View File

@@ -112,6 +112,9 @@ export const TRANSLATIONS: Record<string, Record<string, string>> = {
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<string, Record<string, string>> = {
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<string, Record<string, string>> = {
NO_MORE_ITEMS: 'Все товары загружены',
SHOW: 'Показать',
HIDE: 'Скрыть',
SHOW_ALL: 'Показать все',
HIDE_ALL: 'Скрыть все',
UPDATING_VISIBILITY: 'Обновляем видимость...',
// --- Translations tab ---
TRANSLATIONS_HINT: 'Переводы для локализации маркетплейса.',
@@ -331,6 +340,9 @@ export const TRANSLATIONS: Record<string, Record<string, string>> = {
FAILED_DELETE_SUBCATEGORY: 'Не удалось удалить подкатегорию',
FAILED_DELETE_ITEM: 'Не удалось удалить товар',
FAILED_UPDATE_ITEMS: 'Не удалось обновить товары',
FAILED_UPDATE_VISIBILITY: 'Не удалось обновить видимость',
FAILED_UPLOAD_IMAGE: 'Не удалось загрузить изображение',
ALL_CONTENT_VISIBLE: 'Все категории, подкатегории и товары теперь видимы',
ALL_CONTENT_HIDDEN: 'Все категории, подкатегории и товары теперь скрыты',
},
};

View File

@@ -14,10 +14,38 @@
<mat-sidenav-container class="sidenav-container">
<mat-sidenav mode="side" opened class="categories-sidebar">
<div class="sidebar-header">
<h2>{{ 'CATEGORIES' | translate }}</h2>
<button mat-mini-fab color="primary" (click)="addCategory()" [matTooltip]="'ADD_CATEGORY' | translate">
<mat-icon>add</mat-icon>
</button>
<div class="sidebar-title-row">
<h2>{{ 'CATEGORIES' | translate }}</h2>
<button mat-mini-fab color="primary" (click)="addCategory()" [matTooltip]="'ADD_CATEGORY' | translate">
<mat-icon>add</mat-icon>
</button>
</div>
<div class="sidebar-bulk-row">
<div class="bulk-visibility-actions">
<button
mat-stroked-button
color="primary"
(click)="setAllVisibility(true)"
[disabled]="loading() || bulkVisibilityUpdating() || !categories().length">
<mat-icon>visibility</mat-icon>
{{ 'SHOW_ALL' | translate }}
</button>
<button
mat-stroked-button
color="primary"
(click)="setAllVisibility(false)"
[disabled]="loading() || bulkVisibilityUpdating() || !categories().length">
<mat-icon>visibility_off</mat-icon>
{{ 'HIDE_ALL' | translate }}
</button>
</div>
@if (bulkVisibilityUpdating()) {
<span class="bulk-status">{{ 'UPDATING_VISIBILITY' | translate }}</span>
}
</div>
</div>
@if (loading()) {

View File

@@ -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;
}
}

View File

@@ -60,6 +60,7 @@ export class ProjectViewComponent implements OnInit {
project = signal<Project | null>(null);
categories = signal<Category[]>([]);
loading = signal(true);
bulkVisibilityUpdating = signal(false);
treeData = signal<CategoryNode[]>([]);
selectedNodeId = signal<string | null>(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: {

View File

@@ -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<void> {
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<void> {
return this.getAllSubcategoryItemIds(subcategoryId).pipe(
concatMap(itemIds => itemIds.length ? this.bulkUpdateItems(itemIds, { visible }) : of(void 0))
);
}
private getAllSubcategoryItemIds(subcategoryId: string): Observable<string[]> {
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<never> => {
let errorMessage = 'An unexpected error occurred';