Commit 96081234 authored by Kyle Keating's avatar Kyle Keating Committed by Robert Knight

improve typechecking

- thread-list
- thread-card
- thread
- timestamp
parent 2a96432a
......@@ -27,10 +27,7 @@ export default function AnnotationHeader({
const isCollapsedReply = isReply(annotation) && threadIsCollapsed;
const setExpanded = useStore(store => store.setExpanded);
const annotationIsPrivate = isPrivate(
annotation.permissions,
annotation.user
);
const annotationIsPrivate = isPrivate(annotation.permissions);
const annotationLink = annotation.links ? annotation.links.html : '';
// NB: `created` and `updated` are strings, not `Date`s
......
......@@ -6,6 +6,10 @@ import { isPrivate } from '../util/permissions';
import SvgIcon from '../../shared/components/svg-icon';
/**
* @typedef {import('../../types/api').Group} Group
*/
/**
* Render information about what group an annotation is in and
* whether it is private to the current user (only me)
......@@ -13,21 +17,15 @@ import SvgIcon from '../../shared/components/svg-icon';
function AnnotationShareInfo({ annotation }) {
const group = useStore(store => store.getGroup(annotation.group));
// We may not have access to the group object beyond its ID
const hasGroup = !!group;
// Only show the name of the group and link to it if there is a
// URL (link) returned by the API for this group. Some groups do not have links
const linkToGroup = hasGroup && group.links && group.links.html;
const linkToGroup = group?.links.html;
const annotationIsPrivate = isPrivate(
annotation.permissions,
annotation.user
);
const annotationIsPrivate = isPrivate(annotation.permissions);
return (
<div className="annotation-share-info u-layout-row--align-baseline">
{linkToGroup && (
{group && linkToGroup && (
<a
className="u-layout-row--align-baseline u-color-text--muted"
href={group.links.html}
......
......@@ -53,7 +53,8 @@ function Annotation({
const toggleText = `${toggleAction} (${replyCount})`;
const shouldShowActions = !isSaving && !isEditing;
const shouldShowLicense = isEditing && !isPrivate && group.type !== 'private';
const shouldShowLicense =
isEditing && !isPrivate && group && group.type !== 'private';
const shouldShowReplyToggle = replyCount > 0 && !isReply(annotation);
const onEditTags = ({ tags }) => {
......
......@@ -108,7 +108,7 @@ describe('AnnotationShareInfo', () => {
});
it('should show "only me" text for annotation in third-party group', () => {
fakeGetGroup.returns({ name: 'Some Name' });
fakeGetGroup.returns({ name: 'Some Name', links: {} });
const wrapper = createAnnotationShareInfo();
const privacyText = wrapper.find(
......
......@@ -103,6 +103,15 @@ describe('ThreadList', () => {
wrapper.setProps({});
};
context('invalid scroll container', () => {
it('should throw an error if the scroll container is missing', () => {
fakeScrollContainer.remove();
assert.throws(() => {
createComponent();
}, 'Scroll container is missing');
});
});
context('new annotation created in application', () => {
it('clears the current selection in the store', () => {
const wrapper = createComponent();
......
......@@ -10,11 +10,24 @@ import { withServices } from '../util/service-context';
import Thread from './thread';
/**
* @typedef {import('../../types/config').MergedConfig} MergedConfig
*/
/**
* @typedef ThreadCardProps
* @prop {Thread} thread
* @prop {Object} frameSync - Injected service
* @prop {MergedConfig} [settings] - Injected service
*/
/**
* A "top-level" `Thread`, rendered as a "card" in the sidebar. A `Thread`
* renders its own child `Thread`s within itself.
*
* @param {ThreadCardProps} props
*/
function ThreadCard({ frameSync, settings = {}, thread }) {
function ThreadCard({ frameSync, settings, thread }) {
const threadTag = thread.annotation && thread.annotation.$tag;
const isFocused = useStore(store => store.isAnnotationFocused(threadTag));
const showDocumentInfo = useStore(store => store.route() !== 'sidebar');
......@@ -67,8 +80,6 @@ function ThreadCard({ frameSync, settings = {}, thread }) {
ThreadCard.propTypes = {
thread: propTypes.object.isRequired,
/** injected */
frameSync: propTypes.object.isRequired,
settings: propTypes.object,
};
......
......@@ -13,14 +13,25 @@ import {
import ThreadCard from './thread-card';
/** @typedef {import('../util/build-thread').Thread} Thread */
// The precision of the `scrollPosition` value in pixels; values will be rounded
// down to the nearest multiple of this scale value
const SCROLL_PRECISION = 50;
function getScrollContainer() {
return document.querySelector('.js-thread-list-scroll-root');
const container = document.querySelector('.js-thread-list-scroll-root');
if (!container) {
throw new Error('Scroll container is missing');
}
return container;
}
/**
* @typedef ThreadListProps
* @prop {Thread} thread
*/
/**
* Render a list of threads.
*
......@@ -29,6 +40,8 @@ function getScrollContainer() {
* annotations (and replies) are complex interactive components whose
* user-defined content may include rich media such as images, audio clips,
* embedded YouTube videos, rendered math and more.
*
* @param {ThreadListProps} props
*/
function ThreadList({ thread }) {
const clearSelection = useStore(store => store.clearSelection);
......@@ -51,7 +64,9 @@ function ThreadList({ thread }) {
// ID of thread to scroll to after the next render. If the thread is not
// present, the value persists until it can be "consumed".
const [scrollToId, setScrollToId] = useState(null);
const [scrollToId, setScrollToId] = useState(
/** @type {string|null} */ (null)
);
const topLevelThreads = thread.children;
......@@ -159,7 +174,9 @@ function ThreadList({ thread }) {
setThreadHeights(prevHeights => {
const changedHeights = {};
for (let { id } of visibleThreads) {
const threadElement = document.getElementById(id);
const threadElement = /** @type {HTMLElement} */ (document.getElementById(
id
));
const height = getElementHeightWithMargins(threadElement);
if (height !== prevHeights[id]) {
changedHeights[id] = height;
......
......@@ -10,10 +10,21 @@ import Annotation from './annotation';
import Button from './button';
import ModerationBanner from './moderation-banner';
/** @typedef {import('../util/build-thread').Thread} Thread */
/**
* @typedef ThreadProps
* @prop {boolean} [showDocumentInfo]
* @prop {Thread} thread
* @prop {Object} threadsService - Injected service
*/
/**
* A thread, which contains a single annotation at its top level, and its
* recursively-rendered children (i.e. replies). A thread may have a parent,
* and at any given time it may be `collapsed`.
*
* @param {ThreadProps} props
*/
function Thread({ showDocumentInfo = false, thread, threadsService }) {
const setExpanded = useStore(store => store.setExpanded);
......
......@@ -5,9 +5,18 @@ import propTypes from 'prop-types';
import { format as formatDate } from '../util/date';
import { decayingInterval, toFuzzyString } from '../util/time';
/**
* @typedef TimestampProps
* @prop {string} [className] - Custom class name for the anchor/span element
* @prop {string} [href] - Link destination
* @prop {string} timestamp - The timestamp as an ISO 8601 date string
*/
/**
* Display a relative timestamp (eg. '6 minutes ago') as static text or a link.
*
* @param {TimestampProps} props
*
* The timestamp automatically refreshes at an appropriate frequency.
*/
export default function Timestamp({ className, href, timestamp }) {
......@@ -49,14 +58,7 @@ export default function Timestamp({ className, href, timestamp }) {
}
Timestamp.propTypes = {
/** Custom class name for the anchor/span element. */
className: propTypes.string,
/** Link destination. */
href: propTypes.string,
/**
* The timestamp as an ISO 8601 date string.
*/
timestamp: propTypes.string.isRequired,
};
......@@ -564,6 +564,7 @@ function savedAnnotations(state) {
* @prop {(tags: string[]) => string[]} findIDsForTags
* @prop {() => string[]} focusedAnnotations
* @prop {() => string[]} highlightedAnnotations
* @prop {(tag: string) => boolean} isAnnotationFocused
* @prop {() => boolean} isWaitingToAnchorAnnotations
* @prop {() => Annotation[]} newAnnotations
* @prop {() => Annotation[]} newHighlights
......
......@@ -35,9 +35,6 @@
"sidebar/components/sidebar-content-error.js",
"sidebar/components/sidebar-content.js",
"sidebar/components/stream-content.js",
"sidebar/components/thread-card.js",
"sidebar/components/thread-list.js",
"sidebar/components/thread.js",
"sidebar/components/top-bar.js"
]
}
......@@ -116,7 +116,7 @@
* we assign a default organization on the client.
* @prop {GroupScopes|null} scopes
* @prop {Object} links
* @prop {string} links.html
* @prop {string} [links.html]
*
* // Properties not present on API objects, but added by utilities in the client.
* @prop {string} logo
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment