1
0
Fork 0
mirror of https://github.com/overleaf/overleaf.git synced 2025-04-14 02:36:51 +00:00

Merge pull request from overleaf/ii-history-react-labels-only

[web] History labels only list

GitOrigin-RevId: 58b8e5a5af0754e32841223f9c478c25900df526
This commit is contained in:
Alexandre Bourdin 2023-04-12 10:34:56 +02:00 committed by Copybot
parent 04c204f989
commit 481cd14cb1
21 changed files with 357 additions and 206 deletions

View file

@ -711,6 +711,7 @@
"save_x_percent_or_more": "",
"saved_bibtex_appended_to_galileo_bib": "",
"saved_bibtex_to_new_galileo_bib": "",
"saved_by": "",
"saving": "",
"search": "",
"search_bib_files": "",

View file

@ -3,21 +3,21 @@ import { getProjectOpDoc } from '../../utils/history-details'
import { LoadedUpdate } from '../../services/types/update'
type ChangesProps = {
pathNames: LoadedUpdate['pathnames']
pathnames: LoadedUpdate['pathnames']
projectOps: LoadedUpdate['project_ops']
}
function Changes({ pathNames, projectOps }: ChangesProps) {
function Changes({ pathnames, projectOps }: ChangesProps) {
const { t } = useTranslation()
return (
<ol className="history-version-changes">
{pathNames.map(pathName => (
<li key={pathName}>
{pathnames.map(pathname => (
<li key={pathname}>
<div className="history-version-change-action">
{t('file_action_edited')}
</div>
<div className="history-version-change-doc">{pathName}</div>
<div className="history-version-change-doc">{pathname}</div>
</li>
))}
{projectOps.map((op, index) => (

View file

@ -1,9 +1,10 @@
import LabelBadges from './label-badges'
import TagTooltip from './tag-tooltip'
import Changes from './changes'
import MetadataUsersList from './metadata-users-list'
import Origin from './origin'
import { useUserContext } from '../../../../shared/context/user-context'
import { relativeDate, formatTime } from '../../../utils/format-date'
import { orderBy } from 'lodash'
import { LoadedUpdate } from '../../services/types/update'
type HistoryEntryProps = {
@ -12,6 +13,7 @@ type HistoryEntryProps = {
function HistoryVersion({ update }: HistoryEntryProps) {
const { id: currentUserId } = useUserContext()
const orderedLabels = orderBy(update.labels, ['created_at'], ['desc'])
return (
<div>
@ -21,26 +23,24 @@ function HistoryVersion({ update }: HistoryEntryProps) {
</time>
)}
<div className="history-version-details">
<div>
<time className="history-version-metadata-time">
{formatTime(update.meta.end_ts, 'Do MMMM, h:mm a')}
</time>
<LabelBadges
labels={update.labels}
<time className="history-version-metadata-time">
<b>{formatTime(update.meta.end_ts, 'Do MMMM, h:mm a')}</b>
</time>
{orderedLabels.map(label => (
<TagTooltip
key={label.id}
showTooltip
currentUserId={currentUserId}
label={label}
/>
<Changes
pathNames={update.pathnames}
projectOps={update.project_ops}
/>
<MetadataUsersList
users={update.meta.users}
origin={update.meta.origin}
currentUserId={currentUserId}
/>
<Origin origin={update.meta.origin} />
</div>
))}
<Changes pathnames={update.pathnames} projectOps={update.project_ops} />
<MetadataUsersList
users={update.meta.users}
origin={update.meta.origin}
currentUserId={currentUserId}
/>
<Origin origin={update.meta.origin} />
</div>
</div>
)

View file

@ -1,96 +0,0 @@
import { Fragment } from 'react'
import { useTranslation } from 'react-i18next'
import Icon from '../../../../shared/components/icon'
import Tooltip from '../../../../shared/components/tooltip'
import { useHistoryContext } from '../../context/history-context'
import { isPseudoLabel } from '../../utils/label'
import { formatDate } from '../../../../utils/dates'
import { orderBy } from 'lodash'
import { Label, PseudoCurrentStateLabel } from '../../services/types/update'
type LabelBadgesProps = {
showTooltip: boolean
currentUserId: string
labels: Array<Label | PseudoCurrentStateLabel>
}
function LabelBadges({ labels, currentUserId, showTooltip }: LabelBadgesProps) {
const { t } = useTranslation()
const { labels: allLabels } = useHistoryContext()
const orderedLabels = orderBy(labels, ['created_at'], ['desc'])
const handleDelete = (e: React.MouseEvent, label: Label) => {
e.stopPropagation()
}
return (
<>
{orderedLabels.map(label => {
const isPseudoCurrentStateLabel = isPseudoLabel(label)
const isOwnedByCurrentUser = !isPseudoCurrentStateLabel
? label.user_id === currentUserId
: null
const currentLabelData = allLabels?.find(({ id }) => id === label.id)
const labelOwnerName =
currentLabelData && !isPseudoLabel(currentLabelData)
? currentLabelData.user_display_name
: t('anonymous')
const badgeContent = (
<span className="history-version-badge">
<Icon type="tag" fw />
<span className="history-version-badge-comment">
{isPseudoCurrentStateLabel
? t('history_label_project_current_state')
: label.comment}
</span>
{isOwnedByCurrentUser && !isPseudoCurrentStateLabel && (
<button
type="button"
className="history-version-label-delete-btn"
onClick={e => handleDelete(e, label)}
aria-label={t('delete')}
>
<span aria-hidden="true">&times;</span>
</button>
)}
</span>
)
return (
<Fragment key={label.id}>
{showTooltip && !isPseudoCurrentStateLabel ? (
<Tooltip
key={label.id}
description={
<div className="history-version-label-tooltip">
<div className="history-version-label-tooltip-row">
<b>
<Icon type="tag" fw />
{label.comment}
</b>
</div>
<div className="history-version-label-tooltip-row">
{t('history_label_created_by')} {labelOwnerName}
</div>
<div className="history-version-label-tooltip-row">
<time>{formatDate(label.created_at)}</time>
</div>
</div>
}
id={label.id}
overlayProps={{ placement: 'left' }}
>
{badgeContent}
</Tooltip>
) : (
badgeContent
)}
</Fragment>
)
})}
</>
)
}
export default LabelBadges

View file

@ -1,5 +1,63 @@
import { useTranslation } from 'react-i18next'
import { Fragment } from 'react'
import TagTooltip from './tag-tooltip'
import UserNameWithColoredBadge from './user-name-with-colored-badge'
import { useHistoryContext } from '../../context/history-context'
import { useUserContext } from '../../../../shared/context/user-context'
import { isPseudoLabel } from '../../utils/label'
import { formatTime } from '../../../utils/format-date'
import { groupBy, orderBy } from 'lodash'
import { LoadedLabel } from '../../services/types/label'
function LabelsList() {
return <div>Labels only</div>
const { t } = useTranslation()
const { labels } = useHistoryContext()
const { id: currentUserId } = useUserContext()
let versionWithLabels: { version: number; labels: LoadedLabel[] }[] = []
if (labels) {
const groupedLabelsHash = groupBy(labels, 'version')
versionWithLabels = Object.keys(groupedLabelsHash).map(key => ({
version: parseInt(key, 10),
labels: groupedLabelsHash[key],
}))
versionWithLabels = orderBy(versionWithLabels, ['version'], ['desc'])
}
return (
<>
{versionWithLabels.map(({ version, labels }) => (
<div key={version} className="history-version-details">
{labels.map(label => (
<Fragment key={label.id}>
<TagTooltip
showTooltip={false}
currentUserId={currentUserId}
label={label}
/>
<time className="history-version-metadata-time">
{formatTime(label.created_at, 'Do MMMM, h:mm a')}
</time>
{!isPseudoLabel(label) && (
<div className="history-version-saved-by">
<span className="history-version-saved-by-label">
{t('saved_by')}
</span>
<UserNameWithColoredBadge
user={{
id: label.user_id,
displayName: label.user_display_name,
}}
currentUserId={currentUserId}
/>
</div>
)}
</Fragment>
))}
</div>
))}
</>
)
}
export default LabelsList

View file

@ -1,6 +1,7 @@
import { useTranslation } from 'react-i18next'
import { getUserColor, formatUserName } from '../../utils/history-details'
import { getUserColor } from '../../utils/history-details'
import { LoadedUpdate } from '../../services/types/update'
import UserNameWithColoredBadge from './user-name-with-colored-badge'
type MetadataUsersListProps = {
currentUserId: string
@ -15,26 +16,11 @@ function MetadataUsersList({
return (
<ol className="history-version-metadata-users">
{users.map((user, index) => {
let userName: string
if (!user) {
userName = t('anonymous')
} else if (user?.id === currentUserId) {
userName = t('you')
} else {
userName = formatUserName(user)
}
return (
<li key={index}>
<span
className="history-version-user-badge-color"
style={{ backgroundColor: getUserColor(user) }}
/>
{userName}
</li>
)
})}
{users.map((user, index) => (
<li key={index}>
<UserNameWithColoredBadge user={user} currentUserId={currentUserId} />
</li>
))}
{!users.length && (
<li>
<span

View file

@ -0,0 +1,90 @@
import { useTranslation } from 'react-i18next'
import Icon from '../../../../shared/components/icon'
import Tooltip from '../../../../shared/components/tooltip'
import Badge from '../../../../shared/components/badge'
import { useHistoryContext } from '../../context/history-context'
import { isPseudoLabel } from '../../utils/label'
import { formatDate } from '../../../../utils/dates'
import { LoadedLabel } from '../../services/types/label'
type TagProps = {
label: LoadedLabel
currentUserId: string
}
function Tag({ label, currentUserId, ...props }: TagProps) {
const { t } = useTranslation()
const isPseudoCurrentStateLabel = isPseudoLabel(label)
const isOwnedByCurrentUser = !isPseudoCurrentStateLabel
? label.user_id === currentUserId
: null
const handleDelete = (e: React.MouseEvent, label: LoadedLabel) => {
e.stopPropagation()
}
return (
<Badge
prepend={<Icon type="tag" fw />}
onClose={e => handleDelete(e, label)}
showCloseButton={Boolean(
isOwnedByCurrentUser && !isPseudoCurrentStateLabel
)}
closeBtnProps={{ 'aria-label': t('delete') }}
className="history-version-badge"
{...props}
>
{isPseudoCurrentStateLabel
? t('history_label_project_current_state')
: label.comment}
</Badge>
)
}
type LabelBadgesProps = {
showTooltip: boolean
currentUserId: string
label: LoadedLabel
}
function TagTooltip({ label, currentUserId, showTooltip }: LabelBadgesProps) {
const { t } = useTranslation()
const { labels: allLabels } = useHistoryContext()
const isPseudoCurrentStateLabel = isPseudoLabel(label)
const currentLabelData = allLabels?.find(({ id }) => id === label.id)
const labelOwnerName =
currentLabelData && !isPseudoLabel(currentLabelData)
? currentLabelData.user_display_name
: t('anonymous')
return showTooltip && !isPseudoCurrentStateLabel ? (
<Tooltip
key={label.id}
description={
<div className="history-version-label-tooltip">
<div className="history-version-label-tooltip-row">
<b>
<Icon type="tag" fw />
{label.comment}
</b>
</div>
<div className="history-version-label-tooltip-row">
{t('history_label_created_by')} {labelOwnerName}
</div>
<div className="history-version-label-tooltip-row">
<time>{formatDate(label.created_at)}</time>
</div>
</div>
}
id={label.id}
overlayProps={{ placement: 'left' }}
>
<Tag label={label} currentUserId={currentUserId} />
</Tooltip>
) : (
<Tag label={label} currentUserId={currentUserId} />
)
}
export default TagTooltip

View file

@ -0,0 +1,39 @@
import { useTranslation } from 'react-i18next'
import { formatUserName, getUserColor } from '../../utils/history-details'
import { User } from '../../services/types/shared'
import { Nullable } from '../../../../../../types/utils'
type UserNameWithColoredBadgeProps = {
currentUserId: string
user: Nullable<User | { id: string; displayName: string }>
}
function UserNameWithColoredBadge({
user,
currentUserId,
}: UserNameWithColoredBadgeProps) {
const { t } = useTranslation()
let userName: string
if (!user) {
userName = t('anonymous')
} else if (user.id === currentUserId) {
userName = t('you')
} else if ('displayName' in user) {
userName = user.displayName
} else {
userName = formatUserName(user)
}
return (
<>
<span
className="history-version-user-badge-color"
style={{ backgroundColor: getUserColor(user) }}
/>
{userName}
</>
)
}
export default UserNameWithColoredBadge

View file

@ -1,10 +1,6 @@
import { Nullable } from '../../../../../../types/utils'
import {
Label,
LoadedUpdate,
PseudoCurrentStateLabel,
UpdateSelection,
} from '../../services/types/update'
import { LoadedUpdate, UpdateSelection } from '../../services/types/update'
import { LoadedLabel } from '../../services/types/label'
import { Selection } from '../../../../../../types/history/selection'
import { FileSelection } from '../../services/types/file'
import { ViewMode } from '../../services/types/view-mode'
@ -19,7 +15,7 @@ export type HistoryContextValue = {
selection: Selection
isLoading: boolean
error: Nullable<unknown>
labels: Nullable<Array<Label | PseudoCurrentStateLabel>>
labels: Nullable<LoadedLabel[]>
loadingFileTree: boolean
projectId: string
fileSelection: FileSelection | null

View file

@ -1,6 +1,7 @@
import { getJSON } from '../../../infrastructure/fetch-json'
import { FileDiff } from './types/file'
import { Label, Update } from './types/update'
import { Update } from './types/update'
import { Label } from './types/label'
import { DocDiffResponse } from './types/doc'
const BATCH_SIZE = 10

View file

@ -0,0 +1,22 @@
import { Nullable } from '../../../../../../types/utils'
interface UpdateLabel {
id: string
comment: string
version: number
user_id: string
created_at: string
}
export interface Label extends UpdateLabel {
user_display_name: string
}
export interface PseudoCurrentStateLabel {
id: '1'
isPseudoCurrentStateLabel: true
version: Nullable<number>
created_at: string
}
export type LoadedLabel = Label | PseudoCurrentStateLabel

View file

@ -1,25 +1,7 @@
import { Meta, User } from './shared'
import { Label } from './label'
import { Nullable } from '../../../../../../types/utils'
interface UpdateLabel {
id: string
comment: string
version: number
user_id: string
created_at: string
}
export interface Label extends UpdateLabel {
user_display_name: string
}
export interface PseudoCurrentStateLabel {
id: '1'
isPseudoCurrentStateLabel: true
version: Nullable<number>
created_at: string
}
export interface ProjectOp {
add?: { pathname: string }
rename?: { pathname: string; newPathname: string }
@ -36,7 +18,7 @@ export interface Update {
project_ops: ProjectOp[]
}
interface LoadedUpdateMetaUser extends User {
export interface LoadedUpdateMetaUser extends User {
hue?: number
}

View file

@ -3,11 +3,8 @@ import { Nullable } from '../../../../../types/utils'
import { User } from '../services/types/shared'
import { ProjectOp } from '../services/types/update'
export const getUserColor = (
user?: Nullable<{ id: string; _id?: never } | { id?: never; _id: string }>
) => {
const curUserId = user?.id || user?._id
const hue = ColorManager.getHueForUserId(curUserId) || 100
export const getUserColor = (user?: Nullable<{ id: string }>) => {
const hue = ColorManager.getHueForUserId(user?.id) || 100
return `hsl(${hue}, 70%, 50%)`
}

View file

@ -1,16 +1,18 @@
import { orderBy } from 'lodash'
import { Label, PseudoCurrentStateLabel } from '../services/types/update'
import {
LoadedLabel,
Label,
PseudoCurrentStateLabel,
} from '../services/types/label'
import { Nullable } from '../../../../../types/utils'
export const isPseudoLabel = (
label: Label | PseudoCurrentStateLabel
label: LoadedLabel
): label is PseudoCurrentStateLabel => {
return (label as PseudoCurrentStateLabel).isPseudoCurrentStateLabel === true
}
const sortLabelsByVersionAndDate = (
labels: Array<Label | PseudoCurrentStateLabel>
) => {
const sortLabelsByVersionAndDate = (labels: LoadedLabel[]) => {
return orderBy(
labels,
['isPseudoCurrentStateLabel', 'version', 'created_at'],
@ -18,9 +20,7 @@ const sortLabelsByVersionAndDate = (
)
}
const deletePseudoCurrentStateLabelIfExistent = (
labels: Array<Label | PseudoCurrentStateLabel>
) => {
const deletePseudoCurrentStateLabelIfExistent = (labels: LoadedLabel[]) => {
if (labels.length && isPseudoLabel(labels[0])) {
const [, ...rest] = labels
return rest
@ -29,7 +29,7 @@ const deletePseudoCurrentStateLabelIfExistent = (
}
const addPseudoCurrentStateLabelIfNeeded = (
labels: Array<Label | PseudoCurrentStateLabel>,
labels: LoadedLabel[],
mostRecentVersion: Nullable<number>
) => {
if (!labels.length || labels[0].version !== mostRecentVersion) {

View file

@ -0,0 +1,43 @@
import classnames from 'classnames'
import { MergeAndOverride } from '../../../../types/utils'
type BadgeProps = MergeAndOverride<
React.ComponentProps<'span'>,
{
prepend?: React.ReactNode
children: React.ReactNode
className?: string
showCloseButton?: boolean
onClose?: (e: React.MouseEvent<HTMLButtonElement>) => void
closeBtnProps?: React.ComponentProps<'button'>
}
>
function Badge({
prepend,
children,
className,
showCloseButton = false,
onClose,
closeBtnProps,
...rest
}: BadgeProps) {
return (
<span className={classnames('badge-new', className)} {...rest}>
{prepend}
<span className="badge-new-comment">{children}</span>
{showCloseButton && (
<button
type="button"
className="badge-new-close"
onClick={onClose}
{...closeBtnProps}
>
<span aria-hidden="true">&times;</span>
</button>
)}
</span>
)
}
export default Badge

View file

@ -25,6 +25,7 @@
// Components
@import 'components/tables.less';
@import 'components/forms.less';
@import 'components/badge.less';
@import 'components/buttons.less';
@import 'components/card.less';
//@import "components/code.less";

View file

@ -80,8 +80,8 @@ history-root {
}
.history-version-details {
padding-top: 6px;
padding-bottom: 6px;
padding-top: 8px;
padding-bottom: 8px;
&:hover {
cursor: pointer;
@ -92,8 +92,11 @@ history-root {
.history-version-metadata-time {
display: block;
margin-bottom: 4px;
font-weight: bold;
color: @neutral-90;
&:last-child {
margin-bottom: initial;
}
}
.history-version-metadata-users,
@ -131,7 +134,8 @@ history-root {
.history-version-day,
.history-version-change-action,
.history-version-metadata-users,
.history-version-origin {
.history-version-origin,
.history-version-saved-by {
color: @neutral-70;
}
@ -145,28 +149,20 @@ history-root {
}
.history-version-badge {
display: inline-flex;
align-items: center;
overflow: hidden;
height: 24px;
margin-bottom: 3px;
margin-bottom: 4px;
margin-right: 10px;
padding: 4px;
white-space: nowrap;
color: @ol-blue-gray-6;
background-color: @neutral-20;
border-radius: 4px;
&-comment {
margin-left: 2px;
}
}
.history-version-label-delete-btn {
.reset-button;
marigin-left: 8px;
padding: 4px;
font-size: 24px;
.history-version-saved-by {
margin-bottom: 4px;
&-label {
margin-right: 8px;
}
&:last-child {
margin-bottom: initial;
}
}
.history-loading-panel {

View file

@ -0,0 +1,32 @@
.badge-new {
@padding: 4px;
display: inline-flex;
align-items: center;
overflow: hidden;
height: 24px;
padding: @padding;
white-space: nowrap;
color: @ol-blue-gray-6;
background-color: @neutral-20;
border-radius: 4px;
&:hover {
background-color: @neutral-30;
}
&-comment {
margin-left: 2px;
}
&-close {
.reset-button;
width: 24px;
margin-left: 4px;
margin-right: -@padding;
font-size: 24px;
&:hover {
background-color: @neutral-40;
}
}
}

View file

@ -10,6 +10,7 @@
@gray-lightest: #f0f0f0;
@white: #ffffff;
@neutral-20: #e7e9ee;
@neutral-30: #d0d5dd;
@neutral-90: #1b222c;
@neutral-40: #afb5c0;
@neutral-10: #f4f5f6;

View file

@ -44,6 +44,7 @@
// Components
@import 'components/tables.less';
@import 'components/forms.less';
@import 'components/badge.less';
@import 'components/buttons.less';
@import 'components/card.less';
@import 'components/component-animations.less';

View file

@ -1285,6 +1285,7 @@
"save_x_percent_or_more": "Save __percent__% or more",
"saved_bibtex_appended_to_galileo_bib": "The <strong>__citeKey__</strong> cite key has been added to the <strong>__galileoBib__</strong> file in your project.",
"saved_bibtex_to_new_galileo_bib": "The <strong>__citeKey__</strong> cite key has been copied into a new <strong>__galileoBib__</strong> file in your project. Include this file in your project using the appropriate method for your citation package.",
"saved_by": "Saved by",
"saving": "Saving",
"saving_20_percent": "Saving 20%!",
"saving_notification_with_seconds": "Saving __docname__... (__seconds__ seconds of unsaved changes)",