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,50 @@
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 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(organizationId) {
const loading = ref(Boolean(organizationId));
const organization = ref(null);

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

onMounted(() => {
if (!organizationId) {
return;
}
load().finally(() => {
loading.value = false;
});
});

function update(data) {
return Organization.update(organizationId, 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 })
.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) {
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',
};
20 changes: 19 additions & 1 deletion 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 Expand Up @@ -99,7 +118,6 @@ const router = new VueRouter({
component: SubmissionDetailsModal,
props: true,
},
// Catch-all for unrecognized URLs
{
path: '*',
redirect: { name: RouteNames.CHANNELS_EDITABLE },
Expand Down
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 @@ -240,11 +253,12 @@
return { name: ListTypeToRouteMapping[listType] };
},
updateTitleForPage() {
// Updates the tab title every time the top-level route changes
let title;
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 +279,7 @@
},
$trs: {
channelSets: 'Collections',
myOrganizations: 'My organizations',
catalog: 'Kolibri Library',
libraryTitle: 'Kolibri Content Library Catalog',
frequentlyAskedQuestions: 'Frequently asked questions',
Expand Down
Loading