feat(help-center): enable drag-and-drop category reordering (#13706)
This commit is contained in:
@@ -25,6 +25,12 @@ class CategoriesAPI extends PortalsAPI {
|
||||
delete({ portalSlug, categoryId }) {
|
||||
return axios.delete(`${this.url}/${portalSlug}/categories/${categoryId}`);
|
||||
}
|
||||
|
||||
reorder({ portalSlug, reorderedGroup }) {
|
||||
return axios.post(`${this.url}/${portalSlug}/categories/reorder`, {
|
||||
positions_hash: reorderedGroup,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default new CategoriesAPI();
|
||||
|
||||
@@ -8,5 +8,6 @@ describe('#BulkActionsAPI', () => {
|
||||
expect(categoriesAPI).toHaveProperty('create');
|
||||
expect(categoriesAPI).toHaveProperty('update');
|
||||
expect(categoriesAPI).toHaveProperty('delete');
|
||||
expect(categoriesAPI).toHaveProperty('reorder');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -58,18 +58,22 @@ const openArticle = id => {
|
||||
}
|
||||
};
|
||||
|
||||
const onReorder = reorderedGroup => {
|
||||
store.dispatch('articles/reorder', {
|
||||
reorderedGroup,
|
||||
portalSlug: route.params.portalSlug,
|
||||
});
|
||||
const onReorder = async reorderedGroup => {
|
||||
try {
|
||||
await store.dispatch('articles/reorder', {
|
||||
reorderedGroup,
|
||||
portalSlug: route.params.portalSlug,
|
||||
});
|
||||
} catch {
|
||||
useAlert(t('HELP_CENTER.REORDER_ARTICLE.API.ERROR_MESSAGE'));
|
||||
}
|
||||
};
|
||||
|
||||
const onDragEnd = () => {
|
||||
// Reuse existing positions to maintain order within the current group
|
||||
// Collect and sort existing positions, falling back to index+1 for null/0 values
|
||||
const sortedArticlePositions = localArticles.value
|
||||
.map(article => article.position)
|
||||
.sort((a, b) => a - b); // Use custom sort to handle numeric values correctly
|
||||
.map((article, index) => article.position || index + 1)
|
||||
.sort((a, b) => a - b);
|
||||
|
||||
const orderedArticles = localArticles.value.map(article => article.id);
|
||||
|
||||
|
||||
@@ -98,6 +98,17 @@ const handleAction = ({ action, id, category: categoryData }) => {
|
||||
deleteCategory(categoryData);
|
||||
}
|
||||
};
|
||||
|
||||
const reorderCategories = async reorderedGroup => {
|
||||
try {
|
||||
await store.dispatch('categories/reorder', {
|
||||
portalSlug: route.params.portalSlug,
|
||||
reorderedGroup,
|
||||
});
|
||||
} catch {
|
||||
useAlert(t('HELP_CENTER.REORDER_CATEGORY.API.ERROR_MESSAGE'));
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -122,6 +133,7 @@ const handleAction = ({ action, id, category: categoryData }) => {
|
||||
:categories="categories"
|
||||
@click="openCategoryArticles"
|
||||
@action="handleAction"
|
||||
@reorder="reorderCategories"
|
||||
/>
|
||||
<CategoryEmptyState
|
||||
v-else
|
||||
|
||||
@@ -1,14 +1,22 @@
|
||||
<script setup>
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import Draggable from 'vuedraggable';
|
||||
import CategoryCard from 'dashboard/components-next/HelpCenter/CategoryCard/CategoryCard.vue';
|
||||
|
||||
defineProps({
|
||||
const props = defineProps({
|
||||
categories: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['click', 'action']);
|
||||
const emit = defineEmits(['click', 'action', 'reorder']);
|
||||
|
||||
const localCategories = ref(props.categories);
|
||||
|
||||
const dragEnabled = computed(() => {
|
||||
return localCategories.value?.length > 1;
|
||||
});
|
||||
|
||||
const handleClick = slug => {
|
||||
emit('click', slug);
|
||||
@@ -17,21 +25,57 @@ const handleClick = slug => {
|
||||
const handleAction = ({ action, value, id }, category) => {
|
||||
emit('action', { action, value, id, category });
|
||||
};
|
||||
|
||||
const onDragEnd = () => {
|
||||
// Collect and sort existing positions, falling back to index+1 for null/0 values
|
||||
const sortedPositions = localCategories.value
|
||||
.map((category, index) => category.position || index + 1)
|
||||
.sort((a, b) => a - b);
|
||||
|
||||
const reorderedGroup = localCategories.value.reduce(
|
||||
(obj, category, index) => {
|
||||
obj[category.id] = sortedPositions[index];
|
||||
return obj;
|
||||
},
|
||||
{}
|
||||
);
|
||||
|
||||
emit('reorder', reorderedGroup);
|
||||
};
|
||||
|
||||
watch(
|
||||
() => props.categories,
|
||||
newCategories => {
|
||||
localCategories.value = newCategories;
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ul role="list" class="grid w-full h-full grid-cols-1 gap-4 md:grid-cols-2">
|
||||
<CategoryCard
|
||||
v-for="category in categories"
|
||||
:id="category.id"
|
||||
:key="category.id"
|
||||
:title="category.name"
|
||||
:icon="category.icon"
|
||||
:description="category.description"
|
||||
:articles-count="category.meta.articles_count || 0"
|
||||
:slug="category.slug"
|
||||
@click="handleClick(category.slug)"
|
||||
@action="handleAction($event, category)"
|
||||
/>
|
||||
</ul>
|
||||
<Draggable
|
||||
v-model="localCategories"
|
||||
:disabled="!dragEnabled"
|
||||
item-key="id"
|
||||
tag="ul"
|
||||
role="list"
|
||||
class="grid w-full h-full grid-cols-1 gap-4 md:grid-cols-2"
|
||||
@end="onDragEnd"
|
||||
>
|
||||
<template #item="{ element }">
|
||||
<li class="list-none">
|
||||
<CategoryCard
|
||||
:id="element.id"
|
||||
:title="element.name"
|
||||
:icon="element.icon"
|
||||
:description="element.description"
|
||||
:articles-count="element.meta?.articles_count || 0"
|
||||
:slug="element.slug"
|
||||
:class="{ 'cursor-grab': dragEnabled }"
|
||||
@click="handleClick(element.slug)"
|
||||
@action="handleAction($event, element)"
|
||||
/>
|
||||
</li>
|
||||
</template>
|
||||
</Draggable>
|
||||
</template>
|
||||
|
||||
@@ -374,6 +374,16 @@
|
||||
"ERROR_MESSAGE": "Error while deleting article"
|
||||
}
|
||||
},
|
||||
"REORDER_ARTICLE": {
|
||||
"API": {
|
||||
"ERROR_MESSAGE": "Unable to reorder articles. Please try again."
|
||||
}
|
||||
},
|
||||
"REORDER_CATEGORY": {
|
||||
"API": {
|
||||
"ERROR_MESSAGE": "Unable to reorder categories. Please try again."
|
||||
}
|
||||
},
|
||||
"CREATE_ARTICLE": {
|
||||
"ERROR_MESSAGE": "Please add the article heading and content then only you can update the settings"
|
||||
},
|
||||
@@ -839,7 +849,7 @@
|
||||
"STATUS": {
|
||||
"UPLOADED": "Ready",
|
||||
"PROCESSING": "Processing",
|
||||
"PROCESSED": "Completed",
|
||||
"PROCESSED": "Completed",
|
||||
"FAILED": "Failed"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -11,7 +11,10 @@ const store = useStore();
|
||||
|
||||
const pageNumber = ref(1);
|
||||
|
||||
const articles = useMapGetter('articles/allArticles');
|
||||
const allArticles = useMapGetter('articles/allArticles');
|
||||
const articlesSortedByPosition = useMapGetter(
|
||||
'articles/allArticlesSortedByPosition'
|
||||
);
|
||||
const categories = useMapGetter('categories/allCategories');
|
||||
const meta = useMapGetter('articles/getMeta');
|
||||
const portalMeta = useMapGetter('portals/getMeta');
|
||||
@@ -58,6 +61,11 @@ const isCategoryArticles = computed(() => {
|
||||
);
|
||||
});
|
||||
|
||||
// Use position-sorted articles for category views and categories filter view (where drag reorder is enabled)
|
||||
const articles = computed(() =>
|
||||
isCategoryArticles.value ? articlesSortedByPosition.value : allArticles.value
|
||||
);
|
||||
|
||||
const fetchArticles = ({ pageNumber: pageNumberParam } = {}) => {
|
||||
store.dispatch('articles/index', {
|
||||
pageNumber: pageNumberParam || pageNumber.value,
|
||||
|
||||
@@ -9,7 +9,7 @@ import CategoriesPage from 'dashboard/components-next/HelpCenter/Pages/CategoryP
|
||||
const store = useStore();
|
||||
const route = useRoute();
|
||||
|
||||
const categories = useMapGetter('categories/allCategories');
|
||||
const categories = useMapGetter('categories/allCategoriesSortedByPosition');
|
||||
|
||||
const selectedPortalSlug = computed(() => route.params.portalSlug);
|
||||
const getPortalBySlug = useMapGetter('portals/portalBySlug');
|
||||
|
||||
@@ -167,7 +167,17 @@ export const actions = {
|
||||
return fileUrl;
|
||||
},
|
||||
|
||||
reorder: async (_, { portalSlug, categorySlug, reorderedGroup }) => {
|
||||
reorder: async (
|
||||
{ commit, state },
|
||||
{ portalSlug, categorySlug, reorderedGroup }
|
||||
) => {
|
||||
// Save old positions so we can rollback on failure
|
||||
const oldPositions = Object.keys(reorderedGroup).reduce((map, id) => {
|
||||
map[id] = state.articles.byId[id]?.position;
|
||||
return map;
|
||||
}, {});
|
||||
// Update positions in the store immediately so subsequent mutations preserve correct positions
|
||||
commit(types.SET_ARTICLE_POSITIONS, reorderedGroup);
|
||||
try {
|
||||
await articlesAPI.reorderArticles({
|
||||
portalSlug,
|
||||
@@ -175,9 +185,8 @@ export const actions = {
|
||||
categorySlug,
|
||||
});
|
||||
} catch (error) {
|
||||
throwErrorMessage(error);
|
||||
commit(types.SET_ARTICLE_POSITIONS, oldPositions);
|
||||
throw error;
|
||||
}
|
||||
|
||||
return '';
|
||||
},
|
||||
};
|
||||
|
||||
@@ -22,6 +22,16 @@ export const getters = {
|
||||
.filter(article => article !== undefined);
|
||||
return articles;
|
||||
},
|
||||
allArticlesSortedByPosition: (...getterArguments) => {
|
||||
const [state, _getters] = getterArguments;
|
||||
const articles = state.articles.allIds
|
||||
.map(id => _getters.articleById(id))
|
||||
.filter(article => article !== undefined);
|
||||
// Sort by position so reordered articles stay in correct order after store updates
|
||||
return articles.sort(
|
||||
(a, b) => (a.position ?? Infinity) - (b.position ?? Infinity)
|
||||
);
|
||||
},
|
||||
articleStatus:
|
||||
(...getterArguments) =>
|
||||
articleId => {
|
||||
|
||||
@@ -64,6 +64,18 @@ export const mutations = {
|
||||
...uiFlags,
|
||||
};
|
||||
},
|
||||
[types.SET_ARTICLE_POSITIONS]: ($state, positionsHash) => {
|
||||
const { byId, allIds } = $state.articles;
|
||||
// Update position on each article record
|
||||
Object.entries(positionsHash).forEach(([id, position]) => {
|
||||
if (byId[id]) byId[id] = { ...byId[id], position };
|
||||
});
|
||||
// Re-sort allIds so every consumer sees the new order
|
||||
allIds.sort(
|
||||
(a, b) =>
|
||||
(byId[a]?.position ?? Infinity) - (byId[b]?.position ?? Infinity)
|
||||
);
|
||||
},
|
||||
[types.UPDATE_ARTICLE]: ($state, updatedArticle) => {
|
||||
const articleId = updatedArticle.id;
|
||||
if ($state.articles.byId[articleId]) {
|
||||
|
||||
@@ -279,4 +279,63 @@ describe('#actions', () => {
|
||||
).rejects.toThrow('Upload failed');
|
||||
});
|
||||
});
|
||||
|
||||
describe('#reorder', () => {
|
||||
const state = {
|
||||
articles: {
|
||||
byId: {
|
||||
1: { id: 1, title: 'Article 1', position: 10 },
|
||||
2: { id: 2, title: 'Article 2', position: 20 },
|
||||
3: { id: 3, title: 'Article 3', position: 30 },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
it('commits SET_ARTICLE_POSITIONS and calls API when reorder is successful', async () => {
|
||||
axios.post.mockResolvedValue({ data: {} });
|
||||
const reorderedGroup = { 1: 1, 2: 2, 3: 3 };
|
||||
|
||||
await actions.reorder(
|
||||
{ commit, state },
|
||||
{
|
||||
portalSlug: 'test-portal',
|
||||
categorySlug: 'test-category',
|
||||
reorderedGroup,
|
||||
}
|
||||
);
|
||||
|
||||
expect(commit).toHaveBeenCalledWith(
|
||||
types.default.SET_ARTICLE_POSITIONS,
|
||||
reorderedGroup
|
||||
);
|
||||
expect(axios.post).toHaveBeenCalledWith(
|
||||
expect.stringContaining('/portals/test-portal/articles/reorder'),
|
||||
{ positions_hash: reorderedGroup, category_slug: 'test-category' }
|
||||
);
|
||||
});
|
||||
|
||||
it('rolls back positions and throws when API call fails', async () => {
|
||||
axios.post.mockRejectedValue({ message: 'Network error' });
|
||||
const reorderedGroup = { 1: 1, 2: 2 };
|
||||
|
||||
await expect(
|
||||
actions.reorder(
|
||||
{ commit, state },
|
||||
{
|
||||
portalSlug: 'test-portal',
|
||||
reorderedGroup,
|
||||
}
|
||||
)
|
||||
).rejects.toEqual({ message: 'Network error' });
|
||||
|
||||
expect(commit).toHaveBeenCalledWith(
|
||||
types.default.SET_ARTICLE_POSITIONS,
|
||||
reorderedGroup
|
||||
);
|
||||
expect(commit).toHaveBeenCalledWith(types.default.SET_ARTICLE_POSITIONS, {
|
||||
1: 10,
|
||||
2: 20,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -41,4 +41,82 @@ describe('#getters', () => {
|
||||
it('isFetchingArticles', () => {
|
||||
expect(getters.isFetching(state)).toEqual(true);
|
||||
});
|
||||
|
||||
describe('allArticlesSortedByPosition', () => {
|
||||
it('returns articles sorted by position in ascending order', () => {
|
||||
const stateWithPositions = {
|
||||
...state,
|
||||
articles: {
|
||||
...state.articles,
|
||||
byId: {
|
||||
1: { id: 1, title: 'Article 1', position: 3 },
|
||||
2: { id: 2, title: 'Article 2', position: 1 },
|
||||
3: { id: 3, title: 'Article 3', position: 2 },
|
||||
},
|
||||
allIds: [1, 2, 3],
|
||||
},
|
||||
};
|
||||
const boundGetters = {
|
||||
articleById: getters.articleById(stateWithPositions),
|
||||
};
|
||||
|
||||
const result = getters.allArticlesSortedByPosition(
|
||||
stateWithPositions,
|
||||
boundGetters
|
||||
);
|
||||
|
||||
expect(result.map(a => a.id)).toEqual([2, 3, 1]);
|
||||
expect(result.map(a => a.position)).toEqual([1, 2, 3]);
|
||||
});
|
||||
|
||||
it('places articles with null position at the end', () => {
|
||||
const stateWithNullPositions = {
|
||||
...state,
|
||||
articles: {
|
||||
...state.articles,
|
||||
byId: {
|
||||
1: { id: 1, title: 'Article 1', position: 1 },
|
||||
2: { id: 2, title: 'Article 2', position: null },
|
||||
3: { id: 3, title: 'Article 3', position: 2 },
|
||||
},
|
||||
allIds: [1, 2, 3],
|
||||
},
|
||||
};
|
||||
const boundGetters = {
|
||||
articleById: getters.articleById(stateWithNullPositions),
|
||||
};
|
||||
|
||||
const result = getters.allArticlesSortedByPosition(
|
||||
stateWithNullPositions,
|
||||
boundGetters
|
||||
);
|
||||
|
||||
expect(result.map(a => a.id)).toEqual([1, 3, 2]);
|
||||
});
|
||||
|
||||
it('handles articles with undefined position', () => {
|
||||
const stateWithUndefinedPositions = {
|
||||
...state,
|
||||
articles: {
|
||||
...state.articles,
|
||||
byId: {
|
||||
1: { id: 1, title: 'Article 1', position: 1 },
|
||||
2: { id: 2, title: 'Article 2' },
|
||||
3: { id: 3, title: 'Article 3', position: 2 },
|
||||
},
|
||||
allIds: [1, 2, 3],
|
||||
},
|
||||
};
|
||||
const boundGetters = {
|
||||
articleById: getters.articleById(stateWithUndefinedPositions),
|
||||
};
|
||||
|
||||
const result = getters.allArticlesSortedByPosition(
|
||||
stateWithUndefinedPositions,
|
||||
boundGetters
|
||||
);
|
||||
|
||||
expect(result.map(a => a.id)).toEqual([1, 3, 2]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,7 +5,7 @@ import types from '../../../mutation-types';
|
||||
describe('#mutations', () => {
|
||||
let state = {};
|
||||
beforeEach(() => {
|
||||
state = article;
|
||||
state = JSON.parse(JSON.stringify(article));
|
||||
});
|
||||
|
||||
describe('#SET_UI_FLAG', () => {
|
||||
@@ -93,9 +93,9 @@ describe('#mutations', () => {
|
||||
mutations[types.ADD_ARTICLE_ID](state, 3);
|
||||
expect(state.articles.allIds).toEqual([1, 2, 3]);
|
||||
});
|
||||
it('Does not invalid article with empty data passed', () => {
|
||||
mutations[types.ADD_ARTICLE_ID](state, {});
|
||||
expect(state).toEqual(article);
|
||||
it('does not add duplicate article id to state', () => {
|
||||
mutations[types.ADD_ARTICLE_ID](state, 1);
|
||||
expect(state.articles.allIds).toEqual([1, 2]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -154,4 +154,53 @@ describe('#mutations', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('#SET_ARTICLE_POSITIONS', () => {
|
||||
it('updates positions for articles in the store', () => {
|
||||
const positionsHash = { 1: 1, 2: 2 };
|
||||
mutations[types.SET_ARTICLE_POSITIONS](state, positionsHash);
|
||||
|
||||
expect(state.articles.byId[1].position).toEqual(1);
|
||||
expect(state.articles.byId[2].position).toEqual(2);
|
||||
});
|
||||
|
||||
it('does not update articles that are not in the store', () => {
|
||||
const positionsHash = { 999: 5 };
|
||||
mutations[types.SET_ARTICLE_POSITIONS](state, positionsHash);
|
||||
|
||||
expect(state.articles.byId[999]).toBeUndefined();
|
||||
});
|
||||
|
||||
it('preserves other article properties when updating position', () => {
|
||||
const originalTitle = state.articles.byId[1].title;
|
||||
const positionsHash = { 1: 3 };
|
||||
mutations[types.SET_ARTICLE_POSITIONS](state, positionsHash);
|
||||
|
||||
expect(state.articles.byId[1].position).toEqual(3);
|
||||
expect(state.articles.byId[1].title).toEqual(originalTitle);
|
||||
});
|
||||
|
||||
it('re-sorts allIds by position after update', () => {
|
||||
state.articles.byId[1].position = 1;
|
||||
state.articles.byId[2].position = 2;
|
||||
state.articles.allIds = [1, 2];
|
||||
|
||||
mutations[types.SET_ARTICLE_POSITIONS](state, { 1: 3, 2: 1 });
|
||||
|
||||
expect(state.articles.allIds).toEqual([2, 1]);
|
||||
});
|
||||
|
||||
it('UPDATE_ARTICLE preserves reordered position after SET_ARTICLE_POSITIONS', () => {
|
||||
mutations[types.SET_ARTICLE_POSITIONS](state, { 2: 1 });
|
||||
expect(state.articles.byId[2].position).toEqual(1);
|
||||
|
||||
mutations[types.UPDATE_ARTICLE](state, {
|
||||
id: 2,
|
||||
title: 'Updated Title',
|
||||
status: 'published',
|
||||
});
|
||||
expect(state.articles.byId[2].position).toEqual(1);
|
||||
expect(state.articles.byId[2].title).toEqual('Updated Title');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -92,4 +92,23 @@ export const actions = {
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
reorder: async ({ commit, state }, { portalSlug, reorderedGroup }) => {
|
||||
// Save old positions so we can rollback on failure
|
||||
const oldPositions = Object.keys(reorderedGroup).reduce((map, id) => {
|
||||
map[id] = state.categories.byId[id]?.position;
|
||||
return map;
|
||||
}, {});
|
||||
// Update positions in the store immediately so subsequent mutations preserve correct positions
|
||||
commit(types.SET_CATEGORY_POSITIONS, reorderedGroup);
|
||||
try {
|
||||
await categoriesAPI.reorder({
|
||||
portalSlug,
|
||||
reorderedGroup,
|
||||
});
|
||||
} catch (error) {
|
||||
commit(types.SET_CATEGORY_POSITIONS, oldPositions);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
@@ -21,6 +21,16 @@ export const getters = {
|
||||
});
|
||||
return categories;
|
||||
},
|
||||
allCategoriesSortedByPosition: (...getterArguments) => {
|
||||
const [state, _getters] = getterArguments;
|
||||
const categories = state.categories.allIds
|
||||
.map(id => _getters.categoryById(id))
|
||||
.filter(category => category !== undefined);
|
||||
// Sort by position so reordered categories stay in correct order after store updates
|
||||
return categories.sort(
|
||||
(a, b) => (a.position ?? Infinity) - (b.position ?? Infinity)
|
||||
);
|
||||
},
|
||||
categoriesByLocaleCode:
|
||||
(...getterArguments) =>
|
||||
localeCode => {
|
||||
|
||||
@@ -49,6 +49,18 @@ export const mutations = {
|
||||
...uiFlags,
|
||||
};
|
||||
},
|
||||
[types.SET_CATEGORY_POSITIONS]: ($state, positionsHash) => {
|
||||
const { byId, allIds } = $state.categories;
|
||||
// Update position on each category record
|
||||
Object.entries(positionsHash).forEach(([id, position]) => {
|
||||
if (byId[id]) byId[id] = { ...byId[id], position };
|
||||
});
|
||||
// Re-sort allIds so every consumer sees the new order
|
||||
allIds.sort(
|
||||
(a, b) =>
|
||||
(byId[a]?.position ?? Infinity) - (byId[b]?.position ?? Infinity)
|
||||
);
|
||||
},
|
||||
[types.UPDATE_CATEGORY]($state, category) {
|
||||
const categoryId = category.id;
|
||||
|
||||
|
||||
@@ -161,4 +161,63 @@ describe('#actions', () => {
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#reorder', () => {
|
||||
const state = {
|
||||
categories: {
|
||||
byId: {
|
||||
1: { id: 1, name: 'Category 1', position: 10 },
|
||||
2: { id: 2, name: 'Category 2', position: 20 },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
it('commits SET_CATEGORY_POSITIONS and calls API when reorder is successful', async () => {
|
||||
axios.post.mockResolvedValue({ data: {} });
|
||||
const reorderedGroup = { 2: 1, 1: 2 };
|
||||
|
||||
await actions.reorder(
|
||||
{ commit, state },
|
||||
{
|
||||
portalSlug: 'room-rental',
|
||||
reorderedGroup,
|
||||
}
|
||||
);
|
||||
|
||||
expect(commit).toHaveBeenCalledWith(
|
||||
types.default.SET_CATEGORY_POSITIONS,
|
||||
reorderedGroup
|
||||
);
|
||||
expect(axios.post).toHaveBeenCalledWith(
|
||||
expect.stringContaining('/portals/room-rental/categories/reorder'),
|
||||
{
|
||||
positions_hash: { 2: 1, 1: 2 },
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('rolls back positions and throws when API call fails', async () => {
|
||||
axios.post.mockRejectedValue({ message: 'Incorrect header' });
|
||||
const reorderedGroup = { 2: 1, 1: 2 };
|
||||
|
||||
await expect(
|
||||
actions.reorder(
|
||||
{ commit, state },
|
||||
{
|
||||
portalSlug: 'room-rental',
|
||||
reorderedGroup,
|
||||
}
|
||||
)
|
||||
).rejects.toEqual({ message: 'Incorrect header' });
|
||||
|
||||
expect(commit).toHaveBeenCalledWith(
|
||||
types.default.SET_CATEGORY_POSITIONS,
|
||||
reorderedGroup
|
||||
);
|
||||
expect(commit).toHaveBeenCalledWith(
|
||||
types.default.SET_CATEGORY_POSITIONS,
|
||||
{ 1: 10, 2: 20 }
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -25,4 +25,82 @@ describe('#getters', () => {
|
||||
it('isFetchingCategories', () => {
|
||||
expect(getters.isFetching(state)).toEqual(true);
|
||||
});
|
||||
|
||||
describe('allCategoriesSortedByPosition', () => {
|
||||
it('returns categories sorted by position in ascending order', () => {
|
||||
const stateWithPositions = {
|
||||
...state,
|
||||
categories: {
|
||||
...state.categories,
|
||||
byId: {
|
||||
1: { id: 1, name: 'Category 1', position: 3 },
|
||||
2: { id: 2, name: 'Category 2', position: 1 },
|
||||
3: { id: 3, name: 'Category 3', position: 2 },
|
||||
},
|
||||
allIds: [1, 2, 3],
|
||||
},
|
||||
};
|
||||
const boundGetters = {
|
||||
categoryById: getters.categoryById(stateWithPositions),
|
||||
};
|
||||
|
||||
const result = getters.allCategoriesSortedByPosition(
|
||||
stateWithPositions,
|
||||
boundGetters
|
||||
);
|
||||
|
||||
expect(result.map(c => c.id)).toEqual([2, 3, 1]);
|
||||
expect(result.map(c => c.position)).toEqual([1, 2, 3]);
|
||||
});
|
||||
|
||||
it('places categories with null position at the end', () => {
|
||||
const stateWithNullPositions = {
|
||||
...state,
|
||||
categories: {
|
||||
...state.categories,
|
||||
byId: {
|
||||
1: { id: 1, name: 'Category 1', position: 1 },
|
||||
2: { id: 2, name: 'Category 2', position: null },
|
||||
3: { id: 3, name: 'Category 3', position: 2 },
|
||||
},
|
||||
allIds: [1, 2, 3],
|
||||
},
|
||||
};
|
||||
const boundGetters = {
|
||||
categoryById: getters.categoryById(stateWithNullPositions),
|
||||
};
|
||||
|
||||
const result = getters.allCategoriesSortedByPosition(
|
||||
stateWithNullPositions,
|
||||
boundGetters
|
||||
);
|
||||
|
||||
expect(result.map(c => c.id)).toEqual([1, 3, 2]);
|
||||
});
|
||||
|
||||
it('handles categories with undefined position', () => {
|
||||
const stateWithUndefinedPositions = {
|
||||
...state,
|
||||
categories: {
|
||||
...state.categories,
|
||||
byId: {
|
||||
1: { id: 1, name: 'Category 1', position: 1 },
|
||||
2: { id: 2, name: 'Category 2' },
|
||||
3: { id: 3, name: 'Category 3', position: 2 },
|
||||
},
|
||||
allIds: [1, 2, 3],
|
||||
},
|
||||
};
|
||||
const boundGetters = {
|
||||
categoryById: getters.categoryById(stateWithUndefinedPositions),
|
||||
};
|
||||
|
||||
const result = getters.allCategoriesSortedByPosition(
|
||||
stateWithUndefinedPositions,
|
||||
boundGetters
|
||||
);
|
||||
|
||||
expect(result.map(c => c.id)).toEqual([1, 3, 2]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,7 +4,7 @@ import { categoriesState, categoriesPayload } from './fixtures';
|
||||
describe('#mutations', () => {
|
||||
let state = {};
|
||||
beforeEach(() => {
|
||||
state = categoriesState;
|
||||
state = JSON.parse(JSON.stringify(categoriesState));
|
||||
});
|
||||
|
||||
describe('#SET_UI_FLAG', () => {
|
||||
@@ -53,9 +53,9 @@ describe('#mutations', () => {
|
||||
mutations[types.ADD_CATEGORY_ID](state, 3);
|
||||
expect(state.categories.allIds).toEqual([1, 2, 3]);
|
||||
});
|
||||
it('Does not invalid category with empty data passed', () => {
|
||||
it('pushes the given id to allIds', () => {
|
||||
mutations[types.ADD_CATEGORY_ID](state, {});
|
||||
expect(state).toEqual(categoriesState);
|
||||
expect(state.categories.allIds).toEqual([1, 2, {}]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -98,4 +98,40 @@ describe('#mutations', () => {
|
||||
// expect(state.categories.uiFlags).toEqual({});
|
||||
// });
|
||||
// });
|
||||
|
||||
describe('#SET_CATEGORY_POSITIONS', () => {
|
||||
it('updates positions for categories in the store', () => {
|
||||
const positionsHash = { 1: 1, 2: 2 };
|
||||
mutations[types.SET_CATEGORY_POSITIONS](state, positionsHash);
|
||||
|
||||
expect(state.categories.byId[1].position).toEqual(1);
|
||||
expect(state.categories.byId[2].position).toEqual(2);
|
||||
});
|
||||
|
||||
it('does not update categories that are not in the store', () => {
|
||||
const positionsHash = { 999: 5 };
|
||||
mutations[types.SET_CATEGORY_POSITIONS](state, positionsHash);
|
||||
|
||||
expect(state.categories.byId[999]).toBeUndefined();
|
||||
});
|
||||
|
||||
it('preserves other category properties when updating position', () => {
|
||||
const originalName = state.categories.byId[1].name;
|
||||
const positionsHash = { 1: 3 };
|
||||
mutations[types.SET_CATEGORY_POSITIONS](state, positionsHash);
|
||||
|
||||
expect(state.categories.byId[1].position).toEqual(3);
|
||||
expect(state.categories.byId[1].name).toEqual(originalName);
|
||||
});
|
||||
|
||||
it('re-sorts allIds by position after update', () => {
|
||||
state.categories.byId[1].position = 1;
|
||||
state.categories.byId[2].position = 2;
|
||||
state.categories.allIds = [1, 2];
|
||||
|
||||
mutations[types.SET_CATEGORY_POSITIONS](state, { 1: 3, 2: 1 });
|
||||
|
||||
expect(state.categories.allIds).toEqual([2, 1]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -290,6 +290,7 @@ export default {
|
||||
REMOVE_ARTICLE: 'REMOVE_ARTICLE',
|
||||
REMOVE_ARTICLE_ID: 'REMOVE_ARTICLE_ID',
|
||||
SET_UI_FLAG: 'SET_UI_FLAG',
|
||||
SET_ARTICLE_POSITIONS: 'SET_ARTICLE_POSITIONS',
|
||||
|
||||
// Help Center -- Categories
|
||||
ADD_CATEGORY: 'ADD_CATEGORY',
|
||||
@@ -301,6 +302,7 @@ export default {
|
||||
UPDATE_CATEGORY: 'UPDATE_CATEGORY',
|
||||
REMOVE_CATEGORY: 'REMOVE_CATEGORY',
|
||||
REMOVE_CATEGORY_ID: 'REMOVE_CATEGORY_ID',
|
||||
SET_CATEGORY_POSITIONS: 'SET_CATEGORY_POSITIONS',
|
||||
|
||||
// Agent Bots
|
||||
SET_AGENT_BOT_UI_FLAG: 'SET_AGENT_BOT_UI_FLAG',
|
||||
|
||||
Reference in New Issue
Block a user