diff --git a/contentcuration/contentcuration/frontend/channelList/composables/useChannelOrganizationFilter.js b/contentcuration/contentcuration/frontend/channelList/composables/useChannelOrganizationFilter.js new file mode 100644 index 0000000000..08c8bbc66a --- /dev/null +++ b/contentcuration/contentcuration/frontend/channelList/composables/useChannelOrganizationFilter.js @@ -0,0 +1,63 @@ +import { computed } from 'vue'; +import { useRoute } from 'vue-router/composables'; +import { useFilter } from 'shared/composables/useFilter'; +import { createTranslator } from 'shared/i18n'; + +const strings = createTranslator('ChannelOrganizationFilter', { + filterByOrganization: { + message: 'Filter by organization', + context: 'Label for filtering the current channel list by organization', + }, + allOrganizations: { + message: 'All organizations', + context: 'Show all channels, including channels without an organization', + }, + unavailableOrganization: { + message: 'Unavailable organization', + context: 'Selected organization has no accessible channels in this list', + }, +}); + +// Derive options from the unfiltered list so selecting one organization does not +// remove the others. Association metadata is supplied by the channel Resource. +export function useChannelOrganizationFilter(channels) { + const route = useRoute(); + const selectedId = computed(() => { + const value = route.query.organization; + return typeof value === 'string' ? value : ''; + }); + const filterMap = computed(() => { + const organizations = new Map(); + for (const channel of channels.value) { + if (channel.organization && channel.organization_name) { + organizations.set(channel.organization, channel.organization_name); + } + } + const entries = [...organizations.entries()].sort((a, b) => a[1].localeCompare(b[1])); + const map = Object.fromEntries([ + ['', { label: strings.allOrganizations$() }], + ...entries.map(([id, label]) => [id, { label }]), + ]); + if (selectedId.value && !organizations.has(selectedId.value)) { + map[selectedId.value] = { label: strings.unavailableOrganization$() }; + } + return map; + }); + const { filter, options } = useFilter({ + name: 'organization', + filterMap, + defaultValue: '', + }); + const filteredChannels = computed(() => + selectedId.value + ? channels.value.filter(channel => channel.organization === selectedId.value) + : channels.value, + ); + + return { + organizationFilter: filter, + organizationOptions: options, + filteredChannels, + filterByOrganization$: strings.filterByOrganization$, + }; +} diff --git a/contentcuration/contentcuration/frontend/channelList/composables/useOrganizations.js b/contentcuration/contentcuration/frontend/channelList/composables/useOrganizations.js new file mode 100644 index 0000000000..5c798e2584 --- /dev/null +++ b/contentcuration/contentcuration/frontend/channelList/composables/useOrganizations.js @@ -0,0 +1,53 @@ +import { onMounted, ref } from 'vue'; +import { Organization } from 'shared/data/resources'; + +const PAGE_SIZE = 100; + +function results(data) { + return Array.isArray(data) ? data : data?.results || []; +} + +export function useOrganizationList() { + const loading = ref(true); + const error = ref(false); + const organizations = ref([]); + + async function load() { + loading.value = true; + error.value = false; + try { + organizations.value = results( + await Organization.fetchCollection({ page_size: PAGE_SIZE, ordering: 'name' }), + ); + } catch (e) { + error.value = true; + } finally { + loading.value = false; + } + } + + onMounted(load); + return { organizations, loading, error, load }; +} + +export function useOrganization(organizationId) { + const loading = ref(Boolean(organizationId)); + const error = ref(false); + const organization = ref(null); + + async function load() { + if (!organizationId) return; + loading.value = true; + error.value = false; + try { + organization.value = await Organization.fetchModel(organizationId); + } catch (e) { + error.value = true; + } finally { + loading.value = false; + } + } + + onMounted(load); + return { organization, loading, error, load }; +} diff --git a/contentcuration/contentcuration/frontend/channelList/constants.js b/contentcuration/contentcuration/frontend/channelList/constants.js index b0599932ca..6e69b6a9e2 100644 --- a/contentcuration/contentcuration/frontend/channelList/constants.js +++ b/contentcuration/contentcuration/frontend/channelList/constants.js @@ -14,6 +14,9 @@ export const ChannelInvitationMapping = { export const RouteNames = { CHANNELS_EDITABLE: 'CHANNELS_EDITABLE', + ORGANIZATIONS: 'ORGANIZATIONS', + NEW_ORGANIZATION: 'NEW_ORGANIZATION', + ORGANIZATION_DETAILS: 'ORGANIZATION_DETAILS', CHANNELS_STARRED: 'CHANNELS_STARRED', CHANNELS_VIEW_ONLY: 'CHANNELS_VIEW_ONLY', CHANNELS_PUBLIC: 'CHANNELS_PUBLIC', diff --git a/contentcuration/contentcuration/frontend/channelList/router.js b/contentcuration/contentcuration/frontend/channelList/router.js index dfe37388cd..430aaa8542 100644 --- a/contentcuration/contentcuration/frontend/channelList/router.js +++ b/contentcuration/contentcuration/frontend/channelList/router.js @@ -1,6 +1,9 @@ import VueRouter from 'vue-router'; import CommunityChannelDetailsModal from './views/Channel/CommunityLibraryList/CommunityChannelDetailsModal.vue'; import StudioMyChannels from './views/Channel/StudioMyChannels'; +import StudioOrganizations from './views/Organization/StudioOrganizations.vue'; +import NewOrganization from './views/Organization/NewOrganization.vue'; +import OrganizationDetails from './views/Organization/OrganizationDetails.vue'; import StudioStarredChannels from './views/Channel/StudioStarredChannels'; import StudioViewOnlyChannels from './views/Channel/StudioViewOnlyChannels'; import StudioCollectionsTable from './views/ChannelSet/StudioCollectionsTable'; @@ -20,6 +23,22 @@ const router = new VueRouter({ path: '/my-channels', component: StudioMyChannels, }, + { + name: RouteNames.ORGANIZATIONS, + path: '/organizations', + component: StudioOrganizations, + }, + { + name: RouteNames.NEW_ORGANIZATION, + path: '/organizations/new', + component: NewOrganization, + }, + { + name: RouteNames.ORGANIZATION_DETAILS, + path: '/organizations/:organizationId', + component: OrganizationDetails, + props: true, + }, { name: RouteNames.CHANNEL_SETS, path: '/collections', diff --git a/contentcuration/contentcuration/frontend/channelList/views/Channel/StudioMyChannels/__tests__/StudioMyChannels.spec.js b/contentcuration/contentcuration/frontend/channelList/views/Channel/StudioMyChannels/__tests__/StudioMyChannels.spec.js index 43c1989d6f..8b022df8ef 100644 --- a/contentcuration/contentcuration/frontend/channelList/views/Channel/StudioMyChannels/__tests__/StudioMyChannels.spec.js +++ b/contentcuration/contentcuration/frontend/channelList/views/Channel/StudioMyChannels/__tests__/StudioMyChannels.spec.js @@ -13,6 +13,7 @@ jest.mock('shared/utils/navigation', () => ({ const router = new VueRouter({ routes: [ { name: 'NEW_CHANNEL', path: '/new' }, + { name: 'NEW_ORGANIZATION', path: '/organizations/new' }, { name: 'CHANNEL_DETAILS', path: '/:channelId/details' }, { name: 'CHANNEL_EDIT', path: '/:channelId/:tab' }, ], @@ -22,6 +23,8 @@ const CHANNELS = [ { id: 'channel-id-1', name: 'Channel title 1', + organization: 'organization-1', + organization_name: 'Learning Together', language: 'en', description: 'Channel description', edit: true, @@ -53,7 +56,7 @@ const mockLoadInvitationList = jest.fn(); const mockDeleteChannel = jest.fn(); const mockBookmarkChannel = jest.fn(); -function createStore() { +function createStore(channelData = CHANNELS) { return new Store({ state: { session: { @@ -67,8 +70,8 @@ function createStore() { channel: { namespaced: true, getters: { - channels: () => CHANNELS, - getChannel: () => id => CHANNELS.find(c => c.id === id), + channels: () => channelData, + getChannel: () => id => channelData.find(c => c.id === id), }, actions: { loadChannelList: mockLoadChannelList, @@ -90,9 +93,9 @@ function createStore() { }); } -function renderComponent(props = {}) { +function renderComponent(props = {}, channelData = CHANNELS) { return render(StudioMyChannels, { - store: createStore(), + store: createStore(channelData), routes: router, props: { ...props, @@ -123,6 +126,119 @@ describe('StudioMyChannels', () => { expect(mockLoadInvitationList).toHaveBeenCalled(); }); + describe('organization filter', () => { + it('keeps legacy channels without association metadata visible by default', async () => { + const legacyChannels = CHANNELS.map(channel => ({ + ...channel, + organization: undefined, + organization_name: undefined, + })); + renderComponent({}, legacyChannels); + expect(await screen.findAllByTestId('channel-card')).toHaveLength(2); + expect( + await screen.findByText('All organizations', { selector: '.ui-select-display-value' }), + ).toBeInTheDocument(); + }); + + it('deduplicates organization options and keeps options after filtering', async () => { + renderComponent({}, [ + ...CHANNELS, + { ...CHANNELS[0], id: 'channel-id-3', name: 'Third channel' }, + { + ...CHANNELS[0], + id: 'channel-id-4', + name: 'Fourth channel', + organization: 'organization-2', + organization_name: 'Another organization', + }, + ]); + await screen.findAllByTestId('channel-card'); + await userEvent.click(screen.getByText('Filter by organization')); + expect( + screen.getAllByText('Learning Together', { selector: '.ui-select-option-basic' }), + ).toHaveLength(1); + await userEvent.click( + screen.getByText('Learning Together', { selector: '.ui-select-option-basic' }), + ); + await waitFor(() => expect(screen.getAllByTestId('channel-card')).toHaveLength(2)); + await userEvent.click(screen.getByText('Filter by organization')); + await userEvent.click( + screen.getByText('Another organization', { selector: '.ui-select-option-basic' }), + ); + await waitFor(() => expect(screen.getAllByTestId('channel-card')).toHaveLength(1)); + expect(screen.getByTestId('channel-card')).toHaveTextContent('Fourth channel'); + }); + + it('does not include deleted or noneditable channels in organization options', async () => { + renderComponent({}, [ + ...CHANNELS, + { + ...CHANNELS[0], + id: 'deleted', + deleted: true, + organization: 'deleted-org', + organization_name: 'Deleted organization', + }, + { + ...CHANNELS[0], + id: 'view-only', + edit: false, + organization: 'viewer-org', + organization_name: 'Viewer organization', + }, + ]); + expect(await screen.findAllByTestId('channel-card')).toHaveLength(2); + await userEvent.click(screen.getByText('Filter by organization')); + expect(screen.queryByText('Deleted organization')).not.toBeInTheDocument(); + expect(screen.queryByText('Viewer organization')).not.toBeInTheDocument(); + }); + + it('filters channels from the URL and preserves unrelated query parameters when cleared', async () => { + await router.push({ query: { organization: 'organization-1', other: 'keep' } }); + renderComponent(); + + const cards = await screen.findAllByTestId('channel-card'); + expect(cards).toHaveLength(1); + expect(cards[0]).toHaveTextContent('Channel title 1'); + + await userEvent.click(screen.getByText('Filter by organization')); + await userEvent.click( + await screen.findByText('All organizations', { selector: '.ui-select-option-basic' }), + ); + + await waitFor(() => expect(screen.getAllByTestId('channel-card')).toHaveLength(2)); + expect(router.currentRoute.query).toEqual({ other: 'keep' }); + }); + + it('selects an organization and restores the list when navigating back', async () => { + renderComponent(); + await screen.findAllByTestId('channel-card'); + await userEvent.click(screen.getByText('Filter by organization')); + await userEvent.click( + await screen.findByText('Learning Together', { selector: '.ui-select-option-basic' }), + ); + + await waitFor(() => expect(screen.getAllByTestId('channel-card')).toHaveLength(1)); + expect(router.currentRoute.query.organization).toBe('organization-1'); + + router.back(); + await waitFor(() => expect(screen.getAllByTestId('channel-card')).toHaveLength(2)); + }); + + it('does not silently show all channels for an unavailable organization', async () => { + await router.push({ query: { organization: 'unavailable' } }); + renderComponent(); + + await waitFor(() => expect(screen.getByText('No channels found')).toBeInTheDocument()); + expect(screen.queryAllByTestId('channel-card')).toHaveLength(0); + expect( + await screen.findByText('Unavailable organization', { + selector: '.ui-select-display-value', + }), + ).toBeInTheDocument(); + }); + }); + it('shows the visually hidden title and all channel cards in correct semantic structure', async () => { renderComponent(); const title = screen.getByRole('heading', { name: /my channels/i }); @@ -154,6 +270,17 @@ describe('StudioMyChannels', () => { }); }); + it('navigates to the new organization route from the filter actions', async () => { + renderComponent(); + await screen.findAllByTestId('channel-card'); + + await userEvent.click(screen.getByRole('button', { name: 'Create' })); + + await waitFor(() => { + expect(router.currentRoute.path).toBe('/organizations/new'); + }); + }); + it('navigates to channel via window.location when card clicked', async () => { renderComponent(); const cards = await screen.findAllByTestId('channel-card'); diff --git a/contentcuration/contentcuration/frontend/channelList/views/Channel/StudioMyChannels/index.vue b/contentcuration/contentcuration/frontend/channelList/views/Channel/StudioMyChannels/index.vue index 2f91566a1f..ef61574cdd 100644 --- a/contentcuration/contentcuration/frontend/channelList/views/Channel/StudioMyChannels/index.vue +++ b/contentcuration/contentcuration/frontend/channelList/views/Channel/StudioMyChannels/index.vue @@ -16,6 +16,19 @@ :text="$tr('newChannel')" @click="newChannel" /> +
+ + +
@@ -77,6 +90,7 @@ import { mapActions, mapGetters } from 'vuex'; import { useChannelList } from '../../../composables/useChannelList'; + import { useChannelOrganizationFilter } from '../../../composables/useChannelOrganizationFilter'; import { RouteNames, InvitationShareModes } from '../../../constants'; import StudioChannelsPage from '../StudioChannelsPage'; import StudioChannelCard from '../StudioChannelCard'; @@ -102,9 +116,15 @@ orderFields: ['desc'], }); + const { organizationFilter, organizationOptions, filteredChannels, filterByOrganization$ } = + useChannelOrganizationFilter(channels); + return { loading, - editableChannels: channels, + editableChannels: filteredChannels, + organizationFilter, + organizationOptions, + filterByOrganization$, }; }, data() { @@ -138,6 +158,9 @@ query: { last: this.$route.name }, }); }, + newOrganization() { + this.$router.push({ name: RouteNames.NEW_ORGANIZATION }); + }, onCardClick(channel) { redirectBrowser(window.Urls.channel(channel.id)); }, @@ -177,6 +200,7 @@ }, $trs: { newChannel: 'New channel', + createOrganization: 'Create', title: 'My channels', moreOptions: 'More options', editChannel: 'Edit channel details', @@ -194,9 +218,36 @@ .button-container { display: flex; - justify-content: end; + flex-wrap: wrap; + gap: 16px; + align-items: center; + justify-content: space-between; width: 100%; margin-top: 20px; } + .organization-actions { + display: flex; + gap: 16px; + align-items: center; + margin-inline-start: auto; + } + + .organization-filter { + width: 280px; + max-width: calc(100vw - 160px); + } + + @media (max-width: 600px) { + .organization-actions { + width: 100%; + } + + .organization-filter { + flex: 1 1 auto; + width: auto; + min-width: 0; + } + } + diff --git a/contentcuration/contentcuration/frontend/channelList/views/ChannelListIndex.vue b/contentcuration/contentcuration/frontend/channelList/views/ChannelListIndex.vue index 83e7976db6..1510944179 100644 --- a/contentcuration/contentcuration/frontend/channelList/views/ChannelListIndex.vue +++ b/contentcuration/contentcuration/frontend/channelList/views/ChannelListIndex.vue @@ -130,6 +130,16 @@ badgeValue: this.invitationsByListCounts[listType] || 0, analyticsLabel: ListTypeToAnalyticsLabel[listType], }); + + if (listType === ChannelListTypes.EDITABLE) { + tabs.push({ + id: 'organizations', + label: this.$tr('myOrganizations'), + to: { name: RouteNames.ORGANIZATIONS }, + badgeValue: 0, + analyticsLabel: 'ORGANIZATIONS', + }); + } }); tabs.push({ @@ -245,6 +255,8 @@ const routeName = this.$route.name; if (routeName === RouteNames.CHANNEL_SETS) { title = this.$tr('channelSets'); + } else if (routeName === RouteNames.ORGANIZATIONS) { + title = this.$tr('organizations'); } else if (routeName === RouteNames.CATALOG_ITEMS) { title = this.translateConstant('public'); } else if (routeName === RouteNames.CHANNELS_VIEW_ONLY) { @@ -265,6 +277,8 @@ }, $trs: { channelSets: 'Collections', + organizations: 'Organizations', + myOrganizations: 'My organizations', catalog: 'Kolibri Library', libraryTitle: 'Kolibri Content Library Catalog', frequentlyAskedQuestions: 'Frequently asked questions', diff --git a/contentcuration/contentcuration/frontend/channelList/views/Organization/NewOrganization.vue b/contentcuration/contentcuration/frontend/channelList/views/Organization/NewOrganization.vue new file mode 100644 index 0000000000..3971543e6e --- /dev/null +++ b/contentcuration/contentcuration/frontend/channelList/views/Organization/NewOrganization.vue @@ -0,0 +1,137 @@ + + + + + + + diff --git a/contentcuration/contentcuration/frontend/channelList/views/Organization/OrganizationCard.vue b/contentcuration/contentcuration/frontend/channelList/views/Organization/OrganizationCard.vue new file mode 100644 index 0000000000..7306208d42 --- /dev/null +++ b/contentcuration/contentcuration/frontend/channelList/views/Organization/OrganizationCard.vue @@ -0,0 +1,94 @@ + + + + + + + diff --git a/contentcuration/contentcuration/frontend/channelList/views/Organization/OrganizationDetails.vue b/contentcuration/contentcuration/frontend/channelList/views/Organization/OrganizationDetails.vue new file mode 100644 index 0000000000..e3187bc6b0 --- /dev/null +++ b/contentcuration/contentcuration/frontend/channelList/views/Organization/OrganizationDetails.vue @@ -0,0 +1,351 @@ + + + + + + + diff --git a/contentcuration/contentcuration/frontend/channelList/views/Organization/StudioOrganizations.vue b/contentcuration/contentcuration/frontend/channelList/views/Organization/StudioOrganizations.vue new file mode 100644 index 0000000000..278d8c791b --- /dev/null +++ b/contentcuration/contentcuration/frontend/channelList/views/Organization/StudioOrganizations.vue @@ -0,0 +1,109 @@ + + + + + + + diff --git a/contentcuration/contentcuration/frontend/channelList/views/Organization/__tests__/OrganizationPages.spec.js b/contentcuration/contentcuration/frontend/channelList/views/Organization/__tests__/OrganizationPages.spec.js new file mode 100644 index 0000000000..3a797af432 --- /dev/null +++ b/contentcuration/contentcuration/frontend/channelList/views/Organization/__tests__/OrganizationPages.spec.js @@ -0,0 +1,173 @@ +import { render, screen, waitFor } from '@testing-library/vue'; +import userEvent from '@testing-library/user-event'; +import VueRouter from 'vue-router'; +import NewOrganization from '../NewOrganization.vue'; +import OrganizationDetails from '../OrganizationDetails.vue'; +import StudioOrganizations from '../StudioOrganizations.vue'; +import { RouteNames } from '../../../constants'; +import { Channel, Organization, OrganizationMember } from 'shared/data/resources'; + +jest.mock('shared/data/resources', () => ({ + Channel: { where: jest.fn() }, + Organization: { + fetchCollection: jest.fn(), + fetchModel: jest.fn(), + create: jest.fn(), + }, + OrganizationMember: { fetchCollection: jest.fn() }, +})); + +const organizations = [ + { + id: 'organization-1', + name: 'Learning Together', + description: 'Learning resources for everyone', + public: true, + }, + { + id: 'organization-2', + name: 'Private team', + description: '', + public: false, + }, +]; + +function makeRouter() { + return new VueRouter({ + routes: [ + { name: RouteNames.ORGANIZATIONS, path: '/organizations', component: StudioOrganizations }, + { name: RouteNames.NEW_ORGANIZATION, path: '/organizations/new', component: NewOrganization }, + { + name: RouteNames.ORGANIZATION_DETAILS, + path: '/organizations/:organizationId', + component: OrganizationDetails, + props: true, + }, + ], + }); +} + +describe('organization pages', () => { + beforeEach(() => { + jest.clearAllMocks(); + Organization.fetchCollection.mockResolvedValue({ results: organizations }); + Organization.fetchModel.mockResolvedValue(organizations[0]); + OrganizationMember.fetchCollection.mockResolvedValue({ + results: [ + { + id: 'membership-1', + user_name: 'Taylor Doe', + user_email: 'taylor@example.com', + role: 'admin', + status: 'active', + }, + ], + }); + Channel.where.mockResolvedValue([ + { + id: 'channel-1', + name: 'Organization channel', + description: 'Channel description', + organization: 'organization-1', + published: true, + }, + { + id: 'channel-2', + name: 'Unrelated channel', + organization: 'organization-2', + }, + ]); + }); + + it('lists accessible public and member organizations', async () => { + const router = makeRouter(); + render(StudioOrganizations, { routes: router }); + + expect(await screen.findAllByTestId('organization-card')).toHaveLength(2); + expect(screen.getAllByText('Learning Together').length).toBeGreaterThan(0); + expect(screen.getAllByText('Private team').length).toBeGreaterThan(0); + expect(Organization.fetchCollection).toHaveBeenCalledWith({ + page_size: 100, + ordering: 'name', + }); + }); + + it('opens an organization from its card', async () => { + const router = makeRouter(); + render(StudioOrganizations, { routes: router }); + const cards = await screen.findAllByTestId('organization-card'); + await userEvent.click(cards[0]); + await waitFor(() => expect(router.currentRoute.path).toBe('/organizations/organization-1')); + }); + + it('validates and creates an organization', async () => { + const router = makeRouter(); + Organization.create.mockResolvedValue({ id: 'new-organization' }); + render(NewOrganization, { routes: router }); + + await userEvent.click(screen.getByRole('button', { name: 'Create organization' })); + expect(await screen.findByText('Organization name is required')).toBeInTheDocument(); + expect(Organization.create).not.toHaveBeenCalled(); + + await userEvent.type(screen.getByLabelText('Organization name'), 'New organization'); + await userEvent.type(screen.getByLabelText('Organization description'), 'A description'); + await userEvent.click(screen.getByRole('checkbox', { name: /public/i })); + await userEvent.click(screen.getByRole('button', { name: 'Create organization' })); + + await waitFor(() => + expect(Organization.create).toHaveBeenCalledWith({ + name: 'New organization', + description: 'A description', + public: true, + }), + ); + expect(router.currentRoute.path).toBe('/organizations/new-organization'); + }); + + it('shows organization details and only associated channels', async () => { + const router = makeRouter(); + await router.push('/organizations/organization-1'); + render(OrganizationDetails, { + routes: router, + props: { organizationId: 'organization-1' }, + }); + + expect(await screen.findByRole('heading', { name: 'Learning Together' })).toBeInTheDocument(); + expect(screen.getByText('Organization channel')).toBeInTheDocument(); + expect(screen.queryByText('Unrelated channel')).not.toBeInTheDocument(); + expect(Channel.where).toHaveBeenCalledWith({ edit: true }, true); + expect(Channel.where).toHaveBeenCalledWith({ view: true }, true); + expect(Channel.where).toHaveBeenCalledWith({ public: true }, true); + }); + + it('shows organization members on the users tab', async () => { + const router = makeRouter(); + await router.push('/organizations/organization-1?tab=users'); + render(OrganizationDetails, { + routes: router, + props: { organizationId: 'organization-1' }, + }); + + expect(await screen.findByText('Taylor Doe')).toBeInTheDocument(); + expect(screen.getByText('taylor@example.com')).toBeInTheDocument(); + expect(OrganizationMember.fetchCollection).toHaveBeenCalledWith({ + organization: 'organization-1', + page_size: 100, + }); + }); + + it('does not display unrelated channels when association metadata is unavailable', async () => { + Channel.where.mockResolvedValue([{ id: 'channel-1', name: 'Unscoped channel' }]); + const router = makeRouter(); + await router.push('/organizations/organization-1'); + render(OrganizationDetails, { + routes: router, + props: { organizationId: 'organization-1' }, + }); + + expect( + await screen.findByText('Channel information is not available yet.'), + ).toBeInTheDocument(); + expect(screen.queryByText('Unscoped channel')).not.toBeInTheDocument(); + }); +}); diff --git a/contentcuration/contentcuration/frontend/shared/data/resources.js b/contentcuration/contentcuration/frontend/shared/data/resources.js index 2a458cd2a3..e28bc21c5a 100644 --- a/contentcuration/contentcuration/frontend/shared/data/resources.js +++ b/contentcuration/contentcuration/frontend/shared/data/resources.js @@ -2426,6 +2426,26 @@ export const CommunityLibrarySubmission = new APIResource({ }, }); +export const Organization = new APIResource({ + urlName: 'organization', + fetchCollection(params) { + return client.get(this.collectionUrl(), { params }).then(response => response.data); + }, + fetchModel(id) { + return client.get(this.modelUrl(id)).then(response => response.data); + }, + create(data) { + return client.post(this.collectionUrl(), data).then(response => response.data); + }, +}); + +export const OrganizationMember = new APIResource({ + urlName: 'organization_members', + fetchCollection(params) { + return client.get(this.collectionUrl(), { params }).then(response => response.data); + }, +}); + export const AdminCommunityLibrarySubmission = new APIResource({ urlName: 'admin_community_library_submission', fetchCollection(params) {