* Hidden card view via direct URL & unfurl (#3071) * wip: directly view a hidden (limited) card * wip: use `limited` from Card * feat(plugin): render hidden message when unfurling * feat: `limited` direct card view + tests * style: apply eslint * fix: `large` <Button> * test: update snapshot * fix: wrap raw SVG in TSX component for styling * feat: open pricing modal instead of /pricing URL * fix: close card modal when opening pricing modal * test: update snapshots * chore: update i18n strings * chore(Makefile): tests & dependencies in plugin * test(plugin): add BoardsUnfurl snapshot tests * test(cypress): 'Log in' -> 'Login' * chore: i18n extract * fix: use globstar matching in `i18n-extract` * fix: `i18n-extract`, the sequel * fix: / -> /error button `Login` to `Log in` * style: fix linting Co-authored-by: Mattermod <mattermod@users.noreply.github.com> * fix: add `limited` to `Card` * test: fix tests * test: fix BoardsUnfurl * chore: `npm run i18n-extract`` * Reducing the props used in the svgs to the minimum needed Co-authored-by: Mattermod <mattermod@users.noreply.github.com> Co-authored-by: Scott Bishel <scott.bishel@mattermost.com> Co-authored-by: Jesús Espino <jespinog@gmail.com>
This commit is contained in:
parent
90fcae066b
commit
c704729561
22 changed files with 16001 additions and 3955 deletions
2
Makefile
2
Makefile
|
@ -29,11 +29,13 @@ all: webapp server ## Build server and webapp.
|
|||
|
||||
prebuild: ## Run prebuild actions (install dependencies etc.).
|
||||
cd webapp; npm install
|
||||
cd mattermost-plugin/webapp; npm install
|
||||
|
||||
ci: server-test
|
||||
cd webapp; npm run check
|
||||
cd webapp; npm run test
|
||||
cd webapp; npm run cypress:ci
|
||||
cd mattermost-plugin/webapp; npm run test
|
||||
|
||||
templates-archive: ## Build templates archive file
|
||||
cd server/assets/build-template-archive; go run -tags '$(BUILD_TAGS)' main.go --dir="../templates-boardarchive" --out="../templates.boardarchive"
|
||||
|
|
18899
mattermost-plugin/webapp/package-lock.json
generated
18899
mattermost-plugin/webapp/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -26,6 +26,7 @@
|
|||
"@babel/preset-typescript": "7.16.7",
|
||||
"@babel/runtime": "7.17.8",
|
||||
"@formatjs/ts-transformer": "3.9.2",
|
||||
"@testing-library/react": "11.2.7",
|
||||
"@types/enzyme": "3.10.11",
|
||||
"@types/jest": "27.4.1",
|
||||
"@types/node": "17.0.23",
|
||||
|
@ -35,6 +36,7 @@
|
|||
"@types/react-redux": "7.1.23",
|
||||
"@types/react-router-dom": "5.3.3",
|
||||
"@types/react-transition-group": "4.4.4",
|
||||
"@types/redux-mock-store": "1.0.3",
|
||||
"@typescript-eslint/eslint-plugin": "5.16.0",
|
||||
"@typescript-eslint/parser": "5.16.0",
|
||||
"babel-eslint": "10.1.0",
|
||||
|
@ -62,9 +64,12 @@
|
|||
"imagemin-pngquant": "^9.0.2",
|
||||
"imagemin-svgo": "^10.0.1",
|
||||
"imagemin-webp": "7.0.0",
|
||||
"isomorphic-fetch": "3.0.0",
|
||||
"jest": "27.5.1",
|
||||
"jest-canvas-mock": "2.3.1",
|
||||
"jest-junit": "13.0.0",
|
||||
"jest-mock": "27.5.1",
|
||||
"redux-mock-store": "1.5.4",
|
||||
"sass": "1.49.9",
|
||||
"sass-loader": "12.6.0",
|
||||
"style-loader": "3.3.1",
|
||||
|
@ -78,14 +83,15 @@
|
|||
"glob-parent": "6.0.2",
|
||||
"marked": ">=4.0.12",
|
||||
"mattermost-redux": "5.33.1",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "17.0.2",
|
||||
"react-intl": "^5.24.7",
|
||||
"react-redux": "^7.2.8",
|
||||
"react-router-dom": "5.2.0",
|
||||
"trim-newlines": "4.0.2"
|
||||
},
|
||||
"jest": {
|
||||
"snapshotSerializers": [
|
||||
"<rootDir>/node_modules/enzyme-to-json/serializer"
|
||||
],
|
||||
"testEnvironment": "jsdom",
|
||||
"testPathIgnorePatterns": [
|
||||
"/node_modules/",
|
||||
"/non_npm_dependencies/"
|
||||
|
|
|
@ -0,0 +1,94 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`components/boardsUnfurl/BoardsUnfurl renders normally 1`] = `
|
||||
<div>
|
||||
<a
|
||||
class="FocalboardUnfurl"
|
||||
href="http://localhost:8065/test"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
<div
|
||||
class="header"
|
||||
>
|
||||
<span
|
||||
class="icon"
|
||||
/>
|
||||
<div
|
||||
class="information"
|
||||
>
|
||||
<span
|
||||
class="card_title"
|
||||
>
|
||||
test card
|
||||
</span>
|
||||
<span
|
||||
class="board_title"
|
||||
>
|
||||
test board
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="footer"
|
||||
>
|
||||
<div
|
||||
class="avatar"
|
||||
>
|
||||
|
||||
</div>
|
||||
<div
|
||||
class="timestamp_properties"
|
||||
>
|
||||
<div
|
||||
class="properties"
|
||||
/>
|
||||
<span
|
||||
class="post-preview__time"
|
||||
>
|
||||
Updated
|
||||
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`components/boardsUnfurl/BoardsUnfurl renders when limited 1`] = `
|
||||
<div>
|
||||
<a
|
||||
class="FocalboardUnfurl"
|
||||
href="http://localhost:8065/test"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
<div
|
||||
class="header"
|
||||
>
|
||||
<span
|
||||
class="icon"
|
||||
/>
|
||||
<div
|
||||
class="information"
|
||||
>
|
||||
<span
|
||||
class="card_title"
|
||||
>
|
||||
test card
|
||||
</span>
|
||||
<span
|
||||
class="board_title"
|
||||
>
|
||||
test board
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<p
|
||||
class="limited"
|
||||
>
|
||||
Additional details are hidden due to the card being archived
|
||||
</p>
|
||||
</a>
|
||||
</div>
|
||||
`;
|
|
@ -71,6 +71,12 @@
|
|||
}
|
||||
}
|
||||
|
||||
.limited {
|
||||
font-size: 14px;
|
||||
color: rgba(var(--center-channel-color-rgb), 0.6);
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
|
|
@ -0,0 +1,110 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react'
|
||||
import {act, render} from '@testing-library/react'
|
||||
|
||||
import configureStore from 'redux-mock-store'
|
||||
|
||||
import {Provider as ReduxProvider} from 'react-redux'
|
||||
|
||||
import {mocked} from 'jest-mock'
|
||||
|
||||
import {createCard} from '../../../../../webapp/src/blocks/card'
|
||||
import {createBoard} from '../../../../../webapp/src/blocks/board'
|
||||
import octoClient from '../../../../../webapp/src/octoClient'
|
||||
import {wrapIntl} from '../../../../../webapp/src/testUtils'
|
||||
|
||||
import BoardsUnfurl from './boardsUnfurl'
|
||||
|
||||
jest.mock('../../../../../webapp/src/octoClient')
|
||||
const mockedOctoClient = mocked(octoClient, true)
|
||||
|
||||
describe('components/boardsUnfurl/BoardsUnfurl', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it('renders normally', async () => {
|
||||
const mockStore = configureStore([])
|
||||
const store = mockStore({
|
||||
entities: {
|
||||
users: {
|
||||
currentUserId: 'id_1',
|
||||
profiles: {
|
||||
id_1: {
|
||||
locale: 'en',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const cards = [{...createCard(), title: 'test card'}]
|
||||
const board = {...createBoard(), title: 'test board'}
|
||||
|
||||
mockedOctoClient.getBlocksWithBlockID.mockResolvedValueOnce(cards)
|
||||
mockedOctoClient.getBoard.mockResolvedValueOnce(board)
|
||||
|
||||
const component = (
|
||||
<ReduxProvider store={store}>
|
||||
{wrapIntl(
|
||||
<BoardsUnfurl
|
||||
embed={{data: '{"workspaceID": "foo", "cardID": "bar", "boardID": "baz", "readToken": "abc", "originalPath": "/test"}'}}
|
||||
/>,
|
||||
)}
|
||||
</ReduxProvider>
|
||||
)
|
||||
|
||||
let container: Element | DocumentFragment | null = null
|
||||
|
||||
await act(async () => {
|
||||
const result = render(component)
|
||||
container = result.container
|
||||
})
|
||||
|
||||
expect(container).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('renders when limited', async () => {
|
||||
const mockStore = configureStore([])
|
||||
const store = mockStore({
|
||||
entities: {
|
||||
users: {
|
||||
currentUserId: 'id_1',
|
||||
profiles: {
|
||||
id_1: {
|
||||
locale: 'en',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const cards = [{...createCard(), title: 'test card', limited: true}]
|
||||
const board = {...createBoard(), title: 'test board'}
|
||||
|
||||
mockedOctoClient.getBlocksWithBlockID.mockResolvedValueOnce(cards)
|
||||
mockedOctoClient.getBoard.mockResolvedValueOnce(board)
|
||||
|
||||
const component = (
|
||||
<ReduxProvider store={store}>
|
||||
{wrapIntl(
|
||||
<BoardsUnfurl
|
||||
embed={{data: '{"workspaceID": "foo", "cardID": "bar", "boardID": "baz", "readToken": "abc", "originalPath": "/test"}'}}
|
||||
/>,
|
||||
)}
|
||||
</ReduxProvider>
|
||||
)
|
||||
|
||||
let container: Element | DocumentFragment | null = null
|
||||
|
||||
await act(async () => {
|
||||
const result = render(component)
|
||||
container = result.container
|
||||
})
|
||||
|
||||
expect(container).toMatchSnapshot()
|
||||
})
|
||||
})
|
||||
|
|
@ -14,9 +14,10 @@ import {Board} from './../../../../../webapp/src/blocks/board'
|
|||
import {ContentBlock} from './../../../../../webapp/src/blocks/contentBlock'
|
||||
import octoClient from './../../../../../webapp/src/octoClient'
|
||||
|
||||
const Avatar = (window as any).Components.Avatar
|
||||
const Timestamp = (window as any).Components.Timestamp
|
||||
const imageURLForUser = (window as any).Components.imageURLForUser
|
||||
const noop = () => ''
|
||||
const Avatar = (window as any).Components?.Avatar || noop
|
||||
const Timestamp = (window as any).Components?.Timestamp || noop
|
||||
const imageURLForUser = (window as any).Components?.imageURLForUser || noop
|
||||
|
||||
import './boardsUnfurl.scss'
|
||||
import '../../../../../webapp/src/styles/labels.scss'
|
||||
|
@ -53,7 +54,7 @@ class FocalboardEmbeddedData {
|
|||
}
|
||||
}
|
||||
|
||||
const BoardsUnfurl = (props: Props): JSX.Element => {
|
||||
export const BoardsUnfurl = (props: Props): JSX.Element => {
|
||||
if (!props.embed || !props.embed.data) {
|
||||
return <></>
|
||||
}
|
||||
|
@ -182,7 +183,7 @@ const BoardsUnfurl = (props: Props): JSX.Element => {
|
|||
</div>
|
||||
|
||||
{/* Body of the Card*/}
|
||||
{html !== '' &&
|
||||
{!card.limited && html !== '' &&
|
||||
<div className='body'>
|
||||
<div
|
||||
dangerouslySetInnerHTML={{__html: html}}
|
||||
|
@ -190,7 +191,16 @@ const BoardsUnfurl = (props: Props): JSX.Element => {
|
|||
</div>
|
||||
}
|
||||
|
||||
{card.limited &&
|
||||
<p className='limited'>
|
||||
<FormattedMessage
|
||||
id='BoardsUnfurl.Limited'
|
||||
defaultMessage={'Additional details are hidden due to the card being archived'}
|
||||
/>
|
||||
</p>}
|
||||
|
||||
{/* Footer of the Card*/}
|
||||
{!card.limited &&
|
||||
<div className='footer'>
|
||||
<div className='avatar'>
|
||||
<Avatar
|
||||
|
@ -245,7 +255,7 @@ const BoardsUnfurl = (props: Props): JSX.Element => {
|
|||
/>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>}
|
||||
</a>
|
||||
}
|
||||
{loading &&
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Channel, ChannelMembership} from 'mattermost-redux/types/channels';
|
||||
import {Channel, ChannelMembership} from 'mattermost-redux/types/channels'
|
||||
|
||||
export interface PluginRegistry {
|
||||
registerPostTypeComponent(typeName: string, component: React.ElementType)
|
||||
|
|
|
@ -25,6 +25,7 @@
|
|||
"BoardTemplateSelector.title": "Create a board",
|
||||
"BoardTemplateSelector.use-this-template": "Use this template",
|
||||
"BoardsSwitcher.Title": "Find Boards",
|
||||
"BoardsUnfurl.Limited": "Additional details are hidden due to the card being archived",
|
||||
"BoardsUnfurl.Remainder": "+{remainder} more",
|
||||
"BoardsUnfurl.Updated": "Updated {time}",
|
||||
"Calculations.Options.average.displayName": "Average",
|
||||
|
@ -70,6 +71,9 @@
|
|||
"CardDetail.add-icon": "Add icon",
|
||||
"CardDetail.add-property": "+ Add a property",
|
||||
"CardDetail.addCardText": "add card text",
|
||||
"CardDetail.limited-body": "Upgrade to our Professional or Enterprise plan to view archived cards, have unlimited views per boards, unlimited cards and more.",
|
||||
"CardDetail.limited-button": "Upgrade",
|
||||
"CardDetail.limited-title": "This card is hidden",
|
||||
"CardDetail.moveContent": "Move card content",
|
||||
"CardDetail.new-comment-placeholder": "Add a comment...",
|
||||
"CardDetailProperty.confirm-delete-heading": "Confirm delete property",
|
||||
|
@ -81,7 +85,7 @@
|
|||
"CardDetailProperty.property-changed": "Changed property successfully!",
|
||||
"CardDetailProperty.property-deleted": "Deleted {propertyName} successfully!",
|
||||
"CardDetailProperty.property-name-change-subtext": "type from \"{oldPropType}\" to \"{newPropType}\"",
|
||||
"CardDetailProperty.property-type-change-subtext": "name to \"{newPropName}\"",
|
||||
"CardDetial.limited-link": "Learn more about our plans.",
|
||||
"CardDialog.copiedLink": "Copied!",
|
||||
"CardDialog.copyLink": "Copy link",
|
||||
"CardDialog.delete-confirmation-dialog-button-text": "Delete",
|
||||
|
@ -151,6 +155,7 @@
|
|||
"KanbanCard.delete": "Delete",
|
||||
"KanbanCard.duplicate": "Duplicate",
|
||||
"KanbanCard.untitled": "Untitled",
|
||||
"Mutator.new-board-from-template": "new board from template",
|
||||
"Mutator.new-card-from-template": "new card from template",
|
||||
"Mutator.new-template-from-card": "new template from card",
|
||||
"OnboardingTour.AddComments.Body": "You can comment on issues, and even @mention your fellow Mattermost users to get their attention.",
|
||||
|
@ -260,6 +265,7 @@
|
|||
"View.NewCalendarTitle": "Calendar view",
|
||||
"View.NewGalleryTitle": "Gallery view",
|
||||
"View.NewTableTitle": "Table view",
|
||||
"View.NewTemplateTitle": "Untitled Template",
|
||||
"View.Table": "Table",
|
||||
"ViewHeader.add-template": "New template",
|
||||
"ViewHeader.delete-template": "Delete",
|
||||
|
@ -300,7 +306,15 @@
|
|||
"createImageBlock.failed": "Unable to upload the file. File size limit reached.",
|
||||
"default-properties.badges": "Comments and description",
|
||||
"default-properties.title": "Title",
|
||||
"error.back-to-home": "Back to Home",
|
||||
"error.back-to-team": "Back to team",
|
||||
"error.board-not-found": "Board not found.",
|
||||
"error.go-login": "Login",
|
||||
"error.invalid-read-only-board": "You don’t have access to this board. Log in to access Boards.",
|
||||
"error.not-logged-in": "Your session may have expired or you're not logged in. Log in again to access Boards.",
|
||||
"error.page.title": "Sorry, something went wrong",
|
||||
"error.team-undefined": "Not a valid team.",
|
||||
"error.unknown": "An error occurred.",
|
||||
"generic.previous": "Previous",
|
||||
"imagePaste.upload-failed": "Some files not uploaded. File size limit reached",
|
||||
"limitedCard.title": "Cards Hidden",
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
"check": "eslint --ext .tsx,.ts . --quiet --cache && stylelint **/*.scss",
|
||||
"fix": "eslint --ext .tsx,.ts . --quiet --fix --cache && stylelint --fix **/*.scss",
|
||||
"fix:scss": "prettier --write './src/**/*.scss'",
|
||||
"i18n-extract": "formatjs extract ../mattermost-plugin/webapp/src/*/*/*.ts? src/*.ts? src/*/*.ts? src/*/*/*.ts? src/*/*/*/*.ts? --out-file i18n/tmp.json; formatjs compile i18n/tmp.json --out-file i18n/en.json; rm i18n/tmp.json",
|
||||
"i18n-extract": "formatjs extract '../mattermost-plugin/webapp/src/**/*.{ts,tsx}' 'src/**/*.{ts,tsx}' --ignore '**/*.d.ts' '../**/*.d.ts' --out-file i18n/tmp.json && formatjs compile i18n/tmp.json --out-file i18n/en.json && rm i18n/tmp.json",
|
||||
"runserver-test": "cd cypress && \"../../bin/focalboard-server\"",
|
||||
"cypress:ci": "start-server-and-test runserver-test http://localhost:8088 cypress:run",
|
||||
"cypress:debug": "start-server-and-test runserver-test http://localhost:8088 cypress:open",
|
||||
|
|
|
@ -67,6 +67,7 @@ function createBlock(block?: Block): Block {
|
|||
createAt: block?.createAt || now,
|
||||
updateAt: block?.updateAt || now,
|
||||
deleteAt: block?.deleteAt || 0,
|
||||
limited: Boolean(block?.limited),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -283,6 +283,230 @@ exports[`components/cardDialog already following card 1`] = `
|
|||
</div>
|
||||
`;
|
||||
|
||||
exports[`components/cardDialog limited card shows hidden view (no toolbar) 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="Dialog dialog-back cardDialog"
|
||||
>
|
||||
<div
|
||||
class="backdrop"
|
||||
/>
|
||||
<div
|
||||
class="wrapper"
|
||||
>
|
||||
<div
|
||||
class="dialog"
|
||||
role="dialog"
|
||||
>
|
||||
<div
|
||||
class="toolbar"
|
||||
>
|
||||
<button
|
||||
aria-label="Close dialog"
|
||||
title="Close dialog"
|
||||
type="button"
|
||||
>
|
||||
<i
|
||||
class="CompassIcon icon-close CloseIcon"
|
||||
/>
|
||||
</button>
|
||||
<div
|
||||
class="toolbar--right"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="CardDetail content is-limited"
|
||||
>
|
||||
<div
|
||||
class="IconSelector"
|
||||
>
|
||||
<div
|
||||
class="octo-icon size-l readonly"
|
||||
>
|
||||
<span>
|
||||
i
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="EditableAreaWrap"
|
||||
>
|
||||
<textarea
|
||||
class="EditableArea Editable readonly title"
|
||||
height="0"
|
||||
placeholder="Untitled"
|
||||
readonly=""
|
||||
rows="1"
|
||||
spellcheck="true"
|
||||
title="title"
|
||||
>
|
||||
title
|
||||
</textarea>
|
||||
<div
|
||||
class="EditableAreaContainer"
|
||||
>
|
||||
<textarea
|
||||
aria-hidden="true"
|
||||
class="EditableAreaReference Editable readonly title"
|
||||
dir="auto"
|
||||
disabled=""
|
||||
rows="1"
|
||||
>
|
||||
title
|
||||
</textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="CardDetail__limited-wrapper"
|
||||
>
|
||||
<span
|
||||
class="CardDetail__limited-bg"
|
||||
>
|
||||
<svg
|
||||
fill="none"
|
||||
height="521"
|
||||
viewBox="0 0 468 521"
|
||||
width="468"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<rect
|
||||
fill="#3F4350"
|
||||
fill-opacity="0.08"
|
||||
height="32"
|
||||
rx="4"
|
||||
width="156"
|
||||
/>
|
||||
<rect
|
||||
fill="#3F4350"
|
||||
fill-opacity="0.08"
|
||||
height="32"
|
||||
rx="4"
|
||||
width="296"
|
||||
x="172"
|
||||
/>
|
||||
<rect
|
||||
fill="#3F4350"
|
||||
fill-opacity="0.08"
|
||||
height="32"
|
||||
rx="4"
|
||||
width="156"
|
||||
y="48"
|
||||
/>
|
||||
<rect
|
||||
fill="#3F4350"
|
||||
fill-opacity="0.08"
|
||||
height="32"
|
||||
rx="4"
|
||||
width="296"
|
||||
x="172"
|
||||
y="48"
|
||||
/>
|
||||
<rect
|
||||
fill="#3F4350"
|
||||
fill-opacity="0.08"
|
||||
height="32"
|
||||
rx="4"
|
||||
width="156"
|
||||
y="96"
|
||||
/>
|
||||
<rect
|
||||
fill="#3F4350"
|
||||
fill-opacity="0.08"
|
||||
height="32"
|
||||
rx="4"
|
||||
width="296"
|
||||
x="172"
|
||||
y="96"
|
||||
/>
|
||||
<rect
|
||||
fill="#3F4350"
|
||||
fill-opacity="0.08"
|
||||
height="32"
|
||||
rx="4"
|
||||
width="156"
|
||||
y="144"
|
||||
/>
|
||||
<rect
|
||||
fill="#3F4350"
|
||||
fill-opacity="0.08"
|
||||
height="32"
|
||||
rx="4"
|
||||
width="296"
|
||||
x="172"
|
||||
y="144"
|
||||
/>
|
||||
<rect
|
||||
fill="#3D3C40"
|
||||
fill-opacity="0.16"
|
||||
height="1"
|
||||
width="468"
|
||||
y="192"
|
||||
/>
|
||||
<rect
|
||||
fill="#3F4350"
|
||||
fill-opacity="0.08"
|
||||
height="32"
|
||||
rx="4"
|
||||
width="468"
|
||||
y="209"
|
||||
/>
|
||||
<rect
|
||||
fill="#3F4350"
|
||||
fill-opacity="0.08"
|
||||
height="32"
|
||||
rx="4"
|
||||
width="468"
|
||||
y="257"
|
||||
/>
|
||||
<rect
|
||||
fill="#3D3C40"
|
||||
fill-opacity="0.16"
|
||||
height="1"
|
||||
width="468"
|
||||
y="305"
|
||||
/>
|
||||
<rect
|
||||
fill="#3F4350"
|
||||
fill-opacity="0.08"
|
||||
height="199"
|
||||
rx="4"
|
||||
width="468"
|
||||
y="322"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
<p
|
||||
class="CardDetail__limited-title"
|
||||
>
|
||||
This card is hidden
|
||||
</p>
|
||||
<p
|
||||
class="CardDetail__limited-body"
|
||||
>
|
||||
Upgrade to our Professional or Enterprise plan to view archived cards, have unlimited views per boards, unlimited cards and more.
|
||||
<br />
|
||||
<a
|
||||
class="CardDetail__limited-link"
|
||||
role="button"
|
||||
>
|
||||
Learn more about our plans.
|
||||
</a>
|
||||
</p>
|
||||
<button
|
||||
type="button"
|
||||
>
|
||||
<span>
|
||||
Upgrade
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`components/cardDialog return a cardDialog readonly 1`] = `
|
||||
<div>
|
||||
<div
|
||||
|
|
|
@ -1,5 +1,198 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`components/cardDetail/CardDetail should render hidden view if limited 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="CardDetail content is-limited"
|
||||
>
|
||||
<div
|
||||
class="IconSelector"
|
||||
>
|
||||
<div
|
||||
class="octo-icon size-l readonly"
|
||||
>
|
||||
<span>
|
||||
i
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="EditableAreaWrap"
|
||||
>
|
||||
<textarea
|
||||
class="EditableArea Editable readonly title"
|
||||
height="0"
|
||||
placeholder="Untitled"
|
||||
readonly=""
|
||||
rows="1"
|
||||
spellcheck="true"
|
||||
title="title"
|
||||
>
|
||||
title
|
||||
</textarea>
|
||||
<div
|
||||
class="EditableAreaContainer"
|
||||
>
|
||||
<textarea
|
||||
aria-hidden="true"
|
||||
class="EditableAreaReference Editable readonly title"
|
||||
dir="auto"
|
||||
disabled=""
|
||||
rows="1"
|
||||
>
|
||||
title
|
||||
</textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="CardDetail__limited-wrapper"
|
||||
>
|
||||
<span
|
||||
class="CardDetail__limited-bg"
|
||||
>
|
||||
<svg
|
||||
fill="none"
|
||||
height="521"
|
||||
viewBox="0 0 468 521"
|
||||
width="468"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<rect
|
||||
fill="#3F4350"
|
||||
fill-opacity="0.08"
|
||||
height="32"
|
||||
rx="4"
|
||||
width="156"
|
||||
/>
|
||||
<rect
|
||||
fill="#3F4350"
|
||||
fill-opacity="0.08"
|
||||
height="32"
|
||||
rx="4"
|
||||
width="296"
|
||||
x="172"
|
||||
/>
|
||||
<rect
|
||||
fill="#3F4350"
|
||||
fill-opacity="0.08"
|
||||
height="32"
|
||||
rx="4"
|
||||
width="156"
|
||||
y="48"
|
||||
/>
|
||||
<rect
|
||||
fill="#3F4350"
|
||||
fill-opacity="0.08"
|
||||
height="32"
|
||||
rx="4"
|
||||
width="296"
|
||||
x="172"
|
||||
y="48"
|
||||
/>
|
||||
<rect
|
||||
fill="#3F4350"
|
||||
fill-opacity="0.08"
|
||||
height="32"
|
||||
rx="4"
|
||||
width="156"
|
||||
y="96"
|
||||
/>
|
||||
<rect
|
||||
fill="#3F4350"
|
||||
fill-opacity="0.08"
|
||||
height="32"
|
||||
rx="4"
|
||||
width="296"
|
||||
x="172"
|
||||
y="96"
|
||||
/>
|
||||
<rect
|
||||
fill="#3F4350"
|
||||
fill-opacity="0.08"
|
||||
height="32"
|
||||
rx="4"
|
||||
width="156"
|
||||
y="144"
|
||||
/>
|
||||
<rect
|
||||
fill="#3F4350"
|
||||
fill-opacity="0.08"
|
||||
height="32"
|
||||
rx="4"
|
||||
width="296"
|
||||
x="172"
|
||||
y="144"
|
||||
/>
|
||||
<rect
|
||||
fill="#3D3C40"
|
||||
fill-opacity="0.16"
|
||||
height="1"
|
||||
width="468"
|
||||
y="192"
|
||||
/>
|
||||
<rect
|
||||
fill="#3F4350"
|
||||
fill-opacity="0.08"
|
||||
height="32"
|
||||
rx="4"
|
||||
width="468"
|
||||
y="209"
|
||||
/>
|
||||
<rect
|
||||
fill="#3F4350"
|
||||
fill-opacity="0.08"
|
||||
height="32"
|
||||
rx="4"
|
||||
width="468"
|
||||
y="257"
|
||||
/>
|
||||
<rect
|
||||
fill="#3D3C40"
|
||||
fill-opacity="0.16"
|
||||
height="1"
|
||||
width="468"
|
||||
y="305"
|
||||
/>
|
||||
<rect
|
||||
fill="#3F4350"
|
||||
fill-opacity="0.08"
|
||||
height="199"
|
||||
rx="4"
|
||||
width="468"
|
||||
y="322"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
<p
|
||||
class="CardDetail__limited-title"
|
||||
>
|
||||
This card is hidden
|
||||
</p>
|
||||
<p
|
||||
class="CardDetail__limited-body"
|
||||
>
|
||||
Upgrade to our Professional or Enterprise plan to view archived cards, have unlimited views per boards, unlimited cards and more.
|
||||
<br />
|
||||
<a
|
||||
class="CardDetail__limited-link"
|
||||
role="button"
|
||||
>
|
||||
Learn more about our plans.
|
||||
</a>
|
||||
</p>
|
||||
<button
|
||||
class="Button emphasis--primary size--large CardDetail__limited-button"
|
||||
type="button"
|
||||
>
|
||||
<span>
|
||||
Upgrade
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`components/cardDetail/CardDetail should show add comments tour tip 1`] = `
|
||||
<div
|
||||
class="tippy-box tutorial-tour-tip__box AddCommentTourStep"
|
||||
|
|
|
@ -172,4 +172,55 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.content.is-limited {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
&__limited-wrapper {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
position: relative;
|
||||
|
||||
> :not(.CardDetail__limited-bg) {
|
||||
position: relative;
|
||||
}
|
||||
}
|
||||
|
||||
&__limited-bg {
|
||||
filter: blur(12px);
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 100%;
|
||||
|
||||
> svg {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
|
||||
> rect {
|
||||
fill: rgba(var(--center-channel-color-rgb));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__limited-title {
|
||||
font-weight: bold;
|
||||
font-size: 22px;
|
||||
line-height: 28px;
|
||||
}
|
||||
|
||||
&__limited-link {
|
||||
color: var(--link-color);
|
||||
}
|
||||
|
||||
&__limited-button {
|
||||
margin-top: 24px;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -109,6 +109,7 @@ describe('components/cardDetail/CardDetail', () => {
|
|||
comments={[comment1, comment2]}
|
||||
contents={[]}
|
||||
readonly={false}
|
||||
onClose={jest.fn()}
|
||||
/>,
|
||||
)}
|
||||
</ReduxProvider>
|
||||
|
@ -166,6 +167,7 @@ describe('components/cardDetail/CardDetail', () => {
|
|||
comments={[comment1, comment2]}
|
||||
contents={[]}
|
||||
readonly={true}
|
||||
onClose={jest.fn()}
|
||||
/>,
|
||||
)}
|
||||
</ReduxProvider>
|
||||
|
@ -256,6 +258,7 @@ describe('components/cardDetail/CardDetail', () => {
|
|||
comments={[comment1, comment2]}
|
||||
contents={[]}
|
||||
readonly={false}
|
||||
onClose={jest.fn()}
|
||||
/>,
|
||||
)}
|
||||
</ReduxProvider>
|
||||
|
@ -361,6 +364,7 @@ describe('components/cardDetail/CardDetail', () => {
|
|||
comments={[comment1, comment2]}
|
||||
contents={[]}
|
||||
readonly={false}
|
||||
onClose={jest.fn()}
|
||||
/>,
|
||||
)}
|
||||
</ReduxProvider>
|
||||
|
@ -470,6 +474,7 @@ describe('components/cardDetail/CardDetail', () => {
|
|||
comments={[comment1, comment2]}
|
||||
contents={[text]}
|
||||
readonly={false}
|
||||
onClose={jest.fn()}
|
||||
/>,
|
||||
)}
|
||||
</ReduxProvider>
|
||||
|
@ -507,4 +512,61 @@ describe('components/cardDetail/CardDetail', () => {
|
|||
},
|
||||
)
|
||||
})
|
||||
|
||||
test('should render hidden view if limited', async () => {
|
||||
const limitedCard = {...card, limited: true}
|
||||
const mockStore = configureStore([])
|
||||
const store = mockStore({
|
||||
users: {
|
||||
workspaceUsers: [
|
||||
{username: 'username_1'},
|
||||
],
|
||||
},
|
||||
teams: {
|
||||
current: {id: 'team-id'},
|
||||
},
|
||||
boards: {
|
||||
boards: {
|
||||
[board.id]: board,
|
||||
},
|
||||
current: board.id,
|
||||
myBoardMemberships: {
|
||||
[board.id]: {userId: 'user_id_1', schemeAdmin: true},
|
||||
},
|
||||
},
|
||||
cards: {
|
||||
cards: {
|
||||
[limitedCard.id]: limitedCard,
|
||||
},
|
||||
current: limitedCard.id,
|
||||
},
|
||||
})
|
||||
|
||||
const component = (
|
||||
<ReduxProvider store={store}>
|
||||
{wrapIntl(
|
||||
<CardDetail
|
||||
board={board}
|
||||
activeView={view}
|
||||
views={[view]}
|
||||
cards={[limitedCard]}
|
||||
card={limitedCard}
|
||||
comments={[comment1, comment2]}
|
||||
contents={[]}
|
||||
readonly={false}
|
||||
onClose={jest.fn()}
|
||||
/>,
|
||||
)}
|
||||
</ReduxProvider>
|
||||
)
|
||||
|
||||
let container: Element | DocumentFragment | null = null
|
||||
|
||||
await act(async () => {
|
||||
const result = render(component)
|
||||
container = result.container
|
||||
})
|
||||
|
||||
expect(container).toMatchSnapshot()
|
||||
})
|
||||
})
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import React, {useCallback, useEffect, useRef, useState} from 'react'
|
||||
import {FormattedMessage} from 'react-intl'
|
||||
import React, {useCallback, useEffect, useRef, useState, Fragment} from 'react'
|
||||
import {FormattedMessage, useIntl} from 'react-intl'
|
||||
|
||||
import {BlockIcons} from '../../blockIcons'
|
||||
import {Card} from '../../blocks/card'
|
||||
|
@ -23,6 +23,8 @@ import {setCurrent as setCurrentCard} from '../../store/cards'
|
|||
import {Permission} from '../../constants'
|
||||
import {useHasCurrentBoardPermissions} from '../../hooks/permissions'
|
||||
|
||||
import CardSkeleton from '../../svg/card-skeleton'
|
||||
|
||||
import CommentsList from './commentsList'
|
||||
import {CardDetailProvider} from './cardDetailContext'
|
||||
import CardDetailContents from './cardDetailContents'
|
||||
|
@ -44,10 +46,12 @@ type Props = {
|
|||
comments: CommentBlock[]
|
||||
contents: Array<ContentBlock|ContentBlock[]>
|
||||
readonly: boolean
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
const CardDetail = (props: Props): JSX.Element|null => {
|
||||
const {card, comments} = props
|
||||
const {limited} = card
|
||||
const [title, setTitle] = useState(card.title)
|
||||
const [serverTitle, setServerTitle] = useState(card.title)
|
||||
const titleRef = useRef<Focusable>(null)
|
||||
|
@ -60,6 +64,7 @@ const CardDetail = (props: Props): JSX.Element|null => {
|
|||
|
||||
const saveTitleRef = useRef<() => void>(saveTitle)
|
||||
saveTitleRef.current = saveTitle
|
||||
const intl = useIntl()
|
||||
|
||||
useImagePaste(props.board.id, card.id, card.fields.contentOrder)
|
||||
|
||||
|
@ -99,11 +104,11 @@ const CardDetail = (props: Props): JSX.Element|null => {
|
|||
|
||||
return (
|
||||
<>
|
||||
<div className='CardDetail content'>
|
||||
<div className={`CardDetail content${limited ? ' is-limited' : ''}`}>
|
||||
<BlockIconSelector
|
||||
block={card}
|
||||
size='l'
|
||||
readonly={props.readonly || !canEditBoardCards}
|
||||
readonly={props.readonly || !canEditBoardCards || limited}
|
||||
/>
|
||||
{!props.readonly && canEditBoardCards && !card.fields.icon &&
|
||||
<div className='add-buttons'>
|
||||
|
@ -127,12 +132,58 @@ const CardDetail = (props: Props): JSX.Element|null => {
|
|||
saveOnEsc={true}
|
||||
onSave={saveTitle}
|
||||
onCancel={() => setTitle(props.card.title)}
|
||||
readonly={props.readonly || !canEditBoardCards}
|
||||
readonly={props.readonly || !canEditBoardCards || limited}
|
||||
spellCheck={true}
|
||||
/>
|
||||
|
||||
{/* Hidden (limited) card copy + CTA */}
|
||||
|
||||
{limited && <div className='CardDetail__limited-wrapper'>
|
||||
<CardSkeleton
|
||||
className='CardDetail__limited-bg'
|
||||
/>
|
||||
<p className='CardDetail__limited-title'>
|
||||
<FormattedMessage
|
||||
id='CardDetail.limited-title'
|
||||
defaultMessage='This card is hidden'
|
||||
/>
|
||||
</p>
|
||||
<p className='CardDetail__limited-body'>
|
||||
<FormattedMessage
|
||||
id='CardDetail.limited-body'
|
||||
defaultMessage='Upgrade to our Professional or Enterprise plan to view archived cards, have unlimited views per boards, unlimited cards and more.'
|
||||
/>
|
||||
<br/>
|
||||
<a
|
||||
className='CardDetail__limited-link'
|
||||
role='button'
|
||||
onClick={() => {
|
||||
props.onClose();
|
||||
(window as any).openPricingModal()()
|
||||
}}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='CardDetial.limited-link'
|
||||
defaultMessage='Learn more about our plans.'
|
||||
/>
|
||||
</a>
|
||||
</p>
|
||||
<Button
|
||||
className='CardDetail__limited-button'
|
||||
onClick={() => {
|
||||
props.onClose();
|
||||
(window as any).openPricingModal()()
|
||||
}}
|
||||
emphasis='primary'
|
||||
size='large'
|
||||
>
|
||||
{intl.formatMessage({id: 'CardDetail.limited-button', defaultMessage: 'Upgrade'})}
|
||||
</Button>
|
||||
</div>}
|
||||
|
||||
{/* Property list */}
|
||||
|
||||
{!limited &&
|
||||
<CardDetailProperties
|
||||
board={props.board}
|
||||
card={props.card}
|
||||
|
@ -140,22 +191,24 @@ const CardDetail = (props: Props): JSX.Element|null => {
|
|||
activeView={props.activeView}
|
||||
views={props.views}
|
||||
readonly={props.readonly}
|
||||
/>
|
||||
/>}
|
||||
|
||||
{/* Comments */}
|
||||
|
||||
<hr/>
|
||||
<CommentsList
|
||||
comments={comments}
|
||||
boardId={card.boardId}
|
||||
cardId={card.id}
|
||||
readonly={props.readonly || !canEditBoardCards}
|
||||
/>
|
||||
{!limited && <Fragment>
|
||||
<hr/>
|
||||
<CommentsList
|
||||
comments={comments}
|
||||
boardId={card.boardId}
|
||||
cardId={card.id}
|
||||
readonly={props.readonly || !canEditBoardCards}
|
||||
/>
|
||||
</Fragment>}
|
||||
</div>
|
||||
|
||||
{/* Content blocks */}
|
||||
|
||||
<div className='CardDetail content fullwidth content-blocks'>
|
||||
{!limited && <div className='CardDetail content fullwidth content-blocks'>
|
||||
<CardDetailProvider card={card}>
|
||||
<CardDetailContents
|
||||
card={props.card}
|
||||
|
@ -164,7 +217,7 @@ const CardDetail = (props: Props): JSX.Element|null => {
|
|||
/>
|
||||
{!props.readonly && canEditBoardCards && <CardDetailContentsMenu/>}
|
||||
</CardDetailProvider>
|
||||
</div>
|
||||
</div>}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -353,4 +353,39 @@ describe('components/cardDialog', () => {
|
|||
})
|
||||
expect(container).toMatchSnapshot()
|
||||
})
|
||||
|
||||
test('limited card shows hidden view (no toolbar)', async () => {
|
||||
// simply doing {...state} gives a TypeScript error
|
||||
// when you try updating it's values.
|
||||
const newState = JSON.parse(JSON.stringify(state))
|
||||
const limitedCard = {...card, limited: true}
|
||||
newState.cards = {
|
||||
cards: {
|
||||
[limitedCard.id]: limitedCard,
|
||||
},
|
||||
current: limitedCard.id,
|
||||
}
|
||||
|
||||
const newStore = mockStateStore([], newState)
|
||||
|
||||
let container
|
||||
await act(async () => {
|
||||
const result = render(wrapDNDIntl(
|
||||
<ReduxProvider store={newStore}>
|
||||
<CardDialog
|
||||
board={board}
|
||||
activeView={boardView}
|
||||
views={[boardView]}
|
||||
cards={[limitedCard]}
|
||||
cardId={limitedCard.id}
|
||||
onClose={jest.fn()}
|
||||
showCard={jest.fn()}
|
||||
readonly={false}
|
||||
/>
|
||||
</ReduxProvider>,
|
||||
))
|
||||
container = result.container
|
||||
})
|
||||
expect(container).toMatchSnapshot()
|
||||
})
|
||||
})
|
||||
|
|
|
@ -184,8 +184,8 @@ const CardDialog = (props: Props): JSX.Element => {
|
|||
<Dialog
|
||||
className='cardDialog'
|
||||
onClose={props.onClose}
|
||||
toolsMenu={!props.readonly && menu}
|
||||
toolbar={!isTemplate && Utils.isFocalboardPlugin() && toolbar}
|
||||
toolsMenu={!props.readonly && !card?.limited && menu}
|
||||
toolbar={!isTemplate && Utils.isFocalboardPlugin() && !card?.limited && toolbar}
|
||||
>
|
||||
{isTemplate &&
|
||||
<div className='banner'>
|
||||
|
@ -205,6 +205,7 @@ const CardDialog = (props: Props): JSX.Element => {
|
|||
contents={contents}
|
||||
comments={comments}
|
||||
readonly={props.readonly}
|
||||
onClose={props.onClose}
|
||||
/>}
|
||||
|
||||
{!card &&
|
||||
|
|
|
@ -68,7 +68,7 @@ function errorDefFromId(id: ErrorId | null): ErrorDef {
|
|||
case ErrorId.NotLoggedIn: {
|
||||
errDef.title = intl.formatMessage({id: 'error.not-logged-in', defaultMessage: 'Your session may have expired or you\'re not logged in. Log in again to access Boards.'})
|
||||
errDef.button1Enabled = true
|
||||
errDef.button1Text = intl.formatMessage({id: 'error.go-login', defaultMessage: 'Login'})
|
||||
errDef.button1Text = intl.formatMessage({id: 'error.go-login', defaultMessage: 'Log in'})
|
||||
errDef.button1Redirect = '/login'
|
||||
errDef.button1Redirect = (params: URLSearchParams): string => {
|
||||
const r = params.get('r')
|
||||
|
|
127
webapp/src/svg/card-skeleton.tsx
Normal file
127
webapp/src/svg/card-skeleton.tsx
Normal file
|
@ -0,0 +1,127 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react'
|
||||
|
||||
type Props = {
|
||||
className?: string
|
||||
}
|
||||
|
||||
export default function CardSkeleton(props: Props): JSX.Element {
|
||||
return (
|
||||
<span className={props.className}>
|
||||
<svg
|
||||
width='468'
|
||||
height='521'
|
||||
viewBox='0 0 468 521'
|
||||
fill='none'
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
>
|
||||
<rect
|
||||
width='156'
|
||||
height='32'
|
||||
rx='4'
|
||||
fill='#3F4350'
|
||||
fillOpacity='0.08'
|
||||
/>
|
||||
<rect
|
||||
x='172'
|
||||
width='296'
|
||||
height='32'
|
||||
rx='4'
|
||||
fill='#3F4350'
|
||||
fillOpacity='0.08'
|
||||
/>
|
||||
<rect
|
||||
y='48'
|
||||
width='156'
|
||||
height='32'
|
||||
rx='4'
|
||||
fill='#3F4350'
|
||||
fillOpacity='0.08'
|
||||
/>
|
||||
<rect
|
||||
x='172'
|
||||
y='48'
|
||||
width='296'
|
||||
height='32'
|
||||
rx='4'
|
||||
fill='#3F4350'
|
||||
fillOpacity='0.08'
|
||||
/>
|
||||
<rect
|
||||
y='96'
|
||||
width='156'
|
||||
height='32'
|
||||
rx='4'
|
||||
fill='#3F4350'
|
||||
fillOpacity='0.08'
|
||||
/>
|
||||
<rect
|
||||
x='172'
|
||||
y='96'
|
||||
width='296'
|
||||
height='32'
|
||||
rx='4'
|
||||
fill='#3F4350'
|
||||
fillOpacity='0.08'
|
||||
/>
|
||||
<rect
|
||||
y='144'
|
||||
width='156'
|
||||
height='32'
|
||||
rx='4'
|
||||
fill='#3F4350'
|
||||
fillOpacity='0.08'
|
||||
/>
|
||||
<rect
|
||||
x='172'
|
||||
y='144'
|
||||
width='296'
|
||||
height='32'
|
||||
rx='4'
|
||||
fill='#3F4350'
|
||||
fillOpacity='0.08'
|
||||
/>
|
||||
<rect
|
||||
y='192'
|
||||
width='468'
|
||||
height='1'
|
||||
fill='#3D3C40'
|
||||
fillOpacity='0.16'
|
||||
/>
|
||||
<rect
|
||||
y='209'
|
||||
width='468'
|
||||
height='32'
|
||||
rx='4'
|
||||
fill='#3F4350'
|
||||
fillOpacity='0.08'
|
||||
/>
|
||||
<rect
|
||||
y='257'
|
||||
width='468'
|
||||
height='32'
|
||||
rx='4'
|
||||
fill='#3F4350'
|
||||
fillOpacity='0.08'
|
||||
/>
|
||||
<rect
|
||||
y='305'
|
||||
width='468'
|
||||
height='1'
|
||||
fill='#3D3C40'
|
||||
fillOpacity='0.16'
|
||||
/>
|
||||
<rect
|
||||
y='322'
|
||||
width='468'
|
||||
height='199'
|
||||
rx='4'
|
||||
fill='#3F4350'
|
||||
fillOpacity='0.08'
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
)
|
||||
}
|
|
@ -3,9 +3,9 @@
|
|||
|
||||
import React from 'react'
|
||||
|
||||
export default function ErrorIllustration(props: React.HTMLAttributes<HTMLSpanElement>): JSX.Element {
|
||||
export default function ErrorIllustration(): JSX.Element {
|
||||
return (
|
||||
<span {...props}>
|
||||
<span>
|
||||
<svg
|
||||
width='355'
|
||||
height='250'
|
||||
|
|
|
@ -3,9 +3,9 @@
|
|||
|
||||
import React from 'react'
|
||||
|
||||
export default function SearchIllustration(props: React.HTMLAttributes<HTMLSpanElement>): JSX.Element {
|
||||
export default function SearchIllustration(): JSX.Element {
|
||||
return (
|
||||
<span {...props}>
|
||||
<span>
|
||||
<svg
|
||||
width='160'
|
||||
height='160'
|
||||
|
|
Loading…
Reference in a new issue