Merge pull request #2173 from kamre/gh-2147-url-property-improvement

[GH-2147] URL property improvement
This commit is contained in:
ogi-m 2022-02-01 18:25:06 +01:00 committed by GitHub
commit e3d42c6663
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 552 additions and 254 deletions

View File

@ -0,0 +1,110 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
describe('Card URL Property', () => {
beforeEach(() => {
cy.apiInitServer()
cy.apiResetBoards()
localStorage.setItem('welcomePageViewed', 'true')
})
const url = 'https://mattermost.com'
const changedURL = 'https://mattermost.com/blog'
it('Allows to create and edit URL property', () => {
cy.visit('/')
// Create new board
cy.uiCreateNewBoard('Testing')
// Add a new card
cy.uiAddNewCard('Card')
// Add URL property
cy.log('**Add URL property**')
cy.findByRole('button', {name: '+ Add a property'}).click()
cy.findByRole('button', {name: 'URL'}).click()
cy.findByRole('textbox', {name: 'URL'}).type('{enter}')
// Enter URL
cy.log('**Enter URL**')
cy.findByPlaceholderText('Empty').type(`${url}{enter}`)
// Check buttons
cy.log('**Check buttons**')
cy.findByRole('link', {name: url}).realHover()
cy.findByRole('button', {name: 'Edit'}).should('exist')
cy.findByRole('button', {name: 'Copy'}).should('exist')
// Change URL
cy.log('**Change URL**')
cy.findByRole('link', {name: url}).realHover()
cy.findByRole('button', {name: 'Edit'}).click()
cy.findByRole('textbox', {name: url}).clear().type(`${changedURL}{enter}`)
cy.findByRole('link', {name: changedURL}).should('exist')
// Close card dialog
cy.log('**Close card dialog**')
cy.findByRole('button', {name: 'Close dialog'}).click()
cy.findByRole('dialog').should('not.exist')
// Show URL property
showURLProperty()
// Copy URL to clipboard
cy.log('**Copy URL to clipboard**')
cy.document().then((doc) => cy.spy(doc, 'execCommand')).as('exec')
cy.findByRole('link', {name: changedURL}).realHover()
cy.findByRole('button', {name: 'Edit'}).should('not.exist')
cy.findByRole('button', {name: 'Copy'}).click()
cy.findByText('Copied!').should('exist')
cy.findByText('Copied!').should('not.exist')
cy.get('@exec').should('have.been.calledOnceWith', 'copy')
// Add table view
addView('Table')
// Check buttons
cy.log('**Check buttons**')
cy.findByRole('link', {name: changedURL}).realHover()
cy.findByRole('button', {name: 'Edit'}).should('exist')
cy.findByRole('button', {name: 'Copy'}).should('not.exist')
// Add gallery view
addView('Gallery')
showURLProperty()
// Check buttons
cy.log('**Check buttons**')
cy.findByRole('link', {name: changedURL}).realHover()
cy.findByRole('button', {name: 'Edit'}).should('not.exist')
cy.findByRole('button', {name: 'Copy'}).should('exist')
// Add calendar view
addView('Calendar')
showURLProperty()
// Check buttons
cy.log('**Check buttons**')
cy.findByRole('link', {name: changedURL}).realHover()
cy.findByRole('button', {name: 'Edit'}).should('not.exist')
cy.findByRole('button', {name: 'Copy'}).should('exist')
})
type ViewType = 'Board' | 'Table' | 'Gallery' | 'Calendar'
const addView = (type: ViewType) => {
cy.log(`**Add ${type} view**`)
cy.findByRole('button', {name: 'View menu'}).click()
cy.findByText('Add view').click()
cy.findByRole('button', {name: type}).click()
cy.findByRole('textbox', {name: `${type} view`}).should('exist')
}
const showURLProperty = () => {
cy.log('**Show URL property**')
cy.findByRole('button', {name: 'Properties'}).click()
cy.findByRole('button', {name: 'URL'}).click()
cy.findByRole('link', {name: changedURL}).should('exist')
}
})

View File

@ -1,8 +1,6 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import '@testing-library/cypress/add-commands'
import {Board} from '../../src/blocks/board'
Cypress.Commands.add('apiRegisterUser', (data: Cypress.UserData, token?: string, failOnError?: boolean) => {

View File

@ -1,5 +1,9 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import '@testing-library/cypress/add-commands'
import 'cypress-real-events/support'
import './api_commands'
import './ui_commands'

View File

@ -4,6 +4,7 @@
"noEmit": true,
"types": [
"cypress",
"cypress-real-events",
"@testing-library/cypress"
]
},

View File

@ -208,7 +208,7 @@
"View.DuplicateView": "Duplicate view",
"View.Gallery": "Gallery",
"View.NewBoardTitle": "Board view",
"View.NewCalendarTitle": "Calendar View",
"View.NewCalendarTitle": "Calendar view",
"View.NewGalleryTitle": "Gallery view",
"View.NewTableTitle": "Table view",
"View.NewTemplateTitle": "Untitled Template",

View File

@ -208,7 +208,7 @@
"View.DuplicateView": "Duplicate view",
"View.Gallery": "Gallery",
"View.NewBoardTitle": "Board view",
"View.NewCalendarTitle": "Calendar View",
"View.NewCalendarTitle": "Calendar view",
"View.NewGalleryTitle": "Gallery view",
"View.NewTableTitle": "Table view",
"View.NewTemplateTitle": "Untitled Template",

View File

@ -71,6 +71,7 @@
"copy-webpack-plugin": "^8.1.0",
"cross-env": "^7.0.3",
"css-loader": "^5.2.0",
"cypress-real-events": "^1.6.0",
"eslint": "^7.22.0",
"eslint-import-resolver-webpack": "0.13.0",
"eslint-plugin-babel": "^5.3.1",
@ -5375,6 +5376,15 @@
"node": ">=10.0.0"
}
},
"node_modules/cypress-real-events": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/cypress-real-events/-/cypress-real-events-1.6.0.tgz",
"integrity": "sha512-QxXm0JsQkCrb2uH+fMXNDQ5kNWTzX3OtndBafdsZmNV19j+6JuTK9n52B1YVxrDrr/qzPAojcHJc5PNoQvwp+w==",
"dev": true,
"peerDependencies": {
"cypress": "^4.x || ^5.x || ^6.x || ^7.x || ^8.x || ^9.x"
}
},
"node_modules/cypress/node_modules/@types/node": {
"version": "12.12.50",
"resolved": "https://registry.npmjs.org/@types/node/-/node-12.12.50.tgz",
@ -23543,6 +23553,13 @@
}
}
},
"cypress-real-events": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/cypress-real-events/-/cypress-real-events-1.6.0.tgz",
"integrity": "sha512-QxXm0JsQkCrb2uH+fMXNDQ5kNWTzX3OtndBafdsZmNV19j+6JuTK9n52B1YVxrDrr/qzPAojcHJc5PNoQvwp+w==",
"dev": true,
"requires": {}
},
"dashdash": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz",

View File

@ -105,6 +105,7 @@
"copy-webpack-plugin": "^8.1.0",
"cross-env": "^7.0.3",
"css-loader": "^5.2.0",
"cypress-real-events": "^1.6.0",
"eslint": "^7.22.0",
"eslint-import-resolver-webpack": "0.13.0",
"eslint-plugin-babel": "^5.3.1",

View File

@ -151,7 +151,7 @@ exports[`components/centerPanel return centerPanel and click on card to show car
value="view title"
/>
<div
aria-label="menuwrapper"
aria-label="View menu"
class="MenuWrapper"
role="button"
>
@ -233,7 +233,7 @@ exports[`components/centerPanel return centerPanel and click on card to show car
class="ModalWrapper"
>
<div
aria-label="View menu"
aria-label="View header menu"
class="MenuWrapper"
role="button"
>
@ -710,7 +710,7 @@ exports[`components/centerPanel return centerPanel and click on new card to edit
value="view title"
/>
<div
aria-label="menuwrapper"
aria-label="View menu"
class="MenuWrapper"
role="button"
>
@ -792,7 +792,7 @@ exports[`components/centerPanel return centerPanel and click on new card to edit
class="ModalWrapper"
>
<div
aria-label="View menu"
aria-label="View header menu"
class="MenuWrapper"
role="button"
>
@ -1314,7 +1314,7 @@ exports[`components/centerPanel return centerPanel and press touch 1 with readon
value="view title"
/>
<div
aria-label="menuwrapper"
aria-label="View menu"
class="MenuWrapper"
role="button"
>
@ -1772,7 +1772,7 @@ exports[`components/centerPanel return centerPanel and press touch ctrl+d for on
value="view title"
/>
<div
aria-label="menuwrapper"
aria-label="View menu"
class="MenuWrapper"
role="button"
>
@ -1854,7 +1854,7 @@ exports[`components/centerPanel return centerPanel and press touch ctrl+d for on
class="ModalWrapper"
>
<div
aria-label="View menu"
aria-label="View header menu"
class="MenuWrapper"
role="button"
>
@ -2391,7 +2391,7 @@ exports[`components/centerPanel return centerPanel and press touch del for one c
value="view title"
/>
<div
aria-label="menuwrapper"
aria-label="View menu"
class="MenuWrapper"
role="button"
>
@ -2473,7 +2473,7 @@ exports[`components/centerPanel return centerPanel and press touch del for one c
class="ModalWrapper"
>
<div
aria-label="View menu"
aria-label="View header menu"
class="MenuWrapper"
role="button"
>
@ -3010,7 +3010,7 @@ exports[`components/centerPanel return centerPanel and press touch esc for one c
value="view title"
/>
<div
aria-label="menuwrapper"
aria-label="View menu"
class="MenuWrapper"
role="button"
>
@ -3092,7 +3092,7 @@ exports[`components/centerPanel return centerPanel and press touch esc for one c
class="ModalWrapper"
>
<div
aria-label="View menu"
aria-label="View header menu"
class="MenuWrapper"
role="button"
>
@ -3629,7 +3629,7 @@ exports[`components/centerPanel return centerPanel and press touch esc for one c
value="view title"
/>
<div
aria-label="menuwrapper"
aria-label="View menu"
class="MenuWrapper"
role="button"
>
@ -3711,7 +3711,7 @@ exports[`components/centerPanel return centerPanel and press touch esc for one c
class="ModalWrapper"
>
<div
aria-label="View menu"
aria-label="View header menu"
class="MenuWrapper"
role="button"
>
@ -4248,7 +4248,7 @@ exports[`components/centerPanel return centerPanel and press touch esc for two c
value="view title"
/>
<div
aria-label="menuwrapper"
aria-label="View menu"
class="MenuWrapper"
role="button"
>
@ -4330,7 +4330,7 @@ exports[`components/centerPanel return centerPanel and press touch esc for two c
class="ModalWrapper"
>
<div
aria-label="View menu"
aria-label="View header menu"
class="MenuWrapper"
role="button"
>
@ -4867,7 +4867,7 @@ exports[`components/centerPanel return centerPanel and press touch esc for two c
value="view title"
/>
<div
aria-label="menuwrapper"
aria-label="View menu"
class="MenuWrapper"
role="button"
>
@ -4949,7 +4949,7 @@ exports[`components/centerPanel return centerPanel and press touch esc for two c
class="ModalWrapper"
>
<div
aria-label="View menu"
aria-label="View header menu"
class="MenuWrapper"
role="button"
>
@ -5486,7 +5486,7 @@ exports[`components/centerPanel return centerPanel and press touch esc for two c
value="view title"
/>
<div
aria-label="menuwrapper"
aria-label="View menu"
class="MenuWrapper"
role="button"
>
@ -5568,7 +5568,7 @@ exports[`components/centerPanel return centerPanel and press touch esc for two c
class="ModalWrapper"
>
<div
aria-label="View menu"
aria-label="View header menu"
class="MenuWrapper"
role="button"
>
@ -6105,7 +6105,7 @@ exports[`components/centerPanel return centerPanel and select one card and click
value="view title"
/>
<div
aria-label="menuwrapper"
aria-label="View menu"
class="MenuWrapper"
role="button"
>
@ -6187,7 +6187,7 @@ exports[`components/centerPanel return centerPanel and select one card and click
class="ModalWrapper"
>
<div
aria-label="View menu"
aria-label="View header menu"
class="MenuWrapper"
role="button"
>
@ -6724,7 +6724,7 @@ exports[`components/centerPanel return centerPanel and select one card and click
value="view title"
/>
<div
aria-label="menuwrapper"
aria-label="View menu"
class="MenuWrapper"
role="button"
>
@ -6806,7 +6806,7 @@ exports[`components/centerPanel return centerPanel and select one card and click
class="ModalWrapper"
>
<div
aria-label="View menu"
aria-label="View header menu"
class="MenuWrapper"
role="button"
>
@ -7343,7 +7343,7 @@ exports[`components/centerPanel should match snapshot for Gallery 1`] = `
value="view title"
/>
<div
aria-label="menuwrapper"
aria-label="View menu"
class="MenuWrapper"
role="button"
>
@ -7407,7 +7407,7 @@ exports[`components/centerPanel should match snapshot for Gallery 1`] = `
class="ModalWrapper"
>
<div
aria-label="View menu"
aria-label="View header menu"
class="MenuWrapper"
role="button"
>
@ -7632,7 +7632,7 @@ exports[`components/centerPanel should match snapshot for Kanban 1`] = `
value="view title"
/>
<div
aria-label="menuwrapper"
aria-label="View menu"
class="MenuWrapper"
role="button"
>
@ -7714,7 +7714,7 @@ exports[`components/centerPanel should match snapshot for Kanban 1`] = `
class="ModalWrapper"
>
<div
aria-label="View menu"
aria-label="View header menu"
class="MenuWrapper"
role="button"
>
@ -8157,7 +8157,7 @@ exports[`components/centerPanel should match snapshot for Table 1`] = `
value="view title"
/>
<div
aria-label="menuwrapper"
aria-label="View menu"
class="MenuWrapper"
role="button"
>
@ -8239,7 +8239,7 @@ exports[`components/centerPanel should match snapshot for Table 1`] = `
class="ModalWrapper"
>
<div
aria-label="View menu"
aria-label="View header menu"
class="MenuWrapper"
role="button"
>

View File

@ -16,25 +16,36 @@ exports[`components/propertyValueElement Generic fields should allow cancel 1`]
exports[`components/propertyValueElement URL fields should allow cancel 1`] = `
<div>
<div
class="URLProperty property-link url"
class="URLProperty octo-propertyvalue"
>
<input
class="Editable octo-propertyvalue"
placeholder="Empty"
style="width: 5px;"
title="http://localhost"
value="http://localhost"
/>
<a
class="Link__button"
class="link"
href="http://localhost"
rel="noreferrer"
target="_blank"
>
<i
class="CompassIcon icon-link-variant LinkIcon"
/>
http://localhost
</a>
<button
aria-label="Edit"
class="Button IconButton Button_Edit"
title="Edit"
type="button"
>
<i
class="CompassIcon icon-pencil-outline EditIcon"
/>
</button>
<button
aria-label="Copy"
class="Button IconButton Button_Copy"
title="Copy"
type="button"
>
<i
class="CompassIcon icon-content-copy content-copy"
/>
</button>
</div>
</div>
`;
@ -133,25 +144,36 @@ exports[`components/propertyValueElement should match snapshot, select, read-onl
exports[`components/propertyValueElement should match snapshot, url, array value 1`] = `
<div>
<div
class="URLProperty property-link url"
class="URLProperty octo-propertyvalue"
>
<input
class="Editable octo-propertyvalue"
placeholder="Empty"
style="width: 5px;"
title="http://localhost"
value="http://localhost"
/>
<a
class="Link__button"
class="link"
href="http://localhost"
rel="noreferrer"
target="_blank"
>
<i
class="CompassIcon icon-link-variant LinkIcon"
/>
http://localhost
</a>
<button
aria-label="Edit"
class="Button IconButton Button_Edit"
title="Edit"
type="button"
>
<i
class="CompassIcon icon-pencil-outline EditIcon"
/>
</button>
<button
aria-label="Copy"
class="Button IconButton Button_Copy"
title="Copy"
type="button"
>
<i
class="CompassIcon icon-content-copy content-copy"
/>
</button>
</div>
</div>
`;
@ -159,25 +181,36 @@ exports[`components/propertyValueElement should match snapshot, url, array value
exports[`components/propertyValueElement should match snapshot, url, array value 2`] = `
<div>
<div
class="URLProperty property-link url"
class="URLProperty octo-propertyvalue"
>
<input
class="Editable octo-propertyvalue"
placeholder="Empty"
style="width: 5px;"
title="http://localhost"
value="http://localhost"
/>
<a
class="Link__button"
class="link"
href="http://localhost"
rel="noreferrer"
target="_blank"
>
<i
class="CompassIcon icon-link-variant LinkIcon"
/>
http://localhost
</a>
<button
aria-label="Edit"
class="Button IconButton Button_Edit"
title="Edit"
type="button"
>
<i
class="CompassIcon icon-pencil-outline EditIcon"
/>
</button>
<button
aria-label="Copy"
class="Button IconButton Button_Copy"
title="Copy"
type="button"
>
<i
class="CompassIcon icon-content-copy content-copy"
/>
</button>
</div>
</div>
`;

View File

@ -384,7 +384,7 @@ exports[`src/components/workspace return workspace and showcard 1`] = `
value="view title"
/>
<div
aria-label="menuwrapper"
aria-label="View menu"
class="MenuWrapper"
role="button"
>
@ -466,7 +466,7 @@ exports[`src/components/workspace return workspace and showcard 1`] = `
class="ModalWrapper"
>
<div
aria-label="View menu"
aria-label="View header menu"
class="MenuWrapper"
role="button"
>
@ -935,7 +935,7 @@ exports[`src/components/workspace return workspace readonly and showcard 1`] = `
value="view title"
/>
<div
aria-label="menuwrapper"
aria-label="View menu"
class="MenuWrapper"
role="button"
>
@ -1544,7 +1544,7 @@ exports[`src/components/workspace should match snapshot 1`] = `
value="view title"
/>
<div
aria-label="menuwrapper"
aria-label="View menu"
class="MenuWrapper"
role="button"
>
@ -1626,7 +1626,7 @@ exports[`src/components/workspace should match snapshot 1`] = `
class="ModalWrapper"
>
<div
aria-label="View menu"
aria-label="View header menu"
class="MenuWrapper"
role="button"
>
@ -2095,7 +2095,7 @@ exports[`src/components/workspace should match snapshot with readonly 1`] = `
value="view title"
/>
<div
aria-label="menuwrapper"
aria-label="View menu"
class="MenuWrapper"
role="button"
>

View File

@ -720,12 +720,13 @@ exports[`components/calendar/toolbar return calendar, no date property 1`] = `
>
<a
class="fc-daygrid-event fc-daygrid-block-event fc-h-event fc-event fc-event-start fc-event-end fc-event-past"
tabindex="0"
>
<div
class="fc-event-main"
>
<div>
<div
class="EventContent"
>
<div
class="octo-icontitle"
>
@ -2979,12 +2980,13 @@ exports[`components/calendar/toolbar return calendar, with date property not set
>
<a
class="fc-daygrid-event fc-daygrid-block-event fc-h-event fc-event fc-event-start fc-event-end fc-event-past"
tabindex="0"
>
<div
class="fc-event-main"
>
<div>
<div
class="EventContent"
>
<div
class="octo-icontitle"
>
@ -5936,12 +5938,13 @@ exports[`components/calendar/toolbar return calendar, with date property set 1`]
>
<a
class="fc-daygrid-event fc-daygrid-block-event fc-h-event fc-event fc-event-draggable fc-event-resizable fc-event-start fc-event-end fc-event-past"
tabindex="0"
>
<div
class="fc-event-main"
>
<div>
<div
class="EventContent"
>
<div
class="octo-icontitle"
>

View File

@ -4,7 +4,7 @@
import React, {useCallback, useMemo} from 'react'
import {useIntl} from 'react-intl'
import FullCalendar, {EventClickArg, EventChangeArg, EventInput, EventContentArg, DayCellContentArg} from '@fullcalendar/react'
import FullCalendar, {EventChangeArg, EventInput, EventContentArg, DayCellContentArg} from '@fullcalendar/react'
import interactionPlugin from '@fullcalendar/interaction'
import dayGridPlugin from '@fullcalendar/daygrid'
@ -120,7 +120,10 @@ const CalendarFullView = (props: Props): JSX.Element|null => {
const renderEventContent = (eventProps: EventContentArg): JSX.Element|null => {
const {event} = eventProps
return (
<div>
<div
className='EventContent'
onClick={() => props.showCard(event.id)}
>
<div className='octo-icontitle'>
{ event.extendedProps.icon ? <div className='octo-icon'>{event.extendedProps.icon}</div> : undefined }
<div
@ -150,11 +153,6 @@ const CalendarFullView = (props: Props): JSX.Element|null => {
)
}
const eventClick = useCallback((eventProps: EventClickArg) => {
const {event} = eventProps
props.showCard(event.id)
}, [props.showCard])
const eventChange = useCallback((eventProps: EventChangeArg) => {
const {event} = eventProps
if (!event.start) {
@ -239,7 +237,6 @@ const CalendarFullView = (props: Props): JSX.Element|null => {
eventResizableFromStart={isEditable()}
headerToolbar={toolbar}
buttonText={buttonText}
eventClick={eventClick}
eventContent={renderEventContent}
eventChange={eventChange}

View File

@ -3,15 +3,22 @@
margin-bottom: 10px;
overflow: auto;
a {
.fc-daygrid-event,
.fc-daygrid-day-number {
text-decoration: none;
color: var(--link-color-rgb);
&:hover {
background-color: unset !important;;
background-color: unset;
}
}
.EventContent {
display: flex;
flex-direction: column;
align-items: flex-start;
}
.octo-tooltip {
display: flex;
max-width: 100%;

View File

@ -92,6 +92,10 @@
padding: 5px 10px;
overflow-wrap: anywhere;
.octo-tooltip {
max-width: 100%;
}
.octo-icon {
margin-right: 5px;
}

View File

@ -78,7 +78,7 @@
}
}
.IconButton {
.optionsMenu .IconButton {
border-radius: 3px;
margin-right: 6px;
padding: 0;

View File

@ -1,26 +1,80 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`components/properties/link returns link properties correctly 1`] = `
exports[`components/properties/link should match snapshot for link with empty url 1`] = `
<div>
<div
class="URLProperty property-link url"
class="URLProperty"
>
<input
class="Editable octo-propertyvalue"
style="width: 5px;"
title="https://github.com/mattermost/focalboard"
value="https://github.com/mattermost/focalboard"
title=""
value=""
/>
</div>
</div>
`;
exports[`components/properties/link should match snapshot for link with non-empty url 1`] = `
<div>
<div
class="URLProperty octo-propertyvalue"
>
<a
class="Link__button"
class="link"
href="https://github.com/mattermost/focalboard"
rel="noreferrer"
target="_blank"
>
<i
class="CompassIcon icon-link-variant LinkIcon"
/>
https://github.com/mattermost/focalboard
</a>
<button
aria-label="Edit"
class="Button IconButton Button_Edit"
title="Edit"
type="button"
>
<i
class="CompassIcon icon-pencil-outline EditIcon"
/>
</button>
<button
aria-label="Copy"
class="Button IconButton Button_Copy"
title="Copy"
type="button"
>
<i
class="CompassIcon icon-content-copy content-copy"
/>
</button>
</div>
</div>
`;
exports[`components/properties/link should match snapshot for readonly link with non-empty url 1`] = `
<div>
<div
class="URLProperty octo-propertyvalue"
>
<a
class="link"
href="https://github.com/mattermost/focalboard"
rel="noreferrer"
target="_blank"
>
https://github.com/mattermost/focalboard
</a>
<button
aria-label="Copy"
class="Button IconButton Button_Copy"
title="Copy"
type="button"
>
<i
class="CompassIcon icon-content-copy content-copy"
/>
</button>
</div>
</div>
`;

View File

@ -1,39 +1,39 @@
.property-link {
.URLProperty {
display: flex;
align-items: center;
overflow: hidden;
&.url {
width: 100%;
display: flex;
.link {
flex: 1 1 auto;
padding-left: 1px;
margin-right: 4px;
white-space: nowrap;
overflow: hidden;
color: rgb(var(--center-channel-color-rgb));
text-overflow: ellipsis;
text-decoration: underline rgb(var(--center-channel-color-rgb), 0.5);
}
.Link__button {
.link:hover {
background-color: rgb(var(--center-channel-color-rgb), 0.1);
}
.IconButton {
display: none;
flex: 0 0 24px;
width: 24px;
height: 24px;
align-items: center;
justify-content: center;
border-radius: 4px;
color: rgba(var(--center-channel-color-rgb), 0.56);
font-size: 14.4px;
margin-left: 8px;
margin-right: 4px;
&:hover {
color: rgba(var(--center-channel-color-rgb), 0.72);
background: rgba(var(--center-channel-color-rgb), 0.08);
}
&:active {
color: var(--button-bg-rgb);
background-color: rgb(var(--button-bg-rgb), 0.16);
background-color: rgb(var(--center-channel-color-rgb), 0.1);
}
}
&:hover {
.Link__button {
.IconButton {
display: flex;
}
}
}
#focalboard-app .URLProperty .link:visited {
color: rgb(var(--center-channel-color-rgb));
}

View File

@ -1,26 +1,110 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react'
import {render} from '@testing-library/react'
import React, {useState} from 'react'
import {render, screen} from '@testing-library/react'
import {mocked} from 'ts-jest/utils'
import '@testing-library/jest-dom'
import userEvent from '@testing-library/user-event'
import {wrapIntl} from '../../../testUtils'
import {Utils} from '../../../utils'
import {sendFlashMessage} from '../../flashMessages'
import Link from './link'
jest.mock('../../flashMessages')
const mockedCopy = jest.spyOn(Utils, 'copyTextToClipboard').mockImplementation(() => true)
const mockedSendFlashMessage = mocked(sendFlashMessage, true)
describe('components/properties/link', () => {
test('returns link properties correctly', () => {
const component = (
beforeEach(jest.clearAllMocks)
const linkCallbacks = {
onChange: jest.fn(),
onSave: jest.fn(),
onCancel: jest.fn(),
validator: jest.fn(() => true),
}
const LinkWrapper = (props: {url: string}) => {
const [value, setValue] = useState(props.url)
return (
<Link
value={'https://github.com/mattermost/focalboard'}
onChange={jest.fn()}
onSave={jest.fn()}
onCancel={jest.fn()}
validator={jest.fn(() => {
return true
})}
{...linkCallbacks}
value={value}
onChange={(text) => setValue(text)}
/>
)
const {container} = render(component)
}
it('should match snapshot for link with empty url', () => {
const {container} = render(wrapIntl((
<Link
{...linkCallbacks}
value=''
/>
)))
expect(container).toMatchSnapshot()
})
it('should match snapshot for link with non-empty url', () => {
const {container} = render(wrapIntl((
<Link
{...linkCallbacks}
value='https://github.com/mattermost/focalboard'
/>
)))
expect(container).toMatchSnapshot()
})
it('should match snapshot for readonly link with non-empty url', () => {
const {container} = render(wrapIntl((
<Link
{...linkCallbacks}
value='https://github.com/mattermost/focalboard'
readonly={true}
/>
)))
expect(container).toMatchSnapshot()
})
it('should change to link after entering url', () => {
render(wrapIntl(<LinkWrapper url=''/>))
const url = 'https://mattermost.com'
const input = screen.getByRole('textbox')
userEvent.type(input, `${url}{enter}`)
const link = screen.getByRole('link')
expect(link).toHaveAttribute('href', url)
expect(link).toHaveTextContent(url)
expect(screen.getByRole('button', {name: 'Edit'})).toBeInTheDocument()
expect(screen.getByRole('button', {name: 'Copy'})).toBeInTheDocument()
expect(linkCallbacks.onSave).toHaveBeenCalled()
})
it('should allow to edit link url', () => {
render(wrapIntl(<LinkWrapper url='https://mattermost.com'/>))
screen.getByRole('button', {name: 'Edit'}).click()
const newURL = 'https://github.com/mattermost'
const input = screen.getByRole('textbox')
userEvent.clear(input)
userEvent.type(input, `${newURL}{enter}`)
const link = screen.getByRole('link')
expect(link).toHaveAttribute('href', newURL)
expect(link).toHaveTextContent(newURL)
})
it('should allow to copy url', () => {
const url = 'https://mattermost.com'
render(wrapIntl(<LinkWrapper url={url}/>))
screen.getByRole('button', {name: 'Copy'}).click()
expect(mockedCopy).toHaveBeenCalledWith(url)
expect(mockedSendFlashMessage).toHaveBeenCalledWith({content: 'Copied!', severity: 'high'})
})
})

View File

@ -1,13 +1,17 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {ReactNode} from 'react'
import React, {useEffect, useRef, useState} from 'react'
import {useIntl} from 'react-intl'
import Editable from '../../../widgets/editable'
import Editable, {Focusable} from '../../../widgets/editable'
import './link.scss'
import {Utils} from '../../../utils'
import LinkIcon from '../../../widgets/icons/Link'
import EditIcon from '../../../widgets/icons/edit'
import IconButton from '../../../widgets/buttons/iconButton'
import DuplicateIcon from '../../../widgets/icons/duplicate'
import {sendFlashMessage} from '../../flashMessages'
type Props = {
value: string
@ -20,37 +24,74 @@ type Props = {
}
const URLProperty = (props: Props): JSX.Element => {
let link: ReactNode = null
const hasValue = Boolean(props.value?.trim())
if (hasValue) {
link = (
const [isEditing, setIsEditing] = useState(false)
const isEmpty = !props.value?.trim()
const showEditable = !props.readonly && (isEditing || isEmpty)
const editableRef = useRef<Focusable>(null)
const intl = useIntl()
useEffect(() => {
if (isEditing) {
editableRef.current?.focus()
}
}, [isEditing])
if (showEditable) {
return (
<div className='URLProperty'>
<Editable
className='octo-propertyvalue'
ref={editableRef}
placeholderText={props.placeholder}
value={props.value}
autoExpand={true}
readonly={props.readonly}
onChange={props.onChange}
onSave={() => {
setIsEditing(false)
props.onSave()
}}
onCancel={() => {
setIsEditing(false)
props.onCancel()
}}
onFocus={() => {
setIsEditing(true)
}}
validator={props.validator}
/>
</div>
)
}
return (
<div className='URLProperty octo-propertyvalue'>
<a
className='Link__button'
className='link'
href={Utils.ensureProtocol(props.value.trim())}
target='_blank'
rel='noreferrer'
onClick={(event) => event.stopPropagation()}
>
<LinkIcon/>
{props.value}
</a>
)
}
return (
<div className='URLProperty property-link url'>
{(hasValue || props.placeholder) &&
<Editable
className='octo-propertyvalue'
placeholderText={props.placeholder}
value={props.value}
autoExpand={true}
readonly={props.readonly}
onChange={props.onChange}
onSave={props.onSave}
onCancel={props.onCancel}
validator={props.validator}
{!props.readonly &&
<IconButton
className='Button_Edit'
title={intl.formatMessage({id: 'URLProperty.edit', defaultMessage: 'Edit'})}
icon={<EditIcon/>}
onClick={() => setIsEditing(true)}
/>}
{link}
<IconButton
className='Button_Copy'
title={intl.formatMessage({id: 'URLProperty.copy', defaultMessage: 'Copy'})}
icon={<DuplicateIcon/>}
onClick={(e) => {
e.stopPropagation()
Utils.copyTextToClipboard(props.value)
sendFlashMessage({content: intl.formatMessage({id: 'URLProperty.copiedLink', defaultMessage: 'Copied!'}), severity: 'high'})
}}
/>
</div>
)
}

View File

@ -27,4 +27,8 @@
display: block;
}
}
.URLProperty:hover .Button_Copy {
display: none;
}
}

View File

@ -67,23 +67,9 @@ exports[`components/viewHeader/newCardButtonTemplateItem return NewCardButtonTem
class="MenuOption TextOption menu-option"
role="button"
>
<svg
class="EditIcon Icon"
viewBox="0 0 100 100"
xmlns="http://www.w3.org/2000/svg"
>
<g>
<path
d="M35.2,48.7l-2,14.1c-0.1,0.9,0.2,1.9,0.8,2.5c0.6,0.6,1.3,0.9,2.1,0.9c0.1,0,0.3,0,0.4,0l14.1-2c1.7-0.2,3.4-1.1,4.6-2.3 l28.2-28.2l4.6-4.5c1.8-1.8,2.7-4.1,2.7-6.6c0-2.5-1-4.9-2.7-6.6l-4.5-4.5c-3.7-3.7-9.6-3.7-13.3,0l-4.5,4.6L37.5,44.1 C36.2,45.3,35.4,47,35.2,48.7z M74.5,15.6c1.3-1.3,3.5-1.3,4.8,0l4.5,4.5c0.6,0.6,1,1.5,1,2.4c0,0.9-0.4,1.8-1,2.4c0,0,0,0,0,0 l-2.4,2.4L72.1,18L74.5,15.6z M41.1,49.5c0.1-0.4,0.3-0.9,0.6-1.2l26.1-26.1l4.7,4.7l4.7,4.7L51,57.7c-0.3,0.3-0.7,0.5-1.2,0.6 l-10.2,1.4L41.1,49.5z"
/>
<path
d="M88.3,35.8c-1.7,0-3,1.3-3,3v42.7c0,3.6-2.9,6.5-6.5,6.5H21.2c-3.6,0-6.5-2.9-6.5-6.5v-61c0-3.6,2.9-6.5,6.5-6.5h39.2 c1.7,0,3-1.3,3-3s-1.3-3-3-3H21.2C14.3,8,8.7,13.6,8.7,20.5v61c0,6.9,5.6,12.5,12.5,12.5h57.5c6.9,0,12.5-5.6,12.5-12.5V38.8 C91.3,37.2,89.9,35.8,88.3,35.8z"
/>
<path
d="M24.4,72c-1.7,0-3,1.3-3,3s1.3,3,3,3h28.8c1.7,0,3-1.3,3-3s-1.3-3-3-3H24.4z"
/>
</g>
</svg>
<i
class="CompassIcon icon-pencil-outline EditIcon"
/>
<div
class="menu-name"
>
@ -209,23 +195,9 @@ exports[`components/viewHeader/newCardButtonTemplateItem return NewCardButtonTem
class="MenuOption TextOption menu-option"
role="button"
>
<svg
class="EditIcon Icon"
viewBox="0 0 100 100"
xmlns="http://www.w3.org/2000/svg"
>
<g>
<path
d="M35.2,48.7l-2,14.1c-0.1,0.9,0.2,1.9,0.8,2.5c0.6,0.6,1.3,0.9,2.1,0.9c0.1,0,0.3,0,0.4,0l14.1-2c1.7-0.2,3.4-1.1,4.6-2.3 l28.2-28.2l4.6-4.5c1.8-1.8,2.7-4.1,2.7-6.6c0-2.5-1-4.9-2.7-6.6l-4.5-4.5c-3.7-3.7-9.6-3.7-13.3,0l-4.5,4.6L37.5,44.1 C36.2,45.3,35.4,47,35.2,48.7z M74.5,15.6c1.3-1.3,3.5-1.3,4.8,0l4.5,4.5c0.6,0.6,1,1.5,1,2.4c0,0.9-0.4,1.8-1,2.4c0,0,0,0,0,0 l-2.4,2.4L72.1,18L74.5,15.6z M41.1,49.5c0.1-0.4,0.3-0.9,0.6-1.2l26.1-26.1l4.7,4.7l4.7,4.7L51,57.7c-0.3,0.3-0.7,0.5-1.2,0.6 l-10.2,1.4L41.1,49.5z"
/>
<path
d="M88.3,35.8c-1.7,0-3,1.3-3,3v42.7c0,3.6-2.9,6.5-6.5,6.5H21.2c-3.6,0-6.5-2.9-6.5-6.5v-61c0-3.6,2.9-6.5,6.5-6.5h39.2 c1.7,0,3-1.3,3-3s-1.3-3-3-3H21.2C14.3,8,8.7,13.6,8.7,20.5v61c0,6.9,5.6,12.5,12.5,12.5h57.5c6.9,0,12.5-5.6,12.5-12.5V38.8 C91.3,37.2,89.9,35.8,88.3,35.8z"
/>
<path
d="M24.4,72c-1.7,0-3,1.3-3,3s1.3,3,3,3h28.8c1.7,0,3-1.3,3-3s-1.3-3-3-3H24.4z"
/>
</g>
</svg>
<i
class="CompassIcon icon-pencil-outline EditIcon"
/>
<div
class="menu-name"
>
@ -386,23 +358,9 @@ exports[`components/viewHeader/newCardButtonTemplateItem return NewCardButtonTem
class="MenuOption TextOption menu-option"
role="button"
>
<svg
class="EditIcon Icon"
viewBox="0 0 100 100"
xmlns="http://www.w3.org/2000/svg"
>
<g>
<path
d="M35.2,48.7l-2,14.1c-0.1,0.9,0.2,1.9,0.8,2.5c0.6,0.6,1.3,0.9,2.1,0.9c0.1,0,0.3,0,0.4,0l14.1-2c1.7-0.2,3.4-1.1,4.6-2.3 l28.2-28.2l4.6-4.5c1.8-1.8,2.7-4.1,2.7-6.6c0-2.5-1-4.9-2.7-6.6l-4.5-4.5c-3.7-3.7-9.6-3.7-13.3,0l-4.5,4.6L37.5,44.1 C36.2,45.3,35.4,47,35.2,48.7z M74.5,15.6c1.3-1.3,3.5-1.3,4.8,0l4.5,4.5c0.6,0.6,1,1.5,1,2.4c0,0.9-0.4,1.8-1,2.4c0,0,0,0,0,0 l-2.4,2.4L72.1,18L74.5,15.6z M41.1,49.5c0.1-0.4,0.3-0.9,0.6-1.2l26.1-26.1l4.7,4.7l4.7,4.7L51,57.7c-0.3,0.3-0.7,0.5-1.2,0.6 l-10.2,1.4L41.1,49.5z"
/>
<path
d="M88.3,35.8c-1.7,0-3,1.3-3,3v42.7c0,3.6-2.9,6.5-6.5,6.5H21.2c-3.6,0-6.5-2.9-6.5-6.5v-61c0-3.6,2.9-6.5,6.5-6.5h39.2 c1.7,0,3-1.3,3-3s-1.3-3-3-3H21.2C14.3,8,8.7,13.6,8.7,20.5v61c0,6.9,5.6,12.5,12.5,12.5h57.5c6.9,0,12.5-5.6,12.5-12.5V38.8 C91.3,37.2,89.9,35.8,88.3,35.8z"
/>
<path
d="M24.4,72c-1.7,0-3,1.3-3,3s1.3,3,3,3h28.8c1.7,0,3-1.3,3-3s-1.3-3-3-3H24.4z"
/>
</g>
</svg>
<i
class="CompassIcon icon-pencil-outline EditIcon"
/>
<div
class="menu-name"
>
@ -528,23 +486,9 @@ exports[`components/viewHeader/newCardButtonTemplateItem return NewCardButtonTem
class="MenuOption TextOption menu-option"
role="button"
>
<svg
class="EditIcon Icon"
viewBox="0 0 100 100"
xmlns="http://www.w3.org/2000/svg"
>
<g>
<path
d="M35.2,48.7l-2,14.1c-0.1,0.9,0.2,1.9,0.8,2.5c0.6,0.6,1.3,0.9,2.1,0.9c0.1,0,0.3,0,0.4,0l14.1-2c1.7-0.2,3.4-1.1,4.6-2.3 l28.2-28.2l4.6-4.5c1.8-1.8,2.7-4.1,2.7-6.6c0-2.5-1-4.9-2.7-6.6l-4.5-4.5c-3.7-3.7-9.6-3.7-13.3,0l-4.5,4.6L37.5,44.1 C36.2,45.3,35.4,47,35.2,48.7z M74.5,15.6c1.3-1.3,3.5-1.3,4.8,0l4.5,4.5c0.6,0.6,1,1.5,1,2.4c0,0.9-0.4,1.8-1,2.4c0,0,0,0,0,0 l-2.4,2.4L72.1,18L74.5,15.6z M41.1,49.5c0.1-0.4,0.3-0.9,0.6-1.2l26.1-26.1l4.7,4.7l4.7,4.7L51,57.7c-0.3,0.3-0.7,0.5-1.2,0.6 l-10.2,1.4L41.1,49.5z"
/>
<path
d="M88.3,35.8c-1.7,0-3,1.3-3,3v42.7c0,3.6-2.9,6.5-6.5,6.5H21.2c-3.6,0-6.5-2.9-6.5-6.5v-61c0-3.6,2.9-6.5,6.5-6.5h39.2 c1.7,0,3-1.3,3-3s-1.3-3-3-3H21.2C14.3,8,8.7,13.6,8.7,20.5v61c0,6.9,5.6,12.5,12.5,12.5h57.5c6.9,0,12.5-5.6,12.5-12.5V38.8 C91.3,37.2,89.9,35.8,88.3,35.8z"
/>
<path
d="M24.4,72c-1.7,0-3,1.3-3,3s1.3,3,3,3h28.8c1.7,0,3-1.3,3-3s-1.3-3-3-3H24.4z"
/>
</g>
</svg>
<i
class="CompassIcon icon-pencil-outline EditIcon"
/>
<div
class="menu-name"
>

View File

@ -13,7 +13,7 @@ exports[`components/viewHeader/viewHeader return viewHeader 1`] = `
value="view title"
/>
<div
aria-label="menuwrapper"
aria-label="View menu"
class="MenuWrapper"
role="button"
>
@ -100,7 +100,7 @@ exports[`components/viewHeader/viewHeader return viewHeader 1`] = `
class="ModalWrapper"
>
<div
aria-label="View menu"
aria-label="View header menu"
class="MenuWrapper"
role="button"
>
@ -154,7 +154,7 @@ exports[`components/viewHeader/viewHeader return viewHeader readonly 1`] = `
value="view title"
/>
<div
aria-label="menuwrapper"
aria-label="View menu"
class="MenuWrapper"
role="button"
>

View File

@ -6,7 +6,7 @@ exports[`components/viewHeader/viewHeaderActionsMenu return menu and verify call
class="ModalWrapper"
>
<div
aria-label="View menu"
aria-label="View header menu"
class="MenuWrapper"
role="button"
>
@ -116,7 +116,7 @@ exports[`components/viewHeader/viewHeaderActionsMenu return menu and verify call
class="ModalWrapper"
>
<div
aria-label="View menu"
aria-label="View header menu"
class="MenuWrapper"
role="button"
>
@ -226,7 +226,7 @@ exports[`components/viewHeader/viewHeaderActionsMenu return menu with Share Boar
class="ModalWrapper"
>
<div
aria-label="View menu"
aria-label="View header menu"
class="MenuWrapper"
role="button"
>
@ -336,7 +336,7 @@ exports[`components/viewHeader/viewHeaderActionsMenu return menu without Share B
class="ModalWrapper"
>
<div
aria-label="View menu"
aria-label="View header menu"
class="MenuWrapper"
role="button"
>

View File

@ -1,7 +1,7 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {useState, useEffect} from 'react'
import {FormattedMessage} from 'react-intl'
import {FormattedMessage, useIntl} from 'react-intl'
import ViewMenu from '../../components/viewMenu'
import mutator from '../../mutator'
@ -44,6 +44,7 @@ type Props = {
const ViewHeader = React.memo((props: Props) => {
const [showFilter, setShowFilter] = useState(false)
const intl = useIntl()
const {board, activeView, views, groupByProperty, cards, showShared, dateDisplayProperty} = props
@ -76,7 +77,7 @@ const ViewHeader = React.memo((props: Props) => {
spellCheck={true}
autoExpand={false}
/>
<MenuWrapper>
<MenuWrapper label={intl.formatMessage({id: 'ViewHeader.view-menu', defaultMessage: 'View menu'})}>
<IconButton icon={<DropdownIcon/>}/>
<ViewMenu
board={board}

View File

@ -57,7 +57,7 @@ describe('components/viewHeader/viewHeaderActionsMenu', () => {
),
)
const buttonElement = screen.getByRole('button', {
name: 'View menu',
name: 'View header menu',
})
userEvent.click(buttonElement)
expect(container).toMatchSnapshot()
@ -77,7 +77,7 @@ describe('components/viewHeader/viewHeaderActionsMenu', () => {
),
)
const buttonElement = screen.getByRole('button', {
name: 'View menu',
name: 'View header menu',
})
userEvent.click(buttonElement)
expect(container).toMatchSnapshot()
@ -95,7 +95,7 @@ describe('components/viewHeader/viewHeaderActionsMenu', () => {
</ReduxProvider>,
),
)
const buttonElement = screen.getByRole('button', {name: 'View menu'})
const buttonElement = screen.getByRole('button', {name: 'View header menu'})
userEvent.click(buttonElement)
expect(container).toMatchSnapshot()
const buttonExportCSV = screen.getByRole('button', {name: 'Export to CSV'})
@ -116,7 +116,7 @@ describe('components/viewHeader/viewHeaderActionsMenu', () => {
</ReduxProvider>,
),
)
const buttonElement = screen.getByRole('button', {name: 'View menu'})
const buttonElement = screen.getByRole('button', {name: 'View header menu'})
userEvent.click(buttonElement)
expect(container).toMatchSnapshot()
const buttonExportBoardArchive = screen.getByRole('button', {name: 'Export board archive'})

View File

@ -108,7 +108,7 @@ const ViewHeaderActionsMenu = React.memo((props: Props) => {
return (
<ModalWrapper>
<MenuWrapper label={intl.formatMessage({id: 'ViewHeader.view-menu', defaultMessage: 'View menu'})}>
<MenuWrapper label={intl.formatMessage({id: 'ViewHeader.view-header-menu', defaultMessage: 'View header menu'})}>
<IconButton icon={<OptionsIcon/>}/>
<Menu>
<Menu.Text

View File

@ -174,7 +174,7 @@ const ViewMenu = React.memo((props: Props) => {
Utils.log('addview-calendar')
const view = createBoardView()
view.title = intl.formatMessage({id: 'View.NewCalendarTitle', defaultMessage: 'Calendar View'})
view.title = intl.formatMessage({id: 'View.NewCalendarTitle', defaultMessage: 'Calendar view'})
view.fields.viewType = 'calendar'
view.parentId = board.id
view.rootId = board.rootId

View File

@ -3,4 +3,5 @@
stroke: none;
width: 24px;
height: 24px;
font-size: 18px;
}

View File

@ -4,19 +4,13 @@
import React from 'react'
import './edit.scss'
import CompassIcon from './compassIcon'
export default function EditIcon(): JSX.Element {
return (
<svg
xmlns='http://www.w3.org/2000/svg'
className='EditIcon Icon'
viewBox='0 0 100 100'
>
<g>
<path d='M35.2,48.7l-2,14.1c-0.1,0.9,0.2,1.9,0.8,2.5c0.6,0.6,1.3,0.9,2.1,0.9c0.1,0,0.3,0,0.4,0l14.1-2c1.7-0.2,3.4-1.1,4.6-2.3 l28.2-28.2l4.6-4.5c1.8-1.8,2.7-4.1,2.7-6.6c0-2.5-1-4.9-2.7-6.6l-4.5-4.5c-3.7-3.7-9.6-3.7-13.3,0l-4.5,4.6L37.5,44.1 C36.2,45.3,35.4,47,35.2,48.7z M74.5,15.6c1.3-1.3,3.5-1.3,4.8,0l4.5,4.5c0.6,0.6,1,1.5,1,2.4c0,0.9-0.4,1.8-1,2.4c0,0,0,0,0,0 l-2.4,2.4L72.1,18L74.5,15.6z M41.1,49.5c0.1-0.4,0.3-0.9,0.6-1.2l26.1-26.1l4.7,4.7l4.7,4.7L51,57.7c-0.3,0.3-0.7,0.5-1.2,0.6 l-10.2,1.4L41.1,49.5z'/>
<path d='M88.3,35.8c-1.7,0-3,1.3-3,3v42.7c0,3.6-2.9,6.5-6.5,6.5H21.2c-3.6,0-6.5-2.9-6.5-6.5v-61c0-3.6,2.9-6.5,6.5-6.5h39.2 c1.7,0,3-1.3,3-3s-1.3-3-3-3H21.2C14.3,8,8.7,13.6,8.7,20.5v61c0,6.9,5.6,12.5,12.5,12.5h57.5c6.9,0,12.5-5.6,12.5-12.5V38.8 C91.3,37.2,89.9,35.8,88.3,35.8z'/>
<path d='M24.4,72c-1.7,0-3,1.3-3,3s1.3,3,3,3h28.8c1.7,0,3-1.3,3-3s-1.3-3-3-3H24.4z'/>
</g>
</svg>
<CompassIcon
className='EditIcon'
icon='pencil-outline'
/>
)
}