diff --git a/app/assets/javascripts/work_items/components/work_item_description.vue b/app/assets/javascripts/work_items/components/work_item_description.vue index d5ba2195b0ed2be7c1d2e88b9c197bd312dd681d..3470b31545e152fca769b02c4951dcbbcc997f93 100644 --- a/app/assets/javascripts/work_items/components/work_item_description.vue +++ b/app/assets/javascripts/work_items/components/work_item_description.vue @@ -61,6 +61,7 @@ export default { data() { return { workItem: {}, + disableTruncation: false, isEditing: this.editMode, isSubmitting: false, isSubmittingWithKeydown: false, @@ -181,6 +182,7 @@ export default { }, async startEditing() { this.isEditing = true; + this.disableTruncation = true; this.descriptionText = getDraft(this.autosaveKey) || this.workItemDescription?.description; @@ -359,6 +361,7 @@ export default { :disable-inline-editing="disableInlineEditing" :work-item-description="workItemDescription" :can-edit="canEdit" + :disable-truncation="disableTruncation" @startEditing="startEditing" @descriptionUpdated="handleDescriptionTextUpdated" /> diff --git a/app/assets/javascripts/work_items/components/work_item_description_rendered.vue b/app/assets/javascripts/work_items/components/work_item_description_rendered.vue index 1699f6c419e3ad220924e37f35579f43c802b7fc..c79905005cb8e8b926ccca99e5b4a4741786f3b7 100644 --- a/app/assets/javascripts/work_items/components/work_item_description_rendered.vue +++ b/app/assets/javascripts/work_items/components/work_item_description_rendered.vue @@ -2,6 +2,7 @@ import { GlButton, GlTooltipDirective } from '@gitlab/ui'; import SafeHtml from '~/vue_shared/directives/safe_html'; import { renderGFM } from '~/behaviors/markdown/render_gfm'; +import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; const isCheckbox = (target) => target?.classList.contains('task-list-item-checkbox'); @@ -13,7 +14,13 @@ export default { components: { GlButton, }, + mixins: [glFeatureFlagMixin()], props: { + disableTruncation: { + type: Boolean, + required: false, + default: false, + }, workItemDescription: { type: Object, required: true, @@ -30,6 +37,7 @@ export default { }, data() { return { + truncated: false, checkboxes: [], }; }, @@ -49,6 +57,9 @@ export default { showEditButton() { return this.canEdit && !this.disableInlineEditing; }, + isTruncated() { + return this.truncated && !this.disableTruncation && this.glFeatures.workItemsMvc2; + }, }, watch: { descriptionHtml: { @@ -74,6 +85,7 @@ export default { checkbox.disabled = false; }); } + this.truncateLongDescription(); }, toggleCheckboxes(event) { const { target } = event; @@ -105,6 +117,21 @@ export default { this.$emit('descriptionUpdated', newDescriptionText); } }, + truncateLongDescription() { + /* Truncate when description is > 40% viewport height or 512px. + Update `.work-item-description .truncated` max height if value changes. */ + const defaultMaxHeight = document.documentElement.clientHeight * 0.4; + let maxHeight = defaultMaxHeight; + if (defaultMaxHeight > 512) { + maxHeight = 512; + } else if (defaultMaxHeight < 256) { + maxHeight = 256; + } + this.truncated = this.$refs['gfm-content'].clientHeight > maxHeight; + }, + showAll() { + this.truncated = false; + }, }, }; @@ -130,11 +157,32 @@ export default {
{{ __('None') }}
+ ref="description" + class="work-item-description md gl-mb-5 gl-min-h-8 gl-clearfix gl-relative" + > +
+
+
+ {{ __('Read more') }} +
+
+ diff --git a/app/assets/stylesheets/page_bundles/work_items.scss b/app/assets/stylesheets/page_bundles/work_items.scss index c5f923558d9e8759d4669b22b0828ab571996889..985565b789365abf41a48aba2f73e48b03cc7644 100644 --- a/app/assets/stylesheets/page_bundles/work_items.scss +++ b/app/assets/stylesheets/page_bundles/work_items.scss @@ -388,3 +388,46 @@ $disclosure-hierarchy-chevron-dimension: 1.2rem; max-width: $fixed-layout-width; } } + +.work-item-description .truncated{ + max-height: clamp(16rem, 40vh, 32rem); + overflow: hidden; +} + +.description-more{ + position: absolute; + bottom: 0; + pointer-events: none; + + &::before { + content: ''; + display: block; + height: 3rem; + width: 100%; + background: linear-gradient(180deg, rgba(255, 255, 255, 0.00) 0%, $white 100%); + + .gl-dark & { + background: linear-gradient(180deg, rgba(31, 30, 36, 0.00) 0%, $gray-950 100%); + } + } + + .show-all-btn { + pointer-events: auto; + background-color: $white; + + .gl-dark & { + background-color: $gray-950; + } + + &::before, &::after { + content: ''; + height: 1px; + flex: 1; + border-top: 1px solid $gray-50; + + .gl-dark & { + border-top: 1px solid $gray-900; + } + } + } +} diff --git a/spec/frontend/work_items/components/work_item_description_rendered_spec.js b/spec/frontend/work_items/components/work_item_description_rendered_spec.js index c4c88c7643f45a0e55451026b3158c1d9a363502..cbe77258449bc55b1f064a1c63b30d3ed4c4153c 100644 --- a/spec/frontend/work_items/components/work_item_description_rendered_spec.js +++ b/spec/frontend/work_items/components/work_item_description_rendered_spec.js @@ -21,6 +21,8 @@ describe('WorkItemDescription', () => { workItemDescription = defaultWorkItemDescription, canEdit = false, disableInlineEditing = false, + mockComputed = {}, + hasWorkItemsMvc2 = false, } = {}) => { wrapper = shallowMount(WorkItemDescriptionRendered, { propsData: { @@ -28,6 +30,10 @@ describe('WorkItemDescription', () => { canEdit, disableInlineEditing, }, + computed: mockComputed, + provide: { + workItemsMvc2: hasWorkItemsMvc2, + }, }); }; @@ -39,6 +45,44 @@ describe('WorkItemDescription', () => { expect(renderGFM).toHaveBeenCalled(); }); + describe('with truncation', () => { + it('shows the untruncate action', () => { + createComponent({ + workItemDescription: { + description: 'This is a long description', + descriptionHtml: '

This is a long description

', + }, + mockComputed: { + isTruncated() { + return true; + }, + }, + hasWorkItemsMvc2: true, + }); + + expect(wrapper.find('[data-test-id="description-read-more"]').exists()).toBe(true); + }); + }); + + describe('without truncation', () => { + it('does not show the untruncate action', () => { + createComponent({ + workItemDescription: { + description: 'This is a long description', + descriptionHtml: '

This is a long description

', + }, + mockComputed: { + isTruncated() { + return false; + }, + }, + hasWorkItemsMvc2: true, + }); + + expect(wrapper.find('[data-test-id="description-read-more"]').exists()).toBe(false); + }); + }); + describe('with checkboxes', () => { beforeEach(() => { createComponent({