218 lines
7.3 KiB
JavaScript
218 lines
7.3 KiB
JavaScript
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit';
|
|
import { article, category } from '../api';
|
|
import { catchBackendError, resolveBackendError } from '../utils/backendError';
|
|
import { logout } from './authSlice';
|
|
import { loadCurrentCompany } from './companySlice';
|
|
|
|
export const ROOT_CATEGORY_NAME = 'Home';
|
|
|
|
export function normalizeCategoryNode(item) {
|
|
return {
|
|
...item,
|
|
id: item.id,
|
|
parentId: item.parentId ?? 0,
|
|
name: item.name?.trim() || null,
|
|
};
|
|
}
|
|
|
|
/** Backend: { tree: { tree: [...], articles: [] } } */
|
|
export function parseCategoryTreeResponse(data, companyRootCategoryId = null) {
|
|
const payload = data?.tree ?? data;
|
|
const rawNodes = payload?.tree ?? payload?.categories ?? data?.categories ?? [];
|
|
const list = Array.isArray(rawNodes) ? rawNodes : Object.values(rawNodes);
|
|
|
|
const categories = Object.fromEntries(
|
|
list.map((item) => [String(item.id), normalizeCategoryNode(item)]),
|
|
);
|
|
|
|
const rootCategoryId = data?.rootCategoryId ?? companyRootCategoryId ?? null;
|
|
const rootArticles = Array.isArray(payload?.articles) ? payload.articles : [];
|
|
|
|
return { rootCategoryId, categories, rootArticles };
|
|
}
|
|
|
|
export function categoryDisplayName(name, categoryId, rootCategoryId) {
|
|
if (rootCategoryId != null && String(categoryId) === String(rootCategoryId)) {
|
|
return ROOT_CATEGORY_NAME;
|
|
}
|
|
return name?.trim() || '';
|
|
}
|
|
|
|
export function normalizeCategoryMap(data) {
|
|
return parseCategoryTreeResponse(data).categories;
|
|
}
|
|
|
|
export function getCategoryChildren(categories, parentId) {
|
|
if (!categories) return [];
|
|
const parentKey = String(parentId);
|
|
return Object.values(categories)
|
|
.filter((item) => String(item.parentId ?? 0) === parentKey)
|
|
.sort((a, b) => (a.sort ?? 0) - (b.sort ?? 0) || a.id - b.id);
|
|
}
|
|
|
|
export const loadCategoryTree = createAsyncThunk(
|
|
'category/loadTree',
|
|
async (companyId, { rejectWithValue, getState }) => {
|
|
try {
|
|
const data = await category.getTree({ companyId });
|
|
const fallbackRootId = getState().company.current?.rootCategoryId ?? null;
|
|
const { rootCategoryId, categories, rootArticles } = parseCategoryTreeResponse(
|
|
data,
|
|
fallbackRootId,
|
|
);
|
|
return {
|
|
companyId,
|
|
rootCategoryId,
|
|
categories,
|
|
rootArticles,
|
|
};
|
|
} catch (err) {
|
|
return catchBackendError(rejectWithValue)(err);
|
|
}
|
|
},
|
|
);
|
|
|
|
export const loadCategoryArticles = createAsyncThunk(
|
|
'category/loadArticles',
|
|
async ({ categoryId, offset = 0, maxCount = 200 } = {}, { rejectWithValue }) => {
|
|
try {
|
|
const data = await article.byCategory({ categoryId, offset, maxCount });
|
|
return {
|
|
categoryId,
|
|
recordset: data.recordset ?? [],
|
|
length: data.length ?? data.recordset?.length ?? 0,
|
|
};
|
|
} catch (err) {
|
|
return catchBackendError(rejectWithValue)(err);
|
|
}
|
|
},
|
|
);
|
|
|
|
const initialState = {
|
|
companyId: null,
|
|
categories: null,
|
|
rootCategoryId: null,
|
|
rootArticles: [],
|
|
currentCategoryId: null,
|
|
articles: [],
|
|
articlesTotal: 0,
|
|
treeStatus: 'idle',
|
|
articlesStatus: 'idle',
|
|
error: null,
|
|
};
|
|
|
|
const categorySlice = createSlice({
|
|
name: 'category',
|
|
initialState,
|
|
reducers: {
|
|
navigateToCategory(state, action) {
|
|
state.currentCategoryId = action.payload;
|
|
state.articles = [];
|
|
state.articlesTotal = 0;
|
|
state.articlesStatus = 'idle';
|
|
state.error = null;
|
|
},
|
|
navigateBack(state) {
|
|
const current = state.categories?.[String(state.currentCategoryId)];
|
|
const parentId = current?.parentId;
|
|
if (parentId == null || parentId === 0) return;
|
|
if (String(parentId) === String(state.rootCategoryId) || state.categories?.[String(parentId)]) {
|
|
state.currentCategoryId = parentId;
|
|
state.error = null;
|
|
if (String(parentId) === String(state.rootCategoryId)) {
|
|
state.articles = state.rootArticles;
|
|
state.articlesTotal = state.rootArticles.length;
|
|
state.articlesStatus = 'succeeded';
|
|
} else {
|
|
state.articles = [];
|
|
state.articlesTotal = 0;
|
|
state.articlesStatus = 'idle';
|
|
}
|
|
}
|
|
},
|
|
resetCategoryState() {
|
|
return initialState;
|
|
},
|
|
},
|
|
extraReducers: (builder) => {
|
|
builder
|
|
.addCase(loadCategoryTree.pending, (state) => {
|
|
state.treeStatus = 'loading';
|
|
state.error = null;
|
|
})
|
|
.addCase(loadCategoryTree.fulfilled, (state, action) => {
|
|
state.treeStatus = 'succeeded';
|
|
state.companyId = action.payload.companyId;
|
|
state.categories = action.payload.categories;
|
|
state.rootCategoryId = action.payload.rootCategoryId;
|
|
state.currentCategoryId = action.payload.rootCategoryId;
|
|
state.rootArticles = action.payload.rootArticles ?? [];
|
|
state.articles = state.rootArticles;
|
|
state.articlesTotal = state.rootArticles.length;
|
|
state.articlesStatus = 'succeeded';
|
|
})
|
|
.addCase(loadCategoryTree.rejected, (state, action) => {
|
|
state.treeStatus = 'failed';
|
|
state.error = resolveBackendError(action);
|
|
})
|
|
.addCase(loadCategoryArticles.pending, (state) => {
|
|
state.articlesStatus = 'loading';
|
|
state.error = null;
|
|
})
|
|
.addCase(loadCategoryArticles.fulfilled, (state, action) => {
|
|
state.articlesStatus = 'succeeded';
|
|
state.articles = action.payload.recordset;
|
|
state.articlesTotal = action.payload.length;
|
|
})
|
|
.addCase(loadCategoryArticles.rejected, (state, action) => {
|
|
state.articlesStatus = 'failed';
|
|
state.articles = [];
|
|
state.articlesTotal = 0;
|
|
state.error = resolveBackendError(action);
|
|
})
|
|
.addCase(loadCurrentCompany.fulfilled, (state, action) => {
|
|
const nextCompanyId = action.payload?.id ?? null;
|
|
if (nextCompanyId != null && nextCompanyId !== state.companyId) {
|
|
return {
|
|
...initialState,
|
|
companyId: nextCompanyId,
|
|
};
|
|
}
|
|
})
|
|
.addCase(logout.fulfilled, () => initialState);
|
|
},
|
|
});
|
|
|
|
export const {
|
|
navigateToCategory,
|
|
navigateBack,
|
|
resetCategoryState,
|
|
} = categorySlice.actions;
|
|
|
|
export const selectCategoryMap = (state) => state.category.categories;
|
|
export const selectCurrentCategoryId = (state) => state.category.currentCategoryId;
|
|
export const selectCategoryRootId = (state) => state.category.rootCategoryId;
|
|
export const selectCategoryTreeStatus = (state) => state.category.treeStatus;
|
|
export const selectCategoryArticles = (state) => state.category.articles;
|
|
export const selectCategoryArticlesStatus = (state) => state.category.articlesStatus;
|
|
export const selectCategoryError = (state) => state.category.error;
|
|
|
|
export const selectCurrentCategory = (state) => {
|
|
const id = state.category.currentCategoryId;
|
|
if (id == null) return null;
|
|
return state.category.categories?.[String(id)] ?? null;
|
|
};
|
|
|
|
export const selectChildCategories = (state) =>
|
|
getCategoryChildren(state.category.categories, state.category.currentCategoryId);
|
|
|
|
export const selectCanNavigateBack = (state) => {
|
|
const { currentCategoryId, rootCategoryId, categories } = state.category;
|
|
if (currentCategoryId == null || rootCategoryId == null) return false;
|
|
if (String(currentCategoryId) === String(rootCategoryId)) return false;
|
|
const parentId = categories?.[String(currentCategoryId)]?.parentId;
|
|
return parentId != null && parentId !== 0;
|
|
};
|
|
|
|
export default categorySlice.reducer;
|