Skip to content
Open
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
@@ -0,0 +1,51 @@
import { ref, onMounted } from 'vue';
import { OrganizationRoles } from '../constants';
import { Organization } from 'shared/data/resources';

/**
* Composable for fetching, creating, and updating a single organization.
* Pass a getter returning a falsy organizationId to use this in "create a new
* organization" mode: the fetch is skipped and `create` becomes usable instead
* of `update`.
*/
export function useOrganization(getOrganizationId) {
const loading = ref(Boolean(getOrganizationId()));
const organization = ref(null);

function load() {
return Organization.fetchModel(getOrganizationId()).then(data => {
organization.value = data;
});
}

onMounted(() => {
if (!getOrganizationId()) {
return;
}
load().finally(() => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: No .catch — a failed fetch leaves organization null, indistinguishable from empty. Same in useOrganizationList.js:14, useOrganizationMembers.js:24, useOrganizationInvitations.js:28, all unhandled (client.js:113). Compare useChannelList.js:32-46.

loading.value = false;
});
});

function update(data) {
return Organization.update(getOrganizationId(), data).then(updated => {
organization.value = updated;
return updated;
});
}

function create(data) {
return Organization.create(data).then(created => {
const withAdminRole = { ...created, role: OrganizationRoles.ADMIN };
organization.value = withAdminRole;
return withAdminRole;
});
}

return {
loading,
organization,
update,
create,
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { ref, onMounted } from 'vue';
import { Invitation } from 'shared/data/resources';

/**
* Composable for fetching and responding to organization invitations.
*
* @param {Object} params - fetchCollection params, e.g. `{ invited: 1 }` for
* "invitations addressed to me" (used by the My Organizations banner), or
* `{ organization: organizationId }` for "pending invites for this org"
* (used by the org Sharing tab).
*/
export function useOrganizationInvitations(params = { invited: 1 }) {
const loading = ref(true);
const invitations = ref([]);

function loadInvitations() {
return Invitation.fetchCollection(params).then(data => {
invitations.value = data.filter(
invitation =>
invitation.organization &&
!invitation.accepted &&
!invitation.declined &&
!invitation.revoked,
);
});
}

onMounted(() => {
loadInvitations().finally(() => {
loading.value = false;
});
});

function accept(invitationId) {
return Invitation.accept(invitationId).then(() => {
invitations.value = invitations.value.filter(i => i.id !== invitationId);
});
}

function decline(invitationId) {
return Invitation.decline(invitationId).then(() => {
invitations.value = invitations.value.filter(i => i.id !== invitationId);
});
}

function revoke(invitationId) {
return Invitation.revoke(invitationId).then(() => {
invitations.value = invitations.value.filter(i => i.id !== invitationId);
});
}

return {
loading,
invitations,
accept,
decline,
revoke,
refresh: loadInvitations,
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { ref, onMounted } from 'vue';
import { Organization } from 'shared/data/resources';

const MAX_PAGE_SIZE = 100;

/**
* Composable for fetching the organizations the current user belongs to.
*/
export function useOrganizationList() {
const loading = ref(true);
const organizations = ref([]);

onMounted(() => {
Organization.fetchCollection({ page_size: MAX_PAGE_SIZE, member: true })
.then(data => {
organizations.value = data;
})
.finally(() => {
loading.value = false;
});
});

return {
loading,
organizations,
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { ref, onMounted } from 'vue';
import { OrganizationRoleStatuses } from '../constants';
import { OrganizationRole } from 'shared/data/resources';

const MAX_PAGE_SIZE = 100;

/**
* Composable for fetching and managing an organization's active members.
*/
export function useOrganizationMembers(organizationId) {
const loading = ref(true);
const members = ref([]);

function loadMembers() {
return OrganizationRole.fetchCollection({
organization: organizationId,
status: OrganizationRoleStatuses.ACTIVE,
page_size: MAX_PAGE_SIZE,
}).then(data => {
members.value = data;
});
}

onMounted(() => {
loadMembers().finally(() => {
loading.value = false;
});
});

function changeRole(roleId, role) {
return OrganizationRole.update(roleId, { role }).then(updated => {
members.value = members.value.map(member => (member.id === roleId ? updated : member));
return updated;
});
}

function close(roleId) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: close hard-deletes, losing joined_at; PATCH {status: 'inactive'} keeps it and hits the same guard (viewsets/organization.py:308-313). OrganizationRoleStatuses.INACTIVE (constants.js:63) is unused — intended?

return OrganizationRole.delete(roleId).then(() => {
members.value = members.value.filter(member => member.id !== roleId);
});
}

return {
loading,
members,
changeRole,
close,
refresh: loadMembers,
};
}
20 changes: 20 additions & 0 deletions contentcuration/contentcuration/frontend/channelList/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { ChannelListTypes } from 'shared/constants';
export const InvitationShareModes = {
EDIT: 'edit',
VIEW_ONLY: 'view',
ADMIN: 'admin',
};

export const ChannelInvitationMapping = {
Expand All @@ -14,6 +15,9 @@ export const ChannelInvitationMapping = {

export const RouteNames = {
CHANNELS_EDITABLE: 'CHANNELS_EDITABLE',
MY_ORGANIZATIONS: 'MY_ORGANIZATIONS',
ORGANIZATION_EDIT: 'ORGANIZATION_EDIT',
NEW_ORGANIZATION: 'NEW_ORGANIZATION',
CHANNELS_STARRED: 'CHANNELS_STARRED',
CHANNELS_VIEW_ONLY: 'CHANNELS_VIEW_ONLY',
CHANNELS_PUBLIC: 'CHANNELS_PUBLIC',
Expand Down Expand Up @@ -41,3 +45,19 @@ export const ListTypeToRouteMapping = {
export const RouteToListTypeMapping = invert(ListTypeToRouteMapping);

export const CHANNEL_PAGE_SIZE = 25;

export const OrganizationEditTabs = {
DETAILS: 'details',
SHARING: 'sharing',
};

export const OrganizationRoles = {
ADMIN: 'admin',
EDITOR: 'editor',
VIEWER: 'viewer',
};

export const OrganizationRoleStatuses = {
ACTIVE: 'active',
INACTIVE: 'inactive',
};
19 changes: 19 additions & 0 deletions contentcuration/contentcuration/frontend/channelList/router.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import VueRouter from 'vue-router';
import CommunityChannelDetailsModal from './views/Channel/CommunityLibraryList/CommunityChannelDetailsModal.vue';
import StudioMyChannels from './views/Channel/StudioMyChannels';
import StudioMyOrganizations from './views/Organization/StudioMyOrganizations.vue';
import OrganizationEditPage from './views/Organization/OrganizationEditPage.vue';
import StudioStarredChannels from './views/Channel/StudioStarredChannels';
import StudioViewOnlyChannels from './views/Channel/StudioViewOnlyChannels';
import StudioCollectionsTable from './views/ChannelSet/StudioCollectionsTable';
Expand All @@ -20,6 +22,23 @@ const router = new VueRouter({
path: '/my-channels',
component: StudioMyChannels,
},
{
name: RouteNames.MY_ORGANIZATIONS,
path: '/my-organizations',
component: StudioMyOrganizations,
},
{
name: RouteNames.NEW_ORGANIZATION,
path: '/organization/new',
component: OrganizationEditPage,
props: true,
},
{
name: RouteNames.ORGANIZATION_EDIT,
path: '/organization/:organizationId/:tab',
component: OrganizationEditPage,
props: true,
},
{
name: RouteNames.CHANNEL_SETS,
path: '/collections',
Expand Down
5 changes: 5 additions & 0 deletions contentcuration/contentcuration/frontend/channelList/utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export function getApiErrorMessage(error, fallback) {

@rtibblesbot rtibblesbot Sep 8, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resolved — addressed in the current code.

suggestion: OrganizationUsersTable.handleMembershipError (211-215) is still the verbatim body of this util — 3 of 4 call sites converted.

const data = error && error.response && error.response.data;
const message = Array.isArray(data) ? data[0] : null;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: data[0] is untranslated DRF text (viewsets/organization.py:281-283) that displaces the $tr fallback.

return message || fallback;
}
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,16 @@
badgeValue: this.invitationsByListCounts[listType] || 0,
analyticsLabel: ListTypeToAnalyticsLabel[listType],
});

if (listType === ChannelListTypes.EDITABLE) {
tabs.push({
id: 'myOrganizations',
label: this.$tr('myOrganizations'),
to: this.myOrganizationsLink,
badgeValue: 0,
analyticsLabel: 'MY_ORGANIZATIONS',
});
}
});

tabs.push({
Expand Down Expand Up @@ -195,6 +205,9 @@
channelSetLink() {
return { name: RouteNames.CHANNEL_SETS };
},
myOrganizationsLink() {
return { name: RouteNames.MY_ORGANIZATIONS };
},
catalogLink() {
return { name: RouteNames.CATALOG_ITEMS };
},
Expand Down Expand Up @@ -245,6 +258,8 @@
const routeName = this.$route.name;
if (routeName === RouteNames.CHANNEL_SETS) {
title = this.$tr('channelSets');
} else if (routeName === RouteNames.MY_ORGANIZATIONS) {
title = this.$tr('myOrganizations');
} else if (routeName === RouteNames.CATALOG_ITEMS) {
title = this.translateConstant('public');
} else if (routeName === RouteNames.CHANNELS_VIEW_ONLY) {
Expand All @@ -265,6 +280,7 @@
},
$trs: {
channelSets: 'Collections',
myOrganizations: 'My organizations',
catalog: 'Kolibri Library',
libraryTitle: 'Kolibri Content Library Catalog',
frequentlyAskedQuestions: 'Frequently asked questions',
Expand Down
Loading