Просмотр исходного кода

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

[web] History labels only list

GitOrigin-RevId: 58b8e5a5af0754e32841223f9c478c25900df526
Alexandre Bourdin 3 лет назад
Родитель
Сommit
481cd14cb1
21 измененных файлов с 357 добавлено и 206 удалено
  1. 1 0
      services/web/frontend/extracted-translations.json
  2. 5 5
      services/web/frontend/js/features/history/components/change-list/changes.tsx
  3. 18 18
      services/web/frontend/js/features/history/components/change-list/history-version.tsx
  4. 0 96
      services/web/frontend/js/features/history/components/change-list/label-badges.tsx
  5. 59 1
      services/web/frontend/js/features/history/components/change-list/labels-list.tsx
  6. 7 21
      services/web/frontend/js/features/history/components/change-list/metadata-users-list.tsx
  7. 90 0
      services/web/frontend/js/features/history/components/change-list/tag-tooltip.tsx
  8. 39 0
      services/web/frontend/js/features/history/components/change-list/user-name-with-colored-badge.tsx
  9. 3 7
      services/web/frontend/js/features/history/context/types/history-context-value.ts
  10. 2 1
      services/web/frontend/js/features/history/services/api.ts
  11. 22 0
      services/web/frontend/js/features/history/services/types/label.ts
  12. 2 20
      services/web/frontend/js/features/history/services/types/update.ts
  13. 2 5
      services/web/frontend/js/features/history/utils/history-details.ts
  14. 9 9
      services/web/frontend/js/features/history/utils/label.ts
  15. 43 0
      services/web/frontend/js/shared/components/badge.tsx
  16. 1 0
      services/web/frontend/stylesheets/_style_includes.less
  17. 19 23
      services/web/frontend/stylesheets/app/editor/history-react.less
  18. 32 0
      services/web/frontend/stylesheets/components/badge.less
  19. 1 0
      services/web/frontend/stylesheets/core/variables.less
  20. 1 0
      services/web/frontend/stylesheets/main-style.less
  21. 1 0
      services/web/locales/en.json

+ 1 - 0
services/web/frontend/extracted-translations.json

@@ -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": "",

+ 5 - 5
services/web/frontend/js/features/history/components/change-list/changes.tsx

@@ -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) => (

+ 18 - 18
services/web/frontend/js/features/history/components/change-list/history-version.tsx

@@ -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>
   )

+ 0 - 96
services/web/frontend/js/features/history/components/change-list/label-badges.tsx

@@ -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

+ 59 - 1
services/web/frontend/js/features/history/components/change-list/labels-list.tsx

@@ -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

+ 7 - 21
services/web/frontend/js/features/history/components/change-list/metadata-users-list.tsx

@@ -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

+ 90 - 0
services/web/frontend/js/features/history/components/change-list/tag-tooltip.tsx

@@ -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

+ 39 - 0
services/web/frontend/js/features/history/components/change-list/user-name-with-colored-badge.tsx

@@ -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

+ 3 - 7
services/web/frontend/js/features/history/context/types/history-context-value.ts

@@ -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

+ 2 - 1
services/web/frontend/js/features/history/services/api.ts

@@ -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

+ 22 - 0
services/web/frontend/js/features/history/services/types/label.ts

@@ -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

+ 2 - 20
services/web/frontend/js/features/history/services/types/update.ts

@@ -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
 }
 

+ 2 - 5
services/web/frontend/js/features/history/utils/history-details.ts

@@ -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%)`
 }

+ 9 - 9
services/web/frontend/js/features/history/utils/label.ts

@@ -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) {

+ 43 - 0
services/web/frontend/js/shared/components/badge.tsx

@@ -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

+ 1 - 0
services/web/frontend/stylesheets/_style_includes.less

@@ -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";

+ 19 - 23
services/web/frontend/stylesheets/app/editor/history-react.less

@@ -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 {

+ 32 - 0
services/web/frontend/stylesheets/components/badge.less

@@ -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;
+    }
+  }
+}

+ 1 - 0
services/web/frontend/stylesheets/core/variables.less

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

+ 1 - 0
services/web/frontend/stylesheets/main-style.less

@@ -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';

+ 1 - 0
services/web/locales/en.json

@@ -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)",