Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
Expand Up @@ -17,6 +17,7 @@
<div class="box-icon">
<KIcon
:icon="icon"
:color="iconColor"
:style="{ fontSize: '18px' }"
/>
</div>
Expand Down Expand Up @@ -63,6 +64,8 @@
switch (props.kind) {
case 'warning':
return paletteTheme.red.v_100;
case 'success':
return paletteTheme.green.v_100;
case 'info':
return paletteTheme.grey.v_100;
default:
Expand All @@ -73,6 +76,8 @@
switch (props.kind) {
case 'warning':
return paletteTheme.red.v_300;
case 'success':
return paletteTheme.green.v_300;
case 'info':
return 'transparent';
default:
Expand All @@ -83,6 +88,8 @@
switch (props.kind) {
case 'warning':
return 'error';
case 'success':
return 'circleCheckmark';
case 'info':
return 'infoOutline';
default:
Expand All @@ -91,27 +98,36 @@
});

const titleColor = computed(() => {
return props.kind === 'warning' ? paletteTheme.red.v_600 : tokensTheme.text;
if (props.kind === 'warning') return paletteTheme.red.v_600;
if (props.kind === 'success') return paletteTheme.green.v_600;
return tokensTheme.text;
});

const descriptionColor = computed(() => {
return props.kind === 'warning' ? paletteTheme.grey.v_800 : tokensTheme.text;
});

const iconColor = computed(() => {
if (props.kind === 'warning') return paletteTheme.red.v_600;
if (props.kind === 'success') return paletteTheme.green.v_600;
return tokensTheme.text;
});

return {
boxBackgroundColor,
boxBorderColor,
titleColor,
descriptionColor,
icon,
iconColor,
};
},
props: {
kind: {
type: String,
required: false,
default: 'info',
validator: value => ['warning', 'info'].includes(value),
validator: value => ['warning', 'success', 'info'].includes(value),
},
loading: {
type: Boolean,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,9 @@ import { communityChannelsStrings } from 'shared/strings/communityChannelsString
import { CommunityLibrarySubmission } from 'shared/data/resources';
import CountryField from 'shared/views/form/CountryField.vue';

jest.mock('../composables/usePublishedData', () => ({
usePublishedData: jest.fn(),
}));
jest.mock('../composables/useLatestCommunityLibrarySubmission', () => ({
useLatestCommunityLibrarySubmission: jest.fn(),
}));
jest.mock('../composables/usePublishedData');
jest.mock('../composables/useLatestCommunityLibrarySubmission');
jest.mock('../composables/useLicenseAudit');
jest.mock('shared/data/resources', () => ({
CommunityLibrarySubmission: {
create: jest.fn(() => Promise.resolve()),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { computed, ref } from 'vue';

const MOCK_DEFAULTS = {
isLoading: ref(true),
isFinished: ref(false),
data: computed(() => null),
fetchData: jest.fn(() => Promise.resolve()),
};

export function useLatestCommunityLibrarySubmissionMock(overrides = {}) {
return {
...MOCK_DEFAULTS,
...overrides,
};
}

export const useLatestCommunityLibrarySubmission = jest.fn(() =>
useLatestCommunityLibrarySubmissionMock(),
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { computed, ref } from 'vue';

const MOCK_DEFAULTS = {
isLoading: computed(() => false),
isFinished: computed(() => true),
invalidLicenses: computed(() => []),
specialPermissions: computed(() => []),
includedLicenses: computed(() => []),
isAuditing: ref(false),
hasAuditData: computed(() => false),
auditTaskId: ref(null),
error: ref(null),
checkAndTriggerAudit: jest.fn(),
triggerAudit: jest.fn(),
fetchPublishedData: jest.fn(),
};

export function useLicenseAuditMock(overrides = {}) {
return {
...MOCK_DEFAULTS,
...overrides,
};
}

export const useLicenseAudit = jest.fn(() => useLicenseAuditMock());
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { computed, ref } from 'vue';

const MOCK_DEFAULTS = {
isLoading: ref(true),
isFinished: ref(false),
data: computed(() => null),
fetchData: jest.fn(() => Promise.resolve()),
};

export function usePublishedDataMock(overrides = {}) {
return {
...MOCK_DEFAULTS,
...overrides,
};
}

export const usePublishedData = jest.fn(() => usePublishedDataMock());
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import { computed, ref, unref, watch } from 'vue';
import { Channel } from 'shared/data/resources';

export function useLicenseAudit(channelRef, channelVersionRef) {
const isAuditing = ref(false);
const auditTaskId = ref(null);
const auditError = ref(null);
const publishedData = ref(null);

watch(
() => unref(channelRef)?.published_data,
newPublishedData => {
if (newPublishedData) {
publishedData.value = newPublishedData;
if (isAuditing.value) {
isAuditing.value = false;
auditError.value = null;
}
}
},
{ immediate: true, deep: true },
);

const currentVersionData = computed(() => {
const version = unref(channelVersionRef);
if (!publishedData.value || version == null) {
return undefined;
}
return publishedData.value[version];
});

const hasAuditData = computed(() => {
const versionData = currentVersionData.value;
if (!versionData) {
return false;
}

return (
'community_library_invalid_licenses' in versionData &&
'community_library_special_permissions' in versionData
);
});

const invalidLicenses = computed(() => {
const versionData = currentVersionData.value;
return versionData?.community_library_invalid_licenses || [];
});

const specialPermissions = computed(() => {
const versionData = currentVersionData.value;
return versionData?.community_library_special_permissions || [];
});

const includedLicenses = computed(() => {
const versionData = currentVersionData.value;
return versionData?.included_licenses || [];
});

const isAuditComplete = computed(() => {
return publishedData.value !== null && hasAuditData.value;
});

async function triggerAudit() {
if (isAuditing.value) return;

try {
isAuditing.value = true;
auditError.value = null;

const channelId = unref(channelRef)?.id;
if (!channelId) {
throw new Error('Channel ID is required to trigger audit');
}

const response = await Channel.auditLicenses(channelId);
auditTaskId.value = response.task_id;
} catch (error) {
isAuditing.value = false;
auditError.value = error;
throw error;
}
}

async function fetchPublishedData() {
const channelId = unref(channelRef)?.id;
if (!channelId) return;

try {
const data = await Channel.getPublishedData(channelId);
publishedData.value = data;
} catch (error) {
auditError.value = error;
throw error;
}
}

async function checkAndTriggerAudit() {
if (!publishedData.value) {
await fetchPublishedData();
}

if (hasAuditData.value || isAuditing.value) {
return;
}

await triggerAudit();
}

return {
isLoading: computed(() => {
if (isAuditComplete.value || auditError.value) return false;
return isAuditing.value;
}),
isFinished: computed(() => isAuditComplete.value),
isAuditing,
invalidLicenses,
specialPermissions,
includedLicenses,
hasAuditData,
auditTaskId,
error: auditError,

checkAndTriggerAudit,
triggerAudit,
fetchPublishedData,
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import { computed, ref, unref, watch } from 'vue';
import { AuditedSpecialPermissionsLicense } from 'shared/data/resources';

const ITEMS_PER_PAGE = 3;

/**
* Composable that fetches and paginates audited special-permissions licenses
* for a given set of permission IDs.
*
* @param {Array<string|number>|import('vue').Ref<Array<string|number>>} permissionIds
* A list (or ref to a list) of special-permissions license IDs to fetch.
*
* @returns {{
* permissions: import('vue').Ref<Array<Object>>,
* currentPagePermissions: import('vue').ComputedRef<Array<Object>>,
* isLoading: import('vue').Ref<boolean>,
* error: import('vue').Ref<Error|null>,
* currentPage: import('vue').Ref<number>,
* totalPages: import('vue').ComputedRef<number>,
* nextPage: () => void,
* previousPage: () => void,
* }}
* Reactive state for the fetched, flattened permissions and pagination
* helpers used by `SpecialPermissionsList.vue`.
*/
export function useSpecialPermissions(permissionIds) {
const permissions = ref([]);
const isLoading = ref(false);
const error = ref(null);
const currentPage = ref(1);

const totalPages = computed(() => {
return Math.ceil(permissions.value.length / ITEMS_PER_PAGE);
});

const currentPagePermissions = computed(() => {
const start = (currentPage.value - 1) * ITEMS_PER_PAGE;
const end = start + ITEMS_PER_PAGE;
return permissions.value.slice(start, end);
});

async function fetchPermissions(ids) {
if (!ids || ids.length === 0) {
permissions.value = [];
return;
}

isLoading.value = true;
error.value = null;

try {
const response = await AuditedSpecialPermissionsLicense.fetchCollection({
by_ids: ids.join(','),
distributable: false,
});

permissions.value = response.map(permission => ({
id: permission.id,
description: permission.description,
distributable: permission.distributable,
}));
} catch (err) {
error.value = err;
permissions.value = [];
} finally {
isLoading.value = false;
}
}

function nextPage() {
if (currentPage.value < totalPages.value) {
currentPage.value += 1;
}
}

function previousPage() {
if (currentPage.value > 1) {
currentPage.value -= 1;
}
}

const resolvedPermissionIds = computed(() => {
const ids = unref(permissionIds);
if (!ids || ids.length === 0) {
return [];
}
return ids;
});

watch(
resolvedPermissionIds,
ids => {
fetchPermissions(ids);
},
{ immediate: true },
);

return {
permissions,
currentPagePermissions,
isLoading,
error,
currentPage,
totalPages,
nextPage,
previousPage,
};
}
Loading