소스 검색

Merge pull request #19466 from overleaf/ii-bs5-projects-tags

[web] BS5 projects tags

GitOrigin-RevId: 4cdea8ad374c8b0ecba9281e3bde64517e6b0153
ilkin-overleaf 2 년 전
부모
커밋
bef43020cd

+ 1 - 4
server-ce/test/history.spec.ts

@@ -29,10 +29,7 @@ describe('History', function () {
     cy.log(`download version ${JSON.stringify(name)}`)
     cy.findByText('Labels').click()
     cy.findByText(name)
-      .parent()
-      .parent()
-      .parent()
-      .parent()
+      .closest('[data-testid="history-version-details"]')
       .within(() => {
         cy.get('.history-version-dropdown-menu-btn').click()
         cy.findByText('Download this version').click()

+ 41 - 19
services/web/frontend/js/features/clone-project-modal/components/clone-project-tag.tsx

@@ -1,31 +1,53 @@
 import { FC } from 'react'
-import { Tag } from '../../../../../app/src/Features/Tags/types'
+import { Tag as TagType } from '../../../../../app/src/Features/Tags/types'
 import { getTagColor } from '@/features/project-list/util/tag'
 import Icon from '@/shared/components/icon'
 import { useTranslation } from 'react-i18next'
+import Tag from '@/features/ui/components/bootstrap-5/tag'
+import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
 
 export const CloneProjectTag: FC<{
-  tag: Tag
-  removeTag: (tag: Tag) => void
+  tag: TagType
+  removeTag: (tag: TagType) => void
 }> = ({ tag, removeTag }) => {
   const { t } = useTranslation()
 
   return (
-    <div className="tag-label" role="option" aria-selected>
-      <span className="label label-default tag-label-name">
-        <span style={{ color: getTagColor(tag) }}>
-          <Icon type="circle" aria-hidden />
-        </span>{' '}
-        {tag.name}
-      </span>
-      <button
-        type="button"
-        className="label label-default tag-label-remove"
-        onClick={() => removeTag(tag)}
-        aria-label={t('remove_tag', { tagName: tag.name })}
-      >
-        <span aria-hidden="true">×</span>
-      </button>
-    </div>
+    <BootstrapVersionSwitcher
+      bs3={
+        <div className="tag-label" role="option" aria-selected>
+          <span className="label label-default tag-label-name">
+            <span style={{ color: getTagColor(tag) }}>
+              <Icon type="circle" aria-hidden />
+            </span>{' '}
+            {tag.name}
+          </span>
+          <button
+            type="button"
+            className="label label-default tag-label-remove"
+            onClick={() => removeTag(tag)}
+            aria-label={t('remove_tag', { tagName: tag.name })}
+          >
+            <span aria-hidden="true">×</span>
+          </button>
+        </div>
+      }
+      bs5={
+        <Tag
+          prepend={
+            <i
+              className="badge-tag-circle"
+              style={{ backgroundColor: getTagColor(tag) }}
+            />
+          }
+          closeBtnProps={{
+            onClick: () => removeTag(tag),
+          }}
+          className="ms-2 mb-2"
+        >
+          {tag.name}
+        </Tag>
+      }
+    />
   )
 }

+ 58 - 32
services/web/frontend/js/features/project-list/components/table/cells/inline-tags.tsx

@@ -1,11 +1,13 @@
 import { useCallback, useState, useRef } from 'react'
 import { useTranslation } from 'react-i18next'
-import { Tag } from '../../../../../../../app/src/Features/Tags/types'
+import { Tag as TagType } from '../../../../../../../app/src/Features/Tags/types'
 import Icon from '../../../../../shared/components/icon'
 import { useProjectListContext } from '../../../context/project-list-context'
 import { removeProjectFromTag } from '../../../util/api'
 import classnames from 'classnames'
 import { getTagColor } from '../../../util/tag'
+import Tag from '@/features/ui/components/bootstrap-5/tag'
+import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
 
 type InlineTagsProps = {
   projectId: string
@@ -27,7 +29,7 @@ function InlineTags({ projectId, ...props }: InlineTagsProps) {
 }
 
 type InlineTagProps = {
-  tag: Tag
+  tag: TagType
   projectId: string
 }
 
@@ -57,38 +59,62 @@ function InlineTag({ tag, projectId }: InlineTagProps) {
   const handleCloseMouseOut = () => setClassNames('')
 
   return (
-    // eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions
-    <div
-      className={classnames('tag-label', classNames)}
-      onClick={handleLabelClick}
-      ref={tagLabelRef}
-    >
-      <button
-        className="label label-default tag-label-name"
-        aria-label={t('select_tag', { tagName: tag.name })}
-        ref={tagBtnRef}
-        onClick={() => selectTag(tag._id)}
-      >
-        <span
-          style={{
-            color: getTagColor(tag),
+    <BootstrapVersionSwitcher
+      bs3={
+        // eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions
+        <div
+          className={classnames('tag-label', classNames)}
+          onClick={handleLabelClick}
+          ref={tagLabelRef}
+        >
+          <button
+            className="label label-default tag-label-name"
+            aria-label={t('select_tag', { tagName: tag.name })}
+            ref={tagBtnRef}
+            onClick={() => selectTag(tag._id)}
+          >
+            <span
+              style={{
+                color: getTagColor(tag),
+              }}
+            >
+              <Icon type="circle" aria-hidden="true" />
+            </span>{' '}
+            {tag.name}
+          </button>
+          {/* eslint-disable-next-line jsx-a11y/mouse-events-have-key-events */}
+          <button
+            className="label label-default tag-label-remove"
+            aria-label={t('remove_tag', { tagName: tag.name })}
+            onClick={() => handleRemoveTag(tag._id, projectId)}
+            onMouseOver={handleCloseMouseOver}
+            onMouseOut={handleCloseMouseOut}
+          >
+            <span aria-hidden="true">×</span>
+          </button>
+        </div>
+      }
+      bs5={
+        <Tag
+          prepend={
+            <i
+              className="badge-tag-circle"
+              style={{ backgroundColor: getTagColor(tag) }}
+            />
+          }
+          contentProps={{
+            'aria-label': t('select_tag', { tagName: tag.name }),
+            onClick: () => selectTag(tag._id),
+          }}
+          closeBtnProps={{
+            onClick: () => handleRemoveTag(tag._id, projectId),
           }}
+          className="ms-2"
         >
-          <Icon type="circle" aria-hidden="true" />
-        </span>{' '}
-        {tag.name}
-      </button>
-      {/* eslint-disable-next-line jsx-a11y/mouse-events-have-key-events */}
-      <button
-        className="label label-default tag-label-remove"
-        aria-label={t('remove_tag', { tagName: tag.name })}
-        onClick={() => handleRemoveTag(tag._id, projectId)}
-        onMouseOver={handleCloseMouseOver}
-        onMouseOut={handleCloseMouseOut}
-      >
-        <span aria-hidden="true">×</span>
-      </button>
-    </div>
+          {tag.name}
+        </Tag>
+      }
+    />
   )
 }
 

+ 24 - 4
services/web/frontend/js/features/ui/components/bootstrap-5/tag.tsx

@@ -1,13 +1,14 @@
 import { useTranslation } from 'react-i18next'
-import { Badge } from 'react-bootstrap-5'
+import { Badge, BadgeProps } from 'react-bootstrap-5'
 import MaterialIcon from '@/shared/components/material-icon'
 import { MergeAndOverride } from '../../../../../../types/utils'
 import classnames from 'classnames'
 
 type TagProps = MergeAndOverride<
-  React.ComponentProps<typeof Badge>,
+  BadgeProps,
   {
     prepend?: React.ReactNode
+    contentProps?: React.ComponentProps<'button'>
     closeBtnProps?: React.ComponentProps<'button'>
   }
 >
@@ -15,16 +16,35 @@ type TagProps = MergeAndOverride<
 function Tag({
   prepend,
   children,
+  contentProps,
   closeBtnProps,
   className,
   ...rest
 }: TagProps) {
   const { t } = useTranslation()
 
-  return (
-    <Badge bg="light" className={classnames('badge-tag', className)} {...rest}>
+  const content = (
+    <>
       {prepend && <span className="badge-prepend">{prepend}</span>}
       <span className="badge-content">{children}</span>
+    </>
+  )
+
+  return (
+    <Badge bg="light" className={classnames('badge-tag', className)} {...rest}>
+      {contentProps?.onClick ? (
+        <button
+          type="button"
+          className="badge-tag-content badge-tag-content-btn"
+          {...contentProps}
+        >
+          {content}
+        </button>
+      ) : (
+        <span className="badge-tag-content" {...contentProps}>
+          {content}
+        </span>
+      )}
       {closeBtnProps && (
         <button
           type="button"

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

@@ -21,7 +21,7 @@ function Badge({ prepend, children, bsStyle, className, ...rest }: BadgeProps) {
   return (
     <span className={classNames} {...rest}>
       {prepend && <span className="badge-tag-bs3-prepend">{prepend}</span>}
-      {children}
+      <span className="badge-tag-bs3-content">{children}</span>
     </span>
   )
 }

+ 6 - 2
services/web/frontend/js/shared/components/tag.tsx

@@ -8,6 +8,7 @@ type TagProps = MergeAndOverride<
   {
     prepend?: React.ReactNode
     children: React.ReactNode
+    contentProps?: React.ComponentProps<'button'>
     closeBtnProps?: React.ComponentProps<'button'>
     className?: string
     bsStyle?: React.ComponentProps<typeof Label>['bsStyle'] | null
@@ -17,6 +18,7 @@ type TagProps = MergeAndOverride<
 function Tag({
   prepend,
   children,
+  contentProps,
   closeBtnProps,
   bsStyle,
   className,
@@ -26,8 +28,10 @@ function Tag({
 
   return (
     <span className={classnames('badge-tag-bs3', className)} {...rest}>
-      {prepend && <span className="badge-tag-bs3-prepend">{prepend}</span>}
-      <span className="badge-tag-bs3-content">{children}</span>
+      <span className="badge-tag-bs3-content-wrapper">
+        {prepend && <span className="badge-tag-bs3-prepend">{prepend}</span>}
+        <span className="badge-tag-bs3-content">{children}</span>
+      </span>
       {closeBtnProps && (
         <button
           type="button"

+ 17 - 0
services/web/frontend/stories/ui/tag-bs5.stories.tsx

@@ -58,3 +58,20 @@ export const TagWithCloseButton: Story = {
     )
   },
 }
+
+export const TagWithContentButtonAndCloseButton: Story = {
+  render: args => {
+    return (
+      <Tag
+        prepend={<Icon type="tag" fw />}
+        contentProps={{
+          onClick: () => alert('Content button clicked!'),
+        }}
+        closeBtnProps={{
+          onClick: () => alert('Close triggered!'),
+        }}
+        {...args}
+      />
+    )
+  },
+}

+ 1 - 1
services/web/frontend/stylesheets/bootstrap-5/abstracts/variable-overrides.scss

@@ -46,7 +46,7 @@ $table-striped-bg: var(--bg-light-secondary);
 $table-active-color: $table-color;
 $table-active-bg: $bg-accent-03;
 $table-hover-color: $table-color;
-$table-hover-bg: var(--bg-light-tertiary);
+$table-hover-bg: var(--bg-light-secondary);
 $table-border-color: $border-divider;
 $table-striped-order: even;
 $table-caption-color: var(--content-secondary);

+ 38 - 4
services/web/frontend/stylesheets/bootstrap-5/components/badge.scss

@@ -1,14 +1,19 @@
+$max-width: 160px;
+
 .badge {
   display: inline-flex;
   align-items: stretch;
   max-width: 100%;
   line-height: var(--line-height-01);
   padding: 0 var(--bs-badge-padding-x);
+
+  &:not(.badge-tag) {
+    max-width: $max-width;
+  }
 }
 
 .badge-prepend {
-  margin-left: calc($spacing-01 / -2);
-  margin-right: $spacing-01;
+  margin-right: var(--spacing-02);
   display: flex;
   align-items: center;
 }
@@ -20,8 +25,7 @@
   align-items: center;
   justify-content: center;
   flex-shrink: 0;
-  padding: 0 $spacing-02;
-  margin: 0 (-$spacing-02) 0 $spacing-02;
+  padding: 0 var(--spacing-02);
   border-top-right-radius: inherit;
   border-bottom-right-radius: inherit;
   color: inherit;
@@ -30,7 +34,10 @@
   .badge-close-icon {
     font-size: $font-size-base;
   }
+}
 
+.badge-close,
+.badge-tag-content-btn {
   &:hover {
     background-color: var(--neutral-40);
   }
@@ -45,9 +52,36 @@
 .badge-tag {
   @include body-sm;
 
+  padding: 0;
   color: $dark;
 
   &:hover {
     background-color: var(--neutral-30) !important;
   }
 }
+
+.badge-tag-content {
+  display: flex;
+  align-items: center;
+  overflow: hidden;
+  max-width: $max-width;
+  padding-left: var(--bs-badge-padding-x);
+  padding-right: var(--bs-badge-padding-x);
+  border-top-left-radius: inherit;
+  border-bottom-left-radius: inherit;
+}
+
+.badge-tag-content-btn {
+  @include reset-button;
+
+  padding-left: var(--bs-badge-padding-x);
+  padding-right: var(--bs-badge-padding-x);
+}
+
+.badge-tag-circle {
+  display: block;
+  margin: var(--spacing-02);
+  width: var(--spacing-04);
+  height: var(--spacing-04);
+  border-radius: 50%;
+}

+ 6 - 0
services/web/frontend/stylesheets/bootstrap-5/components/table.scss

@@ -47,4 +47,10 @@
     border-top-width: 0;
     border-bottom-width: 0;
   }
+
+  > tbody > tr:nth-of-type(#{$table-striped-order}) {
+    &:hover > * {
+      --#{$prefix}table-hover-bg: var(--bg-light-tertiary);
+    }
+  }
 }

+ 50 - 24
services/web/frontend/stylesheets/bootstrap-5/pages/project-list.scss

@@ -2,7 +2,6 @@
   display: flex;
   height: 100%;
   flex-flow: column nowrap;
-  flex-wrap: nowrap;
 
   .row:first-child {
     flex-grow: 1; /* fill vertical space so notifications are pushed to bottom */
@@ -51,8 +50,6 @@
         flex-grow: 1;
         padding: var(--spacing-08) var(--spacing-06);
         -ms-overflow-style: -ms-autohiding-scrollbar;
-        padding-top: var(--spacing-08);
-        padding-bottom: var(--spacing-08);
         color: var(--neutral-40);
 
         .small {
@@ -331,27 +328,6 @@
       }
     }
   }
-}
-
-.add-affiliation-mobile-wrapper {
-  padding: var(--spacing-07) 0;
-}
-
-.add-affiliation {
-  .progress {
-    height: var(--spacing-05);
-    margin-bottom: var(--spacing-03);
-  }
-
-  p {
-    margin-bottom: var(--spacing-03);
-  }
-
-  &.is-mobile p {
-    @include body-xs;
-
-    white-space: normal;
-  }
 
   .project-dash-table {
     width: 100%;
@@ -428,6 +404,18 @@
       @include text-truncate;
     }
 
+    .dash-cell-tag,
+    .clone-project-tag {
+      .badge-tag {
+        margin-top: var(--spacing-06);
+        margin-bottom: var(--spacing-06);
+
+        &:first-child {
+          margin-left: initial !important;
+        }
+      }
+    }
+
     @include media-breakpoint-up(sm) {
       .dash-cell-checkbox {
         width: 4%;
@@ -554,6 +542,15 @@
         position: relative;
         display: flex;
         flex-direction: column;
+
+        td {
+          padding-top: var(--spacing-02);
+          padding-bottom: var(--spacing-02);
+        }
+
+        td:not(.dash-cell-actions) {
+          padding-right: 55px;
+        }
       }
 
       .dash-cell-name,
@@ -660,6 +657,14 @@
   margin-bottom: var(--spacing-05);
 }
 
+.clone-project-tag {
+  display: flex;
+
+  .form-label {
+    margin-top: var(--spacing-01);
+  }
+}
+
 form.project-search {
   .form-group {
     margin-bottom: 0;
@@ -670,6 +675,27 @@ form.project-search {
   @include reset-button;
 }
 
+.add-affiliation-mobile-wrapper {
+  padding: var(--spacing-07) 0;
+}
+
+.add-affiliation {
+  .progress {
+    height: var(--spacing-05);
+    margin-bottom: var(--spacing-03);
+  }
+
+  p {
+    margin-bottom: var(--spacing-03);
+  }
+
+  &.is-mobile p {
+    @include body-xs;
+
+    white-space: normal;
+  }
+}
+
 .color-picker-item {
   height: 28px;
   width: 28px;

+ 11 - 3
services/web/frontend/stylesheets/components/badge.less

@@ -6,7 +6,6 @@
   height: @size;
   max-width: 100%;
   min-height: @size;
-  padding: 0 @padding;
   white-space: nowrap;
   color: @ol-blue-gray-6;
   background-color: @neutral-20;
@@ -16,6 +15,16 @@
     background-color: @neutral-30;
   }
 
+  &-content-wrapper {
+    display: flex;
+    align-items: center;
+    overflow: hidden;
+    padding-left: @padding;
+    padding-right: @padding;
+    border-top-left-radius: inherit;
+    border-bottom-left-radius: inherit;
+  }
+
   &-prepend {
     margin-right: 2px;
   }
@@ -23,7 +32,6 @@
   &-close {
     .reset-button;
     width: @size;
-    margin-left: 4px;
     font-size: 20px;
     height: @size;
     line-height: 1;
@@ -31,7 +39,7 @@
     align-items: center;
     justify-content: center;
     flex-shrink: 0;
-    margin-right: -@padding;
+    padding: 0 @padding;
     border-top-right-radius: inherit;
     border-bottom-right-radius: inherit;
     color: inherit;

+ 1 - 0
services/web/frontend/stylesheets/components/labels.less

@@ -40,6 +40,7 @@
   padding: 0.3em 0.6em;
   display: inline-flex;
   align-items: center;
+  max-width: 100%;
 }
 
 // Colors