mirror of
https://github.com/mattermost/focalboard.git
synced 2025-01-02 14:47:55 +02:00
Merge branch 'main' into GH-1543
This commit is contained in:
commit
83fa691fa4
@ -1,4 +1,10 @@
|
||||
# Import scripts
|
||||
|
||||
This subfolder contains scripts to import data from other systems. It is at an early stage. At present, there are examples of basic importing from Trello, Asana, and Notion. [Contribute code](https://www.focalboard.com/contribute/getting-started/) to expand this.
|
||||
This subfolder contains scripts to import data from other systems. It is at an early stage. At present, there are examples of basic importing from the following:
|
||||
* Trello
|
||||
* Asana
|
||||
* Notion
|
||||
* Jira
|
||||
* Todoist
|
||||
|
||||
[Contribute code](https://www.focalboard.com/contribute/getting-started/) to expand this.
|
||||
|
1
import/jira/.gitignore
vendored
Normal file
1
import/jira/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
test
|
23
import/jira/README.md
Normal file
23
import/jira/README.md
Normal file
@ -0,0 +1,23 @@
|
||||
# Jira importer
|
||||
|
||||
This node app converts a Jira xml export into a Focalboard archive. To use:
|
||||
1. Open Jira advanced search, and search for all the items to export
|
||||
2. Select `Export`, then `Export XML`
|
||||
3. Save it locally, e.g. to `jira_export.xml`
|
||||
4. Run `npm install` from within `focalboard/webapp`
|
||||
5. Run `npm install` from within `focalboard/import/jira`
|
||||
6. Run `npx ts-node importJira.ts -i <path-to-jira.xml> -o archive.focalboard` (also from within `focalboard/import/jira`)
|
||||
7. In Focalboard, click `Settings`, then `Import archive` and select `archive.focalboard`
|
||||
|
||||
## Import scope and known limitations
|
||||
|
||||
Currently, the script imports each item as a card into a single board. Note that Jira XML export is limited to 1000 issues at a time.
|
||||
|
||||
Users are imported as Select properties, with the name of the user.
|
||||
|
||||
The following aren't currently imported:
|
||||
* Custom properties
|
||||
* Comments
|
||||
* Embedded files
|
||||
|
||||
[Contribute code](https://www.focalboard.com/contribute/getting-started/) to expand this.
|
16
import/jira/importJira.ts
Normal file
16
import/jira/importJira.ts
Normal file
@ -0,0 +1,16 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import minimist from 'minimist'
|
||||
import {run} from './jiraImporter'
|
||||
|
||||
async function main() {
|
||||
const args: minimist.ParsedArgs = minimist(process.argv.slice(2))
|
||||
|
||||
const inputFile = args['i']
|
||||
const outputFile = args['o'] || 'archive.focalboard'
|
||||
|
||||
return run(inputFile, outputFile)
|
||||
}
|
||||
|
||||
main()
|
53
import/jira/jiraImporter.test.ts
Normal file
53
import/jira/jiraImporter.test.ts
Normal file
@ -0,0 +1,53 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {run} from './jiraImporter'
|
||||
import * as fs from 'fs'
|
||||
import {ArchiveUtils} from '../../webapp/src/blocks/archive'
|
||||
|
||||
const inputFile = './test/jira-export.xml'
|
||||
const outputFile = './test/jira.focalboard'
|
||||
|
||||
describe('import from Jira', () => {
|
||||
test('import', async () => {
|
||||
const blockCount = await run(inputFile, outputFile)
|
||||
expect(blockCount === 4)
|
||||
})
|
||||
|
||||
test('import was complete', async () => {
|
||||
const archiveData = fs.readFileSync(outputFile, 'utf-8')
|
||||
const blocks = ArchiveUtils.parseBlockArchive(archiveData)
|
||||
|
||||
console.debug(blocks)
|
||||
|
||||
|
||||
blocks.forEach(block => {
|
||||
console.log(block.title)
|
||||
})
|
||||
|
||||
expect(blocks).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
title: 'Jira import',
|
||||
type: 'board'
|
||||
}),
|
||||
expect.objectContaining({
|
||||
title: 'Board View',
|
||||
type: 'view'
|
||||
}),
|
||||
expect.objectContaining({
|
||||
title: 'Investigate feature area',
|
||||
type: 'card'
|
||||
}),
|
||||
expect.objectContaining({
|
||||
title: 'Investigate feature',
|
||||
type: 'card'
|
||||
}),
|
||||
])
|
||||
)
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
fs.rmSync(outputFile)
|
||||
});
|
||||
})
|
243
import/jira/jiraImporter.ts
Normal file
243
import/jira/jiraImporter.ts
Normal file
@ -0,0 +1,243 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import * as fs from 'fs'
|
||||
import {exit} from 'process'
|
||||
import {ArchiveUtils} from '../../webapp/src/blocks/archive'
|
||||
import {Block} from '../../webapp/src/blocks/block'
|
||||
import {IPropertyOption, IPropertyTemplate, createBoard} from '../../webapp/src/blocks/board'
|
||||
import {createBoardView} from '../../webapp/src/blocks/boardView'
|
||||
import {Card, createCard} from '../../webapp/src/blocks/card'
|
||||
import {createTextBlock} from '../../webapp/src/blocks/textBlock'
|
||||
import {Utils} from './utils'
|
||||
import xml2js, {ParserOptions} from 'xml2js'
|
||||
import TurndownService from 'turndown'
|
||||
|
||||
// HACKHACK: To allow Utils.CreateGuid to work
|
||||
(global.window as any) = {}
|
||||
|
||||
const optionColors = [
|
||||
'propColorGray',
|
||||
'propColorBrown',
|
||||
'propColorOrange',
|
||||
'propColorYellow',
|
||||
'propColorGreen',
|
||||
'propColorBlue',
|
||||
'propColorPurple',
|
||||
'propColorPink',
|
||||
'propColorRed',
|
||||
]
|
||||
let optionColorIndex = 0
|
||||
|
||||
var turndownService = new TurndownService()
|
||||
|
||||
async function run(inputFile: string, outputFile: string): Promise<number> {
|
||||
console.log(`input: ${inputFile}`)
|
||||
console.log(`output: ${outputFile}`)
|
||||
|
||||
if (!inputFile) {
|
||||
showHelp()
|
||||
}
|
||||
|
||||
if (!fs.existsSync(inputFile)) {
|
||||
console.error(`File not found: ${inputFile}`)
|
||||
exit(2)
|
||||
}
|
||||
|
||||
// Read input
|
||||
console.log(`Reading ${inputFile}`)
|
||||
const inputData = fs.readFileSync(inputFile, 'utf-8')
|
||||
|
||||
if (!inputData) {
|
||||
console.error(`Unable to read data from file: ${inputFile}`)
|
||||
exit(2)
|
||||
}
|
||||
|
||||
console.log(`Read ${Math.round(inputData.length / 1024)} KB`)
|
||||
|
||||
const parserOptions: ParserOptions = {
|
||||
explicitArray: false
|
||||
}
|
||||
const parser = new xml2js.Parser(parserOptions);
|
||||
const input = await parser.parseStringPromise(inputData)
|
||||
|
||||
if (!input?.rss?.channel) {
|
||||
console.error(`No channels in xml: ${inputFile}`)
|
||||
exit(2)
|
||||
}
|
||||
const channel = input.rss.channel
|
||||
const items = channel.item
|
||||
|
||||
// console.dir(items);
|
||||
|
||||
// Convert
|
||||
const blocks = convert(items)
|
||||
|
||||
// Save output
|
||||
// TODO: Stream output
|
||||
const outputData = ArchiveUtils.buildBlockArchive(blocks)
|
||||
fs.writeFileSync(outputFile, outputData)
|
||||
console.log(`Exported ${blocks.length} block(s) to ${outputFile}`)
|
||||
|
||||
return blocks.length
|
||||
}
|
||||
|
||||
function convert(items: any[]) {
|
||||
const blocks: Block[] = []
|
||||
|
||||
// Board
|
||||
const board = createBoard()
|
||||
board.rootId = board.id
|
||||
board.title = 'Jira import'
|
||||
|
||||
// Compile standard properties
|
||||
board.fields.cardProperties = []
|
||||
|
||||
const priorityProperty = buildCardPropertyFromValues('Priority', items.map(o => o.priority?._))
|
||||
board.fields.cardProperties.push(priorityProperty)
|
||||
|
||||
const statusProperty = buildCardPropertyFromValues('Status', items.map(o => o.status?._))
|
||||
board.fields.cardProperties.push(statusProperty)
|
||||
|
||||
const resolutionProperty = buildCardPropertyFromValues('Resolution', items.map(o => o.resolution?._))
|
||||
board.fields.cardProperties.push(resolutionProperty)
|
||||
|
||||
const typeProperty = buildCardPropertyFromValues('Type', items.map(o => o.type?._))
|
||||
board.fields.cardProperties.push(typeProperty)
|
||||
|
||||
const assigneeProperty = buildCardPropertyFromValues('Assignee', items.map(o => o.assignee?._))
|
||||
board.fields.cardProperties.push(assigneeProperty)
|
||||
|
||||
const reporterProperty = buildCardPropertyFromValues('Reporter', items.map(o => o.reporter?._))
|
||||
board.fields.cardProperties.push(reporterProperty)
|
||||
|
||||
const originalUrlProperty: IPropertyTemplate = {
|
||||
id: Utils.createGuid(),
|
||||
name: 'Original URL',
|
||||
type: 'url',
|
||||
options: []
|
||||
}
|
||||
board.fields.cardProperties.push(originalUrlProperty)
|
||||
|
||||
const createdDateProperty: IPropertyTemplate = {
|
||||
id: Utils.createGuid(),
|
||||
name: 'Created Date',
|
||||
type: 'date',
|
||||
options: []
|
||||
}
|
||||
board.fields.cardProperties.push(createdDateProperty)
|
||||
|
||||
blocks.push(board)
|
||||
|
||||
// Board view
|
||||
const view = createBoardView()
|
||||
view.title = 'Board View'
|
||||
view.fields.viewType = 'board'
|
||||
view.rootId = board.id
|
||||
view.parentId = board.id
|
||||
blocks.push(view)
|
||||
|
||||
for (const item of items) {
|
||||
console.log(
|
||||
`Item: ${item.summary}, ` +
|
||||
`priority: ${item.priority?._}, ` +
|
||||
`status: ${item.status?._}, ` +
|
||||
`type: ${item.type?._}`)
|
||||
|
||||
const card = createCard()
|
||||
card.title = item.summary
|
||||
card.rootId = board.id
|
||||
card.parentId = board.id
|
||||
|
||||
// Map standard properties
|
||||
if (item.priority?._) { setSelectProperty(card, priorityProperty, item.priority._) }
|
||||
if (item.status?._) { setSelectProperty(card, statusProperty, item.status._) }
|
||||
if (item.resolution?._) { setSelectProperty(card, resolutionProperty, item.resolution._) }
|
||||
if (item.type?._) { setSelectProperty(card, typeProperty, item.type._) }
|
||||
if (item.assignee?._) { setSelectProperty(card, assigneeProperty, item.assignee._) }
|
||||
if (item.reporter?._) { setSelectProperty(card, reporterProperty, item.reporter._) }
|
||||
|
||||
if (item.link) { setProperty(card, originalUrlProperty.id, item.link)}
|
||||
if (item.created) {
|
||||
const dateInMs = Date.parse(item.created)
|
||||
setProperty(card, createdDateProperty.id, dateInMs.toString())
|
||||
}
|
||||
|
||||
// TODO: Map custom properties
|
||||
|
||||
if (item.description) {
|
||||
const description = turndownService.turndown(item.description)
|
||||
console.log(`\t${description}`)
|
||||
const text = createTextBlock()
|
||||
text.title = description
|
||||
text.rootId = board.id
|
||||
text.parentId = card.id
|
||||
blocks.push(text)
|
||||
|
||||
card.fields.contentOrder = [text.id]
|
||||
}
|
||||
|
||||
blocks.push(card)
|
||||
}
|
||||
|
||||
return blocks
|
||||
}
|
||||
|
||||
function buildCardPropertyFromValues(propertyName: string, allValues: string[]) {
|
||||
const options: IPropertyOption[] = []
|
||||
|
||||
// Remove empty and duplicate values
|
||||
const values = allValues.
|
||||
filter(o => !!o).
|
||||
filter((x, y) => allValues.indexOf(x) == y);
|
||||
|
||||
for (const value of values) {
|
||||
const optionId = Utils.createGuid()
|
||||
const color = optionColors[optionColorIndex % optionColors.length]
|
||||
optionColorIndex += 1
|
||||
const option: IPropertyOption = {
|
||||
id: optionId,
|
||||
value,
|
||||
color,
|
||||
}
|
||||
options.push(option)
|
||||
}
|
||||
|
||||
const cardProperty: IPropertyTemplate = {
|
||||
id: Utils.createGuid(),
|
||||
name: propertyName,
|
||||
type: 'select',
|
||||
options
|
||||
}
|
||||
|
||||
console.log(`Property: ${propertyName}, values: ${values}`)
|
||||
|
||||
return cardProperty
|
||||
}
|
||||
|
||||
function setSelectProperty(card: Card, cardProperty: IPropertyTemplate, propertyValue: string) {
|
||||
const option = optionForPropertyValue(cardProperty, propertyValue)
|
||||
if (option) {
|
||||
card.fields.properties[cardProperty.id] = option.id
|
||||
}
|
||||
}
|
||||
|
||||
function setProperty(card: Card, cardPropertyId: string, propertyValue: string) {
|
||||
card.fields.properties[cardPropertyId] = propertyValue
|
||||
}
|
||||
|
||||
function optionForPropertyValue(cardProperty: IPropertyTemplate, propertyValue: string): IPropertyOption | null {
|
||||
const option = cardProperty.options.find(o => o.value === propertyValue)
|
||||
if (!option) {
|
||||
console.error(`Property value not found: ${propertyValue}`)
|
||||
return null
|
||||
}
|
||||
|
||||
return option
|
||||
}
|
||||
|
||||
function showHelp() {
|
||||
console.log('import -i <input.xml> -o [output.focalboard]')
|
||||
exit(1)
|
||||
}
|
||||
|
||||
export { run }
|
9337
import/jira/package-lock.json
generated
Normal file
9337
import/jira/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
50
import/jira/package.json
Normal file
50
import/jira/package.json
Normal file
@ -0,0 +1,50 @@
|
||||
{
|
||||
"name": "focalboard-jira-importer",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"description": "",
|
||||
"main": "importJira.js",
|
||||
"scripts": {
|
||||
"lint": "eslint --ext .tsx,.ts . --quiet --cache",
|
||||
"fix": "eslint --ext .tsx,.ts . --quiet --fix --cache",
|
||||
"test": "jest",
|
||||
"testRun": "ts-node importJira.ts -i test/jira_export.xml -o test/jira-import.focalboard",
|
||||
"debug:test": "node --inspect=5858 -r ts-node/register importJira.ts -i test/jira_export.xml -o test/jira-import.focalboard"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"jest": {
|
||||
"globals": {
|
||||
"ts-jest": {
|
||||
"tsconfig": "./tsconfig.json"
|
||||
}
|
||||
},
|
||||
"transform": {
|
||||
"^.+\\.tsx?$": "ts-jest"
|
||||
},
|
||||
"collectCoverage": true,
|
||||
"collectCoverageFrom": [
|
||||
"*.{ts,tsx,js,jsx}",
|
||||
"!test/**"
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/jest": "^27.0.2",
|
||||
"@types/minimist": "^1.2.1",
|
||||
"@types/node": "^14.14.28",
|
||||
"@types/turndown": "^5.0.1",
|
||||
"@types/xml2js": "^0.4.9",
|
||||
"@typescript-eslint/eslint-plugin": "^4.15.0",
|
||||
"@typescript-eslint/parser": "^4.15.0",
|
||||
"eslint": "^7.20.0",
|
||||
"jest": "^27.3.1",
|
||||
"ts-jest": "^27.0.7",
|
||||
"ts-node": "^9.1.1",
|
||||
"typescript": "^4.1.5"
|
||||
},
|
||||
"dependencies": {
|
||||
"minimist": "^1.2.5",
|
||||
"turndown": "^7.1.1",
|
||||
"xml2js": "^0.4.23"
|
||||
}
|
||||
}
|
138
import/jira/test/jira-export.xml
Normal file
138
import/jira/test/jira-export.xml
Normal file
@ -0,0 +1,138 @@
|
||||
<!--
|
||||
RSS generated by JIRA (1001.0.0-SNAPSHOT#100183-sha1:347f6eac38020c8d2c450799ab145942a8caa5a4) at Tue Oct 19 19:29:39 UTC 2021
|
||||
|
||||
It is possible to restrict the fields that are returned in this document by specifying the 'field' parameter in your request.
|
||||
For example, to request only the issue key and summary add field=key&field=summary to the URL of your request.
|
||||
-->
|
||||
<!-- If you wish to do custom client-side styling of RSS, uncomment this:
|
||||
<?xml-stylesheet href="<base-url>/styles/jiraxml2html.xsl" type="text/xsl"?>
|
||||
-->
|
||||
<rss version="0.92">
|
||||
<channel>
|
||||
<title>Jira</title>
|
||||
<link>https://areca.atlassian.net/issues/?jql=text+%7E+%22Investigate%22</link>
|
||||
<description>An XML representation of a search request</description>
|
||||
<language>en-us</language>
|
||||
<issue start="0" end="2" total="2"/>
|
||||
<build-info>
|
||||
<version>1001.0.0-SNAPSHOT</version>
|
||||
<build-number>100183</build-number>
|
||||
<build-date>18-10-2021</build-date>
|
||||
</build-info>
|
||||
<item>
|
||||
<title>[AR-9] Investigate feature area</title>
|
||||
<link>https://areca.atlassian.net/browse/AR-9</link>
|
||||
<project id="10000" key="AR">Areca</project>
|
||||
<description></description>
|
||||
<environment></environment>
|
||||
<key id="10008">AR-9</key>
|
||||
<summary>Investigate feature area</summary>
|
||||
<type id="10001" iconUrl="https://areca.atlassian.net/secure/viewavatar?size=medium&avatarId=10318&avatarType=issuetype">Task</type>
|
||||
<priority id="3" iconUrl="https://areca.atlassian.net/images/icons/priorities/medium.svg">Medium</priority>
|
||||
<status id="10001" iconUrl="https://areca.atlassian.net/" description="">In Progress</status>
|
||||
<statusCategory id="4" key="indeterminate" colorName="yellow"/>
|
||||
<resolution id="-1">Unresolved</resolution>
|
||||
<assignee accountid="-1">Unassigned</assignee>
|
||||
<reporter accountid="557058:10df6720-08d0-4747-86f6-1aa1a7e45332">Chen Lim</reporter>
|
||||
<labels>
|
||||
</labels>
|
||||
<created>Fri, 24 Sep 2021 14:22:15 -0700</created>
|
||||
<updated>Fri, 24 Sep 2021 14:22:15 -0700</updated>
|
||||
<due></due>
|
||||
<votes>0</votes>
|
||||
<watches>1</watches>
|
||||
<attachments>
|
||||
</attachments>
|
||||
<subtasks>
|
||||
</subtasks>
|
||||
<customfields>
|
||||
<customfield id="customfield_10000" key="com.atlassian.jira.plugins.jira-development-integration-plugin:devsummarycf">
|
||||
<customfieldname>Development</customfieldname>
|
||||
<customfieldvalues>
|
||||
|
||||
</customfieldvalues>
|
||||
</customfield>
|
||||
<customfield id="customfield_10019" key="com.pyxis.greenhopper.jira:gh-lexo-rank">
|
||||
<customfieldname>Rank</customfieldname>
|
||||
<customfieldvalues>
|
||||
<customfieldvalue>0|i0001r:</customfieldvalue>
|
||||
|
||||
</customfieldvalues>
|
||||
</customfield>
|
||||
<customfield id="customfield_10020" key="com.pyxis.greenhopper.jira:gh-sprint">
|
||||
<customfieldname>Sprint</customfieldname>
|
||||
<customfieldvalues>
|
||||
|
||||
</customfieldvalues>
|
||||
</customfield>
|
||||
</customfields>
|
||||
</item>
|
||||
|
||||
<item>
|
||||
<title>[AR-1] Investigate feature</title>
|
||||
<link>https://areca.atlassian.net/browse/AR-1</link>
|
||||
<project id="10000" key="AR">Areca</project>
|
||||
<description></description>
|
||||
<environment></environment>
|
||||
<key id="10000">AR-1</key>
|
||||
<summary>Investigate feature</summary>
|
||||
<type id="10002" iconUrl="https://areca.atlassian.net/secure/viewavatar?size=medium&avatarId=10307&avatarType=issuetype">Epic</type>
|
||||
<priority id="3" iconUrl="https://areca.atlassian.net/images/icons/priorities/medium.svg">Medium</priority>
|
||||
<status id="10000" iconUrl="https://areca.atlassian.net/" description="">To Do</status>
|
||||
<statusCategory id="2" key="new" colorName="blue-gray"/>
|
||||
<resolution id="-1">Unresolved</resolution>
|
||||
<assignee accountid="557058:10df6720-08d0-4747-86f6-1aa1a7e45332">Chen Lim</assignee>
|
||||
<reporter accountid="557058:10df6720-08d0-4747-86f6-1aa1a7e45332">Chen Lim</reporter>
|
||||
<labels>
|
||||
<label>PM</label>
|
||||
</labels>
|
||||
<created>Fri, 24 Sep 2021 14:19:44 -0700</created>
|
||||
<updated>Fri, 24 Sep 2021 14:21:32 -0700</updated>
|
||||
<due>Fri, 12 Nov 2021 00:00:00 +0000</due>
|
||||
<votes>0</votes>
|
||||
<watches>1</watches>
|
||||
<comments>
|
||||
<comment id="10000" author="557058:10df6720-08d0-4747-86f6-1aa1a7e45332" created="Fri, 24 Sep 2021 14:21:15 -0700" ><p>Kicking off Project Areca&#33; ��</p></comment>
|
||||
</comments>
|
||||
<attachments>
|
||||
</attachments>
|
||||
<subtasks>
|
||||
</subtasks>
|
||||
<customfields>
|
||||
<customfield id="customfield_10000" key="com.atlassian.jira.plugins.jira-development-integration-plugin:devsummarycf">
|
||||
<customfieldname>Development</customfieldname>
|
||||
<customfieldvalues>
|
||||
|
||||
</customfieldvalues>
|
||||
</customfield>
|
||||
<customfield id="customfield_10017" key="com.pyxis.greenhopper.jira:jsw-issue-color">
|
||||
<customfieldname>Issue color</customfieldname>
|
||||
<customfieldvalues>
|
||||
<customfieldvalue>blue</customfieldvalue>
|
||||
|
||||
</customfieldvalues>
|
||||
</customfield>
|
||||
<customfield id="customfield_10019" key="com.pyxis.greenhopper.jira:gh-lexo-rank">
|
||||
<customfieldname>Rank</customfieldname>
|
||||
<customfieldvalues>
|
||||
<customfieldvalue>0|hzzzzz:</customfieldvalue>
|
||||
|
||||
</customfieldvalues>
|
||||
</customfield>
|
||||
<customfield id="customfield_10020" key="com.pyxis.greenhopper.jira:gh-sprint">
|
||||
<customfieldname>Sprint</customfieldname>
|
||||
<customfieldvalues>
|
||||
|
||||
</customfieldvalues>
|
||||
</customfield>
|
||||
<customfield id="customfield_10015" key="com.atlassian.jira.plugin.system.customfieldtypes:datepicker">
|
||||
<customfieldname>Start date</customfieldname>
|
||||
<customfieldvalues>
|
||||
<customfieldvalue>Tue, 28 Sep 2021 00:00:00 +0000</customfieldvalue>
|
||||
|
||||
</customfieldvalues>
|
||||
</customfield>
|
||||
</customfields>
|
||||
</item>
|
||||
</channel>
|
||||
</rss>
|
27
import/jira/tsconfig.json
Normal file
27
import/jira/tsconfig.json
Normal file
@ -0,0 +1,27 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"jsx": "react",
|
||||
"target": "es2019",
|
||||
"module": "commonjs",
|
||||
"esModuleInterop": true,
|
||||
"noImplicitAny": true,
|
||||
"strict": true,
|
||||
"strictNullChecks": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"sourceMap": true,
|
||||
"allowJs": true,
|
||||
"resolveJsonModule": true,
|
||||
"incremental": false,
|
||||
"outDir": "./dist",
|
||||
"moduleResolution": "node"
|
||||
},
|
||||
"include": [
|
||||
"."
|
||||
],
|
||||
"exclude": [
|
||||
".git",
|
||||
"**/node_modules/*",
|
||||
"dist",
|
||||
"pack"
|
||||
]
|
||||
}
|
19
import/jira/utils.ts
Normal file
19
import/jira/utils.ts
Normal file
@ -0,0 +1,19 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import * as crypto from 'crypto'
|
||||
|
||||
class Utils {
|
||||
static createGuid(): string {
|
||||
function randomDigit() {
|
||||
if (crypto && crypto.randomBytes) {
|
||||
const rands = crypto.randomBytes(1)
|
||||
return (rands[0] % 16).toString(16)
|
||||
}
|
||||
|
||||
return (Math.floor((Math.random() * 16))).toString(16)
|
||||
}
|
||||
return 'xxxxxxxx-xxxx-4xxx-8xxx-xxxxxxxxxxxx'.replace(/x/g, randomDigit)
|
||||
}
|
||||
}
|
||||
|
||||
export { Utils }
|
@ -143,7 +143,7 @@ func (p *Plugin) OnActivate() error {
|
||||
|
||||
p.wsPluginAdapter = ws.NewPluginAdapter(p.API, auth.New(cfg, db))
|
||||
|
||||
mentionsBackend, err := createMentionsNotifyBackend(client, cfg.ServerRoot, logger)
|
||||
mentionsBackend, err := createMentionsNotifyBackend(client, baseURL+"/boards", logger)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error creating mentions notifications backend: %w", err)
|
||||
}
|
||||
|
@ -392,7 +392,7 @@ func (a *API) handlePostBlocks(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
session := ctx.Value(sessionContextKey).(*model.Session)
|
||||
|
||||
err = a.app.InsertBlocks(*container, blocks, session.UserID)
|
||||
err = a.app.InsertBlocks(*container, blocks, session.UserID, true)
|
||||
if err != nil {
|
||||
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
|
||||
return
|
||||
@ -898,7 +898,7 @@ func (a *API) handleImport(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
ctx := r.Context()
|
||||
session := ctx.Value(sessionContextKey).(*model.Session)
|
||||
err = a.app.InsertBlocks(*container, blocks, session.UserID)
|
||||
err = a.app.InsertBlocks(*container, blocks, session.UserID, false)
|
||||
if err != nil {
|
||||
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
|
||||
return
|
||||
|
@ -69,7 +69,7 @@ func (a *App) InsertBlock(c store.Container, block model.Block, userID string) e
|
||||
return err
|
||||
}
|
||||
|
||||
func (a *App) InsertBlocks(c store.Container, blocks []model.Block, userID string) error {
|
||||
func (a *App) InsertBlocks(c store.Container, blocks []model.Block, userID string, allowNotifications bool) error {
|
||||
needsNotify := make([]model.Block, 0, len(blocks))
|
||||
for i := range blocks {
|
||||
err := a.store.InsertBlock(c, &blocks[i], userID)
|
||||
@ -87,7 +87,9 @@ func (a *App) InsertBlocks(c store.Container, blocks []model.Block, userID strin
|
||||
for _, b := range needsNotify {
|
||||
block := b
|
||||
a.webhook.NotifyUpdate(block)
|
||||
a.notifyBlockChanged(notify.Add, c, &block, nil, userID)
|
||||
if allowNotifications {
|
||||
a.notifyBlockChanged(notify.Add, c, &block, nil, userID)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
|
@ -26,7 +26,7 @@ type SQLStore struct {
|
||||
|
||||
// New creates a new SQL implementation of the store.
|
||||
func New(dbType, connectionString, tablePrefix string, logger *mlog.Logger, db *sql.DB, isPlugin bool) (*SQLStore, error) {
|
||||
logger.Info("connectDatabase", mlog.String("dbType", dbType), mlog.String("connStr", connectionString))
|
||||
logger.Info("connectDatabase", mlog.String("dbType", dbType))
|
||||
store := &SQLStore{
|
||||
// TODO: add replica DB support too.
|
||||
db: db,
|
||||
|
@ -14,6 +14,9 @@
|
||||
"CardDetail.addCardText": "Kartentext hinzufügen",
|
||||
"CardDetail.moveContent": "Karteninhalt verschieben",
|
||||
"CardDetail.new-comment-placeholder": "Kommentar hinzufügen...",
|
||||
"CardDetailProperty.confirm-delete": "Löschen der Eigenschaft bestätigen",
|
||||
"CardDetailProperty.confirm-delete-subtext": "Sind Sie sicher, dass Sie die Eigenschaft \"{propertyName}\" löschen möchten? Wenn Sie diese löschen, wird die Eigenschaft von allen Karten in diesem Board gelöscht.",
|
||||
"CardDetailProperty.property-deleted": "{propertyName} erfolgreich gelöscht!",
|
||||
"CardDialog.copiedLink": "Kopiert!",
|
||||
"CardDialog.copyLink": "Link kopieren",
|
||||
"CardDialog.editing-template": "Sie bearbeiten eine Vorlage.",
|
||||
|
@ -8,6 +8,7 @@
|
||||
"CardDetail.add-content": "Προσθήκη περιεχομένου",
|
||||
"CardDetail.add-icon": "Προσθήκη εικονιδίου",
|
||||
"CardDetail.new-comment-placeholder": "Προσθήκη σχολίου ...",
|
||||
"CardDialog.copyLink": "Αντιγραφή συνδέσμου",
|
||||
"CardDialog.nocard": "Αυτή η κάρτα δεν υπάρχει ή δεν είναι προσβάσιμη",
|
||||
"Comment.delete": "Διαγραφή",
|
||||
"CommentsList.send": "Αποστολή",
|
||||
@ -19,6 +20,7 @@
|
||||
"ContentBlock.moveDown": "Μετακίνηση κάτω",
|
||||
"ContentBlock.moveUp": "Μετακίνηση επάνω",
|
||||
"ContentBlock.text": "κείμενο",
|
||||
"DashboardPage.showEmpty": "Προβολή άδειου",
|
||||
"EditableDayPicker.today": "Σήμερα",
|
||||
"Filter.includes": "περιέχει",
|
||||
"Filter.is-empty": "είναι άδειο",
|
||||
@ -45,5 +47,9 @@
|
||||
"PropertyType.UpdatedBy": "Ενημερώθηκε από",
|
||||
"PropertyType.UpdatedTime": "Ώρα Ενημέρωσης",
|
||||
"RegistrationLink.copyLink": "Αντιγραφή συνδέσμου",
|
||||
"RegistrationLink.description": "Μοιραστείτε αυτόν τον σύνδεσμο με άλλους για να δημιουργήσουν λογαριασμούς:"
|
||||
"RegistrationLink.description": "Μοιραστείτε αυτόν τον σύνδεσμο με άλλους για να δημιουργήσουν λογαριασμούς:",
|
||||
"ViewTitle.pick-icon": "Επιλογή εικονιδίου",
|
||||
"ViewTitle.random-icon": "Τυχαίο",
|
||||
"ViewTitle.remove-icon": "Αφαίρεση εικονιδίου",
|
||||
"default-properties.title": "Τίτλος"
|
||||
}
|
||||
|
@ -14,6 +14,9 @@
|
||||
"CardDetail.addCardText": "add card text",
|
||||
"CardDetail.moveContent": "move card content",
|
||||
"CardDetail.new-comment-placeholder": "Add a comment...",
|
||||
"CardDetailProperty.confirm-delete": "Confirm Delete Property",
|
||||
"CardDetailProperty.confirm-delete-subtext": "Are you sure you want to delete the property \"{propertyName}\"? Deleting it will delete the property from all cards in this board.",
|
||||
"CardDetailProperty.property-deleted": "Deleted {propertyName} Successfully!",
|
||||
"CardDialog.editing-template": "You're editing a template.",
|
||||
"CardDialog.nocard": "This card doesn't exist or is inaccessible.",
|
||||
"CardDialog.copiedLink": "Copied!",
|
||||
|
@ -7,14 +7,21 @@
|
||||
"BoardComponent.no-property": "Sin {property}",
|
||||
"BoardComponent.no-property-title": "Elementos sin la propiedad {property} irán aquí. Esta columna no se puede eliminar.",
|
||||
"BoardComponent.show": "Mostrar",
|
||||
"BoardPage.syncFailed": "El tablero puede estar eliminado o el acceso fue revocado.",
|
||||
"CardDetail.add-content": "Añadir contenido",
|
||||
"CardDetail.add-icon": "Añadir icono",
|
||||
"CardDetail.add-property": "+ Añadir propiedad",
|
||||
"CardDetail.addCardText": "añade texto a la tarjeta",
|
||||
"CardDetail.moveContent": "mover contenido de la tarjeta",
|
||||
"CardDetail.new-comment-placeholder": "Añadir un comentario...",
|
||||
"CardDialog.editing-template": "Está editando una plantilla",
|
||||
"CardDialog.nocard": "Esta tarjeta no existe o es inaccesible",
|
||||
"CardDetailProperty.confirm-delete": "Confirmar la eliminación de la propiedad",
|
||||
"CardDetailProperty.confirm-delete-subtext": "¿Estas seguro de que quieres eliminar la propiedad \"{nombre de la propiedad}\"? Al eliminarla se borrará la propiedad de todas las tarjetas de este tablero.",
|
||||
"CardDetailProperty.property-deleted": "¡{nombre de la propiedad} ha sido eliminado exitosamente!",
|
||||
"CardDialog.copiedLink": "¡Copiado!",
|
||||
"CardDialog.copyLink": "Copiar enlace",
|
||||
"CardDialog.editing-template": "Estás editando una plantilla.",
|
||||
"CardDialog.nocard": "Esta tarjeta no existe o es inaccesible.",
|
||||
"ColorOption.selectColor": "Seleccionar {color} Color",
|
||||
"Comment.delete": "Borrar",
|
||||
"CommentsList.send": "Enviar",
|
||||
"ContentBlock.Delete": "Borrar",
|
||||
@ -26,25 +33,50 @@
|
||||
"ContentBlock.editCardCheckboxText": "editar texto de la tarjeta",
|
||||
"ContentBlock.editCardText": "editar texto de la tarjeta",
|
||||
"ContentBlock.editText": "Editar texto...",
|
||||
"ContentBlock.image": "imagen",
|
||||
"ContentBlock.insertAbove": "Insertar encima",
|
||||
"ContentBlock.moveDown": "Mover hacia abajo",
|
||||
"ContentBlock.moveUp": "Mover hacia arriba",
|
||||
"ContentBlock.text": "texto",
|
||||
"DashboardPage.CenterPanel.ChangeChannels": "Usa el selector para cambiar fácilmente de canal",
|
||||
"DashboardPage.CenterPanel.NoWorkspaces": "Lo sentimos, no pudimos encontrar ningún canal que coincida con ese término",
|
||||
"DashboardPage.CenterPanel.NoWorkspacesDescription": "Por favor, intenta buscar otro término",
|
||||
"DashboardPage.showEmpty": "Mostrar vacío",
|
||||
"DashboardPage.title": "Tablero",
|
||||
"Dialog.closeDialog": "Cerrar diálogo",
|
||||
"EditableDayPicker.today": "Hoy",
|
||||
"EmptyCenterPanel.no-content": "Añade o selecciona un panel en la barra lateral para comenzar.",
|
||||
"EmptyCenterPanel.workspace": "Este es el espacio de trabajo para:",
|
||||
"Error.websocket-closed": "Conexión de Websocket cerrada, conexión interrumpida. Si esto persiste, verifique la configuración de su servidor o proxy web.",
|
||||
"Filter.includes": "incluye",
|
||||
"Filter.is-empty": "está vacío",
|
||||
"Filter.is-not-empty": "no está vacío",
|
||||
"Filter.not-includes": "no incluye",
|
||||
"FilterComponent.add-filter": "+ Añadir filtro",
|
||||
"FilterComponent.delete": "Borrar",
|
||||
"GalleryCard.copiedLink": "¡Copiado!",
|
||||
"GalleryCard.copyLink": "Copiar enlace",
|
||||
"GalleryCard.delete": "Borrar",
|
||||
"GalleryCard.duplicate": "Duplicar",
|
||||
"General.BoardCount": "{contar, plural, un{# Board} otro {# Boards}}",
|
||||
"GroupBy.ungroup": "Desagrupar",
|
||||
"KanbanCard.copiedLink": "¡Copiado!",
|
||||
"KanbanCard.copyLink": "Copiar enlace",
|
||||
"KanbanCard.delete": "Eliminar",
|
||||
"KanbanCard.duplicate": "Duplicar",
|
||||
"KanbanCard.untitled": "Sin título",
|
||||
"Mutator.duplicate-board": "duplicar panel",
|
||||
"Mutator.new-board-from-template": "nuevo panel desde una plantilla",
|
||||
"Mutator.new-card-from-template": "nueva tarjeta desde una plantilla",
|
||||
"Mutator.new-template-from-board": "nueva plantilla a partir de un panel",
|
||||
"Mutator.new-template-from-card": "nueva plantilla a partir de una tarjeta",
|
||||
"Mutator.new-template-from-card": "nueva plantilla desde una tarjeta",
|
||||
"PropertyMenu.Delete": "Borrar",
|
||||
"PropertyMenu.changeType": "Cambiar el tipo de propiedad",
|
||||
"PropertyMenu.typeTitle": "Tipo",
|
||||
"PropertyType.Checkbox": "Casilla de selección",
|
||||
"PropertyType.CreatedBy": "Creado Por",
|
||||
"PropertyType.CreatedTime": "Hora de Creación",
|
||||
"PropertyType.CreatedBy": "Creado por",
|
||||
"PropertyType.CreatedTime": "Hora de creación",
|
||||
"PropertyType.Date": "Fecha",
|
||||
"PropertyType.Email": "Email",
|
||||
"PropertyType.File": "Fichero o Multimedia",
|
||||
"PropertyType.MultiSelect": "Selección Múltiple",
|
||||
@ -54,8 +86,8 @@
|
||||
"PropertyType.Select": "Selector",
|
||||
"PropertyType.Text": "Texto",
|
||||
"PropertyType.URL": "URL",
|
||||
"PropertyType.UpdatedBy": "Actualizado Por",
|
||||
"PropertyType.UpdatedTime": "Hora de Actualización",
|
||||
"PropertyType.UpdatedBy": "Última actualización por",
|
||||
"PropertyType.UpdatedTime": "Hora de última actualización",
|
||||
"RegistrationLink.confirmRegenerateToken": "Esto invalidará los enlaces compartidos previos. ¿Continuar?",
|
||||
"RegistrationLink.copiedLink": "¡Copiado!",
|
||||
"RegistrationLink.copyLink": "Copiar enlace",
|
||||
@ -66,12 +98,12 @@
|
||||
"ShareBoard.copiedLink": "¡Copiado!",
|
||||
"ShareBoard.copyLink": "Copiar enlace",
|
||||
"ShareBoard.regenerateToken": "Regenerar token",
|
||||
"ShareBoard.share": "Publicar en la red y compartir este panel con cualquiera",
|
||||
"ShareBoard.share": "Publicar y compartir este panel con cualquiera que tenga el enlace",
|
||||
"ShareBoard.tokenRegenrated": "Token regenerado",
|
||||
"ShareBoard.unshare": "Cualquiera con este enlace puede ver este panel",
|
||||
"ShareBoard.unshare": "Cualquiera con este enlace puede ver este panel y todas las tarjetas en él.",
|
||||
"Sidebar.about": "Sobre Focalboard",
|
||||
"Sidebar.add-board": "+ Añadir Panel",
|
||||
"Sidebar.add-template": "+ Nueva plantilla",
|
||||
"Sidebar.add-board": "+ Añadir panel",
|
||||
"Sidebar.add-template": "Nueva plantilla",
|
||||
"Sidebar.changePassword": "Cambiar contraseña",
|
||||
"Sidebar.delete-board": "Borrar Panel",
|
||||
"Sidebar.delete-template": "Borrar",
|
||||
@ -80,9 +112,11 @@
|
||||
"Sidebar.empty-board": "Panel vacío",
|
||||
"Sidebar.export-archive": "Exportar Archivo",
|
||||
"Sidebar.import-archive": "Importar Archivo",
|
||||
"Sidebar.invite-users": "Invitar Usuarios",
|
||||
"Sidebar.invite-users": "Invitar usuarios",
|
||||
"Sidebar.logout": "Cerrar sesión",
|
||||
"Sidebar.no-more-workspaces": "No hay más espacios de trabajo",
|
||||
"Sidebar.no-views-in-board": "No hay páginas dentro",
|
||||
"Sidebar.random-icons": "Íconos random",
|
||||
"Sidebar.select-a-template": "Seleccionar una plantilla",
|
||||
"Sidebar.set-language": "Establecer idioma",
|
||||
"Sidebar.set-theme": "Establecer apariencia",
|
||||
@ -102,12 +136,22 @@
|
||||
"TableHeaderMenu.sort-ascending": "Orden ascendente",
|
||||
"TableHeaderMenu.sort-descending": "Orden descendente",
|
||||
"TableRow.open": "Abrir",
|
||||
"TopBar.give-feedback": "Dar feedback",
|
||||
"ValueSelector.valueSelector": "Valorar el selector",
|
||||
"ValueSelectorLabel.openMenu": "Abrir menú",
|
||||
"View.AddView": "Añadir vista",
|
||||
"View.Board": "Panel",
|
||||
"View.DeleteView": "Eliminar vista",
|
||||
"View.DuplicateView": "Duplicar vista",
|
||||
"View.NewBoardTitle": "Vista de panel",
|
||||
"View.NewGalleryTitle": "Vista de galería",
|
||||
"View.NewTableTitle": "Vista de tabla",
|
||||
"View.Table": "Tabla",
|
||||
"ViewHeader.add-template": "+ Nueva plantilla",
|
||||
"ViewHeader.delete-template": "Borrar",
|
||||
"ViewHeader.edit-template": "Editar",
|
||||
"ViewHeader.empty-card": "Tarjeta vacía",
|
||||
"ViewHeader.export-board-archive": "Exportar archivo de tablero",
|
||||
"ViewHeader.export-complete": "¡Se ha completado la exportación!",
|
||||
"ViewHeader.export-csv": "Exportar a CSV",
|
||||
"ViewHeader.export-failed": "¡Ha fallado la exportación!",
|
||||
@ -126,5 +170,6 @@
|
||||
"ViewTitle.random-icon": "Aleatorio",
|
||||
"ViewTitle.remove-icon": "Quitar Icono",
|
||||
"ViewTitle.show-description": "mostrar descripción",
|
||||
"ViewTitle.untitled-board": "Panel sin título"
|
||||
"ViewTitle.untitled-board": "Panel sin título",
|
||||
"default-properties.title": "Título"
|
||||
}
|
||||
|
@ -14,6 +14,8 @@
|
||||
"CardDetail.addCardText": "ajouter une carte texte",
|
||||
"CardDetail.moveContent": "déplacer le contenu de la carte",
|
||||
"CardDetail.new-comment-placeholder": "Ajouter un commentaire...",
|
||||
"CardDetailProperty.confirm-delete": "Confirmer la suppression de la propriété",
|
||||
"CardDetailProperty.confirm-delete-subtext": "Êtes-vous sûr de vouloir supprimer la propriété « {propertyName} » ? La suppression retirera la propriété de toutes les cartes dans ce tableau.",
|
||||
"CardDialog.copiedLink": "Copié !",
|
||||
"CardDialog.copyLink": "Copier le lien",
|
||||
"CardDialog.editing-template": "Vous éditez un modèle.",
|
||||
|
186
webapp/i18n/hu.json
Normal file
186
webapp/i18n/hu.json
Normal file
@ -0,0 +1,186 @@
|
||||
{
|
||||
"BoardComponent.add-a-group": "+ Csoport hozzáadása",
|
||||
"BoardComponent.delete": "Törlés",
|
||||
"BoardComponent.hidden-columns": "Rejtett oszlopok",
|
||||
"BoardComponent.hide": "Elrejtés",
|
||||
"BoardComponent.new": "+ Új",
|
||||
"BoardComponent.no-property": "Nincs {property}",
|
||||
"BoardComponent.no-property-title": "Elemek üres {property} tulajdonsággal kerülnek ide. Ez az oszlop nem eltávolítható.",
|
||||
"BoardComponent.show": "Mutat",
|
||||
"BoardPage.syncFailed": "A tábla törölve lett vagy hozzáférés vissza lett vonva.",
|
||||
"CardDetail.add-content": "Tartalom hozzáadása",
|
||||
"CardDetail.add-icon": "Ikon hozzáadása",
|
||||
"CardDetail.add-property": "+ Tulajdonság hozzáadása",
|
||||
"CardDetail.addCardText": "kártya szövegének hozzáadása",
|
||||
"CardDetail.moveContent": "kártya tartalmának mozgatása",
|
||||
"CardDetail.new-comment-placeholder": "Megjegyzés hozzáadása...",
|
||||
"CardDetailProperty.confirm-delete": "Tulajdonság törlésének jóváhagyása",
|
||||
"CardDetailProperty.confirm-delete-subtext": "Biztos benne, hogy törölni szeretné a \"{propertyName}\" tulajdonságot? A törléssel a tulajdonság minden kártyáról el lesz távolítva.",
|
||||
"CardDetailProperty.property-deleted": "{propertyName} törlése sikeres!",
|
||||
"CardDialog.copiedLink": "Másolva!",
|
||||
"CardDialog.copyLink": "Link másolása",
|
||||
"CardDialog.editing-template": "Ön egy sablont szerkeszt.",
|
||||
"CardDialog.nocard": "Ez a kártya nem létezik vagy elérhetetlen.",
|
||||
"ColorOption.selectColor": "{color} szín kiválasztása",
|
||||
"Comment.delete": "Törlés",
|
||||
"CommentsList.send": "Küldés",
|
||||
"ContentBlock.Delete": "Törlés",
|
||||
"ContentBlock.DeleteAction": "törlés",
|
||||
"ContentBlock.addElement": "{type} hozzáadása",
|
||||
"ContentBlock.checkbox": "jelölőnégyzet",
|
||||
"ContentBlock.divider": "elválasztó",
|
||||
"ContentBlock.editCardCheckbox": "három állású jelölőnégyzet",
|
||||
"ContentBlock.editCardCheckboxText": "kártya szövegének szerkesztése",
|
||||
"ContentBlock.editCardText": "kártya szövegének szerkesztése",
|
||||
"ContentBlock.editText": "Szöveg szerkesztése...",
|
||||
"ContentBlock.image": "kép",
|
||||
"ContentBlock.insertAbove": "Beszúrás fölé",
|
||||
"ContentBlock.moveDown": "Mozgatás le",
|
||||
"ContentBlock.moveUp": "Mozgatás fel",
|
||||
"ContentBlock.text": "szöveg",
|
||||
"DashboardPage.CenterPanel.ChangeChannels": "Használja a váltót a beszélgetések könnyű váltásához",
|
||||
"DashboardPage.CenterPanel.NoWorkspaces": "Sajnáljuk, de nem találtunk egy beszélgetést sem ami ennek a kifejezésnek megfelelne",
|
||||
"DashboardPage.CenterPanel.NoWorkspacesDescription": "Kérjük próbáljon keresni egy másik kifejezésre",
|
||||
"DashboardPage.showEmpty": "Mutassa az üreseket",
|
||||
"DashboardPage.title": "Vezérlőpult",
|
||||
"Dialog.closeDialog": "Ablak bezárása",
|
||||
"EditableDayPicker.today": "Ma",
|
||||
"EmptyCenterPanel.no-content": "Adja hozzá vagy válassza ki a táblát az oldalsávról a kezdéshez.",
|
||||
"EmptyCenterPanel.workspace": "Ez a munkaterült ehhez:",
|
||||
"Error.websocket-closed": "Websocket kapcsolat bezárult, kapcsolat megszakadt, Ha ez továbbra is fennáll, akkor ellenőrizze le a kiszolgáló vagy web proxy beállítását.",
|
||||
"Filter.includes": "tartalmazza",
|
||||
"Filter.is-empty": "üres",
|
||||
"Filter.is-not-empty": "nem üres",
|
||||
"Filter.not-includes": "nem tartalmazza",
|
||||
"FilterComponent.add-filter": "+ Szűrő hozzáadása",
|
||||
"FilterComponent.delete": "Törlés",
|
||||
"GalleryCard.copiedLink": "Másolva!",
|
||||
"GalleryCard.copyLink": "Link másolása",
|
||||
"GalleryCard.delete": "Törlés",
|
||||
"GalleryCard.duplicate": "Duplikálás",
|
||||
"General.BoardCount": "# Kártya",
|
||||
"GroupBy.ungroup": "Csoportosítás megszüntetése",
|
||||
"KanbanCard.copiedLink": "Másolt!",
|
||||
"KanbanCard.copyLink": "Link másolása",
|
||||
"KanbanCard.delete": "Törlés",
|
||||
"KanbanCard.duplicate": "Duplikálás",
|
||||
"KanbanCard.untitled": "Névtelen",
|
||||
"Mutator.duplicate-board": "tábla duplikálása",
|
||||
"Mutator.new-board-from-template": "új tábla sablonból",
|
||||
"Mutator.new-card-from-template": "új kártya sablonból",
|
||||
"Mutator.new-template-from-board": "új sablon táblából",
|
||||
"Mutator.new-template-from-card": "új sablon kártyából",
|
||||
"PropertyMenu.Delete": "Törlés",
|
||||
"PropertyMenu.changeType": "Tulajdonság típusának módosítása",
|
||||
"PropertyMenu.typeTitle": "Típus",
|
||||
"PropertyType.Checkbox": "Jelölőnégyzet",
|
||||
"PropertyType.CreatedBy": "Létrehozta",
|
||||
"PropertyType.CreatedTime": "Létrehozás ideje",
|
||||
"PropertyType.Date": "Dátum",
|
||||
"PropertyType.Email": "E-mail",
|
||||
"PropertyType.File": "Fájl vagy média",
|
||||
"PropertyType.MultiSelect": "Több kiválasztós",
|
||||
"PropertyType.Number": "Szám",
|
||||
"PropertyType.Person": "Személy",
|
||||
"PropertyType.Phone": "Telefon",
|
||||
"PropertyType.Select": "Kiválasztás",
|
||||
"PropertyType.Text": "Szöveg",
|
||||
"PropertyType.URL": "URL",
|
||||
"PropertyType.UpdatedBy": "Utoljára frissítette",
|
||||
"PropertyType.UpdatedTime": "Utolsó frissítés ideje",
|
||||
"RegistrationLink.confirmRegenerateToken": "Ez érvényteleníteni fogja a korábban megosztott linkeket. Folytassuk?",
|
||||
"RegistrationLink.copiedLink": "Másolt!",
|
||||
"RegistrationLink.copyLink": "Link másolása",
|
||||
"RegistrationLink.description": "Ossza meg ezt a linket másokkat a fiók létrehozásához:",
|
||||
"RegistrationLink.regenerateToken": "Token újragenerálása",
|
||||
"RegistrationLink.tokenRegenerated": "Regisztrációs link újragenerálva",
|
||||
"ShareBoard.confirmRegenerateToken": "Ez érvényteleníteni fogja a korábban megosztott linkeket. Folytassuk?",
|
||||
"ShareBoard.copiedLink": "Másolt!",
|
||||
"ShareBoard.copyLink": "Link másolása",
|
||||
"ShareBoard.regenerateToken": "Token újragenerálása",
|
||||
"ShareBoard.share": "Tegye közzé és ossza meg bárkivel a táblát aki ismeri a linket",
|
||||
"ShareBoard.tokenRegenrated": "Token újragenerálva",
|
||||
"ShareBoard.unshare": "Mindenki a link birtokában megtekintheti ezt a táblát és benne az összes kártyát.",
|
||||
"Sidebar.about": "Focalboard névjegye",
|
||||
"Sidebar.add-board": "+ Tábla hozzáadása",
|
||||
"Sidebar.add-template": "Új sablon",
|
||||
"Sidebar.changePassword": "Jelszó módosítása",
|
||||
"Sidebar.delete-board": "Tábla törlése",
|
||||
"Sidebar.delete-template": "Törlés",
|
||||
"Sidebar.duplicate-board": "Tábla duplikálása",
|
||||
"Sidebar.edit-template": "Szerkesztés",
|
||||
"Sidebar.empty-board": "Üres tábla",
|
||||
"Sidebar.export-archive": "Archiváltak exportálása",
|
||||
"Sidebar.import-archive": "Archiváltak importálása",
|
||||
"Sidebar.invite-users": "Felhasználók meghívása",
|
||||
"Sidebar.logout": "Kijelentkezés",
|
||||
"Sidebar.no-more-workspaces": "Nincs több munkaterület",
|
||||
"Sidebar.no-views-in-board": "Nincsennek benne lapok",
|
||||
"Sidebar.random-icons": "Véletlen ikonok",
|
||||
"Sidebar.select-a-template": "Sablon kiválasztása",
|
||||
"Sidebar.set-language": "Nyelv megadása",
|
||||
"Sidebar.set-theme": "Téma megadása",
|
||||
"Sidebar.settings": "Beállítások",
|
||||
"Sidebar.template-from-board": "Új sablon táblából",
|
||||
"Sidebar.untitled": "Névtelen",
|
||||
"Sidebar.untitled-board": "(Névtelen tábla)",
|
||||
"Sidebar.untitled-view": "(Névtelen nézet)",
|
||||
"TableComponent.add-icon": "Ikon hozzáadása",
|
||||
"TableComponent.name": "Név",
|
||||
"TableComponent.plus-new": "+ Új",
|
||||
"TableHeaderMenu.delete": "Törlés",
|
||||
"TableHeaderMenu.duplicate": "Duplikálás",
|
||||
"TableHeaderMenu.hide": "Elrejtés",
|
||||
"TableHeaderMenu.insert-left": "Beillesztés balra",
|
||||
"TableHeaderMenu.insert-right": "Beillesztés jobbra",
|
||||
"TableHeaderMenu.sort-ascending": "Rendezés növekvő sorrendben",
|
||||
"TableHeaderMenu.sort-descending": "Rendezés csökkenő sorrendben",
|
||||
"TableRow.open": "Megnyitás",
|
||||
"TopBar.give-feedback": "Visszajelzés",
|
||||
"ValueSelector.valueSelector": "Érték kiválasztó",
|
||||
"ValueSelectorLabel.openMenu": "Menü megnyitása",
|
||||
"View.AddView": "Nézet hozzáadása",
|
||||
"View.Board": "Tábla",
|
||||
"View.DeleteView": "Nézet törlése",
|
||||
"View.DuplicateView": "Nézet duplikálása",
|
||||
"View.NewBoardTitle": "Tábla nézet",
|
||||
"View.NewGalleryTitle": "Galéria nézet",
|
||||
"View.NewTableTitle": "Táblázat nézet",
|
||||
"View.Table": "Táblázat",
|
||||
"ViewHeader.add-template": "Új sablon",
|
||||
"ViewHeader.delete-template": "Törlés",
|
||||
"ViewHeader.edit-template": "Szerkesztés",
|
||||
"ViewHeader.empty-card": "Üres kártya",
|
||||
"ViewHeader.export-board-archive": "Archivált tábla exportálása",
|
||||
"ViewHeader.export-complete": "Exportálás kész!",
|
||||
"ViewHeader.export-csv": "Exportálás CSV-be",
|
||||
"ViewHeader.export-failed": "Exportálás meghiúsult!",
|
||||
"ViewHeader.filter": "Szűrő",
|
||||
"ViewHeader.group-by": "Csoportosítás: {property}",
|
||||
"ViewHeader.new": "Új",
|
||||
"ViewHeader.properties": "Tulajdonságok",
|
||||
"ViewHeader.search": "Keresés",
|
||||
"ViewHeader.search-text": "Szöveg keresése",
|
||||
"ViewHeader.select-a-template": "Sablon kiválasztása",
|
||||
"ViewHeader.share-board": "Tábla megosztása",
|
||||
"ViewHeader.sort": "Rendezés",
|
||||
"ViewHeader.untitled": "Névtelen",
|
||||
"ViewTitle.hide-description": "leírás elrejtése",
|
||||
"ViewTitle.pick-icon": "Válasszon ikont",
|
||||
"ViewTitle.random-icon": "Véletlen",
|
||||
"ViewTitle.remove-icon": "Ikon eltávolítása",
|
||||
"ViewTitle.show-description": "leírás mutatása",
|
||||
"ViewTitle.untitled-board": "Névtelen tábla",
|
||||
"WelcomePage.Description": "A Táblák egy projekt kezelő segédeszköz ami segít azonosítani, rendezni, követni és vezetni a munkát csapatok között, egy ismerős kanban táblás nézet segítségével",
|
||||
"WelcomePage.Explore.Button": "Felfedezés",
|
||||
"WelcomePage.Heading": "Üdvözöljük a Táblákban",
|
||||
"Workspace.editing-board-template": "Ön egy sablon táblát szerkeszt.",
|
||||
"default-properties.title": "Cím",
|
||||
"error.no-workspace": "Az Ön munkamenete lejárt vagy nincs hozzáférése ehhez a munkaterülethez.",
|
||||
"error.relogin": "Jelentkezzen be újra",
|
||||
"login.log-in-button": "Bejelentkezés",
|
||||
"login.log-in-title": "Bejelentkezés",
|
||||
"login.register-button": "vagy hozzon létre egy fiókot ha még nincs",
|
||||
"register.login-button": "vagy jelentkezzen be ha már van fiókja",
|
||||
"register.signup-title": "Regisztráljon fiókjáért"
|
||||
}
|
@ -14,6 +14,9 @@
|
||||
"CardDetail.addCardText": "カードテキストを追加する",
|
||||
"CardDetail.moveContent": "カード内容の移動",
|
||||
"CardDetail.new-comment-placeholder": "コメントを追加する...",
|
||||
"CardDetailProperty.confirm-delete": "プロパティの削除を確定する",
|
||||
"CardDetailProperty.confirm-delete-subtext": "本当にプロパティ \"{propertyName}\" を削除しますか? 削除すると、このボードのすべてのカードからそのプロパティが削除されます。",
|
||||
"CardDetailProperty.property-deleted": "{propertyName} が正常に削除されました!",
|
||||
"CardDialog.copiedLink": "コピーしました!",
|
||||
"CardDialog.copyLink": "リンクをコピー",
|
||||
"CardDialog.editing-template": "テンプレートを編集しています。",
|
||||
@ -38,6 +41,7 @@
|
||||
"DashboardPage.CenterPanel.ChangeChannels": "スイッチャーで簡単にチャンネルを変更できます",
|
||||
"DashboardPage.CenterPanel.NoWorkspaces": "その言葉に一致するチャンネルは見つかりませんでした",
|
||||
"DashboardPage.CenterPanel.NoWorkspacesDescription": "別の言葉で検索してみてください",
|
||||
"DashboardPage.showEmpty": "空の表示",
|
||||
"DashboardPage.title": "ダッシュボード",
|
||||
"Dialog.closeDialog": "ダイアログを閉じる",
|
||||
"EditableDayPicker.today": "今日",
|
||||
|
@ -14,6 +14,9 @@
|
||||
"CardDetail.addCardText": "kaarttekst toevoegen",
|
||||
"CardDetail.moveContent": "inhoud van de kaart verplaatsen",
|
||||
"CardDetail.new-comment-placeholder": "Voeg commentaar toe...",
|
||||
"CardDetailProperty.confirm-delete": "Bevestig verwijderen eigenschap",
|
||||
"CardDetailProperty.confirm-delete-subtext": "Weet je zeker dat je de eigenschap \"{propertyName}\" wilt verwijderen? Dit verwijderen zal de eigenschap van alle kaarten in dit bord verwijderen.",
|
||||
"CardDetailProperty.property-deleted": "{propertyName} werd succesvol verwijderd!",
|
||||
"CardDialog.copiedLink": "Gekopieerd!",
|
||||
"CardDialog.copyLink": "Kopieer link",
|
||||
"CardDialog.editing-template": "Je bent een sjabloon aan het bewerken.",
|
||||
|
@ -14,6 +14,9 @@
|
||||
"CardDetail.addCardText": "apondre una zòna de tèxt",
|
||||
"CardDetail.moveContent": "desplaçar contengut de la carta",
|
||||
"CardDetail.new-comment-placeholder": "Apondre un comentari...",
|
||||
"CardDetailProperty.confirm-delete": "Confirmar la supression de proprietat",
|
||||
"CardDetailProperty.confirm-delete-subtext": "Volètz vertadièrament suprimir la proprietat « {propertyName} » ? La supression levarà la proprietat de totas las cartas d’aquesta tablèu.",
|
||||
"CardDetailProperty.property-deleted": "Supression de {propertyName} reüssida !",
|
||||
"CardDialog.copiedLink": "Copiat !",
|
||||
"CardDialog.copyLink": "Copiar ligam",
|
||||
"CardDialog.editing-template": "Sètz a modificar un modèl.",
|
||||
@ -38,6 +41,7 @@
|
||||
"DashboardPage.CenterPanel.ChangeChannels": "Utilizatz l’alternator per cambiar facilament de cadenas",
|
||||
"DashboardPage.CenterPanel.NoWorkspaces": "O planhèm, avèm pas trobat cap de cadena correspondenta al tèrme",
|
||||
"DashboardPage.CenterPanel.NoWorkspacesDescription": "Mercés d’ensajar d’autres tèrmes",
|
||||
"DashboardPage.showEmpty": "Mostrar void",
|
||||
"DashboardPage.title": "Tablèu de bòrd",
|
||||
"Dialog.closeDialog": "Tampar la fenèstra de dialòg",
|
||||
"EditableDayPicker.today": "Uèi",
|
||||
@ -168,6 +172,7 @@
|
||||
"ViewTitle.show-description": "mostrar la descripcion",
|
||||
"ViewTitle.untitled-board": "Tablèu sens títol",
|
||||
"WelcomePage.Explore.Button": "Explorar",
|
||||
"WelcomePage.Heading": "La benvengudas als tablèus",
|
||||
"Workspace.editing-board-template": "Modificatz un modèl de tablèu.",
|
||||
"default-properties.title": "Títol",
|
||||
"error.no-workspace": "Vòstra session pòt aver expirat o avètz pas accès a aqueste espaci de trabalh.",
|
||||
|
@ -14,6 +14,9 @@
|
||||
"CardDetail.addCardText": "dodaj tekst karty",
|
||||
"CardDetail.moveContent": "przenieś zawartość karty",
|
||||
"CardDetail.new-comment-placeholder": "Dodaj komentarz...",
|
||||
"CardDetailProperty.confirm-delete": "Potwierdź Usuń Właściwość",
|
||||
"CardDetailProperty.confirm-delete-subtext": "Czy na pewno chcesz usunąć właściwość \"{propertyName}\"? Usunięcie tej właściwości spowoduje usunięcie jej z wszystkich kart na tej tablicy.",
|
||||
"CardDetailProperty.property-deleted": "Usunięto pomyślnie {propertyName}!",
|
||||
"CardDialog.copiedLink": "Skopiowane!",
|
||||
"CardDialog.copyLink": "Kopiuj odnośnik",
|
||||
"CardDialog.editing-template": "Edytujesz szablon.",
|
||||
|
@ -14,6 +14,9 @@
|
||||
"CardDetail.addCardText": "lägg till korttext",
|
||||
"CardDetail.moveContent": "flytta kortinnehåll",
|
||||
"CardDetail.new-comment-placeholder": "Lägg till kommentar...",
|
||||
"CardDetailProperty.confirm-delete": "Bekräfta att ta bort egenskap",
|
||||
"CardDetailProperty.confirm-delete-subtext": "Är du säker på att du vill ta bort egenskapen \"{propertyName}\"? Om du raderar den kommer egenskapen tas bort från alla kort på tavlan.",
|
||||
"CardDetailProperty.property-deleted": "{propertyName} har raderats!",
|
||||
"CardDialog.copiedLink": "Kopierad!",
|
||||
"CardDialog.copyLink": "Kopiera länk",
|
||||
"CardDialog.editing-template": "Du redigerar en mall.",
|
||||
@ -38,6 +41,7 @@
|
||||
"DashboardPage.CenterPanel.ChangeChannels": "Använd kanalväljaren för att smidigt växla mellan kanaler",
|
||||
"DashboardPage.CenterPanel.NoWorkspaces": "Tyvärr hittade vi inga kanaler som matchar den termen",
|
||||
"DashboardPage.CenterPanel.NoWorkspacesDescription": "Försök att söka efter en annan term",
|
||||
"DashboardPage.showEmpty": "Visa tomma",
|
||||
"DashboardPage.title": "Dashboard",
|
||||
"Dialog.closeDialog": "Stäng dialog",
|
||||
"EditableDayPicker.today": "Idag",
|
||||
|
@ -14,6 +14,9 @@
|
||||
"CardDetail.addCardText": "kart metni ekle",
|
||||
"CardDetail.moveContent": "kart içeriğini taşı",
|
||||
"CardDetail.new-comment-placeholder": "Bir yorum ekle...",
|
||||
"CardDetailProperty.confirm-delete": "Özelliği silme onayı",
|
||||
"CardDetailProperty.confirm-delete-subtext": "\"{propertyName}\" özelliğini silmek istediğinize emin misiniz? Bu işlem özelliği panodaki tüm kartlardan siler.",
|
||||
"CardDetailProperty.property-deleted": "{propertyName} silindi!",
|
||||
"CardDialog.copiedLink": "Kopyalandı!",
|
||||
"CardDialog.copyLink": "Bağlantıyı kopyala",
|
||||
"CardDialog.editing-template": "Bir kalıbı düzenliyorsunuz.",
|
||||
|
@ -13,6 +13,9 @@
|
||||
"CardDetail.addCardText": "新增卡片文本",
|
||||
"CardDetail.moveContent": "移动卡片内容",
|
||||
"CardDetail.new-comment-placeholder": "新增评论...",
|
||||
"CardDetailProperty.property-deleted": "成功删除 {propertyName}!",
|
||||
"CardDialog.copiedLink": "已复制!",
|
||||
"CardDialog.copyLink": "复制链接",
|
||||
"CardDialog.editing-template": "您正在编辑模板。",
|
||||
"CardDialog.nocard": "卡片不存在或者无法被存取。",
|
||||
"ColorOption.selectColor": "选择{color}",
|
||||
|
@ -16,6 +16,7 @@ import {createBrowserHistory} from 'history'
|
||||
|
||||
import TelemetryClient from './telemetry/telemetryClient'
|
||||
|
||||
import {IAppWindow} from './types'
|
||||
import {getMessages} from './i18n'
|
||||
import {FlashMessages} from './components/flashMessages'
|
||||
import BoardPage from './pages/boardPage'
|
||||
@ -36,6 +37,8 @@ import {fetchClientConfig} from './store/clientConfig'
|
||||
import {IUser} from './user'
|
||||
import {UserSettings} from './userSettings'
|
||||
|
||||
declare let window: IAppWindow
|
||||
|
||||
export const history = createBrowserHistory({basename: Utils.getFrontendBaseURL()})
|
||||
|
||||
const UUID_REGEX = new RegExp(/^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/)
|
||||
@ -47,30 +50,30 @@ if (Utils.isDesktop() && Utils.isFocalboardPlugin()) {
|
||||
}
|
||||
|
||||
const pathName = event.data.message?.pathName
|
||||
if (!pathName || !pathName.startsWith((window as any).frontendBaseURL)) {
|
||||
if (!pathName || !pathName.startsWith(window.frontendBaseURL)) {
|
||||
return
|
||||
}
|
||||
|
||||
Utils.log(`Navigating Boards to ${pathName}`)
|
||||
history.replace(pathName.replace((window as any).frontendBaseURL, ''))
|
||||
history.replace(pathName.replace(window.frontendBaseURL, ''))
|
||||
})
|
||||
}
|
||||
|
||||
const browserHistory = {
|
||||
const browserHistory: typeof history = {
|
||||
...history,
|
||||
push: (path: string, ...args: any[]) => {
|
||||
push: (path: string, state?: unknown) => {
|
||||
if (Utils.isDesktop() && Utils.isFocalboardPlugin()) {
|
||||
window.postMessage(
|
||||
{
|
||||
type: 'browser-history-push',
|
||||
message: {
|
||||
path: `${(window as any).frontendBaseURL}${path}`,
|
||||
path: `${window.frontendBaseURL}${path}`,
|
||||
},
|
||||
},
|
||||
window.location.origin,
|
||||
)
|
||||
} else {
|
||||
history.push(path, ...args)
|
||||
history.push(path, state)
|
||||
}
|
||||
},
|
||||
}
|
||||
@ -97,7 +100,9 @@ const App = React.memo((): JSX.Element => {
|
||||
|
||||
if (Utils.isFocalboardPlugin()) {
|
||||
useEffect(() => {
|
||||
history.replace(window.location.pathname.replace((window as any).frontendBaseURL, ''))
|
||||
if (window.frontendBaseURL) {
|
||||
history.replace(window.location.pathname.replace(window.frontendBaseURL, ''))
|
||||
}
|
||||
}, [])
|
||||
}
|
||||
|
||||
|
@ -1,5 +1,7 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {IAppWindow} from './types'
|
||||
import {ArchiveUtils, ArchiveHeader, ArchiveLine, BlockArchiveLine} from './blocks/archive'
|
||||
import {Block} from './blocks/block'
|
||||
import {Board} from './blocks/board'
|
||||
@ -7,6 +9,8 @@ import {LineReader} from './lineReader'
|
||||
import mutator from './mutator'
|
||||
import {Utils} from './utils'
|
||||
|
||||
declare let window: IAppWindow
|
||||
|
||||
class Archiver {
|
||||
static async exportBoardArchive(board: Board): Promise<void> {
|
||||
const blocks = await mutator.exportArchive(board.id)
|
||||
@ -35,8 +39,8 @@ class Archiver {
|
||||
link.click()
|
||||
|
||||
// TODO: Review if this is needed in the future, this is to fix the problem with linux webview links
|
||||
if ((window as any).openInNewBrowser) {
|
||||
(window as any).openInNewBrowser(link.href)
|
||||
if (window.openInNewBrowser) {
|
||||
window.openInNewBrowser(link.href)
|
||||
}
|
||||
|
||||
// TODO: Remove or reuse link
|
||||
|
1312
webapp/src/components/__snapshots__/cardDialog.test.tsx.snap
Normal file
1312
webapp/src/components/__snapshots__/cardDialog.test.tsx.snap
Normal file
File diff suppressed because it is too large
Load Diff
@ -151,6 +151,8 @@ describe('components/calculations/calculation logic', () => {
|
||||
updatedBy: {id: 'property_lastUpdatedBy', type: 'updatedBy', name: '', options: []},
|
||||
}
|
||||
|
||||
const autofilledProperties = new Set([properties.createdBy, properties.createdTime, properties.updatedBy, properties.updatedTime])
|
||||
|
||||
const intl = createIntl({locale: 'en-us'})
|
||||
|
||||
// testing count
|
||||
@ -160,6 +162,34 @@ describe('components/calculations/calculation logic', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// testing count empty
|
||||
Object.values(properties).filter((p) => !autofilledProperties.has(p)).forEach((property) => {
|
||||
it(`should correctly count empty for property type "${property.type}"`, function() {
|
||||
expect(Calculations.countEmpty(cards, property, intl)).toBe('1')
|
||||
})
|
||||
})
|
||||
|
||||
// testing percent empty
|
||||
Object.values(properties).filter((p) => !autofilledProperties.has(p)).forEach((property) => {
|
||||
it(`should correctly compute empty percent for property type "${property.type}"`, function() {
|
||||
expect(Calculations.percentEmpty(cards, property, intl)).toBe('25%')
|
||||
})
|
||||
})
|
||||
|
||||
// testing count not empty
|
||||
Object.values(properties).filter((p) => !autofilledProperties.has(p)).forEach((property) => {
|
||||
it(`should correctly count not empty for property type "${property.type}"`, function() {
|
||||
expect(Calculations.countNotEmpty(cards, property, intl)).toBe('3')
|
||||
})
|
||||
})
|
||||
|
||||
// testing percent not empty
|
||||
Object.values(properties).filter((p) => !autofilledProperties.has(p)).forEach((property) => {
|
||||
it(`should correctly compute not empty percent for property type "${property.type}"`, function() {
|
||||
expect(Calculations.percentNotEmpty(cards, property, intl)).toBe('75%')
|
||||
})
|
||||
})
|
||||
|
||||
// testing countValues
|
||||
const countValueTests: Record<string, string> = {
|
||||
text: '3',
|
||||
|
@ -53,6 +53,28 @@ function count(cards: readonly Card[], property: IPropertyTemplate): string {
|
||||
return String(cards.length)
|
||||
}
|
||||
|
||||
function countEmpty(cards: readonly Card[], property: IPropertyTemplate): string {
|
||||
return String(cards.length - cardsWithValue(cards, property).length)
|
||||
}
|
||||
|
||||
function countNotEmpty(cards: readonly Card[], property: IPropertyTemplate): string {
|
||||
return String(cardsWithValue(cards, property).length)
|
||||
}
|
||||
|
||||
function percentEmpty(cards: readonly Card[], property: IPropertyTemplate): string {
|
||||
if (cards.length === 0) {
|
||||
return ''
|
||||
}
|
||||
return String((((cards.length - cardsWithValue(cards, property).length) / cards.length) * 100).toFixed(0)) + '%'
|
||||
}
|
||||
|
||||
function percentNotEmpty(cards: readonly Card[], property: IPropertyTemplate): string {
|
||||
if (cards.length === 0) {
|
||||
return ''
|
||||
}
|
||||
return String(((cardsWithValue(cards, property).length / cards.length) * 100).toFixed(0)) + '%'
|
||||
}
|
||||
|
||||
function countValueHelper(cards: readonly Card[], property: IPropertyTemplate): number {
|
||||
let values = 0
|
||||
|
||||
@ -260,7 +282,7 @@ function getTimestampsFromPropertyValue(value: number | string | string[]): numb
|
||||
return []
|
||||
}
|
||||
|
||||
function dateRange(cards: readonly Card[], property: IPropertyTemplate): string {
|
||||
function dateRange(cards: readonly Card[], property: IPropertyTemplate, intl: IntlShape): string {
|
||||
const resultEarliest = earliestEpoch(cards, property)
|
||||
if (resultEarliest === Number.POSITIVE_INFINITY) {
|
||||
return ''
|
||||
@ -269,11 +291,15 @@ function dateRange(cards: readonly Card[], property: IPropertyTemplate): string
|
||||
if (resultLatest === Number.NEGATIVE_INFINITY) {
|
||||
return ''
|
||||
}
|
||||
return moment.duration(resultLatest - resultEarliest, 'milliseconds').humanize()
|
||||
return moment.duration(resultLatest - resultEarliest, 'milliseconds').locale(intl.locale.toLowerCase()).humanize()
|
||||
}
|
||||
|
||||
const Calculations: Record<string, (cards: readonly Card[], property: IPropertyTemplate, intl: IntlShape) => string> = {
|
||||
count,
|
||||
countEmpty,
|
||||
countNotEmpty,
|
||||
percentEmpty,
|
||||
percentNotEmpty,
|
||||
countValue,
|
||||
countUniqueValue,
|
||||
countChecked,
|
||||
|
@ -19,6 +19,10 @@ type Option = {
|
||||
export const Options:Record<string, Option> = {
|
||||
none: {value: 'none', label: 'None', displayName: 'Calculate'},
|
||||
count: {value: 'count', label: 'Count', displayName: 'Count'},
|
||||
countEmpty: {value: 'countEmpty', label: 'Count Empty', displayName: 'Empty'},
|
||||
countNotEmpty: {value: 'countNotEmpty', label: 'Count Not Empty', displayName: 'Not Empty'},
|
||||
percentEmpty: {value: 'percentEmpty', label: 'Percent Empty', displayName: 'Empty'},
|
||||
percentNotEmpty: {value: 'percentNotEmpty', label: 'Percent Not Empty', displayName: 'Not Empty'},
|
||||
countValue: {value: 'countValue', label: 'Count Value', displayName: 'Values'},
|
||||
countChecked: {value: 'countChecked', label: 'Count Checked', displayName: 'Checked'},
|
||||
percentChecked: {value: 'percentChecked', label: 'Percent Checked', displayName: 'Checked'},
|
||||
@ -37,7 +41,8 @@ export const Options:Record<string, Option> = {
|
||||
}
|
||||
|
||||
export const optionsByType: Map<string, Option[]> = new Map([
|
||||
['common', [Options.none, Options.count, Options.countValue, Options.countUniqueValue]],
|
||||
['common', [Options.none, Options.count, Options.countEmpty, Options.countNotEmpty, Options.percentEmpty,
|
||||
Options.percentNotEmpty, Options.countValue, Options.countUniqueValue]],
|
||||
['checkbox', [Options.countChecked, Options.countUnchecked, Options.percentChecked, Options.percentUnchecked]],
|
||||
['number', [Options.sum, Options.average, Options.median, Options.min, Options.max, Options.range]],
|
||||
['date', [Options.earliest, Options.latest, Options.dateRange]],
|
||||
|
@ -36,7 +36,7 @@ exports[`components/cardDetail/cardDetailContents should match snapshot 1`] = `
|
||||
class="EasyMDEContainer"
|
||||
>
|
||||
<div
|
||||
class="CodeMirror cm-s-easymde CodeMirror-wrap CodeMirror-focused"
|
||||
class="CodeMirror cm-s-easymde CodeMirror-wrap"
|
||||
>
|
||||
<div
|
||||
style="overflow: hidden; position: relative; width: 3px; height: 0px;"
|
||||
@ -168,14 +168,14 @@ exports[`components/cardDetail/cardDetailContents should match snapshot after dr
|
||||
class="MenuWrapper"
|
||||
role="button"
|
||||
>
|
||||
<div
|
||||
<button
|
||||
class="Button IconButton"
|
||||
role="button"
|
||||
type="button"
|
||||
>
|
||||
<i
|
||||
class="CompassIcon icon-dots-horizontal OptionsIcon"
|
||||
/>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
class="dnd-handle"
|
||||
@ -232,14 +232,14 @@ exports[`components/cardDetail/cardDetailContents should match snapshot after dr
|
||||
class="MenuWrapper"
|
||||
role="button"
|
||||
>
|
||||
<div
|
||||
<button
|
||||
class="Button IconButton"
|
||||
role="button"
|
||||
type="button"
|
||||
>
|
||||
<i
|
||||
class="CompassIcon icon-dots-horizontal OptionsIcon"
|
||||
/>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
class="dnd-handle"
|
||||
@ -287,14 +287,14 @@ exports[`components/cardDetail/cardDetailContents should match snapshot after dr
|
||||
class="MenuWrapper"
|
||||
role="button"
|
||||
>
|
||||
<div
|
||||
<button
|
||||
class="Button IconButton"
|
||||
role="button"
|
||||
type="button"
|
||||
>
|
||||
<i
|
||||
class="CompassIcon icon-dots-horizontal OptionsIcon"
|
||||
/>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
class="dnd-handle"
|
||||
@ -357,14 +357,14 @@ exports[`components/cardDetail/cardDetailContents should match snapshot after dr
|
||||
class="MenuWrapper"
|
||||
role="button"
|
||||
>
|
||||
<div
|
||||
<button
|
||||
class="Button IconButton"
|
||||
role="button"
|
||||
type="button"
|
||||
>
|
||||
<i
|
||||
class="CompassIcon icon-dots-horizontal OptionsIcon"
|
||||
/>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
class="dnd-handle"
|
||||
@ -417,14 +417,14 @@ exports[`components/cardDetail/cardDetailContents should match snapshot after dr
|
||||
class="MenuWrapper"
|
||||
role="button"
|
||||
>
|
||||
<div
|
||||
<button
|
||||
class="Button IconButton"
|
||||
role="button"
|
||||
type="button"
|
||||
>
|
||||
<i
|
||||
class="CompassIcon icon-dots-horizontal OptionsIcon"
|
||||
/>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
class="dnd-handle"
|
||||
@ -490,14 +490,14 @@ exports[`components/cardDetail/cardDetailContents should match snapshot after dr
|
||||
class="MenuWrapper"
|
||||
role="button"
|
||||
>
|
||||
<div
|
||||
<button
|
||||
class="Button IconButton"
|
||||
role="button"
|
||||
type="button"
|
||||
>
|
||||
<i
|
||||
class="CompassIcon icon-dots-horizontal OptionsIcon"
|
||||
/>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
class="dnd-handle"
|
||||
@ -550,14 +550,14 @@ exports[`components/cardDetail/cardDetailContents should match snapshot after dr
|
||||
class="MenuWrapper"
|
||||
role="button"
|
||||
>
|
||||
<div
|
||||
<button
|
||||
class="Button IconButton"
|
||||
role="button"
|
||||
type="button"
|
||||
>
|
||||
<i
|
||||
class="CompassIcon icon-dots-horizontal OptionsIcon"
|
||||
/>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
class="dnd-handle"
|
||||
@ -773,14 +773,14 @@ exports[`components/cardDetail/cardDetailContents should match snapshot with con
|
||||
class="MenuWrapper"
|
||||
role="button"
|
||||
>
|
||||
<div
|
||||
<button
|
||||
class="Button IconButton"
|
||||
role="button"
|
||||
type="button"
|
||||
>
|
||||
<i
|
||||
class="CompassIcon icon-dots-horizontal OptionsIcon"
|
||||
/>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
class="dnd-handle"
|
||||
@ -846,14 +846,14 @@ exports[`components/cardDetail/cardDetailContents should match snapshot with con
|
||||
class="MenuWrapper"
|
||||
role="button"
|
||||
>
|
||||
<div
|
||||
<button
|
||||
class="Button IconButton"
|
||||
role="button"
|
||||
type="button"
|
||||
>
|
||||
<i
|
||||
class="CompassIcon icon-dots-horizontal OptionsIcon"
|
||||
/>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
class="dnd-handle"
|
||||
@ -910,14 +910,14 @@ exports[`components/cardDetail/cardDetailContents should match snapshot with con
|
||||
class="MenuWrapper"
|
||||
role="button"
|
||||
>
|
||||
<div
|
||||
<button
|
||||
class="Button IconButton"
|
||||
role="button"
|
||||
type="button"
|
||||
>
|
||||
<i
|
||||
class="CompassIcon icon-dots-horizontal OptionsIcon"
|
||||
/>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
class="dnd-handle"
|
||||
@ -965,14 +965,14 @@ exports[`components/cardDetail/cardDetailContents should match snapshot with con
|
||||
class="MenuWrapper"
|
||||
role="button"
|
||||
>
|
||||
<div
|
||||
<button
|
||||
class="Button IconButton"
|
||||
role="button"
|
||||
type="button"
|
||||
>
|
||||
<i
|
||||
class="CompassIcon icon-dots-horizontal OptionsIcon"
|
||||
/>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
class="dnd-handle"
|
||||
|
@ -16,23 +16,28 @@ exports[`components/cardDetail/comment return comment 1`] = `
|
||||
class="comment-username"
|
||||
/>
|
||||
<div
|
||||
class="comment-date"
|
||||
class="octo-tooltip tooltip-top"
|
||||
data-tooltip="October 01, 2020, 12:00 AM"
|
||||
>
|
||||
October 01, 2020, 12:00 AM
|
||||
<div
|
||||
class="comment-date"
|
||||
>
|
||||
a day ago
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
aria-label="menuwrapper"
|
||||
class="MenuWrapper"
|
||||
role="button"
|
||||
>
|
||||
<div
|
||||
<button
|
||||
class="Button IconButton"
|
||||
role="button"
|
||||
type="button"
|
||||
>
|
||||
<i
|
||||
class="CompassIcon icon-dots-horizontal OptionsIcon"
|
||||
/>
|
||||
</div>
|
||||
</button>
|
||||
<div
|
||||
class="Menu noselect left"
|
||||
>
|
||||
@ -121,23 +126,28 @@ exports[`components/cardDetail/comment return comment and delete comment 1`] = `
|
||||
class="comment-username"
|
||||
/>
|
||||
<div
|
||||
class="comment-date"
|
||||
class="octo-tooltip tooltip-top"
|
||||
data-tooltip="October 01, 2020, 12:00 AM"
|
||||
>
|
||||
October 01, 2020, 12:00 AM
|
||||
<div
|
||||
class="comment-date"
|
||||
>
|
||||
a day ago
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
aria-label="menuwrapper"
|
||||
class="MenuWrapper"
|
||||
role="button"
|
||||
>
|
||||
<div
|
||||
<button
|
||||
class="Button IconButton"
|
||||
role="button"
|
||||
type="button"
|
||||
>
|
||||
<i
|
||||
class="CompassIcon icon-dots-horizontal OptionsIcon"
|
||||
/>
|
||||
</div>
|
||||
</button>
|
||||
<div
|
||||
class="Menu noselect left"
|
||||
>
|
||||
@ -226,9 +236,14 @@ exports[`components/cardDetail/comment return comment readonly 1`] = `
|
||||
class="comment-username"
|
||||
/>
|
||||
<div
|
||||
class="comment-date"
|
||||
class="octo-tooltip tooltip-top"
|
||||
data-tooltip="October 01, 2020, 12:00 AM"
|
||||
>
|
||||
October 01, 2020, 12:00 AM
|
||||
<div
|
||||
class="comment-date"
|
||||
>
|
||||
a day ago
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
|
@ -6,7 +6,7 @@
|
||||
.add-buttons {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 30px;
|
||||
min-height: 32px;
|
||||
color: rgba(var(--center-channel-color-rgb), 0.4);
|
||||
width: 100%;
|
||||
align-items: flex-start;
|
||||
|
@ -1,7 +1,8 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import React from 'react'
|
||||
import {FormattedMessage} from 'react-intl'
|
||||
|
||||
import React, {useState} from 'react'
|
||||
import {useIntl, FormattedMessage} from 'react-intl'
|
||||
|
||||
import {Board, PropertyType, IPropertyTemplate} from '../../blocks/board'
|
||||
import {Card} from '../../blocks/card'
|
||||
@ -14,6 +15,8 @@ import MenuWrapper from '../../widgets/menuWrapper'
|
||||
import PropertyMenu from '../../widgets/propertyMenu'
|
||||
|
||||
import PropertyValueElement from '../propertyValueElement'
|
||||
import {ConfirmationDialogBox} from '../confirmationDialogBox'
|
||||
import {sendFlashMessage} from '../flashMessages'
|
||||
|
||||
type Props = {
|
||||
board: Board
|
||||
@ -27,8 +30,13 @@ type Props = {
|
||||
}
|
||||
|
||||
const CardDetailProperties = React.memo((props: Props) => {
|
||||
const intl = useIntl()
|
||||
const {board, card, cards, views, activeView, contents, comments} = props
|
||||
|
||||
const [showConfirmationDialog, setShowConfirmationDialog] = useState<boolean>(false)
|
||||
const [deletingPropId, setDeletingPropId] = useState<string>('')
|
||||
const [deletingPropName, setDeletingPropName] = useState<string>('')
|
||||
|
||||
return (
|
||||
<div className='octo-propertylist CardDetailProperties'>
|
||||
{board.fields.cardProperties.map((propertyTemplate: IPropertyTemplate) => {
|
||||
@ -47,7 +55,12 @@ const CardDetailProperties = React.memo((props: Props) => {
|
||||
propertyName={propertyTemplate.name}
|
||||
propertyType={propertyTemplate.type}
|
||||
onTypeAndNameChanged={(newType: PropertyType, newName: string) => mutator.changePropertyTypeAndName(board, cards, propertyTemplate, newType, newName)}
|
||||
onDelete={(id: string) => mutator.deleteProperty(board, views, cards, id)}
|
||||
onDelete={(id: string) => {
|
||||
setDeletingPropId(id)
|
||||
setDeletingPropName(propertyTemplate.name)
|
||||
setShowConfirmationDialog(true)
|
||||
}
|
||||
}
|
||||
/>
|
||||
</MenuWrapper>
|
||||
}
|
||||
@ -64,6 +77,26 @@ const CardDetailProperties = React.memo((props: Props) => {
|
||||
)
|
||||
})}
|
||||
|
||||
{showConfirmationDialog && (
|
||||
<ConfirmationDialogBox
|
||||
propertyId={deletingPropId}
|
||||
onClose={() => setShowConfirmationDialog(false)}
|
||||
onConfirm={() => {
|
||||
mutator.deleteProperty(board, views, cards, deletingPropId)
|
||||
setShowConfirmationDialog(false)
|
||||
sendFlashMessage({content: intl.formatMessage({id: 'CardDetailProperty.property-deleted', defaultMessage: 'Deleted {propertyName} Successfully!'}, {propertyName: deletingPropName}), severity: 'high'})
|
||||
}}
|
||||
|
||||
heading={intl.formatMessage({id: 'CardDetailProperty.confirm-delete', defaultMessage: 'Confirm Delete Property'})}
|
||||
subText={intl.formatMessage({
|
||||
id: 'CardDetailProperty.confirm-delete-subtext',
|
||||
defaultMessage: 'Are you sure you want to delete the property "{propertyName}"? Deleting it will delete the property from all cards in this board.',
|
||||
},
|
||||
{propertyName: deletingPropName})
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
{!props.readonly &&
|
||||
<div className='octo-propertyname add-property'>
|
||||
<Button
|
||||
|
@ -4,6 +4,7 @@ import {render, screen} from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import React from 'react'
|
||||
import {Provider as ReduxProvider} from 'react-redux'
|
||||
import moment from 'moment'
|
||||
|
||||
import {mocked} from 'ts-jest/utils'
|
||||
|
||||
@ -37,9 +38,20 @@ describe('components/cardDetail/comment', () => {
|
||||
},
|
||||
}
|
||||
const store = mockStateStore([], state)
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
moment.now = () => {
|
||||
return dateFixed + (24 * 60 * 60 * 1000)
|
||||
}
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
moment.now = () => {
|
||||
return Number(new Date())
|
||||
}
|
||||
})
|
||||
|
||||
test('return comment', () => {
|
||||
const {container} = render(wrapIntl(
|
||||
<ReduxProvider store={store}>
|
||||
|
@ -13,6 +13,7 @@ import Menu from '../../widgets/menu'
|
||||
import MenuWrapper from '../../widgets/menuWrapper'
|
||||
import {getUser} from '../../store/users'
|
||||
import {useAppSelector} from '../../store/hooks'
|
||||
import Tooltip from '../../widgets/tooltip'
|
||||
|
||||
import './comment.scss'
|
||||
|
||||
@ -28,6 +29,7 @@ const Comment: FC<Props> = (props: Props) => {
|
||||
const intl = useIntl()
|
||||
const html = Utils.htmlFromMarkdown(comment.title)
|
||||
const user = useAppSelector(getUser(userId))
|
||||
const date = new Date(comment.createAt)
|
||||
|
||||
return (
|
||||
<div
|
||||
@ -40,9 +42,11 @@ const Comment: FC<Props> = (props: Props) => {
|
||||
src={userImageUrl}
|
||||
/>
|
||||
<div className='comment-username'>{user?.username}</div>
|
||||
<div className='comment-date'>
|
||||
{Utils.displayDateTime(new Date(comment.createAt), intl)}
|
||||
</div>
|
||||
<Tooltip title={Utils.displayDateTime(date, intl)}>
|
||||
<div className='comment-date'>
|
||||
{Utils.relativeDisplayDateTime(date, intl)}
|
||||
</div>
|
||||
</Tooltip>
|
||||
|
||||
{!props.readonly && (
|
||||
<MenuWrapper>
|
||||
|
211
webapp/src/components/cardDialog.test.tsx
Normal file
211
webapp/src/components/cardDialog.test.tsx
Normal file
@ -0,0 +1,211 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import '@testing-library/jest-dom'
|
||||
import {act, render, screen} from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
|
||||
import React from 'react'
|
||||
import {Provider as ReduxProvider} from 'react-redux'
|
||||
import {mocked} from 'ts-jest/utils'
|
||||
|
||||
import mutator from '../mutator'
|
||||
import {Utils} from '../utils'
|
||||
import {TestBlockFactory} from '../test/testBlockFactory'
|
||||
import {mockDOM, mockStateStore, wrapDNDIntl} from '../testUtils'
|
||||
|
||||
import CardDialog from './cardDialog'
|
||||
|
||||
jest.mock('../mutator')
|
||||
jest.mock('../utils')
|
||||
const mockedUtils = mocked(Utils, true)
|
||||
const mockedMutator = mocked(mutator, true)
|
||||
mockedUtils.createGuid.mockReturnValue('test-id')
|
||||
|
||||
beforeAll(() => {
|
||||
mockDOM()
|
||||
})
|
||||
describe('components/cardDialog', () => {
|
||||
const board = TestBlockFactory.createBoard()
|
||||
board.fields.cardProperties = []
|
||||
board.id = 'test-id'
|
||||
board.rootId = board.id
|
||||
const boardView = TestBlockFactory.createBoardView(board)
|
||||
boardView.id = board.id
|
||||
const card = TestBlockFactory.createCard(board)
|
||||
card.id = board.id
|
||||
card.createdBy = 'user-id-1'
|
||||
|
||||
const state = {
|
||||
comments: {
|
||||
comments: {},
|
||||
},
|
||||
contents: {},
|
||||
cards: {
|
||||
cards: {
|
||||
[card.id]: card,
|
||||
},
|
||||
},
|
||||
}
|
||||
const store = mockStateStore([], state)
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
test('should match snapshot', async () => {
|
||||
let container
|
||||
await act(async () => {
|
||||
const result = render(wrapDNDIntl(
|
||||
<ReduxProvider store={store}>
|
||||
<CardDialog
|
||||
board={board}
|
||||
activeView={boardView}
|
||||
views={[boardView]}
|
||||
cards={[card]}
|
||||
cardId={card.id}
|
||||
onClose={jest.fn()}
|
||||
showCard={jest.fn()}
|
||||
readonly={false}
|
||||
/>
|
||||
</ReduxProvider>,
|
||||
))
|
||||
container = result.container
|
||||
})
|
||||
expect(container).toMatchSnapshot()
|
||||
})
|
||||
test('return a cardDialog readonly', async () => {
|
||||
let container
|
||||
await act(async () => {
|
||||
const result = render(wrapDNDIntl(
|
||||
<ReduxProvider store={store}>
|
||||
<CardDialog
|
||||
board={board}
|
||||
activeView={boardView}
|
||||
views={[boardView]}
|
||||
cards={[card]}
|
||||
cardId={card.id}
|
||||
onClose={jest.fn()}
|
||||
showCard={jest.fn()}
|
||||
readonly={false}
|
||||
/>
|
||||
</ReduxProvider>,
|
||||
))
|
||||
container = result.container
|
||||
})
|
||||
expect(container).toMatchSnapshot()
|
||||
})
|
||||
test('return cardDialog and do a close action', async () => {
|
||||
const closeFn = jest.fn()
|
||||
await act(async () => {
|
||||
render(wrapDNDIntl(
|
||||
<ReduxProvider store={store}>
|
||||
<CardDialog
|
||||
board={board}
|
||||
activeView={boardView}
|
||||
views={[boardView]}
|
||||
cards={[card]}
|
||||
cardId={card.id}
|
||||
onClose={closeFn}
|
||||
showCard={jest.fn()}
|
||||
readonly={false}
|
||||
/>
|
||||
</ReduxProvider>,
|
||||
))
|
||||
})
|
||||
const buttonElement = screen.getByRole('button', {name: 'Close dialog'})
|
||||
userEvent.click(buttonElement)
|
||||
expect(closeFn).toBeCalledTimes(1)
|
||||
})
|
||||
test('return cardDialog menu content', async () => {
|
||||
let container
|
||||
await act(async () => {
|
||||
const result = render(wrapDNDIntl(
|
||||
<ReduxProvider store={store}>
|
||||
<CardDialog
|
||||
board={board}
|
||||
activeView={boardView}
|
||||
views={[boardView]}
|
||||
cards={[card]}
|
||||
cardId={card.id}
|
||||
onClose={jest.fn()}
|
||||
showCard={jest.fn()}
|
||||
readonly={false}
|
||||
/>
|
||||
</ReduxProvider>,
|
||||
))
|
||||
container = result.container
|
||||
})
|
||||
const buttonMenu = screen.getAllByRole('button', {name: 'menuwrapper'})[0]
|
||||
userEvent.click(buttonMenu)
|
||||
expect(container).toMatchSnapshot()
|
||||
})
|
||||
test('return cardDialog menu content and verify delete action', async () => {
|
||||
await act(async () => {
|
||||
render(wrapDNDIntl(
|
||||
<ReduxProvider store={store}>
|
||||
<CardDialog
|
||||
board={board}
|
||||
activeView={boardView}
|
||||
views={[boardView]}
|
||||
cards={[card]}
|
||||
cardId={card.id}
|
||||
onClose={jest.fn()}
|
||||
showCard={jest.fn()}
|
||||
readonly={false}
|
||||
/>
|
||||
</ReduxProvider>,
|
||||
))
|
||||
})
|
||||
const buttonMenu = screen.getAllByRole('button', {name: 'menuwrapper'})[0]
|
||||
userEvent.click(buttonMenu)
|
||||
const buttonDelete = screen.getByRole('button', {name: 'Delete'})
|
||||
userEvent.click(buttonDelete)
|
||||
expect(mockedMutator.deleteBlock).toBeCalledTimes(1)
|
||||
})
|
||||
test('return cardDialog menu content and do a New template from card', async () => {
|
||||
await act(async () => {
|
||||
render(wrapDNDIntl(
|
||||
<ReduxProvider store={store}>
|
||||
<CardDialog
|
||||
board={board}
|
||||
activeView={boardView}
|
||||
views={[boardView]}
|
||||
cards={[card]}
|
||||
cardId={card.id}
|
||||
onClose={jest.fn()}
|
||||
showCard={jest.fn()}
|
||||
readonly={false}
|
||||
/>
|
||||
</ReduxProvider>,
|
||||
))
|
||||
})
|
||||
const buttonMenu = screen.getAllByRole('button', {name: 'menuwrapper'})[0]
|
||||
userEvent.click(buttonMenu)
|
||||
const buttonTemplate = screen.getByRole('button', {name: 'New template from card'})
|
||||
userEvent.click(buttonTemplate)
|
||||
expect(mockedMutator.duplicateCard).toBeCalledTimes(1)
|
||||
})
|
||||
|
||||
test('return cardDialog menu content and do a copy Link', async () => {
|
||||
await act(async () => {
|
||||
render(wrapDNDIntl(
|
||||
<ReduxProvider store={store}>
|
||||
<CardDialog
|
||||
board={board}
|
||||
activeView={boardView}
|
||||
views={[boardView]}
|
||||
cards={[card]}
|
||||
cardId={card.id}
|
||||
onClose={jest.fn()}
|
||||
showCard={jest.fn()}
|
||||
readonly={false}
|
||||
/>
|
||||
</ReduxProvider>,
|
||||
))
|
||||
})
|
||||
const buttonMenu = screen.getAllByRole('button', {name: 'menuwrapper'})[0]
|
||||
userEvent.click(buttonMenu)
|
||||
const buttonCopy = screen.getByRole('button', {name: 'Copy link'})
|
||||
userEvent.click(buttonCopy)
|
||||
expect(mockedUtils.copyTextToClipboard).toBeCalledTimes(1)
|
||||
})
|
||||
})
|
60
webapp/src/components/confirmationDialogBox.scss
Normal file
60
webapp/src/components/confirmationDialogBox.scss
Normal file
@ -0,0 +1,60 @@
|
||||
.confirmation-dialog-box {
|
||||
.dialog {
|
||||
position: fixed;
|
||||
top: 30%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: max-content;
|
||||
height: max-content;
|
||||
z-index: 300;
|
||||
|
||||
background-color: rgb(var(--center-channel-bg-rgb));
|
||||
box-shadow: rgba(var(--center-channel-color-rgb), 0.1) 0 0 0 1px,
|
||||
rgba(var(--center-channel-color-rgb), 0.1) 0 2px 4px;
|
||||
|
||||
border-radius: var(--modal-rad);
|
||||
padding: 0;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
|
||||
> .toolbar {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
padding: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.box-area {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
|
||||
.heading {
|
||||
margin-top: 2rem;
|
||||
padding: 2px 4px;
|
||||
}
|
||||
|
||||
.sub-text {
|
||||
width: 26rem;
|
||||
word-wrap: normal;
|
||||
margin: 0.5rem 3rem;
|
||||
padding: 2px;
|
||||
|
||||
@media screen and (max-width: 400px) {
|
||||
width: 12rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
margin: 1rem;
|
||||
justify-content: space-between;
|
||||
|
||||
.Button {
|
||||
margin: 2px 1rem;
|
||||
}
|
||||
}
|
56
webapp/src/components/confirmationDialogBox.tsx
Normal file
56
webapp/src/components/confirmationDialogBox.tsx
Normal file
@ -0,0 +1,56 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react'
|
||||
import {FormattedMessage} from 'react-intl'
|
||||
|
||||
import Button from '../widgets/buttons/button'
|
||||
|
||||
import Dialog from './dialog'
|
||||
import './confirmationDialogBox.scss'
|
||||
|
||||
type Props = {
|
||||
propertyId: string;
|
||||
onClose: () => void;
|
||||
onConfirm: () => void;
|
||||
heading: string;
|
||||
subText?: string;
|
||||
}
|
||||
|
||||
export const ConfirmationDialogBox = (props: Props) => {
|
||||
return (
|
||||
<Dialog
|
||||
className='confirmation-dialog-box'
|
||||
onClose={props.onClose}
|
||||
>
|
||||
<div className='box-area'>
|
||||
<h3 className='heading'>{props.heading}</h3>
|
||||
<p className='sub-text'>{props.subText}</p>
|
||||
|
||||
<div className='action-buttons'>
|
||||
<Button
|
||||
title='Cancel'
|
||||
active={true}
|
||||
onClick={props.onClose}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='ConfirmationDialog.cancel-action'
|
||||
defaultMessage='Cancel'
|
||||
/>
|
||||
</Button>
|
||||
<Button
|
||||
title='Delete'
|
||||
submit={true}
|
||||
emphasis='danger'
|
||||
onClick={props.onConfirm}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='ConfirmationDialog.delete-action'
|
||||
defaultMessage='Delete'
|
||||
/>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`components/content/contentElement return an element 1`] = `
|
||||
exports[`components/content/contentElement should match snapshot for checkbox type 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="CheckboxElement"
|
||||
|
@ -3,14 +3,15 @@
|
||||
exports[`components/content/TextElement return a textElement 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="MarkdownEditor octo-editor "
|
||||
class="MarkdownEditor octo-editor active"
|
||||
>
|
||||
<div
|
||||
class="octo-editor-preview octo-placeholder"
|
||||
style="display: none;"
|
||||
/>
|
||||
<div
|
||||
class="octo-editor-active Editor"
|
||||
style="visibility: hidden; position: absolute; top: 0px; left: 0px;"
|
||||
style=""
|
||||
>
|
||||
<div
|
||||
id="test-id-wrapper"
|
||||
@ -140,6 +141,7 @@ exports[`components/content/TextElement return a textElement and do a blur event
|
||||
>
|
||||
<div
|
||||
class="octo-editor-preview octo-placeholder"
|
||||
style=""
|
||||
/>
|
||||
<div
|
||||
class="octo-editor-active Editor"
|
||||
|
@ -1,52 +1,67 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import React, {ReactElement, ReactNode} from 'react'
|
||||
|
||||
import '@testing-library/jest-dom'
|
||||
|
||||
import {render} from '@testing-library/react'
|
||||
|
||||
import {wrapIntl} from '../../testUtils'
|
||||
|
||||
import {ContentBlock} from '../../blocks/contentBlock'
|
||||
|
||||
import {CardDetailProvider} from '../cardDetail/cardDetailContext'
|
||||
import {TestBlockFactory} from '../../test/testBlockFactory'
|
||||
|
||||
import ContentElement from './contentElement'
|
||||
|
||||
const board = TestBlockFactory.createBoard()
|
||||
const card = TestBlockFactory.createCard(board)
|
||||
const contentBlock: ContentBlock = {
|
||||
id: 'test-id',
|
||||
workspaceId: '',
|
||||
parentId: card.id,
|
||||
rootId: card.rootId,
|
||||
modifiedBy: 'test-user-id',
|
||||
schema: 0,
|
||||
type: 'checkbox',
|
||||
title: 'test-title',
|
||||
fields: {},
|
||||
createdBy: 'test-user-id',
|
||||
createAt: 0,
|
||||
updateAt: 0,
|
||||
deleteAt: 0,
|
||||
}
|
||||
|
||||
const wrap = (child: ReactNode): ReactElement => (
|
||||
wrapIntl(
|
||||
<CardDetailProvider card={card}>
|
||||
{child}
|
||||
</CardDetailProvider>,
|
||||
)
|
||||
)
|
||||
|
||||
describe('components/content/contentElement', () => {
|
||||
test('return an element', () => {
|
||||
const contentBlock: ContentBlock = {
|
||||
id: 'test-id',
|
||||
workspaceId: '',
|
||||
parentId: '',
|
||||
rootId: '',
|
||||
modifiedBy: 'test-user-id',
|
||||
schema: 0,
|
||||
type: 'checkbox',
|
||||
title: 'test-title',
|
||||
fields: {},
|
||||
createdBy: 'test-user-id',
|
||||
createAt: 0,
|
||||
updateAt: 0,
|
||||
deleteAt: 0,
|
||||
}
|
||||
const checkBoxElement = ContentElement({block: contentBlock, readonly: false})
|
||||
const {container} = render(wrapIntl(checkBoxElement))
|
||||
it('should match snapshot for checkbox type', () => {
|
||||
const {container} = render(wrap(
|
||||
<ContentElement
|
||||
block={contentBlock}
|
||||
readonly={false}
|
||||
cords={{x: 0}}
|
||||
/>,
|
||||
))
|
||||
expect(container).toMatchSnapshot()
|
||||
})
|
||||
|
||||
test('return null', () => {
|
||||
const contentBlock: ContentBlock = {
|
||||
id: 'test-id',
|
||||
workspaceId: '',
|
||||
parentId: '',
|
||||
rootId: '',
|
||||
modifiedBy: 'test-user-id',
|
||||
schema: 0,
|
||||
type: 'unknown',
|
||||
title: 'test-title',
|
||||
fields: {},
|
||||
createdBy: 'test-user-id',
|
||||
createAt: 0,
|
||||
updateAt: 0,
|
||||
deleteAt: 0,
|
||||
}
|
||||
const contentElement = ContentElement({block: contentBlock, readonly: false})
|
||||
expect(contentElement).toBeNull()
|
||||
it('should return null for unknown type', () => {
|
||||
const block: ContentBlock = {...contentBlock, type: 'unknown'}
|
||||
const {container} = render(wrap(
|
||||
<ContentElement
|
||||
block={block}
|
||||
readonly={false}
|
||||
cords={{x: 0}}
|
||||
/>,
|
||||
))
|
||||
expect(container).toBeEmptyDOMElement()
|
||||
})
|
||||
})
|
||||
|
@ -12,7 +12,7 @@ import './dialog.scss'
|
||||
|
||||
type Props = {
|
||||
children: React.ReactNode
|
||||
toolsMenu: React.ReactNode
|
||||
toolsMenu?: React.ReactNode // some dialogs may not require a toolmenu
|
||||
hideCloseButton?: boolean
|
||||
className?: string
|
||||
onClose: () => void,
|
||||
|
@ -17,7 +17,7 @@
|
||||
font-size: 18px;
|
||||
vertical-align: middle;
|
||||
border-radius: 20px;
|
||||
z-index: 12;
|
||||
z-index: 999;
|
||||
|
||||
&.flashIn {
|
||||
visibility: visible;
|
||||
|
@ -0,0 +1,247 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`src/components/gallery/Gallery return Gallery and click new 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="Gallery"
|
||||
>
|
||||
<div
|
||||
class="GalleryCard selected"
|
||||
draggable="true"
|
||||
style="opacity: 1;"
|
||||
>
|
||||
<div
|
||||
aria-label="menuwrapper"
|
||||
class="MenuWrapper optionsMenu"
|
||||
role="button"
|
||||
>
|
||||
<button
|
||||
class="Button IconButton"
|
||||
type="button"
|
||||
>
|
||||
<i
|
||||
class="CompassIcon icon-dots-horizontal OptionsIcon"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
class="gallery-item"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="GalleryCard"
|
||||
draggable="true"
|
||||
style="opacity: 1;"
|
||||
>
|
||||
<div
|
||||
aria-label="menuwrapper"
|
||||
class="MenuWrapper optionsMenu"
|
||||
role="button"
|
||||
>
|
||||
<button
|
||||
class="Button IconButton"
|
||||
type="button"
|
||||
>
|
||||
<i
|
||||
class="CompassIcon icon-dots-horizontal OptionsIcon"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
class="gallery-item"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="octo-gallery-new"
|
||||
>
|
||||
+ New
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`src/components/gallery/Gallery return Gallery readonly 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="Gallery"
|
||||
>
|
||||
<div
|
||||
class="GalleryCard selected"
|
||||
draggable="true"
|
||||
style="opacity: 1;"
|
||||
>
|
||||
<div
|
||||
class="gallery-item"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="GalleryCard"
|
||||
draggable="true"
|
||||
style="opacity: 1;"
|
||||
>
|
||||
<div
|
||||
class="gallery-item"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`src/components/gallery/Gallery should match snapshot 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="Gallery"
|
||||
>
|
||||
<div
|
||||
class="GalleryCard selected"
|
||||
draggable="true"
|
||||
style="opacity: 1;"
|
||||
>
|
||||
<div
|
||||
aria-label="menuwrapper"
|
||||
class="MenuWrapper optionsMenu"
|
||||
role="button"
|
||||
>
|
||||
<button
|
||||
class="Button IconButton"
|
||||
type="button"
|
||||
>
|
||||
<i
|
||||
class="CompassIcon icon-dots-horizontal OptionsIcon"
|
||||
/>
|
||||
</button>
|
||||
<div
|
||||
class="Menu noselect left"
|
||||
>
|
||||
<div
|
||||
class="menu-contents"
|
||||
>
|
||||
<div
|
||||
class="menu-options"
|
||||
>
|
||||
<div
|
||||
aria-label="Delete"
|
||||
class="MenuOption TextOption menu-option"
|
||||
role="button"
|
||||
>
|
||||
<svg
|
||||
class="DeleteIcon Icon"
|
||||
viewBox="0 0 448 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M268 416h24a12 12 0 0 0 12-12V188a12 12 0 0 0-12-12h-24a12 12 0 0 0-12 12v216a12 12 0 0 0 12 12zM432 80h-82.41l-34-56.7A48 48 0 0 0 274.41 0H173.59a48 48 0 0 0-41.16 23.3L98.41 80H16A16 16 0 0 0 0 96v16a16 16 0 0 0 16 16h16v336a48 48 0 0 0 48 48h288a48 48 0 0 0 48-48V128h16a16 16 0 0 0 16-16V96a16 16 0 0 0-16-16zM171.84 50.91A6 6 0 0 1 177 48h94a6 6 0 0 1 5.15 2.91L293.61 80H154.39zM368 464H80V128h288zm-212-48h24a12 12 0 0 0 12-12V188a12 12 0 0 0-12-12h-24a12 12 0 0 0-12 12v216a12 12 0 0 0 12 12z"
|
||||
/>
|
||||
</svg>
|
||||
<div
|
||||
class="menu-name"
|
||||
>
|
||||
Delete
|
||||
</div>
|
||||
<div
|
||||
class="noicon"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
aria-label="Duplicate"
|
||||
class="MenuOption TextOption menu-option"
|
||||
role="button"
|
||||
>
|
||||
<svg
|
||||
class="DuplicateIcon Icon"
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M464 0H144c-26.51 0-48 21.49-48 48v48H48c-26.51 0-48 21.49-48 48v320c0 26.51 21.49 48 48 48h320c26.51 0 48-21.49 48-48v-48h48c26.51 0 48-21.49 48-48V48c0-26.51-21.49-48-48-48zM362 464H54a6 6 0 0 1-6-6V150a6 6 0 0 1 6-6h42v224c0 26.51 21.49 48 48 48h224v42a6 6 0 0 1-6 6zm96-96H150a6 6 0 0 1-6-6V54a6 6 0 0 1 6-6h308a6 6 0 0 1 6 6v308a6 6 0 0 1-6 6z"
|
||||
/>
|
||||
</svg>
|
||||
<div
|
||||
class="menu-name"
|
||||
>
|
||||
Duplicate
|
||||
</div>
|
||||
<div
|
||||
class="noicon"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
aria-label="Copy link"
|
||||
class="MenuOption TextOption menu-option"
|
||||
role="button"
|
||||
>
|
||||
<i
|
||||
class="CompassIcon icon-link-variant LinkIcon"
|
||||
/>
|
||||
<div
|
||||
class="menu-name"
|
||||
>
|
||||
Copy link
|
||||
</div>
|
||||
<div
|
||||
class="noicon"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="menu-spacer hideOnWidescreen"
|
||||
/>
|
||||
<div
|
||||
class="menu-options hideOnWidescreen"
|
||||
>
|
||||
<div
|
||||
aria-label="Cancel"
|
||||
class="MenuOption TextOption menu-option menu-cancel"
|
||||
role="button"
|
||||
>
|
||||
<div
|
||||
class="noicon"
|
||||
/>
|
||||
<div
|
||||
class="menu-name"
|
||||
>
|
||||
Cancel
|
||||
</div>
|
||||
<div
|
||||
class="noicon"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="gallery-item"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="GalleryCard"
|
||||
draggable="true"
|
||||
style="opacity: 1;"
|
||||
>
|
||||
<div
|
||||
aria-label="menuwrapper"
|
||||
class="MenuWrapper optionsMenu"
|
||||
role="button"
|
||||
>
|
||||
<button
|
||||
class="Button IconButton"
|
||||
type="button"
|
||||
>
|
||||
<i
|
||||
class="CompassIcon icon-dots-horizontal OptionsIcon"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
class="gallery-item"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="octo-gallery-new"
|
||||
>
|
||||
+ New
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
@ -0,0 +1,940 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`src/components/gallery/GalleryCard with a comment content return GalleryCard with content readonly 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="GalleryCard selected"
|
||||
draggable="true"
|
||||
style="opacity: 1;"
|
||||
>
|
||||
<div
|
||||
class="gallery-item"
|
||||
/>
|
||||
<div
|
||||
class="gallery-title"
|
||||
>
|
||||
<div
|
||||
class="octo-icon"
|
||||
>
|
||||
i
|
||||
</div>
|
||||
<div>
|
||||
title
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`src/components/gallery/GalleryCard with a comment content should match snapshot 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="GalleryCard selected"
|
||||
draggable="true"
|
||||
style="opacity: 1;"
|
||||
>
|
||||
<div
|
||||
aria-label="menuwrapper"
|
||||
class="MenuWrapper optionsMenu"
|
||||
role="button"
|
||||
>
|
||||
<div
|
||||
class="Button IconButton"
|
||||
role="button"
|
||||
>
|
||||
<i
|
||||
class="CompassIcon icon-dots-horizontal OptionsIcon"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="Menu noselect left"
|
||||
>
|
||||
<div
|
||||
class="menu-contents"
|
||||
>
|
||||
<div
|
||||
class="menu-options"
|
||||
>
|
||||
<div
|
||||
aria-label="Delete"
|
||||
class="MenuOption TextOption menu-option"
|
||||
role="button"
|
||||
>
|
||||
<svg
|
||||
class="DeleteIcon Icon"
|
||||
viewBox="0 0 448 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M268 416h24a12 12 0 0 0 12-12V188a12 12 0 0 0-12-12h-24a12 12 0 0 0-12 12v216a12 12 0 0 0 12 12zM432 80h-82.41l-34-56.7A48 48 0 0 0 274.41 0H173.59a48 48 0 0 0-41.16 23.3L98.41 80H16A16 16 0 0 0 0 96v16a16 16 0 0 0 16 16h16v336a48 48 0 0 0 48 48h288a48 48 0 0 0 48-48V128h16a16 16 0 0 0 16-16V96a16 16 0 0 0-16-16zM171.84 50.91A6 6 0 0 1 177 48h94a6 6 0 0 1 5.15 2.91L293.61 80H154.39zM368 464H80V128h288zm-212-48h24a12 12 0 0 0 12-12V188a12 12 0 0 0-12-12h-24a12 12 0 0 0-12 12v216a12 12 0 0 0 12 12z"
|
||||
/>
|
||||
</svg>
|
||||
<div
|
||||
class="menu-name"
|
||||
>
|
||||
Delete
|
||||
</div>
|
||||
<div
|
||||
class="noicon"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
aria-label="Duplicate"
|
||||
class="MenuOption TextOption menu-option"
|
||||
role="button"
|
||||
>
|
||||
<svg
|
||||
class="DuplicateIcon Icon"
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M464 0H144c-26.51 0-48 21.49-48 48v48H48c-26.51 0-48 21.49-48 48v320c0 26.51 21.49 48 48 48h320c26.51 0 48-21.49 48-48v-48h48c26.51 0 48-21.49 48-48V48c0-26.51-21.49-48-48-48zM362 464H54a6 6 0 0 1-6-6V150a6 6 0 0 1 6-6h42v224c0 26.51 21.49 48 48 48h224v42a6 6 0 0 1-6 6zm96-96H150a6 6 0 0 1-6-6V54a6 6 0 0 1 6-6h308a6 6 0 0 1 6 6v308a6 6 0 0 1-6 6z"
|
||||
/>
|
||||
</svg>
|
||||
<div
|
||||
class="menu-name"
|
||||
>
|
||||
Duplicate
|
||||
</div>
|
||||
<div
|
||||
class="noicon"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
aria-label="Copy link"
|
||||
class="MenuOption TextOption menu-option"
|
||||
role="button"
|
||||
>
|
||||
<i
|
||||
class="CompassIcon icon-link-variant LinkIcon"
|
||||
/>
|
||||
<div
|
||||
class="menu-name"
|
||||
>
|
||||
Copy link
|
||||
</div>
|
||||
<div
|
||||
class="noicon"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="menu-spacer hideOnWidescreen"
|
||||
/>
|
||||
<div
|
||||
class="menu-options hideOnWidescreen"
|
||||
>
|
||||
<div
|
||||
aria-label="Cancel"
|
||||
class="MenuOption TextOption menu-option menu-cancel"
|
||||
role="button"
|
||||
>
|
||||
<div
|
||||
class="noicon"
|
||||
/>
|
||||
<div
|
||||
class="menu-name"
|
||||
>
|
||||
Cancel
|
||||
</div>
|
||||
<div
|
||||
class="noicon"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="gallery-item"
|
||||
/>
|
||||
<div
|
||||
class="gallery-title"
|
||||
>
|
||||
<div
|
||||
class="octo-icon"
|
||||
>
|
||||
i
|
||||
</div>
|
||||
<div>
|
||||
title
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`src/components/gallery/GalleryCard with an image content should match snapshot 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="GalleryCard selected"
|
||||
draggable="true"
|
||||
style="opacity: 1;"
|
||||
>
|
||||
<div
|
||||
aria-label="menuwrapper"
|
||||
class="MenuWrapper optionsMenu"
|
||||
role="button"
|
||||
>
|
||||
<div
|
||||
class="Button IconButton"
|
||||
role="button"
|
||||
>
|
||||
<i
|
||||
class="CompassIcon icon-dots-horizontal OptionsIcon"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="Menu noselect left"
|
||||
>
|
||||
<div
|
||||
class="menu-contents"
|
||||
>
|
||||
<div
|
||||
class="menu-options"
|
||||
>
|
||||
<div
|
||||
aria-label="Delete"
|
||||
class="MenuOption TextOption menu-option"
|
||||
role="button"
|
||||
>
|
||||
<svg
|
||||
class="DeleteIcon Icon"
|
||||
viewBox="0 0 448 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M268 416h24a12 12 0 0 0 12-12V188a12 12 0 0 0-12-12h-24a12 12 0 0 0-12 12v216a12 12 0 0 0 12 12zM432 80h-82.41l-34-56.7A48 48 0 0 0 274.41 0H173.59a48 48 0 0 0-41.16 23.3L98.41 80H16A16 16 0 0 0 0 96v16a16 16 0 0 0 16 16h16v336a48 48 0 0 0 48 48h288a48 48 0 0 0 48-48V128h16a16 16 0 0 0 16-16V96a16 16 0 0 0-16-16zM171.84 50.91A6 6 0 0 1 177 48h94a6 6 0 0 1 5.15 2.91L293.61 80H154.39zM368 464H80V128h288zm-212-48h24a12 12 0 0 0 12-12V188a12 12 0 0 0-12-12h-24a12 12 0 0 0-12 12v216a12 12 0 0 0 12 12z"
|
||||
/>
|
||||
</svg>
|
||||
<div
|
||||
class="menu-name"
|
||||
>
|
||||
Delete
|
||||
</div>
|
||||
<div
|
||||
class="noicon"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
aria-label="Duplicate"
|
||||
class="MenuOption TextOption menu-option"
|
||||
role="button"
|
||||
>
|
||||
<svg
|
||||
class="DuplicateIcon Icon"
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M464 0H144c-26.51 0-48 21.49-48 48v48H48c-26.51 0-48 21.49-48 48v320c0 26.51 21.49 48 48 48h320c26.51 0 48-21.49 48-48v-48h48c26.51 0 48-21.49 48-48V48c0-26.51-21.49-48-48-48zM362 464H54a6 6 0 0 1-6-6V150a6 6 0 0 1 6-6h42v224c0 26.51 21.49 48 48 48h224v42a6 6 0 0 1-6 6zm96-96H150a6 6 0 0 1-6-6V54a6 6 0 0 1 6-6h308a6 6 0 0 1 6 6v308a6 6 0 0 1-6 6z"
|
||||
/>
|
||||
</svg>
|
||||
<div
|
||||
class="menu-name"
|
||||
>
|
||||
Duplicate
|
||||
</div>
|
||||
<div
|
||||
class="noicon"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
aria-label="Copy link"
|
||||
class="MenuOption TextOption menu-option"
|
||||
role="button"
|
||||
>
|
||||
<i
|
||||
class="CompassIcon icon-link-variant LinkIcon"
|
||||
/>
|
||||
<div
|
||||
class="menu-name"
|
||||
>
|
||||
Copy link
|
||||
</div>
|
||||
<div
|
||||
class="noicon"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="menu-spacer hideOnWidescreen"
|
||||
/>
|
||||
<div
|
||||
class="menu-options hideOnWidescreen"
|
||||
>
|
||||
<div
|
||||
aria-label="Cancel"
|
||||
class="MenuOption TextOption menu-option menu-cancel"
|
||||
role="button"
|
||||
>
|
||||
<div
|
||||
class="noicon"
|
||||
/>
|
||||
<div
|
||||
class="menu-name"
|
||||
>
|
||||
Cancel
|
||||
</div>
|
||||
<div
|
||||
class="noicon"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="gallery-image"
|
||||
>
|
||||
<img
|
||||
alt=""
|
||||
class="ImageElement"
|
||||
src="test.jpg"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="gallery-title"
|
||||
>
|
||||
<div
|
||||
class="octo-icon"
|
||||
>
|
||||
i
|
||||
</div>
|
||||
<div>
|
||||
title
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`src/components/gallery/GalleryCard with many contents return GalleryCard with contents readonly 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="GalleryCard selected"
|
||||
draggable="true"
|
||||
style="opacity: 1;"
|
||||
>
|
||||
<div
|
||||
class="gallery-item"
|
||||
>
|
||||
<div
|
||||
class="DividerElement"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="gallery-title"
|
||||
>
|
||||
<div
|
||||
class="octo-icon"
|
||||
>
|
||||
i
|
||||
</div>
|
||||
<div>
|
||||
title
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`src/components/gallery/GalleryCard with many contents should match snapshot 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="GalleryCard selected"
|
||||
draggable="true"
|
||||
style="opacity: 1;"
|
||||
>
|
||||
<div
|
||||
aria-label="menuwrapper"
|
||||
class="MenuWrapper optionsMenu"
|
||||
role="button"
|
||||
>
|
||||
<div
|
||||
class="Button IconButton"
|
||||
role="button"
|
||||
>
|
||||
<i
|
||||
class="CompassIcon icon-dots-horizontal OptionsIcon"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="Menu noselect left"
|
||||
>
|
||||
<div
|
||||
class="menu-contents"
|
||||
>
|
||||
<div
|
||||
class="menu-options"
|
||||
>
|
||||
<div
|
||||
aria-label="Delete"
|
||||
class="MenuOption TextOption menu-option"
|
||||
role="button"
|
||||
>
|
||||
<svg
|
||||
class="DeleteIcon Icon"
|
||||
viewBox="0 0 448 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M268 416h24a12 12 0 0 0 12-12V188a12 12 0 0 0-12-12h-24a12 12 0 0 0-12 12v216a12 12 0 0 0 12 12zM432 80h-82.41l-34-56.7A48 48 0 0 0 274.41 0H173.59a48 48 0 0 0-41.16 23.3L98.41 80H16A16 16 0 0 0 0 96v16a16 16 0 0 0 16 16h16v336a48 48 0 0 0 48 48h288a48 48 0 0 0 48-48V128h16a16 16 0 0 0 16-16V96a16 16 0 0 0-16-16zM171.84 50.91A6 6 0 0 1 177 48h94a6 6 0 0 1 5.15 2.91L293.61 80H154.39zM368 464H80V128h288zm-212-48h24a12 12 0 0 0 12-12V188a12 12 0 0 0-12-12h-24a12 12 0 0 0-12 12v216a12 12 0 0 0 12 12z"
|
||||
/>
|
||||
</svg>
|
||||
<div
|
||||
class="menu-name"
|
||||
>
|
||||
Delete
|
||||
</div>
|
||||
<div
|
||||
class="noicon"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
aria-label="Duplicate"
|
||||
class="MenuOption TextOption menu-option"
|
||||
role="button"
|
||||
>
|
||||
<svg
|
||||
class="DuplicateIcon Icon"
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M464 0H144c-26.51 0-48 21.49-48 48v48H48c-26.51 0-48 21.49-48 48v320c0 26.51 21.49 48 48 48h320c26.51 0 48-21.49 48-48v-48h48c26.51 0 48-21.49 48-48V48c0-26.51-21.49-48-48-48zM362 464H54a6 6 0 0 1-6-6V150a6 6 0 0 1 6-6h42v224c0 26.51 21.49 48 48 48h224v42a6 6 0 0 1-6 6zm96-96H150a6 6 0 0 1-6-6V54a6 6 0 0 1 6-6h308a6 6 0 0 1 6 6v308a6 6 0 0 1-6 6z"
|
||||
/>
|
||||
</svg>
|
||||
<div
|
||||
class="menu-name"
|
||||
>
|
||||
Duplicate
|
||||
</div>
|
||||
<div
|
||||
class="noicon"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
aria-label="Copy link"
|
||||
class="MenuOption TextOption menu-option"
|
||||
role="button"
|
||||
>
|
||||
<i
|
||||
class="CompassIcon icon-link-variant LinkIcon"
|
||||
/>
|
||||
<div
|
||||
class="menu-name"
|
||||
>
|
||||
Copy link
|
||||
</div>
|
||||
<div
|
||||
class="noicon"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="menu-spacer hideOnWidescreen"
|
||||
/>
|
||||
<div
|
||||
class="menu-options hideOnWidescreen"
|
||||
>
|
||||
<div
|
||||
aria-label="Cancel"
|
||||
class="MenuOption TextOption menu-option menu-cancel"
|
||||
role="button"
|
||||
>
|
||||
<div
|
||||
class="noicon"
|
||||
/>
|
||||
<div
|
||||
class="menu-name"
|
||||
>
|
||||
Cancel
|
||||
</div>
|
||||
<div
|
||||
class="noicon"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="gallery-item"
|
||||
>
|
||||
<div
|
||||
class="DividerElement"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="gallery-title"
|
||||
>
|
||||
<div
|
||||
class="octo-icon"
|
||||
>
|
||||
i
|
||||
</div>
|
||||
<div>
|
||||
title
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`src/components/gallery/GalleryCard with many images content should match snapshot with only first image 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="GalleryCard selected"
|
||||
draggable="true"
|
||||
style="opacity: 1;"
|
||||
>
|
||||
<div
|
||||
aria-label="menuwrapper"
|
||||
class="MenuWrapper optionsMenu"
|
||||
role="button"
|
||||
>
|
||||
<div
|
||||
class="Button IconButton"
|
||||
role="button"
|
||||
>
|
||||
<i
|
||||
class="CompassIcon icon-dots-horizontal OptionsIcon"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="Menu noselect left"
|
||||
>
|
||||
<div
|
||||
class="menu-contents"
|
||||
>
|
||||
<div
|
||||
class="menu-options"
|
||||
>
|
||||
<div
|
||||
aria-label="Delete"
|
||||
class="MenuOption TextOption menu-option"
|
||||
role="button"
|
||||
>
|
||||
<svg
|
||||
class="DeleteIcon Icon"
|
||||
viewBox="0 0 448 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M268 416h24a12 12 0 0 0 12-12V188a12 12 0 0 0-12-12h-24a12 12 0 0 0-12 12v216a12 12 0 0 0 12 12zM432 80h-82.41l-34-56.7A48 48 0 0 0 274.41 0H173.59a48 48 0 0 0-41.16 23.3L98.41 80H16A16 16 0 0 0 0 96v16a16 16 0 0 0 16 16h16v336a48 48 0 0 0 48 48h288a48 48 0 0 0 48-48V128h16a16 16 0 0 0 16-16V96a16 16 0 0 0-16-16zM171.84 50.91A6 6 0 0 1 177 48h94a6 6 0 0 1 5.15 2.91L293.61 80H154.39zM368 464H80V128h288zm-212-48h24a12 12 0 0 0 12-12V188a12 12 0 0 0-12-12h-24a12 12 0 0 0-12 12v216a12 12 0 0 0 12 12z"
|
||||
/>
|
||||
</svg>
|
||||
<div
|
||||
class="menu-name"
|
||||
>
|
||||
Delete
|
||||
</div>
|
||||
<div
|
||||
class="noicon"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
aria-label="Duplicate"
|
||||
class="MenuOption TextOption menu-option"
|
||||
role="button"
|
||||
>
|
||||
<svg
|
||||
class="DuplicateIcon Icon"
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M464 0H144c-26.51 0-48 21.49-48 48v48H48c-26.51 0-48 21.49-48 48v320c0 26.51 21.49 48 48 48h320c26.51 0 48-21.49 48-48v-48h48c26.51 0 48-21.49 48-48V48c0-26.51-21.49-48-48-48zM362 464H54a6 6 0 0 1-6-6V150a6 6 0 0 1 6-6h42v224c0 26.51 21.49 48 48 48h224v42a6 6 0 0 1-6 6zm96-96H150a6 6 0 0 1-6-6V54a6 6 0 0 1 6-6h308a6 6 0 0 1 6 6v308a6 6 0 0 1-6 6z"
|
||||
/>
|
||||
</svg>
|
||||
<div
|
||||
class="menu-name"
|
||||
>
|
||||
Duplicate
|
||||
</div>
|
||||
<div
|
||||
class="noicon"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
aria-label="Copy link"
|
||||
class="MenuOption TextOption menu-option"
|
||||
role="button"
|
||||
>
|
||||
<i
|
||||
class="CompassIcon icon-link-variant LinkIcon"
|
||||
/>
|
||||
<div
|
||||
class="menu-name"
|
||||
>
|
||||
Copy link
|
||||
</div>
|
||||
<div
|
||||
class="noicon"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="menu-spacer hideOnWidescreen"
|
||||
/>
|
||||
<div
|
||||
class="menu-options hideOnWidescreen"
|
||||
>
|
||||
<div
|
||||
aria-label="Cancel"
|
||||
class="MenuOption TextOption menu-option menu-cancel"
|
||||
role="button"
|
||||
>
|
||||
<div
|
||||
class="noicon"
|
||||
/>
|
||||
<div
|
||||
class="menu-name"
|
||||
>
|
||||
Cancel
|
||||
</div>
|
||||
<div
|
||||
class="noicon"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="gallery-image"
|
||||
>
|
||||
<img
|
||||
alt=""
|
||||
class="ImageElement"
|
||||
src="test.jpg"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="gallery-title"
|
||||
>
|
||||
<div
|
||||
class="octo-icon"
|
||||
>
|
||||
i
|
||||
</div>
|
||||
<div>
|
||||
title
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`src/components/gallery/GalleryCard without block content return GalleryCard and cancel 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="GalleryCard selected"
|
||||
draggable="true"
|
||||
style="opacity: 1;"
|
||||
>
|
||||
<div
|
||||
aria-label="menuwrapper"
|
||||
class="MenuWrapper optionsMenu"
|
||||
role="button"
|
||||
>
|
||||
<div
|
||||
class="Button IconButton"
|
||||
role="button"
|
||||
>
|
||||
<i
|
||||
class="CompassIcon icon-dots-horizontal OptionsIcon"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="gallery-item"
|
||||
/>
|
||||
<div
|
||||
class="gallery-title"
|
||||
>
|
||||
<div
|
||||
class="octo-icon"
|
||||
>
|
||||
i
|
||||
</div>
|
||||
<div>
|
||||
title
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`src/components/gallery/GalleryCard without block content return GalleryCard and copy link 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="GalleryCard selected"
|
||||
draggable="true"
|
||||
style="opacity: 1;"
|
||||
>
|
||||
<div
|
||||
aria-label="menuwrapper"
|
||||
class="MenuWrapper optionsMenu"
|
||||
role="button"
|
||||
>
|
||||
<div
|
||||
class="Button IconButton"
|
||||
role="button"
|
||||
>
|
||||
<i
|
||||
class="CompassIcon icon-dots-horizontal OptionsIcon"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="gallery-item"
|
||||
/>
|
||||
<div
|
||||
class="gallery-title"
|
||||
>
|
||||
<div
|
||||
class="octo-icon"
|
||||
>
|
||||
i
|
||||
</div>
|
||||
<div>
|
||||
title
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`src/components/gallery/GalleryCard without block content return GalleryCard and delete card 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="GalleryCard selected"
|
||||
draggable="true"
|
||||
style="opacity: 1;"
|
||||
>
|
||||
<div
|
||||
aria-label="menuwrapper"
|
||||
class="MenuWrapper optionsMenu"
|
||||
role="button"
|
||||
>
|
||||
<div
|
||||
class="Button IconButton"
|
||||
role="button"
|
||||
>
|
||||
<i
|
||||
class="CompassIcon icon-dots-horizontal OptionsIcon"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="gallery-item"
|
||||
/>
|
||||
<div
|
||||
class="gallery-title"
|
||||
>
|
||||
<div
|
||||
class="octo-icon"
|
||||
>
|
||||
i
|
||||
</div>
|
||||
<div>
|
||||
title
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`src/components/gallery/GalleryCard without block content return GalleryCard and duplicate card 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="GalleryCard selected"
|
||||
draggable="true"
|
||||
style="opacity: 1;"
|
||||
>
|
||||
<div
|
||||
aria-label="menuwrapper"
|
||||
class="MenuWrapper optionsMenu"
|
||||
role="button"
|
||||
>
|
||||
<div
|
||||
class="Button IconButton"
|
||||
role="button"
|
||||
>
|
||||
<i
|
||||
class="CompassIcon icon-dots-horizontal OptionsIcon"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="gallery-item"
|
||||
/>
|
||||
<div
|
||||
class="gallery-title"
|
||||
>
|
||||
<div
|
||||
class="octo-icon"
|
||||
>
|
||||
i
|
||||
</div>
|
||||
<div>
|
||||
title
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`src/components/gallery/GalleryCard without block content should match snapshot 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="GalleryCard selected"
|
||||
draggable="true"
|
||||
style="opacity: 1;"
|
||||
>
|
||||
<div
|
||||
aria-label="menuwrapper"
|
||||
class="MenuWrapper optionsMenu"
|
||||
role="button"
|
||||
>
|
||||
<div
|
||||
class="Button IconButton"
|
||||
role="button"
|
||||
>
|
||||
<i
|
||||
class="CompassIcon icon-dots-horizontal OptionsIcon"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="Menu noselect left"
|
||||
>
|
||||
<div
|
||||
class="menu-contents"
|
||||
>
|
||||
<div
|
||||
class="menu-options"
|
||||
>
|
||||
<div
|
||||
aria-label="Delete"
|
||||
class="MenuOption TextOption menu-option"
|
||||
role="button"
|
||||
>
|
||||
<svg
|
||||
class="DeleteIcon Icon"
|
||||
viewBox="0 0 448 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M268 416h24a12 12 0 0 0 12-12V188a12 12 0 0 0-12-12h-24a12 12 0 0 0-12 12v216a12 12 0 0 0 12 12zM432 80h-82.41l-34-56.7A48 48 0 0 0 274.41 0H173.59a48 48 0 0 0-41.16 23.3L98.41 80H16A16 16 0 0 0 0 96v16a16 16 0 0 0 16 16h16v336a48 48 0 0 0 48 48h288a48 48 0 0 0 48-48V128h16a16 16 0 0 0 16-16V96a16 16 0 0 0-16-16zM171.84 50.91A6 6 0 0 1 177 48h94a6 6 0 0 1 5.15 2.91L293.61 80H154.39zM368 464H80V128h288zm-212-48h24a12 12 0 0 0 12-12V188a12 12 0 0 0-12-12h-24a12 12 0 0 0-12 12v216a12 12 0 0 0 12 12z"
|
||||
/>
|
||||
</svg>
|
||||
<div
|
||||
class="menu-name"
|
||||
>
|
||||
Delete
|
||||
</div>
|
||||
<div
|
||||
class="noicon"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
aria-label="Duplicate"
|
||||
class="MenuOption TextOption menu-option"
|
||||
role="button"
|
||||
>
|
||||
<svg
|
||||
class="DuplicateIcon Icon"
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M464 0H144c-26.51 0-48 21.49-48 48v48H48c-26.51 0-48 21.49-48 48v320c0 26.51 21.49 48 48 48h320c26.51 0 48-21.49 48-48v-48h48c26.51 0 48-21.49 48-48V48c0-26.51-21.49-48-48-48zM362 464H54a6 6 0 0 1-6-6V150a6 6 0 0 1 6-6h42v224c0 26.51 21.49 48 48 48h224v42a6 6 0 0 1-6 6zm96-96H150a6 6 0 0 1-6-6V54a6 6 0 0 1 6-6h308a6 6 0 0 1 6 6v308a6 6 0 0 1-6 6z"
|
||||
/>
|
||||
</svg>
|
||||
<div
|
||||
class="menu-name"
|
||||
>
|
||||
Duplicate
|
||||
</div>
|
||||
<div
|
||||
class="noicon"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
aria-label="Copy link"
|
||||
class="MenuOption TextOption menu-option"
|
||||
role="button"
|
||||
>
|
||||
<i
|
||||
class="CompassIcon icon-link-variant LinkIcon"
|
||||
/>
|
||||
<div
|
||||
class="menu-name"
|
||||
>
|
||||
Copy link
|
||||
</div>
|
||||
<div
|
||||
class="noicon"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="menu-spacer hideOnWidescreen"
|
||||
/>
|
||||
<div
|
||||
class="menu-options hideOnWidescreen"
|
||||
>
|
||||
<div
|
||||
aria-label="Cancel"
|
||||
class="MenuOption TextOption menu-option menu-cancel"
|
||||
role="button"
|
||||
>
|
||||
<div
|
||||
class="noicon"
|
||||
/>
|
||||
<div
|
||||
class="menu-name"
|
||||
>
|
||||
Cancel
|
||||
</div>
|
||||
<div
|
||||
class="noicon"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="gallery-item"
|
||||
/>
|
||||
<div
|
||||
class="gallery-title"
|
||||
>
|
||||
<div
|
||||
class="octo-icon"
|
||||
>
|
||||
i
|
||||
</div>
|
||||
<div>
|
||||
title
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="gallery-props"
|
||||
>
|
||||
<div
|
||||
class="octo-tooltip tooltip-top"
|
||||
data-tooltip="testTemplateProperty"
|
||||
>
|
||||
<div
|
||||
class="octo-propertyvalue"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
126
webapp/src/components/gallery/gallery.test.tsx
Normal file
126
webapp/src/components/gallery/gallery.test.tsx
Normal file
@ -0,0 +1,126 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react'
|
||||
import {render, screen, fireEvent} from '@testing-library/react'
|
||||
|
||||
import {Provider as ReduxProvider} from 'react-redux'
|
||||
|
||||
import userEvent from '@testing-library/user-event'
|
||||
|
||||
import {mocked} from 'ts-jest/utils'
|
||||
|
||||
import {wrapDNDIntl, mockStateStore} from '../../testUtils'
|
||||
|
||||
import {TestBlockFactory} from '../../test/testBlockFactory'
|
||||
|
||||
import mutator from '../../mutator'
|
||||
|
||||
import Gallery from './gallery'
|
||||
|
||||
jest.mock('../../mutator')
|
||||
const mockedMutator = mocked(mutator, true)
|
||||
|
||||
describe('src/components/gallery/Gallery', () => {
|
||||
const board = TestBlockFactory.createBoard()
|
||||
const activeView = TestBlockFactory.createBoardView(board)
|
||||
activeView.fields.sortOptions = []
|
||||
const card = TestBlockFactory.createCard(board)
|
||||
const card2 = TestBlockFactory.createCard(board)
|
||||
const contents = [TestBlockFactory.createDivider(card), TestBlockFactory.createDivider(card), TestBlockFactory.createDivider(card2)]
|
||||
const state = {
|
||||
contents,
|
||||
cards: {
|
||||
cards: {
|
||||
[card.id]: card,
|
||||
},
|
||||
},
|
||||
comments: {
|
||||
comments: {},
|
||||
},
|
||||
}
|
||||
const store = mockStateStore([], state)
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
test('should match snapshot', () => {
|
||||
const {container} = render(wrapDNDIntl(
|
||||
<ReduxProvider store={store}>
|
||||
<Gallery
|
||||
board={board}
|
||||
cards={[card, card2]}
|
||||
activeView={activeView}
|
||||
readonly={false}
|
||||
addCard={jest.fn()}
|
||||
selectedCardIds={[card.id]}
|
||||
onCardClicked={jest.fn()}
|
||||
/>
|
||||
</ReduxProvider>,
|
||||
))
|
||||
const buttonElement = screen.getAllByRole('button', {name: 'menuwrapper'})[0]
|
||||
userEvent.click(buttonElement)
|
||||
expect(container).toMatchSnapshot()
|
||||
})
|
||||
test('return Gallery and click new', () => {
|
||||
const mockAddCard = jest.fn()
|
||||
const {container} = render(wrapDNDIntl(
|
||||
<ReduxProvider store={store}>
|
||||
<Gallery
|
||||
board={board}
|
||||
cards={[card, card2]}
|
||||
activeView={activeView}
|
||||
readonly={false}
|
||||
addCard={mockAddCard}
|
||||
selectedCardIds={[card.id]}
|
||||
onCardClicked={jest.fn()}
|
||||
/>
|
||||
</ReduxProvider>,
|
||||
))
|
||||
expect(container).toMatchSnapshot()
|
||||
|
||||
const elementNew = container.querySelector('.octo-gallery-new')!
|
||||
expect(elementNew).toBeDefined()
|
||||
userEvent.click(elementNew)
|
||||
expect(mockAddCard).toBeCalledTimes(1)
|
||||
})
|
||||
|
||||
test('return Gallery readonly', () => {
|
||||
const {container} = render(wrapDNDIntl(
|
||||
<ReduxProvider store={store}>
|
||||
<Gallery
|
||||
board={board}
|
||||
cards={[card, card2]}
|
||||
activeView={activeView}
|
||||
readonly={true}
|
||||
addCard={jest.fn()}
|
||||
selectedCardIds={[card.id]}
|
||||
onCardClicked={jest.fn()}
|
||||
/>
|
||||
</ReduxProvider>,
|
||||
))
|
||||
expect(container).toMatchSnapshot()
|
||||
})
|
||||
test('return Gallery and drag and drop card', async () => {
|
||||
const {container} = render(wrapDNDIntl(
|
||||
<ReduxProvider store={store}>
|
||||
<Gallery
|
||||
board={board}
|
||||
cards={[card, card2]}
|
||||
activeView={activeView}
|
||||
readonly={false}
|
||||
addCard={jest.fn()}
|
||||
selectedCardIds={[]}
|
||||
onCardClicked={jest.fn()}
|
||||
/>
|
||||
</ReduxProvider>,
|
||||
))
|
||||
const allGalleryCard = container.querySelectorAll('.GalleryCard')
|
||||
const drag = allGalleryCard[0]
|
||||
const drop = allGalleryCard[1]
|
||||
fireEvent.dragStart(drag)
|
||||
fireEvent.dragEnter(drop)
|
||||
fireEvent.dragOver(drop)
|
||||
fireEvent.drop(drop)
|
||||
expect(mockedMutator.performAsUndoGroup).toBeCalledTimes(1)
|
||||
})
|
||||
})
|
424
webapp/src/components/gallery/galleryCard.test.tsx
Normal file
424
webapp/src/components/gallery/galleryCard.test.tsx
Normal file
@ -0,0 +1,424 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react'
|
||||
import {act, render, screen} from '@testing-library/react'
|
||||
|
||||
import {Provider as ReduxProvider} from 'react-redux'
|
||||
|
||||
import userEvent from '@testing-library/user-event'
|
||||
|
||||
import {mocked} from 'ts-jest/utils'
|
||||
|
||||
import {MockStoreEnhanced} from 'redux-mock-store'
|
||||
|
||||
import {wrapDNDIntl, mockStateStore} from '../../testUtils'
|
||||
|
||||
import {TestBlockFactory} from '../../test/testBlockFactory'
|
||||
|
||||
import mutator from '../../mutator'
|
||||
|
||||
import {Utils} from '../../utils'
|
||||
|
||||
import octoClient from '../../octoClient'
|
||||
|
||||
import GalleryCard from './galleryCard'
|
||||
|
||||
jest.mock('../../mutator')
|
||||
jest.mock('../../utils')
|
||||
jest.mock('../../octoClient')
|
||||
|
||||
describe('src/components/gallery/GalleryCard', () => {
|
||||
const mockedMutator = mocked(mutator, true)
|
||||
const mockedUtils = mocked(Utils, true)
|
||||
const mockedOcto = mocked(octoClient, true)
|
||||
mockedOcto.getFileAsDataUrl.mockResolvedValue('test.jpg')
|
||||
|
||||
const board = TestBlockFactory.createBoard()
|
||||
board.id = 'boardId'
|
||||
|
||||
const activeView = TestBlockFactory.createBoardView(board)
|
||||
activeView.fields.sortOptions = []
|
||||
|
||||
const card = TestBlockFactory.createCard(board)
|
||||
card.id = 'cardId'
|
||||
|
||||
const contentImage = TestBlockFactory.createImage(card)
|
||||
contentImage.id = 'contentId-image'
|
||||
contentImage.fields.fileId = 'test.jpg'
|
||||
|
||||
const contentComment = TestBlockFactory.createComment(card)
|
||||
contentComment.id = 'contentId-Comment'
|
||||
|
||||
let store:MockStoreEnhanced<unknown, unknown>
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
describe('without block content', () => {
|
||||
beforeEach(() => {
|
||||
const state = {
|
||||
contents: {
|
||||
contents: {
|
||||
},
|
||||
},
|
||||
cards: {
|
||||
cards: {
|
||||
[card.id]: card,
|
||||
},
|
||||
},
|
||||
comments: {
|
||||
comments: {},
|
||||
},
|
||||
}
|
||||
store = mockStateStore([], state)
|
||||
})
|
||||
test('should match snapshot', () => {
|
||||
const {container} = render(wrapDNDIntl(
|
||||
<ReduxProvider store={store}>
|
||||
<GalleryCard
|
||||
board={board}
|
||||
card={card}
|
||||
onClick={jest.fn()}
|
||||
visiblePropertyTemplates={[{id: card.id, name: 'testTemplateProperty', type: 'text', options: [{id: '1', value: 'testValue', color: 'blue'}]}]}
|
||||
visibleTitle={true}
|
||||
isSelected={true}
|
||||
readonly={false}
|
||||
isManualSort={true}
|
||||
onDrop={jest.fn()}
|
||||
/>
|
||||
</ReduxProvider>,
|
||||
))
|
||||
const buttonElement = screen.getByRole('button', {name: 'menuwrapper'})
|
||||
userEvent.click(buttonElement)
|
||||
expect(container).toMatchSnapshot()
|
||||
})
|
||||
test('return GalleryCard and click on it', () => {
|
||||
const mockedOnClick = jest.fn()
|
||||
const {container} = render(wrapDNDIntl(
|
||||
<ReduxProvider store={store}>
|
||||
<GalleryCard
|
||||
board={board}
|
||||
card={card}
|
||||
onClick={mockedOnClick}
|
||||
visiblePropertyTemplates={[]}
|
||||
visibleTitle={true}
|
||||
isSelected={true}
|
||||
readonly={false}
|
||||
isManualSort={true}
|
||||
onDrop={jest.fn()}
|
||||
/>
|
||||
</ReduxProvider>,
|
||||
))
|
||||
const galleryCardElement = container.querySelector('.GalleryCard')
|
||||
userEvent.click(galleryCardElement!)
|
||||
expect(mockedOnClick).toBeCalledTimes(1)
|
||||
})
|
||||
test('return GalleryCard and delete card', () => {
|
||||
const {container} = render(wrapDNDIntl(
|
||||
<ReduxProvider store={store}>
|
||||
<GalleryCard
|
||||
board={board}
|
||||
card={card}
|
||||
onClick={jest.fn()}
|
||||
visiblePropertyTemplates={[]}
|
||||
visibleTitle={true}
|
||||
isSelected={true}
|
||||
readonly={false}
|
||||
isManualSort={true}
|
||||
onDrop={jest.fn()}
|
||||
/>
|
||||
</ReduxProvider>,
|
||||
))
|
||||
const buttonElement = screen.getByRole('button', {name: 'menuwrapper'})
|
||||
userEvent.click(buttonElement)
|
||||
const buttonDelete = screen.getByRole('button', {name: 'Delete'})
|
||||
userEvent.click(buttonDelete)
|
||||
expect(container).toMatchSnapshot()
|
||||
expect(mockedMutator.deleteBlock).toBeCalledTimes(1)
|
||||
expect(mockedMutator.deleteBlock).toBeCalledWith(card, 'delete card')
|
||||
})
|
||||
|
||||
test('return GalleryCard and duplicate card', () => {
|
||||
const {container} = render(wrapDNDIntl(
|
||||
<ReduxProvider store={store}>
|
||||
<GalleryCard
|
||||
board={board}
|
||||
card={card}
|
||||
onClick={jest.fn()}
|
||||
visiblePropertyTemplates={[]}
|
||||
visibleTitle={true}
|
||||
isSelected={true}
|
||||
readonly={false}
|
||||
isManualSort={true}
|
||||
onDrop={jest.fn()}
|
||||
/>
|
||||
</ReduxProvider>,
|
||||
))
|
||||
const buttonElement = screen.getByRole('button', {name: 'menuwrapper'})
|
||||
userEvent.click(buttonElement)
|
||||
const buttonDuplicate = screen.getByRole('button', {name: 'Duplicate'})
|
||||
userEvent.click(buttonDuplicate)
|
||||
expect(container).toMatchSnapshot()
|
||||
expect(mockedMutator.duplicateCard).toBeCalledTimes(1)
|
||||
expect(mockedMutator.duplicateCard).toBeCalledWith(card.id)
|
||||
})
|
||||
test('return GalleryCard and copy link', () => {
|
||||
const {container} = render(wrapDNDIntl(
|
||||
<ReduxProvider store={store}>
|
||||
<GalleryCard
|
||||
board={board}
|
||||
card={card}
|
||||
onClick={jest.fn()}
|
||||
visiblePropertyTemplates={[]}
|
||||
visibleTitle={true}
|
||||
isSelected={true}
|
||||
readonly={false}
|
||||
isManualSort={true}
|
||||
onDrop={jest.fn()}
|
||||
/>
|
||||
</ReduxProvider>,
|
||||
))
|
||||
const buttonElement = screen.getByRole('button', {name: 'menuwrapper'})
|
||||
userEvent.click(buttonElement)
|
||||
const buttonCopyLink = screen.getByRole('button', {name: 'Copy link'})
|
||||
userEvent.click(buttonCopyLink)
|
||||
expect(container).toMatchSnapshot()
|
||||
expect(mockedUtils.copyTextToClipboard).toBeCalledTimes(1)
|
||||
})
|
||||
test('return GalleryCard and cancel', () => {
|
||||
const {container} = render(wrapDNDIntl(
|
||||
<ReduxProvider store={store}>
|
||||
<GalleryCard
|
||||
board={board}
|
||||
card={card}
|
||||
onClick={jest.fn()}
|
||||
visiblePropertyTemplates={[]}
|
||||
visibleTitle={true}
|
||||
isSelected={true}
|
||||
readonly={false}
|
||||
isManualSort={true}
|
||||
onDrop={jest.fn()}
|
||||
/>
|
||||
</ReduxProvider>,
|
||||
))
|
||||
const buttonElement = screen.getByRole('button', {name: 'menuwrapper'})
|
||||
userEvent.click(buttonElement)
|
||||
const buttonCancel = screen.getByRole('button', {name: 'Cancel'})
|
||||
userEvent.click(buttonCancel)
|
||||
expect(container).toMatchSnapshot()
|
||||
})
|
||||
})
|
||||
describe('with an image content', () => {
|
||||
beforeEach(() => {
|
||||
card.fields.contentOrder = [contentImage.id]
|
||||
const state = {
|
||||
contents: {
|
||||
contents: {
|
||||
[contentImage.id]: contentImage,
|
||||
},
|
||||
},
|
||||
cards: {
|
||||
cards: {
|
||||
[card.id]: card,
|
||||
},
|
||||
},
|
||||
comments: {
|
||||
comments: {},
|
||||
},
|
||||
}
|
||||
store = mockStateStore([], state)
|
||||
})
|
||||
test('should match snapshot', async () => {
|
||||
const {container} = render(wrapDNDIntl(
|
||||
<ReduxProvider store={store}>
|
||||
<GalleryCard
|
||||
board={board}
|
||||
card={card}
|
||||
onClick={jest.fn()}
|
||||
visiblePropertyTemplates={[]}
|
||||
visibleTitle={true}
|
||||
isSelected={true}
|
||||
readonly={false}
|
||||
isManualSort={true}
|
||||
onDrop={jest.fn()}
|
||||
/>
|
||||
</ReduxProvider>,
|
||||
))
|
||||
await act(async () => {
|
||||
const buttonElement = screen.getByRole('button', {name: 'menuwrapper'})
|
||||
userEvent.click(buttonElement)
|
||||
})
|
||||
expect(container).toMatchSnapshot()
|
||||
})
|
||||
})
|
||||
|
||||
describe('with many images content', () => {
|
||||
beforeEach(() => {
|
||||
const contentImage2 = TestBlockFactory.createImage(card)
|
||||
contentImage2.id = 'contentId-image2'
|
||||
contentImage2.fields.fileId = 'test2.jpg'
|
||||
card.fields.contentOrder = [contentImage.id, contentImage2.id]
|
||||
const state = {
|
||||
contents: {
|
||||
contents: {
|
||||
[contentImage.id]: [contentImage],
|
||||
[contentImage2.id]: [contentImage2],
|
||||
},
|
||||
},
|
||||
cards: {
|
||||
cards: {
|
||||
[card.id]: card,
|
||||
},
|
||||
},
|
||||
comments: {
|
||||
comments: {},
|
||||
},
|
||||
}
|
||||
store = mockStateStore([], state)
|
||||
})
|
||||
test('should match snapshot with only first image', async () => {
|
||||
const {container} = render(wrapDNDIntl(
|
||||
<ReduxProvider store={store}>
|
||||
<GalleryCard
|
||||
board={board}
|
||||
card={card}
|
||||
onClick={jest.fn()}
|
||||
visiblePropertyTemplates={[]}
|
||||
visibleTitle={true}
|
||||
isSelected={true}
|
||||
readonly={false}
|
||||
isManualSort={true}
|
||||
onDrop={jest.fn()}
|
||||
/>
|
||||
</ReduxProvider>,
|
||||
))
|
||||
await act(async () => {
|
||||
const buttonElement = screen.getByRole('button', {name: 'menuwrapper'})
|
||||
userEvent.click(buttonElement)
|
||||
})
|
||||
expect(container).toMatchSnapshot()
|
||||
})
|
||||
})
|
||||
describe('with a comment content', () => {
|
||||
beforeEach(() => {
|
||||
card.fields.contentOrder = [contentComment.id]
|
||||
const state = {
|
||||
contents: {
|
||||
contents: {
|
||||
[contentComment.id]: contentComment,
|
||||
},
|
||||
},
|
||||
cards: {
|
||||
cards: {
|
||||
[card.id]: card,
|
||||
},
|
||||
},
|
||||
comments: {
|
||||
comments: {},
|
||||
},
|
||||
}
|
||||
store = mockStateStore([], state)
|
||||
})
|
||||
test('should match snapshot', () => {
|
||||
const {container} = render(wrapDNDIntl(
|
||||
<ReduxProvider store={store}>
|
||||
<GalleryCard
|
||||
board={board}
|
||||
card={card}
|
||||
onClick={jest.fn()}
|
||||
visiblePropertyTemplates={[]}
|
||||
visibleTitle={true}
|
||||
isSelected={true}
|
||||
readonly={false}
|
||||
isManualSort={true}
|
||||
onDrop={jest.fn()}
|
||||
/>
|
||||
</ReduxProvider>,
|
||||
))
|
||||
const buttonElement = screen.getByRole('button', {name: 'menuwrapper'})
|
||||
userEvent.click(buttonElement)
|
||||
expect(container).toMatchSnapshot()
|
||||
})
|
||||
test('return GalleryCard with content readonly', () => {
|
||||
const {container} = render(wrapDNDIntl(
|
||||
<ReduxProvider store={store}>
|
||||
<GalleryCard
|
||||
board={board}
|
||||
card={card}
|
||||
onClick={jest.fn()}
|
||||
visiblePropertyTemplates={[]}
|
||||
visibleTitle={true}
|
||||
isSelected={true}
|
||||
readonly={true}
|
||||
isManualSort={true}
|
||||
onDrop={jest.fn()}
|
||||
/>
|
||||
</ReduxProvider>,
|
||||
))
|
||||
expect(container).toMatchSnapshot()
|
||||
})
|
||||
})
|
||||
describe('with many contents', () => {
|
||||
const contentDivider = TestBlockFactory.createDivider(card)
|
||||
contentDivider.id = 'contentId-Text2'
|
||||
beforeEach(() => {
|
||||
card.fields.contentOrder = [contentComment.id, contentDivider.id]
|
||||
const state = {
|
||||
contents: {
|
||||
contents: {
|
||||
[contentComment.id]: [contentComment, contentDivider],
|
||||
},
|
||||
},
|
||||
cards: {
|
||||
cards: {
|
||||
[card.id]: card,
|
||||
},
|
||||
},
|
||||
comments: {
|
||||
comments: {},
|
||||
},
|
||||
}
|
||||
store = mockStateStore([], state)
|
||||
})
|
||||
test('should match snapshot', () => {
|
||||
const {container} = render(wrapDNDIntl(
|
||||
<ReduxProvider store={store}>
|
||||
<GalleryCard
|
||||
board={board}
|
||||
card={card}
|
||||
onClick={jest.fn()}
|
||||
visiblePropertyTemplates={[]}
|
||||
visibleTitle={true}
|
||||
isSelected={true}
|
||||
readonly={false}
|
||||
isManualSort={true}
|
||||
onDrop={jest.fn()}
|
||||
/>
|
||||
</ReduxProvider>,
|
||||
))
|
||||
const buttonElement = screen.getByRole('button', {name: 'menuwrapper'})
|
||||
userEvent.click(buttonElement)
|
||||
expect(container).toMatchSnapshot()
|
||||
})
|
||||
test('return GalleryCard with contents readonly', () => {
|
||||
const {container} = render(wrapDNDIntl(
|
||||
<ReduxProvider store={store}>
|
||||
<GalleryCard
|
||||
board={board}
|
||||
card={card}
|
||||
onClick={jest.fn()}
|
||||
visiblePropertyTemplates={[]}
|
||||
visibleTitle={true}
|
||||
isSelected={true}
|
||||
readonly={true}
|
||||
isManualSort={true}
|
||||
onDrop={jest.fn()}
|
||||
/>
|
||||
</ReduxProvider>,
|
||||
))
|
||||
expect(container).toMatchSnapshot()
|
||||
})
|
||||
})
|
||||
})
|
@ -28,6 +28,7 @@ import {getCardContents} from '../../store/contents'
|
||||
import {getCardComments} from '../../store/comments'
|
||||
|
||||
import './galleryCard.scss'
|
||||
import {CardDetailProvider} from '../cardDetail/cardDetailContext'
|
||||
|
||||
type Props = {
|
||||
board: Board
|
||||
@ -120,29 +121,31 @@ const GalleryCard = React.memo((props: Props) => {
|
||||
<ImageElement block={image}/>
|
||||
</div>}
|
||||
{!image &&
|
||||
<div className='gallery-item'>
|
||||
{contents.map((block) => {
|
||||
if (Array.isArray(block)) {
|
||||
return block.map((b) => (
|
||||
<CardDetailProvider card={card}>
|
||||
<div className='gallery-item'>
|
||||
{contents.map((block) => {
|
||||
if (Array.isArray(block)) {
|
||||
return block.map((b) => (
|
||||
<ContentElement
|
||||
key={b.id}
|
||||
block={b}
|
||||
readonly={true}
|
||||
cords={{x: 0}}
|
||||
/>
|
||||
))
|
||||
}
|
||||
|
||||
return (
|
||||
<ContentElement
|
||||
key={b.id}
|
||||
block={b}
|
||||
key={block.id}
|
||||
block={block}
|
||||
readonly={true}
|
||||
cords={{x: 0}}
|
||||
/>
|
||||
))
|
||||
}
|
||||
|
||||
return (
|
||||
<ContentElement
|
||||
key={block.id}
|
||||
block={block}
|
||||
readonly={true}
|
||||
cords={{x: 0}}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</div>}
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</CardDetailProvider>}
|
||||
{props.visibleTitle &&
|
||||
<div className='gallery-title'>
|
||||
{ card.fields.icon ? <div className='octo-icon'>{card.fields.icon}</div> : undefined }
|
||||
|
@ -45,7 +45,7 @@ exports[`components/kanban/calculation/KanbanCalculation calculations menu open
|
||||
<span
|
||||
id="aria-context"
|
||||
>
|
||||
3 results available. Use Up and Down to choose options, press Enter to select the currently focused option, press Escape to exit the menu, press Tab to select the option and exit the menu.
|
||||
7 results available. Use Up and Down to choose options, press Enter to select the currently focused option, press Escape to exit the menu, press Tab to select the option and exit the menu.
|
||||
</span>
|
||||
</span>
|
||||
<div
|
||||
@ -115,6 +115,50 @@ exports[`components/kanban/calculation/KanbanCalculation calculations menu open
|
||||
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="KanbanCalculationOptions_CustomOption "
|
||||
>
|
||||
<span>
|
||||
Count Empty
|
||||
|
||||
<i
|
||||
class="CompassIcon icon-chevron-right ChevronRightIcon"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="KanbanCalculationOptions_CustomOption "
|
||||
>
|
||||
<span>
|
||||
Count Not Empty
|
||||
|
||||
<i
|
||||
class="CompassIcon icon-chevron-right ChevronRightIcon"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="KanbanCalculationOptions_CustomOption "
|
||||
>
|
||||
<span>
|
||||
Percent Empty
|
||||
|
||||
<i
|
||||
class="CompassIcon icon-chevron-right ChevronRightIcon"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="KanbanCalculationOptions_CustomOption "
|
||||
>
|
||||
<span>
|
||||
Percent Not Empty
|
||||
|
||||
<i
|
||||
class="CompassIcon icon-chevron-right ChevronRightIcon"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="KanbanCalculationOptions_CustomOption "
|
||||
>
|
||||
|
@ -99,7 +99,7 @@ exports[`components/kanban/calculations/KanbanCalculationOptions with menu open
|
||||
<span
|
||||
id="aria-context"
|
||||
>
|
||||
3 results available. Use Up and Down to choose options, press Enter to select the currently focused option, press Escape to exit the menu, press Tab to select the option and exit the menu.
|
||||
7 results available. Use Up and Down to choose options, press Enter to select the currently focused option, press Escape to exit the menu, press Tab to select the option and exit the menu.
|
||||
</span>
|
||||
</span>
|
||||
<div
|
||||
@ -169,6 +169,50 @@ exports[`components/kanban/calculations/KanbanCalculationOptions with menu open
|
||||
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="KanbanCalculationOptions_CustomOption "
|
||||
>
|
||||
<span>
|
||||
Count Empty
|
||||
|
||||
<i
|
||||
class="CompassIcon icon-chevron-right ChevronRightIcon"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="KanbanCalculationOptions_CustomOption "
|
||||
>
|
||||
<span>
|
||||
Count Not Empty
|
||||
|
||||
<i
|
||||
class="CompassIcon icon-chevron-right ChevronRightIcon"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="KanbanCalculationOptions_CustomOption "
|
||||
>
|
||||
<span>
|
||||
Percent Empty
|
||||
|
||||
<i
|
||||
class="CompassIcon icon-chevron-right ChevronRightIcon"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="KanbanCalculationOptions_CustomOption "
|
||||
>
|
||||
<span>
|
||||
Percent Not Empty
|
||||
|
||||
<i
|
||||
class="CompassIcon icon-chevron-right ChevronRightIcon"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="KanbanCalculationOptions_CustomOption "
|
||||
>
|
||||
@ -219,7 +263,7 @@ exports[`components/kanban/calculations/KanbanCalculationOptions with submenu op
|
||||
<span
|
||||
id="aria-context"
|
||||
>
|
||||
3 results available. Use Up and Down to choose options, press Enter to select the currently focused option, press Escape to exit the menu, press Tab to select the option and exit the menu.
|
||||
7 results available. Use Up and Down to choose options, press Enter to select the currently focused option, press Escape to exit the menu, press Tab to select the option and exit the menu.
|
||||
</span>
|
||||
</span>
|
||||
<div
|
||||
@ -289,6 +333,50 @@ exports[`components/kanban/calculations/KanbanCalculationOptions with submenu op
|
||||
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="KanbanCalculationOptions_CustomOption "
|
||||
>
|
||||
<span>
|
||||
Count Empty
|
||||
|
||||
<i
|
||||
class="CompassIcon icon-chevron-right ChevronRightIcon"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="KanbanCalculationOptions_CustomOption "
|
||||
>
|
||||
<span>
|
||||
Count Not Empty
|
||||
|
||||
<i
|
||||
class="CompassIcon icon-chevron-right ChevronRightIcon"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="KanbanCalculationOptions_CustomOption "
|
||||
>
|
||||
<span>
|
||||
Percent Empty
|
||||
|
||||
<i
|
||||
class="CompassIcon icon-chevron-right ChevronRightIcon"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="KanbanCalculationOptions_CustomOption "
|
||||
>
|
||||
<span>
|
||||
Percent Not Empty
|
||||
|
||||
<i
|
||||
class="CompassIcon icon-chevron-right ChevronRightIcon"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="KanbanCalculationOptions_CustomOption "
|
||||
>
|
||||
|
@ -33,7 +33,6 @@ const MarkdownEditor = (props: Props): JSX. Element => {
|
||||
autoDownloadFontAwesome: true,
|
||||
toolbar: false,
|
||||
status: false,
|
||||
autofocus: true,
|
||||
spellChecker: true,
|
||||
nativeSpellcheck: true,
|
||||
minHeight: '10px',
|
||||
|
@ -4,6 +4,7 @@
|
||||
padding: 5px;
|
||||
color: rgb(var(--center-channel-color-rgb));
|
||||
max-width: 500px;
|
||||
white-space: normal;
|
||||
|
||||
.Switch {
|
||||
margin-left: 8px;
|
||||
|
@ -67,9 +67,9 @@ exports[`components/sidebarSidebar sidebar in dashboard page 1`] = `
|
||||
<div
|
||||
class="sidebarSwitcher"
|
||||
>
|
||||
<div
|
||||
<button
|
||||
class="Button IconButton"
|
||||
role="button"
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
class="HideSidebarIcon Icon"
|
||||
@ -83,7 +83,7 @@ exports[`components/sidebarSidebar sidebar in dashboard page 1`] = `
|
||||
points="50,20 20,50, 50,80"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
|
@ -17,10 +17,14 @@ import {Utils} from '../../utils'
|
||||
|
||||
import ModalWrapper from '../modalWrapper'
|
||||
|
||||
import {IAppWindow} from '../../types'
|
||||
|
||||
import RegistrationLink from './registrationLink'
|
||||
|
||||
import './sidebarUserMenu.scss'
|
||||
|
||||
declare let window: IAppWindow
|
||||
|
||||
const SidebarUserMenu = React.memo(() => {
|
||||
const history = useHistory()
|
||||
const [showRegistrationLinkDialog, setShowRegistrationLinkDialog] = useState(false)
|
||||
@ -81,8 +85,8 @@ const SidebarUserMenu = React.memo(() => {
|
||||
window.open('https://www.focalboard.com?utm_source=webapp', '_blank')
|
||||
|
||||
// TODO: Review if this is needed in the future, this is to fix the problem with linux webview links
|
||||
if ((window as any).openInNewBrowser) {
|
||||
(window as any).openInNewBrowser('https://www.focalboard.com?utm_source=webapp')
|
||||
if (window.openInNewBrowser) {
|
||||
window.openInNewBrowser('https://www.focalboard.com?utm_source=webapp')
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
@ -1927,9 +1927,9 @@ exports[`components/table/Table should match snapshot with GroupBy 1`] = `
|
||||
class="octo-table-cell"
|
||||
style="width: 100px;"
|
||||
>
|
||||
<div
|
||||
<button
|
||||
class="Button IconButton"
|
||||
role="button"
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
class="DisclosureTriangleIcon Icon"
|
||||
@ -1940,7 +1940,7 @@ exports[`components/table/Table should match snapshot with GroupBy 1`] = `
|
||||
points="37,35 37,65 63,50"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</button>
|
||||
<span
|
||||
class="Label empty "
|
||||
title="Items with an empty Property 1 property will go here. This column cannot be removed."
|
||||
@ -1962,23 +1962,23 @@ exports[`components/table/Table should match snapshot with GroupBy 1`] = `
|
||||
class="MenuWrapper"
|
||||
role="button"
|
||||
>
|
||||
<div
|
||||
<button
|
||||
class="Button IconButton"
|
||||
role="button"
|
||||
type="button"
|
||||
>
|
||||
<i
|
||||
class="CompassIcon icon-dots-horizontal OptionsIcon"
|
||||
/>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
<button
|
||||
class="Button IconButton"
|
||||
role="button"
|
||||
type="button"
|
||||
>
|
||||
<i
|
||||
class="CompassIcon icon-plus AddIcon"
|
||||
/>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -11,9 +11,9 @@ exports[`should match snapshot on read only 1`] = `
|
||||
class="octo-table-cell"
|
||||
style="width: 100px;"
|
||||
>
|
||||
<div
|
||||
<button
|
||||
class="Button IconButton readonly"
|
||||
role="button"
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
class="DisclosureTriangleIcon Icon"
|
||||
@ -24,7 +24,7 @@ exports[`should match snapshot on read only 1`] = `
|
||||
points="37,35 37,65 63,50"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</button>
|
||||
<span
|
||||
class="Label propColorBrown "
|
||||
>
|
||||
@ -61,9 +61,9 @@ exports[`should match snapshot with Group 1`] = `
|
||||
class="octo-table-cell"
|
||||
style="width: 100px;"
|
||||
>
|
||||
<div
|
||||
<button
|
||||
class="Button IconButton"
|
||||
role="button"
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
class="DisclosureTriangleIcon Icon"
|
||||
@ -74,7 +74,7 @@ exports[`should match snapshot with Group 1`] = `
|
||||
points="37,35 37,65 63,50"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</button>
|
||||
<span
|
||||
class="Label propColorBrown "
|
||||
>
|
||||
@ -100,23 +100,23 @@ exports[`should match snapshot with Group 1`] = `
|
||||
class="MenuWrapper"
|
||||
role="button"
|
||||
>
|
||||
<div
|
||||
<button
|
||||
class="Button IconButton"
|
||||
role="button"
|
||||
type="button"
|
||||
>
|
||||
<i
|
||||
class="CompassIcon icon-dots-horizontal OptionsIcon"
|
||||
/>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
<button
|
||||
class="Button IconButton"
|
||||
role="button"
|
||||
type="button"
|
||||
>
|
||||
<i
|
||||
class="CompassIcon icon-plus AddIcon"
|
||||
/>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@ -132,9 +132,9 @@ exports[`should match snapshot, add new 1`] = `
|
||||
class="octo-table-cell"
|
||||
style="width: 100px;"
|
||||
>
|
||||
<div
|
||||
<button
|
||||
class="Button IconButton"
|
||||
role="button"
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
class="DisclosureTriangleIcon Icon"
|
||||
@ -145,7 +145,7 @@ exports[`should match snapshot, add new 1`] = `
|
||||
points="37,35 37,65 63,50"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</button>
|
||||
<span
|
||||
class="Label propColorBrown "
|
||||
>
|
||||
@ -171,23 +171,23 @@ exports[`should match snapshot, add new 1`] = `
|
||||
class="MenuWrapper"
|
||||
role="button"
|
||||
>
|
||||
<div
|
||||
<button
|
||||
class="Button IconButton"
|
||||
role="button"
|
||||
type="button"
|
||||
>
|
||||
<i
|
||||
class="CompassIcon icon-dots-horizontal OptionsIcon"
|
||||
/>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
<button
|
||||
class="Button IconButton"
|
||||
role="button"
|
||||
type="button"
|
||||
>
|
||||
<i
|
||||
class="CompassIcon icon-plus AddIcon"
|
||||
/>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@ -203,9 +203,9 @@ exports[`should match snapshot, edit title 1`] = `
|
||||
class="octo-table-cell"
|
||||
style="width: 100px;"
|
||||
>
|
||||
<div
|
||||
<button
|
||||
class="Button IconButton"
|
||||
role="button"
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
class="DisclosureTriangleIcon Icon"
|
||||
@ -216,7 +216,7 @@ exports[`should match snapshot, edit title 1`] = `
|
||||
points="37,35 37,65 63,50"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</button>
|
||||
<span
|
||||
class="Label propColorBrown "
|
||||
>
|
||||
@ -242,23 +242,23 @@ exports[`should match snapshot, edit title 1`] = `
|
||||
class="MenuWrapper"
|
||||
role="button"
|
||||
>
|
||||
<div
|
||||
<button
|
||||
class="Button IconButton"
|
||||
role="button"
|
||||
type="button"
|
||||
>
|
||||
<i
|
||||
class="CompassIcon icon-dots-horizontal OptionsIcon"
|
||||
/>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
<button
|
||||
class="Button IconButton"
|
||||
role="button"
|
||||
type="button"
|
||||
>
|
||||
<i
|
||||
class="CompassIcon icon-plus AddIcon"
|
||||
/>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@ -274,9 +274,9 @@ exports[`should match snapshot, hide group 1`] = `
|
||||
class="octo-table-cell"
|
||||
style="width: 100px;"
|
||||
>
|
||||
<div
|
||||
<button
|
||||
class="Button IconButton"
|
||||
role="button"
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
class="DisclosureTriangleIcon Icon"
|
||||
@ -287,7 +287,7 @@ exports[`should match snapshot, hide group 1`] = `
|
||||
points="37,35 37,65 63,50"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</button>
|
||||
<span
|
||||
class="Label propColorBrown "
|
||||
>
|
||||
@ -313,23 +313,23 @@ exports[`should match snapshot, hide group 1`] = `
|
||||
class="MenuWrapper"
|
||||
role="button"
|
||||
>
|
||||
<div
|
||||
<button
|
||||
class="Button IconButton"
|
||||
role="button"
|
||||
type="button"
|
||||
>
|
||||
<i
|
||||
class="CompassIcon icon-dots-horizontal OptionsIcon"
|
||||
/>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
<button
|
||||
class="Button IconButton"
|
||||
role="button"
|
||||
type="button"
|
||||
>
|
||||
<i
|
||||
class="CompassIcon icon-plus AddIcon"
|
||||
/>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@ -345,9 +345,9 @@ exports[`should match snapshot, no groups 1`] = `
|
||||
class="octo-table-cell"
|
||||
style="width: 100px;"
|
||||
>
|
||||
<div
|
||||
<button
|
||||
class="Button IconButton"
|
||||
role="button"
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
class="DisclosureTriangleIcon Icon"
|
||||
@ -358,7 +358,7 @@ exports[`should match snapshot, no groups 1`] = `
|
||||
points="37,35 37,65 63,50"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</button>
|
||||
<span
|
||||
class="Label empty "
|
||||
title="Items with an empty Property 1 property will go here. This column cannot be removed."
|
||||
@ -380,23 +380,23 @@ exports[`should match snapshot, no groups 1`] = `
|
||||
class="MenuWrapper"
|
||||
role="button"
|
||||
>
|
||||
<div
|
||||
<button
|
||||
class="Button IconButton"
|
||||
role="button"
|
||||
type="button"
|
||||
>
|
||||
<i
|
||||
class="CompassIcon icon-dots-horizontal OptionsIcon"
|
||||
/>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
<button
|
||||
class="Button IconButton"
|
||||
role="button"
|
||||
type="button"
|
||||
>
|
||||
<i
|
||||
class="CompassIcon icon-plus AddIcon"
|
||||
/>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
@ -31,14 +31,14 @@ exports[`components/viewHeader/emptyCardButton return EmptyCardButton 1`] = `
|
||||
class="MenuWrapper"
|
||||
role="button"
|
||||
>
|
||||
<div
|
||||
<button
|
||||
class="Button IconButton"
|
||||
role="button"
|
||||
type="button"
|
||||
>
|
||||
<i
|
||||
class="CompassIcon icon-dots-horizontal OptionsIcon"
|
||||
/>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -75,14 +75,14 @@ exports[`components/viewHeader/emptyCardButton return EmptyCardButton and Set Te
|
||||
class="MenuWrapper"
|
||||
role="button"
|
||||
>
|
||||
<div
|
||||
<button
|
||||
class="Button IconButton"
|
||||
role="button"
|
||||
type="button"
|
||||
>
|
||||
<i
|
||||
class="CompassIcon icon-dots-horizontal OptionsIcon"
|
||||
/>
|
||||
</div>
|
||||
</button>
|
||||
<div
|
||||
class="Menu noselect left"
|
||||
>
|
||||
@ -178,14 +178,14 @@ exports[`components/viewHeader/emptyCardButton return EmptyCardButton and addCar
|
||||
class="MenuWrapper"
|
||||
role="button"
|
||||
>
|
||||
<div
|
||||
<button
|
||||
class="Button IconButton"
|
||||
role="button"
|
||||
type="button"
|
||||
>
|
||||
<i
|
||||
class="CompassIcon icon-dots-horizontal OptionsIcon"
|
||||
/>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -8,16 +8,16 @@ exports[`components/viewHeader/filterComponent return filterComponent 1`] = `
|
||||
<div
|
||||
class="toolbar hideOnWidescreen"
|
||||
>
|
||||
<div
|
||||
<button
|
||||
aria-label="Close"
|
||||
class="Button IconButton"
|
||||
role="button"
|
||||
title="Close"
|
||||
type="button"
|
||||
>
|
||||
<i
|
||||
class="CompassIcon icon-close CloseIcon"
|
||||
/>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
class="FilterComponent"
|
||||
@ -191,16 +191,16 @@ exports[`components/viewHeader/filterComponent return filterComponent and add Fi
|
||||
<div
|
||||
class="toolbar hideOnWidescreen"
|
||||
>
|
||||
<div
|
||||
<button
|
||||
aria-label="Close"
|
||||
class="Button IconButton"
|
||||
role="button"
|
||||
title="Close"
|
||||
type="button"
|
||||
>
|
||||
<i
|
||||
class="CompassIcon icon-close CloseIcon"
|
||||
/>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
class="FilterComponent"
|
||||
@ -374,16 +374,16 @@ exports[`components/viewHeader/filterComponent return filterComponent and click
|
||||
<div
|
||||
class="toolbar hideOnWidescreen"
|
||||
>
|
||||
<div
|
||||
<button
|
||||
aria-label="Close"
|
||||
class="Button IconButton"
|
||||
role="button"
|
||||
title="Close"
|
||||
type="button"
|
||||
>
|
||||
<i
|
||||
class="CompassIcon icon-close CloseIcon"
|
||||
/>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
class="FilterComponent"
|
||||
@ -557,16 +557,16 @@ exports[`components/viewHeader/filterComponent return filterComponent and filter
|
||||
<div
|
||||
class="toolbar hideOnWidescreen"
|
||||
>
|
||||
<div
|
||||
<button
|
||||
aria-label="Close"
|
||||
class="Button IconButton"
|
||||
role="button"
|
||||
title="Close"
|
||||
type="button"
|
||||
>
|
||||
<i
|
||||
class="CompassIcon icon-close CloseIcon"
|
||||
/>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
class="FilterComponent"
|
||||
|
@ -60,14 +60,14 @@ exports[`components/viewHeader/newCardButton return NewCardButton 1`] = `
|
||||
class="MenuWrapper"
|
||||
role="button"
|
||||
>
|
||||
<div
|
||||
<button
|
||||
class="Button IconButton"
|
||||
role="button"
|
||||
type="button"
|
||||
>
|
||||
<i
|
||||
class="CompassIcon icon-dots-horizontal OptionsIcon"
|
||||
/>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
@ -179,14 +179,14 @@ exports[`components/viewHeader/newCardButton return NewCardButton and addCard 1`
|
||||
class="MenuWrapper"
|
||||
role="button"
|
||||
>
|
||||
<div
|
||||
<button
|
||||
class="Button IconButton"
|
||||
role="button"
|
||||
type="button"
|
||||
>
|
||||
<i
|
||||
class="CompassIcon icon-dots-horizontal OptionsIcon"
|
||||
/>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
@ -298,14 +298,14 @@ exports[`components/viewHeader/newCardButton return NewCardButton and addCardTem
|
||||
class="MenuWrapper"
|
||||
role="button"
|
||||
>
|
||||
<div
|
||||
<button
|
||||
class="Button IconButton"
|
||||
role="button"
|
||||
type="button"
|
||||
>
|
||||
<i
|
||||
class="CompassIcon icon-dots-horizontal OptionsIcon"
|
||||
/>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
|
@ -22,14 +22,14 @@ exports[`components/viewHeader/newCardButtonTemplateItem return NewCardButtonTem
|
||||
class="MenuWrapper"
|
||||
role="button"
|
||||
>
|
||||
<div
|
||||
<button
|
||||
class="Button IconButton"
|
||||
role="button"
|
||||
type="button"
|
||||
>
|
||||
<i
|
||||
class="CompassIcon icon-dots-horizontal OptionsIcon"
|
||||
/>
|
||||
</div>
|
||||
</button>
|
||||
<div
|
||||
class="Menu noselect left"
|
||||
>
|
||||
@ -170,14 +170,14 @@ exports[`components/viewHeader/newCardButtonTemplateItem return NewCardButtonTem
|
||||
class="MenuWrapper"
|
||||
role="button"
|
||||
>
|
||||
<div
|
||||
<button
|
||||
class="Button IconButton"
|
||||
role="button"
|
||||
type="button"
|
||||
>
|
||||
<i
|
||||
class="CompassIcon icon-dots-horizontal OptionsIcon"
|
||||
/>
|
||||
</div>
|
||||
</button>
|
||||
<div
|
||||
class="Menu noselect left"
|
||||
>
|
||||
@ -318,14 +318,14 @@ exports[`components/viewHeader/newCardButtonTemplateItem return NewCardButtonTem
|
||||
class="MenuWrapper"
|
||||
role="button"
|
||||
>
|
||||
<div
|
||||
<button
|
||||
class="Button IconButton"
|
||||
role="button"
|
||||
type="button"
|
||||
>
|
||||
<i
|
||||
class="CompassIcon icon-dots-horizontal OptionsIcon"
|
||||
/>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -353,14 +353,14 @@ exports[`components/viewHeader/newCardButtonTemplateItem return NewCardButtonTem
|
||||
class="MenuWrapper"
|
||||
role="button"
|
||||
>
|
||||
<div
|
||||
<button
|
||||
class="Button IconButton"
|
||||
role="button"
|
||||
type="button"
|
||||
>
|
||||
<i
|
||||
class="CompassIcon icon-dots-horizontal OptionsIcon"
|
||||
/>
|
||||
</div>
|
||||
</button>
|
||||
<div
|
||||
class="Menu noselect left"
|
||||
>
|
||||
@ -501,14 +501,14 @@ exports[`components/viewHeader/newCardButtonTemplateItem return NewCardButtonTem
|
||||
class="MenuWrapper"
|
||||
role="button"
|
||||
>
|
||||
<div
|
||||
<button
|
||||
class="Button IconButton"
|
||||
role="button"
|
||||
type="button"
|
||||
>
|
||||
<i
|
||||
class="CompassIcon icon-dots-horizontal OptionsIcon"
|
||||
/>
|
||||
</div>
|
||||
</button>
|
||||
<div
|
||||
class="Menu noselect left"
|
||||
>
|
||||
|
@ -17,14 +17,14 @@ exports[`components/viewHeader/viewHeader return viewHeader 1`] = `
|
||||
class="MenuWrapper"
|
||||
role="button"
|
||||
>
|
||||
<div
|
||||
<button
|
||||
class="Button IconButton"
|
||||
role="button"
|
||||
type="button"
|
||||
>
|
||||
<i
|
||||
class="CompassIcon icon-chevron-down DropdownIcon"
|
||||
/>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
class="octo-spacer"
|
||||
@ -104,14 +104,14 @@ exports[`components/viewHeader/viewHeader return viewHeader 1`] = `
|
||||
class="MenuWrapper"
|
||||
role="button"
|
||||
>
|
||||
<div
|
||||
<button
|
||||
class="Button IconButton"
|
||||
role="button"
|
||||
type="button"
|
||||
>
|
||||
<i
|
||||
class="CompassIcon icon-dots-horizontal OptionsIcon"
|
||||
/>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
@ -158,14 +158,14 @@ exports[`components/viewHeader/viewHeader return viewHeader readonly 1`] = `
|
||||
class="MenuWrapper"
|
||||
role="button"
|
||||
>
|
||||
<div
|
||||
<button
|
||||
class="Button IconButton"
|
||||
role="button"
|
||||
type="button"
|
||||
>
|
||||
<i
|
||||
class="CompassIcon icon-chevron-down DropdownIcon"
|
||||
/>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
class="octo-spacer"
|
||||
|
@ -10,14 +10,14 @@ exports[`components/viewHeader/viewHeaderActionsMenu return menu and verify call
|
||||
class="MenuWrapper"
|
||||
role="button"
|
||||
>
|
||||
<div
|
||||
<button
|
||||
class="Button IconButton"
|
||||
role="button"
|
||||
type="button"
|
||||
>
|
||||
<i
|
||||
class="CompassIcon icon-dots-horizontal OptionsIcon"
|
||||
/>
|
||||
</div>
|
||||
</button>
|
||||
<div
|
||||
class="Menu noselect bottom"
|
||||
>
|
||||
@ -120,14 +120,14 @@ exports[`components/viewHeader/viewHeaderActionsMenu return menu and verify call
|
||||
class="MenuWrapper"
|
||||
role="button"
|
||||
>
|
||||
<div
|
||||
<button
|
||||
class="Button IconButton"
|
||||
role="button"
|
||||
type="button"
|
||||
>
|
||||
<i
|
||||
class="CompassIcon icon-dots-horizontal OptionsIcon"
|
||||
/>
|
||||
</div>
|
||||
</button>
|
||||
<div
|
||||
class="Menu noselect bottom"
|
||||
>
|
||||
@ -230,14 +230,14 @@ exports[`components/viewHeader/viewHeaderActionsMenu return menu with Share Boar
|
||||
class="MenuWrapper"
|
||||
role="button"
|
||||
>
|
||||
<div
|
||||
<button
|
||||
class="Button IconButton"
|
||||
role="button"
|
||||
type="button"
|
||||
>
|
||||
<i
|
||||
class="CompassIcon icon-dots-horizontal OptionsIcon"
|
||||
/>
|
||||
</div>
|
||||
</button>
|
||||
<div
|
||||
class="Menu noselect bottom"
|
||||
>
|
||||
@ -340,14 +340,14 @@ exports[`components/viewHeader/viewHeaderActionsMenu return menu without Share B
|
||||
class="MenuWrapper"
|
||||
role="button"
|
||||
>
|
||||
<div
|
||||
<button
|
||||
class="Button IconButton"
|
||||
role="button"
|
||||
type="button"
|
||||
>
|
||||
<i
|
||||
class="CompassIcon icon-dots-horizontal OptionsIcon"
|
||||
/>
|
||||
</div>
|
||||
</button>
|
||||
<div
|
||||
class="Menu noselect bottom"
|
||||
>
|
||||
|
@ -70,6 +70,7 @@ const ViewHeader = React.memo((props: Props) => {
|
||||
saveOnEsc={true}
|
||||
readonly={props.readonly}
|
||||
spellCheck={true}
|
||||
autoExpand={false}
|
||||
/>
|
||||
<MenuWrapper>
|
||||
<IconButton icon={<DropdownIcon/>}/>
|
||||
|
@ -1,8 +1,9 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import React, {useState, useRef, useEffect} from 'react'
|
||||
import React, {useState, useRef, useEffect, useMemo} from 'react'
|
||||
import {FormattedMessage, useIntl} from 'react-intl'
|
||||
import {useHotkeys} from 'react-hotkeys-hook'
|
||||
import {debounce} from 'lodash'
|
||||
|
||||
import Button from '../../widgets/buttons/button'
|
||||
import Editable from '../../widgets/editable'
|
||||
@ -19,6 +20,19 @@ const ViewHeaderSearch = (): JSX.Element => {
|
||||
const [isSearching, setIsSearching] = useState(Boolean(searchText))
|
||||
const [searchValue, setSearchValue] = useState(searchText)
|
||||
|
||||
const dispatchSearchText = (value: string) => {
|
||||
dispatch(setSearchText(value))
|
||||
}
|
||||
|
||||
const debouncedDispatchSearchText = useMemo(
|
||||
() => debounce(dispatchSearchText, 200), [])
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
debouncedDispatchSearchText.cancel()
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
searchFieldRef.current?.focus()
|
||||
}, [isSearching])
|
||||
@ -34,17 +48,20 @@ const ViewHeaderSearch = (): JSX.Element => {
|
||||
ref={searchFieldRef}
|
||||
value={searchValue}
|
||||
placeholderText={intl.formatMessage({id: 'ViewHeader.search-text', defaultMessage: 'Search text'})}
|
||||
onChange={setSearchValue}
|
||||
onChange={(value) => {
|
||||
setSearchValue(value)
|
||||
debouncedDispatchSearchText(value)
|
||||
}}
|
||||
onCancel={() => {
|
||||
setSearchValue('')
|
||||
setIsSearching(false)
|
||||
dispatch(setSearchText(''))
|
||||
debouncedDispatchSearchText('')
|
||||
}}
|
||||
onSave={() => {
|
||||
if (searchValue === '') {
|
||||
setIsSearching(false)
|
||||
}
|
||||
dispatch(setSearchText(searchValue))
|
||||
debouncedDispatchSearchText(searchValue)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
|
@ -7,6 +7,9 @@ import {Board, IPropertyTemplate} from './blocks/board'
|
||||
import {Card} from './blocks/card'
|
||||
import {OctoUtils} from './octoUtils'
|
||||
import {Utils} from './utils'
|
||||
import {IAppWindow} from './types'
|
||||
|
||||
declare let window: IAppWindow
|
||||
|
||||
class CsvExporter {
|
||||
static exportTableCsv(board: Board, activeView: BoardView, cards: Card[], intl: IntlShape, view?: BoardView): void {
|
||||
@ -36,8 +39,8 @@ class CsvExporter {
|
||||
link.click()
|
||||
|
||||
// TODO: Review if this is needed in the future, this is to fix the problem with linux webview links
|
||||
if ((window as any).openInNewBrowser) {
|
||||
(window as any).openInNewBrowser(encodedUri)
|
||||
if (window.openInNewBrowser) {
|
||||
window.openInNewBrowser(encodedUri)
|
||||
}
|
||||
|
||||
// TODO: Remove or reuse link
|
||||
@ -69,6 +72,9 @@ class CsvExporter {
|
||||
if (template.type === 'number') {
|
||||
const numericValue = propertyValue ? Number(propertyValue).toString() : ''
|
||||
row.push(numericValue)
|
||||
} else if (template.type === 'multiSelect') {
|
||||
const multiSelectValue = ((displayValue as unknown || []) as string[]).join('|')
|
||||
row.push(multiSelectValue)
|
||||
} else {
|
||||
// Export as string
|
||||
row.push(`"${this.encodeText(displayValue)}"`)
|
||||
|
@ -1,6 +1,7 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {IAppWindow} from './types'
|
||||
import {exportUserSettingsBlob, importUserSettingsBlob} from './userSettings'
|
||||
|
||||
declare interface INativeApp {
|
||||
@ -8,6 +9,7 @@ declare interface INativeApp {
|
||||
}
|
||||
|
||||
declare const NativeApp: INativeApp
|
||||
declare let window: IAppWindow
|
||||
|
||||
export function importNativeAppSettings(): void {
|
||||
if (typeof NativeApp === 'undefined' || !NativeApp.settingsBlob) {
|
||||
@ -23,6 +25,6 @@ export function notifySettingsChanged(key: string): void {
|
||||
postWebKitMessage({type: 'didChangeUserSettings', settingsBlob: exportUserSettingsBlob(), key})
|
||||
}
|
||||
|
||||
function postWebKitMessage(message: any) {
|
||||
(window as any).webkit?.messageHandlers.nativeApp?.postMessage(message)
|
||||
function postWebKitMessage<T>(message: T) {
|
||||
window.webkit?.messageHandlers.nativeApp?.postMessage(message)
|
||||
}
|
||||
|
@ -92,7 +92,7 @@ const BoardPage = (props: Props): JSX.Element => {
|
||||
|
||||
// Backward compatibility end
|
||||
const boardId = match.params.boardId
|
||||
const viewId = match.params.viewId
|
||||
const viewId = match.params.viewId === '0' ? '' : match.params.viewId
|
||||
|
||||
if (!boardId) {
|
||||
// Load last viewed boardView
|
||||
@ -146,12 +146,13 @@ const BoardPage = (props: Props): JSX.Element => {
|
||||
}, [board?.title, activeView?.title])
|
||||
|
||||
useEffect(() => {
|
||||
let loadAction: any = initialLoad
|
||||
let loadAction: any = initialLoad /* eslint-disable-line @typescript-eslint/no-explicit-any */
|
||||
let token = localStorage.getItem('focalboardSessionId') || ''
|
||||
if (props.readonly) {
|
||||
loadAction = initialReadOnlyLoad
|
||||
token = token || queryString.get('r') || ''
|
||||
}
|
||||
|
||||
dispatch(loadAction(match.params.boardId))
|
||||
|
||||
if (wsClient.state === 'open') {
|
||||
|
@ -70,9 +70,9 @@ exports[`pages/dashboard/DashboardPage base case 1`] = `
|
||||
<div
|
||||
class="sidebarSwitcher"
|
||||
>
|
||||
<div
|
||||
<button
|
||||
class="Button IconButton"
|
||||
role="button"
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
class="HideSidebarIcon Icon"
|
||||
@ -86,7 +86,7 @@ exports[`pages/dashboard/DashboardPage base case 1`] = `
|
||||
points="50,20 20,50, 50,80"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
|
@ -5,6 +5,8 @@
|
||||
--sidebar-text-rgb: 255, 255, 255;
|
||||
--button-color-rgb: 255, 255, 255;
|
||||
--button-bg-rgb: 28, 88, 217;
|
||||
--button-danger-color-rgb: 255, 255, 255;
|
||||
--button-danger-bg-rgb: 210, 75, 78;
|
||||
--link-color-rgb: 56, 111, 229;
|
||||
--error-text-rgb: #d24b4e;
|
||||
}
|
||||
|
10
webapp/src/types/index.d.ts
vendored
Normal file
10
webapp/src/types/index.d.ts
vendored
Normal file
@ -0,0 +1,10 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
export interface IAppWindow extends Window {
|
||||
baseURL?: string
|
||||
frontendBaseURL?: string
|
||||
isFocalboardPlugin?: boolean
|
||||
msCrypto: Crypto
|
||||
openInNewBrowser?: ((href: string) => void) | null
|
||||
webkit?: {messageHandlers: {nativeApp?: {postMessage: <T>(message: T) => void}}}
|
||||
}
|
@ -4,6 +4,9 @@
|
||||
import {createIntl} from 'react-intl'
|
||||
|
||||
import {Utils, IDType} from './utils'
|
||||
import {IAppWindow} from './types'
|
||||
|
||||
declare let window: IAppWindow
|
||||
|
||||
describe('utils', () => {
|
||||
describe('assureProtocol', () => {
|
||||
@ -44,11 +47,10 @@ describe('utils', () => {
|
||||
})
|
||||
|
||||
test('should not allow XSS on links href on the desktop app', () => {
|
||||
const windowAsAny = window as any
|
||||
windowAsAny.openInNewBrowser = () => null
|
||||
window.openInNewBrowser = () => null
|
||||
const expectedHtml = '<p><a target="_blank" rel="noreferrer" href="%22xss-attack=%22true%22other=%22whatever" title="" onclick="event.stopPropagation(); openInNewBrowser && openInNewBrowser(event.target.href);"></a></p>'
|
||||
expect(Utils.htmlFromMarkdown('[]("xss-attack="true"other="whatever)')).toBe(expectedHtml)
|
||||
windowAsAny.openInNewBrowser = null
|
||||
window.openInNewBrowser = null
|
||||
})
|
||||
})
|
||||
|
||||
@ -62,8 +64,7 @@ describe('utils', () => {
|
||||
})
|
||||
|
||||
test('buildURL, base no slash', () => {
|
||||
const windowAsAny = window as any
|
||||
windowAsAny.baseURL = 'base'
|
||||
window.baseURL = 'base'
|
||||
|
||||
expect(Utils.buildURL('test', true)).toBe('http://localhost/base/test')
|
||||
expect(Utils.buildURL('/test', true)).toBe('http://localhost/base/test')
|
||||
@ -73,8 +74,7 @@ describe('utils', () => {
|
||||
})
|
||||
|
||||
test('buildUrl, base with slash', () => {
|
||||
const windowAsAny = window as any
|
||||
windowAsAny.baseURL = '/base/'
|
||||
window.baseURL = '/base/'
|
||||
|
||||
expect(Utils.buildURL('test', true)).toBe('http://localhost/base/test')
|
||||
expect(Utils.buildURL('/test', true)).toBe('http://localhost/base/test')
|
||||
|
@ -2,18 +2,16 @@
|
||||
// See LICENSE.txt for license information.
|
||||
import marked from 'marked'
|
||||
import {IntlShape} from 'react-intl'
|
||||
import moment from 'moment'
|
||||
|
||||
import {Block} from './blocks/block'
|
||||
import {createBoard} from './blocks/board'
|
||||
import {createBoardView} from './blocks/boardView'
|
||||
import {createCard} from './blocks/card'
|
||||
import {createCommentBlock} from './blocks/commentBlock'
|
||||
import {IAppWindow} from './types'
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
msCrypto: Crypto
|
||||
}
|
||||
}
|
||||
declare let window: IAppWindow
|
||||
|
||||
const IconClass = 'octo-icon'
|
||||
const OpenButtonClass = 'open-button'
|
||||
@ -221,7 +219,7 @@ class Utils {
|
||||
'rel="noreferrer" ' +
|
||||
`href="${encodeURI(href || '')}" ` +
|
||||
`title="${title ? encodeURI(title) : ''}" ` +
|
||||
`onclick="event.stopPropagation();${((window as any).openInNewBrowser ? ' openInNewBrowser && openInNewBrowser(event.target.href);' : '')}"` +
|
||||
`onclick="event.stopPropagation();${(window.openInNewBrowser ? ' openInNewBrowser && openInNewBrowser(event.target.href);' : '')}"` +
|
||||
'>' + contents + '</a>'
|
||||
}
|
||||
|
||||
@ -265,6 +263,10 @@ class Utils {
|
||||
})
|
||||
}
|
||||
|
||||
static relativeDisplayDateTime(date: Date, intl: IntlShape): string {
|
||||
return moment(date).locale(intl.locale.toLowerCase()).fromNow()
|
||||
}
|
||||
|
||||
static sleep(miliseconds: number): Promise<void> {
|
||||
return new Promise((resolve) => setTimeout(resolve, miliseconds))
|
||||
}
|
||||
@ -460,7 +462,7 @@ class Utils {
|
||||
}
|
||||
|
||||
static getBaseURL(absolute?: boolean): string {
|
||||
let baseURL = (window as any).baseURL || ''
|
||||
let baseURL = window.baseURL || ''
|
||||
baseURL = baseURL.replace(/\/+$/, '')
|
||||
if (baseURL.indexOf('/') === 0) {
|
||||
baseURL = baseURL.slice(1)
|
||||
@ -472,7 +474,7 @@ class Utils {
|
||||
}
|
||||
|
||||
static getFrontendBaseURL(absolute?: boolean): string {
|
||||
let frontendBaseURL = (window as any).frontendBaseURL || this.getBaseURL(absolute)
|
||||
let frontendBaseURL = window.frontendBaseURL || this.getBaseURL(absolute)
|
||||
frontendBaseURL = frontendBaseURL.replace(/\/+$/, '')
|
||||
if (frontendBaseURL.indexOf('/') === 0) {
|
||||
frontendBaseURL = frontendBaseURL.slice(1)
|
||||
@ -503,7 +505,7 @@ class Utils {
|
||||
}
|
||||
|
||||
static isFocalboardPlugin(): boolean {
|
||||
return Boolean((window as any).isFocalboardPlugin)
|
||||
return Boolean(window.isFocalboardPlugin)
|
||||
}
|
||||
|
||||
static fixBlock(block: Block): Block {
|
||||
|
@ -58,6 +58,16 @@
|
||||
}
|
||||
}
|
||||
|
||||
&.emphasis--danger {
|
||||
color: rgb(var(--button-danger-color-rgb));
|
||||
background-color: rgb(var(--button-danger-bg-rgb));
|
||||
|
||||
&:hover {
|
||||
background-color: rgb(var(--button-danger-bg-rgb), 0.8);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
&.active {
|
||||
background: rgba(var(--button-bg-rgb), 0.08);
|
||||
color: rgb(var(--button-bg-rgb));
|
||||
|
@ -5,11 +5,11 @@ import React from 'react'
|
||||
import './iconButton.scss'
|
||||
|
||||
type Props = {
|
||||
onClick?: (e: React.MouseEvent<HTMLDivElement>) => void
|
||||
onClick?: (e: React.MouseEvent<HTMLButtonElement>) => void
|
||||
title?: string
|
||||
icon?: React.ReactNode
|
||||
className?: string
|
||||
onMouseDown?: (e: React.MouseEvent<HTMLDivElement>) => void
|
||||
onMouseDown?: (e: React.MouseEvent<HTMLButtonElement>) => void
|
||||
}
|
||||
|
||||
function IconButton(props: Props): JSX.Element {
|
||||
@ -18,8 +18,8 @@ function IconButton(props: Props): JSX.Element {
|
||||
className += ' ' + props.className
|
||||
}
|
||||
return (
|
||||
<div
|
||||
role='button'
|
||||
<button
|
||||
type='button'
|
||||
onClick={props.onClick}
|
||||
onMouseDown={props.onMouseDown}
|
||||
className={className}
|
||||
@ -27,7 +27,7 @@ function IconButton(props: Props): JSX.Element {
|
||||
aria-label={props.title}
|
||||
>
|
||||
{props.icon}
|
||||
</div>
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -30,7 +30,6 @@ function SubMenuOption(props: SubMenuOptionProps): JSX.Element {
|
||||
setIsOpen(true)
|
||||
}, 50)
|
||||
}}
|
||||
onMouseLeave={() => setIsOpen(false)}
|
||||
onClick={(e: React.MouseEvent) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
|
@ -1,7 +1,7 @@
|
||||
.Switch {
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
|
||||
align-items: center;
|
||||
box-sizing: content-box;
|
||||
height: 18px;
|
||||
width: 26px;
|
||||
|
@ -87,7 +87,7 @@ const ValueSelectorLabel = React.memo((props: LabelProps): JSX.Element => {
|
||||
onClick={() => props.onDeleteOption(option)}
|
||||
/>
|
||||
<Menu.Separator/>
|
||||
{Object.entries(Constants.menuColors).map(([key, color]: any) => (
|
||||
{Object.entries(Constants.menuColors).map(([key, color]: [string, string]) => (
|
||||
<Menu.Color
|
||||
key={key}
|
||||
id={key}
|
||||
@ -190,7 +190,7 @@ function ValueSelector(props: Props): JSX.Element {
|
||||
onBlur={props.onBlur}
|
||||
onCreateOption={props.onCreate}
|
||||
autoFocus={true}
|
||||
value={props.value}
|
||||
value={props.value || null}
|
||||
closeMenuOnSelect={true}
|
||||
placeholder={props.emptyValue}
|
||||
hideSelectedOptions={false}
|
||||
|
@ -33,7 +33,7 @@ export const ACTION_UPDATE_CLIENT_CONFIG = 'UPDATE_CLIENT_CONFIG'
|
||||
// The Mattermost websocket client interface
|
||||
export interface MMWebSocketClient {
|
||||
conn: WebSocket | null;
|
||||
sendMessage(action: string, data: any, responseCallback?: () => void): void
|
||||
sendMessage(action: string, data: any, responseCallback?: () => void): void /* eslint-disable-line @typescript-eslint/no-explicit-any */
|
||||
setReconnectCallback(callback: () => void): void
|
||||
setErrorCallback(callback: (event: Event) => void): void
|
||||
setCloseCallback(callback: (connectFailCount: number) => void): void
|
||||
|
@ -5,78 +5,27 @@ section: "download"
|
||||
weight: 2
|
||||
---
|
||||
|
||||
Focalboard is now integrated with Mattermost v5.36 and later.
|
||||
Focalboard is installed with Mattermost v6.0, where it's called Boards. To access and use Boards, [install or upgrade to Mattermost v6.0 or later](https://mattermost.com/get-started/). Then, select the Product menu in the top left corner of Mattermost and choose **Boards**.
|
||||
|
||||
## Enable Focalboard plugin
|
||||
No additional server or web-proxy configuration is required.
|
||||
|
||||
To enable Focalboard, open your Mattermost instance, then:
|
||||
### Enable shared boards
|
||||
|
||||
1. Go to **Main Menu > Marketplace**.
|
||||
2. Search for "Focalboard".
|
||||
3. Install the Focalboard plugin.
|
||||
4. Select **Configure** and enable the plugin.
|
||||
5. Select **Save**.
|
||||
The shared boards feature is disabled by default in Mattermost. To enable it:
|
||||
|
||||
The Focalboard plugin requires websocket traffic to be passed by the proxy. Update your NGINX or Apache web proxy config following the steps below.
|
||||
1. Open the System Console.
|
||||
2. Go to **Plugins** and select Mattermost Boards.
|
||||
3. Set **Enable Publicly-Shared Boards** to **true**.
|
||||
4. Choose **Save**.
|
||||
|
||||
### With NGINX
|
||||
### Permissions
|
||||
|
||||
After following the standard [Mattermost install steps](https://docs.mattermost.com/install/installing-ubuntu-1804-LTS.html#configuring-nginx-as-a-proxy-for-mattermost-server), edit `/etc/nginx/sites-available/mattermost` and add this section to it:
|
||||
|
||||
```
|
||||
location ~ /plugins/focalboard/ws/* {
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
client_max_body_size 50M;
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header X-Frame-Options SAMEORIGIN;
|
||||
proxy_buffers 256 16k;
|
||||
proxy_buffer_size 16k;
|
||||
client_body_timeout 60;
|
||||
send_timeout 300;
|
||||
lingering_timeout 5;
|
||||
proxy_connect_timeout 90;
|
||||
proxy_send_timeout 300;
|
||||
proxy_read_timeout 90s;
|
||||
proxy_pass http://backend;
|
||||
}
|
||||
```
|
||||
|
||||
Restart NGINX with `sudo systemctl restart nginx`.
|
||||
|
||||
### With Apache (unofficial)
|
||||
|
||||
After following the [install guide for Apache and Mattermost](https://docs.mattermost.com/configure/configuring-apache2.html#configuring-apache2-as-a-proxy-for-mattermost-server-unofficial), modify the web sockets section in `/etc/apache2/sites-available` as follows:
|
||||
|
||||
```
|
||||
# Set web sockets
|
||||
RewriteEngine On
|
||||
RewriteCond %{REQUEST_URI} /api/v[0-9]+/(users/)?websocket|/plugins/focalboard/ws/* [NC,OR]
|
||||
RewriteCond %{HTTP:UPGRADE} ^WebSocket$ [NC,OR]
|
||||
RewriteCond %{HTTP:CONNECTION} ^Upgrade$ [NC]
|
||||
RewriteRule .* ws://127.0.0.1:8065%{REQUEST_URI} [P,QSA,L]
|
||||
```
|
||||
|
||||
Restart Apache with `sudo systemctl restart apache2`
|
||||
|
||||
## Accessing Focalboard from Mattermost
|
||||
|
||||
<img src='https://user-images.githubusercontent.com/46905241/121930013-bbd12880-ccf6-11eb-9647-c9e367690111.png' style='max-height: 50px' />
|
||||
|
||||
In Mattermost, select the Focalboard icon in the channel header to access boards for that channel.
|
||||
|
||||
### Focalboard Permissions
|
||||
|
||||
Focalboard currently uses channel-based permissions, meaning that only members of the associated channel can access (read / write) the boards for that channel.
|
||||
Mattermost Boards currently uses channel-based permissions, meaning that only members of the associated channel can access (read/write) the boards for that channel.
|
||||
|
||||
You can use this to create private boards:
|
||||
1. Create or join a private channel (or group channel or direct-message)
|
||||
2. Click on the Focalboard icon in the channel header
|
||||
3. Create a board
|
||||
|
||||
Only members of that private channel can access the board.
|
||||
Create or join a Private channel, Group Message, or Direct Message. Then, select the Boards icon in the channel header to create a board. Only members of the Private channel, Group Message, or Direct Message can access the board.
|
||||
|
||||
You can use the [share board](/guide/user/#sharing-boards) feature to share a read-only board with anyone (incuding unauthenticated users) who has the generated link.
|
||||
|
||||
For more information about using Mattermost Boards, refer to the main [product documentation here](https://docs.mattermost.com/guides/boards.html).
|
||||
|
@ -13,7 +13,7 @@ Follow the steps in the [Mattermost admin guide to enable custom plugins](https:
|
||||
1. Manually set `PluginSettings > EnableUploads` to `true` in your `config.json`
|
||||
2. Restart the Mattermost server
|
||||
|
||||
Download `mattermost-plugin-focalboard.tar.gz` from the build or release, e.g. the [Focalboard 0.7.3 release](https://github.com/mattermost/focalboard/releases/tag/v0.7.3).
|
||||
Download `mattermost-plugin-focalboard.tar.gz` from the latest build or [release](https://github.com/mattermost/focalboard/releases).
|
||||
|
||||
Then upload the Focalboard plugin:
|
||||
1. Navigate to **System Console > Plugins > Plugin Management**
|
||||
|
@ -12,8 +12,8 @@ Use the URL of the Ubuntu archive package, `focalboard-server-linux-amd64.tar.gz
|
||||
Create and use a clean directory, or delete any existing packages first, then run:
|
||||
|
||||
```
|
||||
# Download the new version (e.g. 0.7.0 here, check the release for the latest one)
|
||||
wget https://github.com/mattermost/focalboard/releases/download/v0.7.0/focalboard-server-linux-amd64.tar.gz
|
||||
# Download the new version (e.g. 0.9.2 here, check the release for the latest one)
|
||||
wget https://github.com/mattermost/focalboard/releases/download/v0.9.2/focalboard-server-linux-amd64.tar.gz
|
||||
tar -xvzf focalboard-server-linux-amd64.tar.gz
|
||||
|
||||
# Stop the server
|
||||
|
@ -5,7 +5,7 @@ subsection: Personal Edition
|
||||
weight: 2
|
||||
---
|
||||
|
||||
Focalboard Personal Server allows your team to work together on shared project boards.
|
||||
Focalboard Personal Server is a standalone server for development and personal use. For team use, check out [Mattermost Boards](../../mattermost/), which supports private boards, team communication, and more.
|
||||
|
||||
Follow these steps it up on an Ubuntu server. To upgrade an existing installation, see [the upgrade guide](../ubuntu-upgrade).
|
||||
|
||||
@ -17,10 +17,10 @@ Popular hosted options include:
|
||||
|
||||
## Install Focalboard
|
||||
|
||||
Download the Ubuntu archive package from the appropriate [release in GitHub](https://github.com/mattermost/focalboard/releases). E.g. this is the link for v0.7.0 (which may no longer be the latest one):
|
||||
Download the Ubuntu archive package from the appropriate [release in GitHub](https://github.com/mattermost/focalboard/releases). E.g. this is the link for v0.9.2 (which may no longer be the latest one):
|
||||
|
||||
```
|
||||
wget https://github.com/mattermost/focalboard/releases/download/v0.7.0/focalboard-server-linux-amd64.tar.gz
|
||||
wget https://github.com/mattermost/focalboard/releases/download/v0.9.2/focalboard-server-linux-amd64.tar.gz
|
||||
tar -xvzf focalboard-server-linux-amd64.tar.gz
|
||||
sudo mv focalboard /opt
|
||||
```
|
||||
@ -197,7 +197,7 @@ Change the dbconfig setting to use the MySQL database you created:
|
||||
|
||||
When MySQL is being used, using collation is recommended over using charset.
|
||||
|
||||
Using a variant of `utf8mb4` collation is required. For example, `utf8mb4_general_ci`
|
||||
Using a variant of `utf8mb4` collation is required. For example, `utf8mb4_general_ci`
|
||||
is used by default when no collation is specified.
|
||||
|
||||
If you're using Focalboard as a Mattermost Plugin prior to version 0.9 with MySQL,
|
||||
|
@ -6,16 +6,21 @@
|
||||
</h2>
|
||||
|
||||
<p>
|
||||
It's a project management tool that helps define, organize, track and manage work across teams, using a familiar kanban board view. Focalboard comes in two editions:
|
||||
It's a project management tool that helps define, organize, track and manage work across teams, using a familiar kanban board view. Focalboard comes in two main editions:
|
||||
</p>
|
||||
|
||||
<div class="text-left">
|
||||
<ul >
|
||||
<li><b><a href="https://www.focalboard.com/download/personal-edition/desktop/">Focalboard Personal Desktop</a></b>: A stand-alone desktop app for your todos and personal projects</li>
|
||||
<li><b><a href="https://www.focalboard.com/download/personal-edition/ubuntu/">Focalboard Personal Server</a></b>: A self-hosted server for your team to collaborate</li>
|
||||
<li><b><a href="download/personal-edition/desktop/">Focalboard Personal Desktop</a></b>: A stand-alone single-user desktop app for your todos and personal projects</li>
|
||||
<li><b><a href="download/mattermost/">Mattermost Boards</a></b>: A cloud or self-hosted server for your team to plan and collaborate</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p>
|
||||
Focalboard can also be installed as a standalone <a href="download/personal-edition/ubuntu/">personal server</a> for development.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
<p class="text-center">
|
||||
<a href="download/personal-edition">
|
||||
|
Loading…
Reference in New Issue
Block a user