toggle
This commit is contained in:
@@ -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: 'Все категории, подкатегории и товары теперь скрыты',
|
||||
},
|
||||
};
|
||||
|
||||
@@ -14,12 +14,40 @@
|
||||
<mat-sidenav-container class="sidenav-container">
|
||||
<mat-sidenav mode="side" opened class="categories-sidebar">
|
||||
<div class="sidebar-header">
|
||||
<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()) {
|
||||
<app-loading-skeleton type="tree"></app-loading-skeleton>
|
||||
} @else {
|
||||
|
||||
@@ -46,15 +46,44 @@
|
||||
|
||||
.categories-sidebar {
|
||||
width: 420px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-right: 1px solid #e0e0e0;
|
||||
background-color: #fff;
|
||||
|
||||
.sidebar-header {
|
||||
padding: 1rem;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
display: flex;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
Reference in New Issue
Block a user