Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
7ce3c72
Move useFilter and useKeywordSearch to shared folder
AlexVelezLl Dec 16, 2025
4beed82
Migrate useFilter to be compatible with KSelect
AlexVelezLl Dec 16, 2025
243d379
Add initial NotificationsModal implementation + NotificationsFilters
AlexVelezLl Dec 16, 2025
7e6fd06
Add useCommunityLibraryUpdates to load and process community library …
AlexVelezLl Dec 16, 2025
df01c8b
Add base structure of NotificationsList
AlexVelezLl Dec 16, 2025
16cf72a
Complete notifications list component
AlexVelezLl Dec 16, 2025
9d577fd
Fix multiple parallel reloads on admin channels and users table
AlexVelezLl Dec 16, 2025
9d3bb0d
Connect NotificationModal to Appbar and MainNavigationDrawer
AlexVelezLl Dec 16, 2025
a8d97d7
Add notifications datetimes to User model
AlexVelezLl Dec 16, 2025
ec89d20
Add red dot on AppBar and MainNavigationDrawer
AlexVelezLl Dec 16, 2025
6260dc7
Add support for unread/all notifications
AlexVelezLl Dec 17, 2025
ba58940
Add error handling and add documentation
AlexVelezLl Dec 17, 2025
89ed694
Pin KDS and small styles updates
AlexVelezLl Dec 17, 2025
abef292
Use StudioImmersiveModal instead of FullscreenModal
AlexVelezLl Jan 15, 2026
5f5d5b0
Miscelaneous fixes
AlexVelezLl Feb 19, 2026
5810489
Add page setup and channel info header
AlexVelezLl Feb 20, 2026
327eece
Add Channel details accordion
AlexVelezLl Feb 20, 2026
e815186
Add ActivityHistory component
AlexVelezLl Feb 23, 2026
8b7e0ef
Add activity line
AlexVelezLl Feb 23, 2026
4e999d2
Polish submission details modal
AlexVelezLl Feb 23, 2026
5bb8610
Display SubmissionDetailsModal on channel admin table
AlexVelezLl Feb 23, 2026
fbafc5c
Fix StudioImmersiveModal always hiding html scroll
AlexVelezLl Feb 23, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<template>

<SidePanelModal
v-if="isModalVisible"
alignment="right"
sidePanelWidth="700px"
@closePanel="$emit('close')"
Expand All @@ -20,7 +21,15 @@
class="submission-info-text"
>
<span class="author-name">{{ authorName }}</span>
<div class="editor-chip">Editor</div>
<div
class="editor-chip"
:style="{
color: chipTextColor,
backgroundColor: chipColor,
}"
>
Editor
</div>
<span>
submitted
<ActionLink
Expand All @@ -39,7 +48,10 @@
<div v-else>Error loading submission data.</div>

<div class="details">
<span class="detail-annotation">Country(s)</span>
<span
class="detail-annotation"
:style="{ color: annotationColor }"
>Country(s)</span>
<span
v-if="countriesString"
data-test="countries"
Expand All @@ -49,7 +61,10 @@
<template v-else>
<KEmptyPlaceholder />
</template>
<span class="detail-annotation">Language(s)</span>
<span
class="detail-annotation"
:style="{ color: annotationColor }"
>Language(s)</span>
<span
v-if="languagesString"
data-test="languages"
Expand All @@ -59,7 +74,10 @@
<template v-else>
<KEmptyPlaceholder />
</template>
<span class="detail-annotation">Categories</span>
<span
class="detail-annotation"
:style="{ color: annotationColor }"
>Categories</span>
<span
v-if="categoriesString"
data-test="categories"
Expand All @@ -69,7 +87,10 @@
<template v-else>
<KEmptyPlaceholder />
</template>
<span class="detail-annotation">License(s)</span>
<span
class="detail-annotation"
:style="{ color: annotationColor }"
>License(s)</span>
<span
v-if="licensesString"
data-test="licenses"
Expand All @@ -79,18 +100,31 @@
<template v-else>
<KEmptyPlaceholder />
</template>
<span class="detail-annotation">Status</span>
<CommunityLibraryStatusChip
<span
class="detail-annotation"
:style="{ color: annotationColor }"
>Status</span>
<div
v-if="submissionIsFinished"
:status="submission.status"
/>
style="display: flex"
>
<CommunityLibraryStatusChip :status="submission.status" />
</div>
<template v-else>
<KEmptyPlaceholder />
</template>
</div>

<div class="box">
<h3 class="box-title">Submission notes</h3>
<div
class="box"
:style="{ backgroundColor: boxBackgroundColor }"
>
<h3
class="box-title"
:style="{ color: boxTitleColor }"
>
Submission notes
</h3>
<span
v-if="submissionNotes"
data-test="submission-notes"
Expand Down Expand Up @@ -237,6 +271,7 @@
CommunityLibraryStatusChip,
},
setup(props, { emit }) {
const isModalVisible = ref(true);
const tokensTheme = themeTokens();
const paletteTheme = themePalette();

Expand Down Expand Up @@ -474,6 +509,8 @@

showSnackbar({ text: snackbarText });
updateStatusInStore(statusChoice.value);
emit('change');
emit('close');
} catch (error) {
showSnackbar({ text: 'Changing channel status failed' });
} finally {
Expand All @@ -487,13 +524,19 @@
actionText: 'Cancel',
actionCallback: () => {
clearTimeout(timer);
// Do not emit close just yet, so that the component isn't unmounted
// this will keep the component live until the submit function finishes, allowing
// to keep communicating with the parent component, and in particular allowing the
// "change" event to be emitted. It also allows us to keep the working information
// on the component, and show the side panel in the same state if the user cancels
isModalVisible.value = true;
currentlySubmitting.value = false;
showSnackbar({
text: 'Action cancelled',
});
},
});

emit('close');
isModalVisible.value = false;
}

const chipColor = computed(() => paletteTheme.grey.v_200);
Expand All @@ -517,6 +560,7 @@

return {
isLoading,
isModalVisible,
chipColor,
chipTextColor,
annotationColor,
Expand Down Expand Up @@ -594,8 +638,6 @@
height: 20px;
padding: 2px 5px;
font-size: 10px;
color: v-bind('chipTextColor');
background-color: v-bind('chipColor');
border-radius: 16px;
}

Expand All @@ -610,15 +652,13 @@

.detail-annotation {
grid-column-start: 1;
color: v-bind('annotationColor');
}

.box {
display: flex;
flex-direction: column;
gap: 8px;
padding: 12px;
background-color: v-bind('boxBackgroundColor');
border-radius: 8px;
}

Expand All @@ -632,7 +672,6 @@
.box-title {
font-size: 12px;
font-weight: 600;
color: v-bind('boxTitleColor');
}

.details-box-title {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ describe('ReviewSubmissionSidePanel', () => {
expect(wrapper.find('[data-test="languages"]').text()).toBe('English, Czech');
expect(wrapper.find('[data-test="categories"]').text()).toBe('School, Algebra');
expect(wrapper.find('[data-test="licenses"]').text()).toBe('CC BY, CC BY-SA');
expect(wrapper.findComponent(CommunityLibraryStatusChip).props('status')).toEqual(
expect(wrapper.findComponent(CommunityLibraryStatusChip).attributes('status')).toEqual(
submission.status,
);
expect(wrapper.find('[data-test="submission-notes"]').text()).toBe(submission.description);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,53 +1,13 @@
/* eslint-disable vue/one-component-per-file */
import { defineComponent, ref } from 'vue';
import { mount } from '@vue/test-utils';

import VueRouter from 'vue-router';
import { useTable } from '../useTable';
import { useKeywordSearch } from '../useKeywordSearch';
import { useFilter } from '../useFilter';

// Because we are testing composables that use the router,
// we need to create a dummy component that uses the composable
// and test that component with a router instance.

function makeFilterWrapper() {
const router = new VueRouter({
routes: [],
});
const component = defineComponent({
setup() {
const filterMap = ref({});
return {
...useFilter({ name: 'testFilter', filterMap }),
// eslint-disable-next-line vue/no-unused-properties
filterMap,
};
},
});

return mount(component, {
router,
});
}

function makeKeywordSearchWrapper() {
const router = new VueRouter({
routes: [],
});
const component = defineComponent({
setup() {
return {
...useKeywordSearch(),
};
},
});

return mount(component, {
router,
});
}

function makeTableWrapper() {
const router = new VueRouter({
routes: [],
Expand All @@ -74,114 +34,6 @@ function makeTableWrapper() {
});
}

describe('useFilter', () => {
let wrapper;
beforeEach(() => {
wrapper = makeFilterWrapper();
wrapper.vm.$router.push({ query: {} }).catch(() => {});
});

it('setting filter sets query params', () => {
wrapper.vm.filterMap = {
a: { label: 'A', params: { a: '1', b: '2' } },
b: { label: 'B', params: { b: '3', c: '4' } },
};
wrapper.vm.$router.push({ query: { testFilter: 'b', otherParam: 'value' } });

wrapper.vm.filter = 'a';
expect(wrapper.vm.$route.query).toEqual({ testFilter: 'a', otherParam: 'value' });
});

describe('filter is determined from query params', () => {
it('when filter params are provided', () => {
wrapper.vm.filterMap = {
a: { label: 'A', params: { a: '1', b: '2' } },
b: { label: 'B', params: { b: '3', c: '4' } },
};
wrapper.vm.$router.push({ query: { testFilter: 'a', otherParam: 'value' } });
expect(wrapper.vm.filter).toBe('a');
});

it('when filter params are not provided', () => {
wrapper.vm.filterMap = {
a: { label: 'A', params: { a: '1', b: '2' } },
b: { label: 'B', params: { b: '3', c: '4' } },
};
wrapper.vm.$router.push({ query: { otherParam: 'value' } });
expect(wrapper.vm.filter).toBe(undefined);
});
});

it('setting the filter updates fetch query params', () => {
wrapper.vm.filterMap = {
a: { label: 'A', params: { a: '1', b: '2' } },
b: { label: 'B', params: { b: '3', c: '4' } },
};
wrapper.vm.filter = 'a';
expect(wrapper.vm.fetchQueryParams).toEqual({ a: '1', b: '2' });
});

it('filters are correctly computed from filterMap', () => {
wrapper.vm.filterMap = {
a: { label: 'A', params: { a: '1', b: '2' } },
b: { label: 'B', params: { b: '3', c: '4' } },
};
expect(wrapper.vm.filters).toEqual([
{ key: 'a', label: 'A' },
{ key: 'b', label: 'B' },
]);
});
});

describe('useKeywordSearch', () => {
let wrapper;
beforeEach(() => {
wrapper = makeKeywordSearchWrapper();
wrapper.vm.$router.push({ query: {} }).catch(() => {});
});

it('setting keywords sets query params', () => {
wrapper.vm.$router.push({ query: { a: '1', page: '2' } });
wrapper.vm.keywordInput = 'test';

jest.useFakeTimers();
wrapper.vm.setKeywords();
jest.runAllTimers();
jest.useRealTimers();

expect(wrapper.vm.$route.query).toEqual({ a: '1', keywords: 'test', page: '2' });
});

it('setting query params sets keywords', async () => {
wrapper.vm.$router.push({ query: { keywords: 'test' } });
await wrapper.vm.$nextTick();

expect(wrapper.vm.keywordInput).toBe('test');
});

it('calling clearSearch clears keywords and query param', async () => {
wrapper.vm.$router.push({ query: { keywords: 'test', a: '1', page: '2' } });
await wrapper.vm.$nextTick();

wrapper.vm.clearSearch();
await wrapper.vm.$nextTick();

expect(wrapper.vm.keywordInput).toBe('');
expect(wrapper.vm.$route.query).toEqual({ a: '1', page: '2' });
});

it('setting keywords updates fetch query params', () => {
wrapper.vm.keywordInput = 'test';

jest.useFakeTimers();
wrapper.vm.setKeywords();
jest.runAllTimers();
jest.useRealTimers();

expect(wrapper.vm.fetchQueryParams).toEqual({ keywords: 'test' });
});
});

describe('useTable', () => {
let wrapper;
beforeEach(() => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import pickBy from 'lodash/pickBy';
import isEqual from 'lodash/isEqual';
import { ref, computed, unref, watch, nextTick } from 'vue';
import { useRoute } from 'vue-router/composables';
import { useQueryParams } from './useQueryParams';
import { useQueryParams } from 'shared/composables/useQueryParams';

/**
* @typedef {Object} Pagination
Expand Down Expand Up @@ -100,7 +100,10 @@ export function useTable({ fetchFunc, filterFetchQueryParams }) {

watch(
allFetchQueryParams,
() => {
(newValue, oldValue) => {
if (isEqual(newValue, oldValue)) {
return;
}
// Use nextTick to ensure that pagination can be updated before fetching
nextTick().then(() => {
loadItems();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ export const RouteNames = {
CHANNEL: 'CHANNEL',
USERS: 'USERS',
USER: 'USER',
COMMUNITY_LIBRARY_SUBMISSION: 'COMMUNITY_LIBRARY_SUBMISSION',
};

export const rowsPerPageItems = [25, 50, 75, 100];
Loading