Skip to content

Commit

Permalink
feat: improve collection sidebar
Browse files Browse the repository at this point in the history
  • Loading branch information
rpenido committed Sep 24, 2024
1 parent 353ef50 commit 87c44fd
Show file tree
Hide file tree
Showing 15 changed files with 420 additions and 16 deletions.
152 changes: 152 additions & 0 deletions src/library-authoring/collections/CollectionDetails.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n';
import { Icon, Stack } from '@openedx/paragon';
import { useContext, useState } from 'react';
import classNames from 'classnames';

import { getItemIcon } from '../../generic/block-type-utils';
import { ToastContext } from '../../generic/toast-context';
import { BlockTypeLabel, type CollectionHit, useSearchContext } from '../../search-manager';
import type { ContentLibrary } from '../data/api';
import { useUpdateCollection } from '../data/apiHooks';
import HistoryWidget from '../generic/history-widget';
import messages from './messages';

interface BlockCountProps {
count: number,
blockType?: string,
label: React.ReactNode,
className?: string,
}

const BlockCount = ({
count,
blockType,
label,
className,
}: BlockCountProps) => {
const icon = blockType && getItemIcon(blockType);
return (
<Stack className={classNames('text-center', className)}>
<span className="text-muted">{label}</span>
<Stack direction="horizontal" gap={1} className="justify-content-center">
{icon && <Icon src={icon} size="lg" />}
<span>{count}</span>
</Stack>
</Stack>
);
};

const CollectionStatsWidget = () => {
const {
blockTypes,
} = useSearchContext();

const blockTypesArray = Object.entries(blockTypes)
.map(([blockType, count]) => ({ blockType, count }))
.sort((a, b) => b.count - a.count);

const totalBlocksCount = blockTypesArray.reduce((acc, { count }) => acc + count, 0);
const otherBlocks = blockTypesArray.splice(3);
const otherBlocksCount = otherBlocks.reduce((acc, { count }) => acc + count, 0);

if (totalBlocksCount === 0) {
return (
<div className="text-center text-muted">
<FormattedMessage {...messages.detailsTabStatsNoComponents} />
</div>
);
}

return (
<Stack direction="horizontal" className="p-2 justify-content-between" gap={2}>
<BlockCount
label={<FormattedMessage {...messages.detailsTabStatsTotalComponents} />}
count={totalBlocksCount}
className="border-right"
/>
{blockTypesArray.map(({ blockType, count }) => (
<BlockCount
key={blockType}
label={<BlockTypeLabel type={blockType} />}
blockType={blockType}
count={count}
/>
))}
{otherBlocks.length > 0 && (
<BlockCount
label={<FormattedMessage {...messages.detailsTabStatsOtherComponents} />}
count={otherBlocksCount}
/>
)}
</Stack>
);
};

interface CollectionDetailsProps {
library: ContentLibrary,
collection: CollectionHit,
}

const CollectionDetails = ({ library, collection }: CollectionDetailsProps) => {
const intl = useIntl();
const { showToast } = useContext(ToastContext);

const [description, setDescription] = useState(collection.description);

const updateMutation = useUpdateCollection(library.id, collection.blockId);

// istanbul ignore if: this should never happen
if (!collection) {
return null;
}

const onSubmit = (e: React.FocusEvent<HTMLTextAreaElement>) => {
const newDescription = e.target.value;
if (newDescription === collection.description) {
return;
}
updateMutation.mutateAsync({
description: newDescription,
}).then(() => {
showToast(intl.formatMessage(messages.updateCollectionSuccessMsg));
}).catch(() => {
showToast(intl.formatMessage(messages.updateCollectionErrorMsg));
});
};

return (
<Stack gap={3}>
<div>
<h3 className="h5">
{intl.formatMessage(messages.detailsTabDescriptionTitle)}
</h3>
{library.canEditLibrary ? (
<textarea
className="form-control"
value={description}
onChange={(e) => setDescription(e.target.value)}
onBlur={onSubmit}
/>
) : collection.description}
</div>
<div>
<h3 className="h5">
{intl.formatMessage(messages.detailsTabStatsTitle)}
</h3>
<CollectionStatsWidget />
</div>
<hr className="w-100" />
<div>
<h3 className="h5">
{intl.formatMessage(messages.detailsTabHistoryTitle)}
</h3>
<HistoryWidget
created={collection.created ? new Date(collection.created * 1000) : null}
modified={collection.modified ? new Date(collection.modified * 1000) : null}
/>
</div>
</Stack>
);
};

export default CollectionDetails;
12 changes: 10 additions & 2 deletions src/library-authoring/collections/CollectionInfo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,17 @@ import {
Tabs,
} from '@openedx/paragon';

import type { ContentLibrary } from '../data/api';
import type { CollectionHit } from '../../search-manager';
import messages from './messages';
import CollectionDetails from './CollectionDetails';

const CollectionInfo = () => {
interface CollectionInfoProps {
library: ContentLibrary,
collection: CollectionHit,
}

const CollectionInfo = ({ library, collection }: CollectionInfoProps) => {
const intl = useIntl();

return (
Expand All @@ -19,7 +27,7 @@ const CollectionInfo = () => {
Manage tab placeholder
</Tab>
<Tab eventKey="details" title={intl.formatMessage(messages.detailsTabTitle)}>
Details tab placeholder
<CollectionDetails library={library} collection={collection} />
</Tab>
</Tabs>
);
Expand Down
95 changes: 88 additions & 7 deletions src/library-authoring/collections/CollectionInfoHeader.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,94 @@
import { type CollectionHit } from '../../search-manager/data/api';
import React, { useState, useContext, useCallback } from 'react';
import { useIntl } from '@edx/frontend-platform/i18n';
import {
Icon,
IconButton,
Stack,
Form,
} from '@openedx/paragon';
import { Edit } from '@openedx/paragon/icons';

import { ToastContext } from '../../generic/toast-context';
import type { ContentLibrary } from '../data/api';
import type { CollectionHit } from '../../search-manager/data/api';
import { useUpdateCollection } from '../data/apiHooks';
import messages from './messages';

interface CollectionInfoHeaderProps {
collection?: CollectionHit;
library: ContentLibrary;
collection: CollectionHit;
}

const CollectionInfoHeader = ({ collection } : CollectionInfoHeaderProps) => (
<div className="d-flex flex-wrap">
{collection?.displayName}
</div>
);
const CollectionInfoHeader = ({ library, collection } : CollectionInfoHeaderProps) => {
const intl = useIntl();
const [inputIsActive, setIsActive] = useState(false);

const updateMutation = useUpdateCollection(library.id, collection.blockId);
const { showToast } = useContext(ToastContext);

const handleSaveDisplayName = useCallback(
(event) => {
const newTitle = event.target.value;
if (newTitle && newTitle !== collection?.displayName) {
updateMutation.mutateAsync({
title: newTitle,
}).then(() => {
showToast(intl.formatMessage(messages.updateCollectionSuccessMsg));
}).catch(() => {
showToast(intl.formatMessage(messages.updateCollectionErrorMsg));
}).finally(() => {
setIsActive(false);
});
}
},
[collection, showToast, intl],
);

const handleClick = () => {
setIsActive(true);
};

const handleOnKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
if (event.key === 'Enter') {
handleSaveDisplayName(event);
} else if (event.key === 'Escape') {
setIsActive(false);
}
};

return (
<Stack direction="horizontal">
{inputIsActive
? (
<Form.Control
autoFocus
name="title"
id="title"
type="text"
aria-label="Title input"
defaultValue={collection?.displayName}
onBlur={handleSaveDisplayName}
onKeyDown={handleOnKeyDown}
/>
)
: (
<>
<span className="font-weight-bold m-1.5">
{collection?.displayName}
</span>
{library.canEditLibrary && (
<IconButton
src={Edit}
iconAs={Icon}
alt={intl.formatMessage(messages.editTitleButtonAlt)}
onClick={handleClick}
size="inline"
/>
)}
</>
)}
</Stack>
);
};

export default CollectionInfoHeader;
50 changes: 50 additions & 0 deletions src/library-authoring/collections/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,41 @@ const messages = defineMessages({
defaultMessage: 'Details',
description: 'Title for details tab',
},
detailsTabDescriptionTitle: {
id: 'course-authoring.library-authoring.collections-sidebar.details-tab.description-title',
defaultMessage: 'Description / Card Preview Text',
description: 'Title for the Description container in the details tab',
},
detailsTabDescriptionPlaceholder: {
id: 'course-authoring.library-authoring.collections-sidebar.details-tab.description-placeholder',
defaultMessage: 'Add description',
description: 'Placeholder for the Description container in the details tab',
},
detailsTabStatsTitle: {
id: 'course-authoring.library-authoring.collections-sidebar.details-tab.stats-title',
defaultMessage: 'Collection Stats',
description: 'Title for the Collection Stats container in the details tab',
},
detailsTabStatsNoComponents: {
id: 'course-authoring.library-authoring.collections-sidebar.details-tab.stats-no-components',
defaultMessage: 'This collection is currently empty.',
description: 'Message displayed when no components are found in the Collection Stats container',
},
detailsTabStatsTotalComponents: {
id: 'course-authoring.library-authoring.collections-sidebar.details-tab.stats-total-components',
defaultMessage: 'Total ',
description: 'Label for total components in the Collection Stats container',
},
detailsTabStatsOtherComponents: {
id: 'course-authoring.library-authoring.collections-sidebar.details-tab.stats-other-components',
defaultMessage: 'Other',
description: 'Label for other components in the Collection Stats container',
},
detailsTabHistoryTitle: {
id: 'course-authoring.library-authoring.collections-sidebar.details-tab.history-title',
defaultMessage: 'Collection History',
description: 'Title for the Collection History container in the details tab',
},
noComponentsInCollection: {
id: 'course-authoring.library-authoring.collections-pag.no-components.text',
defaultMessage: 'This collection is currently empty.',
Expand Down Expand Up @@ -71,6 +106,21 @@ const messages = defineMessages({
defaultMessage: 'Add collection',
description: 'Button text to add a new collection',
},
updateCollectionSuccessMsg: {
id: 'course-authoring.library-authoring.update-collection-success-msg',
defaultMessage: 'Collection updated successfully.',
description: 'Message displayed when collection is updated successfully',
},
updateCollectionErrorMsg: {
id: 'course-authoring.library-authoring.update-collection-error-msg',
defaultMessage: 'Failed to update collection.',
description: 'Message displayed when collection update fails',
},
editTitleButtonAlt: {
id: 'course-authoring.library-authoring.collection.sidebar.edit-name.alt',
defaultMessage: 'Edit collection title',
description: 'Alt text for edit collection title icon button',
},
});

export default messages;
2 changes: 2 additions & 0 deletions src/library-authoring/components/CollectionCard.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ const CollectionHitSample: CollectionHit = {
id: '1',
type: 'collection',
contextKey: 'lb:org1:Demo_Course',
usageKey: 'lb:org1:Demo_Course:collection1',
blockId: 'collection1',
org: 'org1',
breadcrumbs: [{ displayName: 'Demo Lib' }],
displayName: 'Collection Display Name',
Expand Down
6 changes: 4 additions & 2 deletions src/library-authoring/components/CollectionCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
IconButton,
} from '@openedx/paragon';
import { MoreVert } from '@openedx/paragon/icons';
import { useNavigate } from 'react-router-dom';

import { type CollectionHit } from '../../search-manager';
import messages from './messages';
Expand All @@ -14,8 +15,9 @@ type CollectionCardProps = {
collectionHit: CollectionHit,
};

const CollectionCard = ({ collectionHit } : CollectionCardProps) => {
const CollectionCard = ({ collectionHit }: CollectionCardProps) => {
const intl = useIntl();
const navigate = useNavigate();

const {
type,
Expand Down Expand Up @@ -46,7 +48,7 @@ const CollectionCard = ({ collectionHit } : CollectionCardProps) => {
</ActionRow>
)}
blockTypeDisplayName={blockTypeDisplayName}
openInfoSidebar={() => {}}
openInfoSidebar={() => navigate(`/library/${collectionHit.contextKey}/collection/${collectionHit.blockId}`)}
/>
);
};
Expand Down
Loading

0 comments on commit 87c44fd

Please sign in to comment.