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',
|
NO_MORE_ITEMS: 'No more items to load',
|
||||||
SHOW: 'Show',
|
SHOW: 'Show',
|
||||||
HIDE: 'Hide',
|
HIDE: 'Hide',
|
||||||
|
SHOW_ALL: 'Show All',
|
||||||
|
HIDE_ALL: 'Hide All',
|
||||||
|
UPDATING_VISIBILITY: 'Updating visibility...',
|
||||||
|
|
||||||
// --- Translations tab ---
|
// --- Translations tab ---
|
||||||
TRANSLATIONS_HINT: 'Fill in translations for marketplace localization.',
|
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_SUBCATEGORY: 'Failed to delete subcategory',
|
||||||
FAILED_DELETE_ITEM: 'Failed to delete item',
|
FAILED_DELETE_ITEM: 'Failed to delete item',
|
||||||
FAILED_UPDATE_ITEMS: 'Failed to update items',
|
FAILED_UPDATE_ITEMS: 'Failed to update items',
|
||||||
|
FAILED_UPDATE_VISIBILITY: 'Failed to update visibility',
|
||||||
FAILED_UPLOAD_IMAGE: 'Failed to upload image',
|
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: {
|
ru: {
|
||||||
@@ -279,6 +285,9 @@ export const TRANSLATIONS: Record<string, Record<string, string>> = {
|
|||||||
NO_MORE_ITEMS: 'Все товары загружены',
|
NO_MORE_ITEMS: 'Все товары загружены',
|
||||||
SHOW: 'Показать',
|
SHOW: 'Показать',
|
||||||
HIDE: 'Скрыть',
|
HIDE: 'Скрыть',
|
||||||
|
SHOW_ALL: 'Показать все',
|
||||||
|
HIDE_ALL: 'Скрыть все',
|
||||||
|
UPDATING_VISIBILITY: 'Обновляем видимость...',
|
||||||
|
|
||||||
// --- Translations tab ---
|
// --- Translations tab ---
|
||||||
TRANSLATIONS_HINT: 'Переводы для локализации маркетплейса.',
|
TRANSLATIONS_HINT: 'Переводы для локализации маркетплейса.',
|
||||||
@@ -331,6 +340,9 @@ export const TRANSLATIONS: Record<string, Record<string, string>> = {
|
|||||||
FAILED_DELETE_SUBCATEGORY: 'Не удалось удалить подкатегорию',
|
FAILED_DELETE_SUBCATEGORY: 'Не удалось удалить подкатегорию',
|
||||||
FAILED_DELETE_ITEM: 'Не удалось удалить товар',
|
FAILED_DELETE_ITEM: 'Не удалось удалить товар',
|
||||||
FAILED_UPDATE_ITEMS: 'Не удалось обновить товары',
|
FAILED_UPDATE_ITEMS: 'Не удалось обновить товары',
|
||||||
|
FAILED_UPDATE_VISIBILITY: 'Не удалось обновить видимость',
|
||||||
FAILED_UPLOAD_IMAGE: 'Не удалось загрузить изображение',
|
FAILED_UPLOAD_IMAGE: 'Не удалось загрузить изображение',
|
||||||
|
ALL_CONTENT_VISIBLE: 'Все категории, подкатегории и товары теперь видимы',
|
||||||
|
ALL_CONTENT_HIDDEN: 'Все категории, подкатегории и товары теперь скрыты',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -14,10 +14,38 @@
|
|||||||
<mat-sidenav-container class="sidenav-container">
|
<mat-sidenav-container class="sidenav-container">
|
||||||
<mat-sidenav mode="side" opened class="categories-sidebar">
|
<mat-sidenav mode="side" opened class="categories-sidebar">
|
||||||
<div class="sidebar-header">
|
<div class="sidebar-header">
|
||||||
<h2>{{ 'CATEGORIES' | translate }}</h2>
|
<div class="sidebar-title-row">
|
||||||
<button mat-mini-fab color="primary" (click)="addCategory()" [matTooltip]="'ADD_CATEGORY' | translate">
|
<h2>{{ 'CATEGORIES' | translate }}</h2>
|
||||||
<mat-icon>add</mat-icon>
|
<button mat-mini-fab color="primary" (click)="addCategory()" [matTooltip]="'ADD_CATEGORY' | translate">
|
||||||
</button>
|
<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>
|
</div>
|
||||||
|
|
||||||
@if (loading()) {
|
@if (loading()) {
|
||||||
|
|||||||
@@ -46,6 +46,8 @@
|
|||||||
|
|
||||||
.categories-sidebar {
|
.categories-sidebar {
|
||||||
width: 420px;
|
width: 420px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
border-right: 1px solid #e0e0e0;
|
border-right: 1px solid #e0e0e0;
|
||||||
background-color: #fff;
|
background-color: #fff;
|
||||||
|
|
||||||
@@ -53,8 +55,35 @@
|
|||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
border-bottom: 1px solid #e0e0e0;
|
border-bottom: 1px solid #e0e0e0;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
flex-direction: column;
|
||||||
justify-content: space-between;
|
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 {
|
h2 {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
@@ -73,7 +102,8 @@
|
|||||||
|
|
||||||
.tree-container {
|
.tree-container {
|
||||||
overflow-y: auto;
|
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);
|
project = signal<Project | null>(null);
|
||||||
categories = signal<Category[]>([]);
|
categories = signal<Category[]>([]);
|
||||||
loading = signal(true);
|
loading = signal(true);
|
||||||
|
bulkVisibilityUpdating = signal(false);
|
||||||
treeData = signal<CategoryNode[]>([]);
|
treeData = signal<CategoryNode[]>([]);
|
||||||
selectedNodeId = signal<string | null>(null);
|
selectedNodeId = signal<string | null>(null);
|
||||||
|
|
||||||
@@ -201,6 +202,26 @@ export class ProjectViewComponent implements OnInit {
|
|||||||
this.router.navigate(['/']);
|
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() {
|
addCategory() {
|
||||||
const dialogRef = this.dialog.open(CreateDialogComponent, {
|
const dialogRef = this.dialog.open(CreateDialogComponent, {
|
||||||
data: {
|
data: {
|
||||||
|
|||||||
@@ -1,7 +1,18 @@
|
|||||||
import { Injectable, inject, signal } from '@angular/core';
|
import { Injectable, inject, signal } from '@angular/core';
|
||||||
import { HttpClient, HttpParams } from '@angular/common/http';
|
import { HttpClient, HttpParams } from '@angular/common/http';
|
||||||
import { Observable, Subject, throwError } from 'rxjs';
|
import { EMPTY, Observable, Subject, from, of, throwError } from 'rxjs';
|
||||||
import { debounceTime, retry, catchError, map, groupBy, mergeMap } from 'rxjs/operators';
|
import {
|
||||||
|
catchError,
|
||||||
|
concatMap,
|
||||||
|
debounceTime,
|
||||||
|
expand,
|
||||||
|
groupBy,
|
||||||
|
map,
|
||||||
|
mergeMap,
|
||||||
|
reduce,
|
||||||
|
retry,
|
||||||
|
toArray,
|
||||||
|
} from 'rxjs/operators';
|
||||||
import { Project, Category, Subcategory, Item, ItemsListResponse } from '../models';
|
import { Project, Category, Subcategory, Item, ItemsListResponse } from '../models';
|
||||||
import { ItemName } from '../models/item.model';
|
import { ItemName } from '../models/item.model';
|
||||||
import { MockDataService } from './mock-data.service';
|
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
|
// Image upload
|
||||||
uploadImage(file: File): Observable<{ url: string }> {
|
uploadImage(file: File): Observable<{ url: string }> {
|
||||||
if (environment.useMockData) return this.mockService.uploadImage(file);
|
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> => {
|
private handleError = (error: any): Observable<never> => {
|
||||||
let errorMessage = 'An unexpected error occurred';
|
let errorMessage = 'An unexpected error occurred';
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user