Skip to content

Commit

Permalink
feat: initial version of the LibraryTeam modal
Browse files Browse the repository at this point in the history
  • Loading branch information
pomegranited committed Oct 8, 2024
1 parent 8c125df commit bd21593
Show file tree
Hide file tree
Showing 15 changed files with 523 additions and 2 deletions.
2 changes: 2 additions & 0 deletions src/library-authoring/LibraryLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
import LibraryAuthoringPage from './LibraryAuthoringPage';
import { LibraryProvider } from './common/context';
import { CreateCollectionModal } from './create-collection';
import { LibraryTeamModal } from './library-team';
import LibraryCollectionPage from './collections/LibraryCollectionPage';
import { ComponentEditorModal } from './components/ComponentEditorModal';

Expand All @@ -32,6 +33,7 @@ const LibraryLayout = () => {
</Routes>
<CreateCollectionModal />
<ComponentEditorModal />
<LibraryTeamModal />
</LibraryProvider>
);
};
Expand Down
11 changes: 11 additions & 0 deletions src/library-authoring/common/context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ export interface LibraryContextData {
openInfoSidebar: () => void;
openComponentInfoSidebar: (usageKey: string) => void;
currentComponentUsageKey?: string;
// "Library Team" modal
isLibraryTeamModalOpen: boolean;
openLibraryTeamModal: () => void;
closeLibraryTeamModal: () => void;
// "Create New Collection" modal
isCreateCollectionModalOpen: boolean;
openCreateCollectionModal: () => void;
Expand Down Expand Up @@ -48,6 +52,7 @@ const LibraryContext = React.createContext<LibraryContextData | undefined>(undef
export const LibraryProvider = (props: { children?: React.ReactNode, libraryId: string }) => {
const [sidebarBodyComponent, setSidebarBodyComponent] = React.useState<SidebarBodyComponentId | null>(null);
const [currentComponentUsageKey, setCurrentComponentUsageKey] = React.useState<string>();
const [isLibraryTeamModalOpen, openLibraryTeamModal, closeLibraryTeamModal] = useToggle(false);
const [currentCollectionId, setcurrentCollectionId] = React.useState<string>();
const [isCreateCollectionModalOpen, openCreateCollectionModal, closeCreateCollectionModal] = useToggle(false);
const [componentBeingEdited, openComponentEditor] = React.useState<string | undefined>();
Expand Down Expand Up @@ -93,6 +98,9 @@ export const LibraryProvider = (props: { children?: React.ReactNode, libraryId:
openInfoSidebar,
openComponentInfoSidebar,
currentComponentUsageKey,
isLibraryTeamModalOpen,
openLibraryTeamModal,
closeLibraryTeamModal,
isCreateCollectionModalOpen,
openCreateCollectionModal,
closeCreateCollectionModal,
Expand All @@ -109,6 +117,9 @@ export const LibraryProvider = (props: { children?: React.ReactNode, libraryId:
openInfoSidebar,
openComponentInfoSidebar,
currentComponentUsageKey,
isLibraryTeamModalOpen,
openLibraryTeamModal,
closeLibraryTeamModal,
isCreateCollectionModalOpen,
openCreateCollectionModal,
closeCreateCollectionModal,
Expand Down
19 changes: 19 additions & 0 deletions src/library-authoring/data/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@ export const getContentLibraryApiUrl = (libraryId: string) => `${getApiBaseUrl()
*/
export const getCreateLibraryBlockUrl = (libraryId: string) => `${getApiBaseUrl()}/api/libraries/v2/${libraryId}/blocks/`;

/**
* Get the URL for the content library team API.
*/
export const getLibraryTeamApiUrl = (libraryId: string) => `${getApiBaseUrl()}/api/libraries/v2/${libraryId}/team/`;

/**
* Get the URL for library block metadata.
*/
Expand Down Expand Up @@ -78,6 +83,12 @@ export interface ContentLibrary {
updated: string | null;
}

export interface LibraryTeamMember {
username: string;
email: string;
groupName: string;
}

export interface Collection {
id: number;
key: string;
Expand Down Expand Up @@ -246,6 +257,14 @@ export async function revertLibraryChanges(libraryId: string) {
await client.delete(getCommitLibraryChangesUrl(libraryId));
}

/**
* Fetch a content library's team by its ID.
*/
export async function getLibraryTeam(libraryId: string): Promise<LibraryTeamMember[]> {
const { data } = await getAuthenticatedHttpClient().get(getLibraryTeamApiUrl(libraryId));
return camelCaseObject(data);
}

/**
* Paste clipboard content into library.
*/
Expand Down
17 changes: 17 additions & 0 deletions src/library-authoring/data/apiHooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
commitLibraryChanges,
revertLibraryChanges,
updateLibraryMetadata,
getLibraryTeam,
libraryPasteClipboard,
getLibraryBlockMetadata,
getXBlockFields,
Expand Down Expand Up @@ -58,6 +59,11 @@ export const libraryAuthoringQueryKeys = {
'list',
...(customParams ? [customParams] : []),
],
libraryTeam: (libraryId?: string) => [
...libraryAuthoringQueryKeys.all,
'list',
libraryId,
],
collection: (libraryId?: string, collectionId?: string) => [
...libraryAuthoringQueryKeys.all,
libraryId,
Expand Down Expand Up @@ -181,6 +187,17 @@ export const useRevertLibraryChanges = () => {
});
};

/**
* Hook to fetch a content library's team members
*/
export const useLibraryTeam = (libraryId: string | undefined) => (
useQuery({
queryKey: libraryAuthoringQueryKeys.libraryTeam(libraryId),
queryFn: () => getLibraryTeam(libraryId!),
enabled: libraryId !== undefined,
})
);

export const useLibraryPasteClipboard = () => {
const queryClient = useQueryClient();
return useMutation({
Expand Down
5 changes: 4 additions & 1 deletion src/library-authoring/library-info/LibraryInfo.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
waitFor,
} from '@testing-library/react';
import LibraryInfo from './LibraryInfo';
import { LibraryProvider } from '../common/context';
import { ToastProvider } from '../../generic/toast-context';
import { ContentLibrary, getCommitLibraryChangesUrl } from '../data/api';
import initializeStore from '../../store';
Expand Down Expand Up @@ -59,7 +60,9 @@ const RootWrapper = ({ data } : WrapperProps) => (
<IntlProvider locale="en" messages={{}}>
<QueryClientProvider client={queryClient}>
<ToastProvider>
<LibraryInfo library={data} />
<LibraryProvider libraryId={data.id}>
<LibraryInfo library={data} />
</LibraryProvider>
</ToastProvider>
</QueryClientProvider>
</IntlProvider>
Expand Down
7 changes: 6 additions & 1 deletion src/library-authoring/library-info/LibraryInfo.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import React from 'react';
import { Stack } from '@openedx/paragon';
import { Button, Stack } from '@openedx/paragon';
import { FormattedDate, useIntl } from '@edx/frontend-platform/i18n';
import messages from './messages';
import LibraryPublishStatus from './LibraryPublishStatus';
import { useLibraryContext } from '../common/context';
import { ContentLibrary } from '../data/api';

type LibraryInfoProps = {
Expand All @@ -11,6 +12,7 @@ type LibraryInfoProps = {

const LibraryInfo = ({ library } : LibraryInfoProps) => {
const intl = useIntl();
const { openLibraryTeamModal } = useLibraryContext();

return (
<Stack direction="vertical" gap={2.5}>
Expand All @@ -22,6 +24,9 @@ const LibraryInfo = ({ library } : LibraryInfoProps) => {
<span>
{library.org}
</span>
<Button variant="outline-primary" onClick={openLibraryTeamModal}>
{intl.formatMessage(messages.libraryTeamButtonTitle)}
</Button>
</Stack>
<Stack gap={3}>
<span className="font-weight-bold">
Expand Down
5 changes: 5 additions & 0 deletions src/library-authoring/library-info/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ const messages = defineMessages({
defaultMessage: 'Organization',
description: 'Title for Organization section in Library info sidebar.',
},
libraryTeamButtonTitle: {
id: 'course-authoring.library-authoring.sidebar.info.library-team.button.title',
defaultMessage: 'Manage Access',
description: 'Title to use for the button that allows viewing/editing the Library Team user access.',
},
libraryHistorySectionTitle: {
id: 'course-authoring.library-authoring.sidebar.info.history.title',
defaultMessage: 'Library History',
Expand Down
28 changes: 28 additions & 0 deletions src/library-authoring/library-team/AddLibraryTeamMember.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { FormattedMessage } from '@edx/frontend-platform/i18n';
import {
Button,
} from '@openedx/paragon';

import messages from './messages';

const AddLibraryTeamMember = ({
onSave,
onCancel,
}: {
onSave: () => void,
onCancel: () => void,
}) => {

Check failure on line 14 in src/library-authoring/library-team/AddLibraryTeamMember.tsx

View workflow job for this annotation

GitHub Actions / tests (18)

Unexpected block statement surrounding arrow body; move the returned value immediately after the `=>`

Check failure on line 14 in src/library-authoring/library-team/AddLibraryTeamMember.tsx

View workflow job for this annotation

GitHub Actions / tests (20)

Unexpected block statement surrounding arrow body; move the returned value immediately after the `=>`
return (
<div>
<div>Add form goes here</div>
<Button variant='tertiary' size='sm' onClick={onCancel}>

Check failure on line 18 in src/library-authoring/library-team/AddLibraryTeamMember.tsx

View workflow job for this annotation

GitHub Actions / tests (18)

Unexpected usage of singlequote

Check failure on line 18 in src/library-authoring/library-team/AddLibraryTeamMember.tsx

View workflow job for this annotation

GitHub Actions / tests (18)

Unexpected usage of singlequote

Check failure on line 18 in src/library-authoring/library-team/AddLibraryTeamMember.tsx

View workflow job for this annotation

GitHub Actions / tests (20)

Unexpected usage of singlequote

Check failure on line 18 in src/library-authoring/library-team/AddLibraryTeamMember.tsx

View workflow job for this annotation

GitHub Actions / tests (20)

Unexpected usage of singlequote
<FormattedMessage {...messages.cancel } />

Check failure on line 19 in src/library-authoring/library-team/AddLibraryTeamMember.tsx

View workflow job for this annotation

GitHub Actions / tests (18)

There should be no space before '}'

Check failure on line 19 in src/library-authoring/library-team/AddLibraryTeamMember.tsx

View workflow job for this annotation

GitHub Actions / tests (20)

There should be no space before '}'
</Button>
<Button variant='primary' size='sm' onClick={onSave}>

Check failure on line 21 in src/library-authoring/library-team/AddLibraryTeamMember.tsx

View workflow job for this annotation

GitHub Actions / tests (18)

Unexpected usage of singlequote

Check failure on line 21 in src/library-authoring/library-team/AddLibraryTeamMember.tsx

View workflow job for this annotation

GitHub Actions / tests (18)

Unexpected usage of singlequote

Check failure on line 21 in src/library-authoring/library-team/AddLibraryTeamMember.tsx

View workflow job for this annotation

GitHub Actions / tests (20)

Unexpected usage of singlequote

Check failure on line 21 in src/library-authoring/library-team/AddLibraryTeamMember.tsx

View workflow job for this annotation

GitHub Actions / tests (20)

Unexpected usage of singlequote
<FormattedMessage {...messages.saveMember} />
</Button>
</div>
);
};

export default AddLibraryTeamMember;
107 changes: 107 additions & 0 deletions src/library-authoring/library-team/LibraryTeam.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import React from 'react';
import { FormattedMessage } from '@edx/frontend-platform/i18n';
import { Button, Container } from '@openedx/paragon';
import { Add as IconAdd } from '@openedx/paragon/icons';

import Loading from '../../generic/Loading';
import AlertError from '../../generic/alert-error';
import { useContentLibrary, useLibraryTeam } from '../data/apiHooks';
import LibraryTeamMember from './LibraryTeamMember';
import AddLibraryTeamMember from './AddLibraryTeamMember';
import { LibraryRole } from './constants';
import { LibraryTeamProvider, useLibraryTeamContext } from './context';
import messages from './messages';

const _LibraryTeam = () => {
const {
libraryId,
isAddLibraryTeamMemberOpen,
openAddLibraryTeamMember,
closeAddLibraryTeamMember,
} = useLibraryTeamContext();

const {
data: libraryData,
isLoading: isLibraryLoading,
} = useContentLibrary(libraryId);

const {
data: libraryTeamUsers,
isLoading: isTeamLoading,
isError,
error,
} = useLibraryTeam(libraryId);

if (isLibraryLoading || isTeamLoading) {
return <Loading />;
}

const canChangeRole = libraryData ? libraryData.canEditLibrary : false;
const changeRole = (email: string, role: LibraryRole) => {
alert(`Change ${email}'s role to ${role}`);
};
const onAddMember = (email: string, role: LibraryRole) => {
alert(`Add ${email} ${role}`);
closeAddLibraryTeamMember();
};
const onDeleteRole = (email: string) => {
alert(`Delete ${email} role`);
};
const onMakeAdmin = (email) => {
changeRole(email, LibraryRole.admin);
};
const onMakeAuthor = (email) => {
changeRole(email, LibraryRole.author);
};
const onMakeReader = (email) => {
changeRole(email, LibraryRole.read);
};

return (
<Container size="xl" className="library-team px-4">
{canChangeRole && (
<Button
variant="primary"
iconBefore={IconAdd}
size="sm"
onClick={openAddLibraryTeamMember}
>
<FormattedMessage {...messages.addNewMember} />}
</Button>
)}
<section className="library-team-section">
{canChangeRole && isAddLibraryTeamMemberOpen && (
<AddLibraryTeamMember
onSave={onAddMember}
onCancel={closeAddLibraryTeamMember}
/>
)}
<div className="members-container">
{libraryTeamUsers && libraryTeamUsers.length ? (
libraryTeamUsers.map(({ username, accessLevel: role, email }) => (
<LibraryTeamMember
userName={username}
role={LibraryRole[role] ?? LibraryRole.unknown}
email={email}
canChangeRole={canChangeRole}
onMakeAdmin={canChangeRole && role !== LibraryRole.admin ? onMakeAdmin : null}
onMakeAuthor={canChangeRole && role !== LibraryRole.author ? onMakeAuthor : null}
onMakeReader={canChangeRole && role !== LibraryRole.read ? onMakeReader : null}
onDeleteRole={canChangeRole ? onDeleteRole : null}
/>
))
) : <FormattedMessage {...messages.noMembersFound} />}
</div>
</section>
{isError && (<AlertError error={error} />)}
</Container>
);
};

const LibraryTeam = ({ libraryId }: { libraryId: string }) => (
<LibraryTeamProvider libraryId={libraryId}>
<_LibraryTeam />
</LibraryTeamProvider>
);

export default LibraryTeam;
Loading

0 comments on commit bd21593

Please sign in to comment.