mirror of
https://github.com/hcengineering/platform.git
synced 2025-04-23 16:56:07 +00:00
[TSK-1327] Clickable links for string presenters and rich text editor (#3088)
Signed-off-by: Sergei Ogorelkov <sergei.ogorelkov@icloud.com>
This commit is contained in:
parent
324ff317cb
commit
073e55b15c
@ -269,6 +269,7 @@ specifiers:
|
|||||||
'@types/xml2js': ~0.4.9
|
'@types/xml2js': ~0.4.9
|
||||||
'@typescript-eslint/eslint-plugin': ^5.41.0
|
'@typescript-eslint/eslint-plugin': ^5.41.0
|
||||||
'@typescript-eslint/parser': ^5.41.0
|
'@typescript-eslint/parser': ^5.41.0
|
||||||
|
autolinker: 4.0.0
|
||||||
autoprefixer: ^10.4.14
|
autoprefixer: ^10.4.14
|
||||||
body-parser: ~1.19.1
|
body-parser: ~1.19.1
|
||||||
browserslist: 4.21.5
|
browserslist: 4.21.5
|
||||||
@ -641,6 +642,7 @@ dependencies:
|
|||||||
'@types/xml2js': 0.4.11
|
'@types/xml2js': 0.4.11
|
||||||
'@typescript-eslint/eslint-plugin': 5.42.1_d506b9be61cb4ac2646ecbc6e0680464
|
'@typescript-eslint/eslint-plugin': 5.42.1_d506b9be61cb4ac2646ecbc6e0680464
|
||||||
'@typescript-eslint/parser': 5.42.1_eslint@8.27.0+typescript@4.8.4
|
'@typescript-eslint/parser': 5.42.1_eslint@8.27.0+typescript@4.8.4
|
||||||
|
autolinker: 4.0.0
|
||||||
autoprefixer: 10.4.14_postcss@8.4.20
|
autoprefixer: 10.4.14_postcss@8.4.20
|
||||||
body-parser: 1.19.2
|
body-parser: 1.19.2
|
||||||
browserslist: 4.21.5
|
browserslist: 4.21.5
|
||||||
@ -4267,6 +4269,12 @@ packages:
|
|||||||
engines: {node: '>=8.0.0'}
|
engines: {node: '>=8.0.0'}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/autolinker/4.0.0:
|
||||||
|
resolution: {integrity: sha512-fl5Kh6BmEEZx+IWBfEirnRUU5+cOiV0OK7PEt0RBKvJMJ8GaRseIOeDU3FKf4j3CE5HVefcjHmhYPOcaVt0bZw==}
|
||||||
|
dependencies:
|
||||||
|
tslib: 2.4.1
|
||||||
|
dev: false
|
||||||
|
|
||||||
/autoprefixer/10.4.14_postcss@8.4.20:
|
/autoprefixer/10.4.14_postcss@8.4.20:
|
||||||
resolution: {integrity: sha512-FQzyfOsTlwVzjHxKEqRIAdJx9niO6VCBCoEwax/VLSoQF29ggECcPuBqUMZ+u8jCZOPSy8b8/8KnuFbp0SaFZQ==}
|
resolution: {integrity: sha512-FQzyfOsTlwVzjHxKEqRIAdJx9niO6VCBCoEwax/VLSoQF29ggECcPuBqUMZ+u8jCZOPSy8b8/8KnuFbp0SaFZQ==}
|
||||||
engines: {node: ^10 || ^12 || >=14}
|
engines: {node: ^10 || ^12 || >=14}
|
||||||
@ -16711,7 +16719,7 @@ packages:
|
|||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
file:projects/ui.tgz_a1d864769aaf53d09b76fe134ab55e60:
|
file:projects/ui.tgz_a1d864769aaf53d09b76fe134ab55e60:
|
||||||
resolution: {integrity: sha512-hn+I2VojXSb6t3PYHND/JyhwX8tZ8Jr7D+rzs1XjmVxhNMHnO+HA8QHzXyqZBjggTsBc8kUqWlnm44W5tt3m8Q==, tarball: file:projects/ui.tgz}
|
resolution: {integrity: sha512-TvtqATO5uYXW+jS7m2OEAikTIUwJ5JSDwTdxk0OqD62FO4nkED8FA63goDvEoBPUZtoFn7yTzy2cWiXepZNiXA==, tarball: file:projects/ui.tgz}
|
||||||
id: file:projects/ui.tgz
|
id: file:projects/ui.tgz
|
||||||
name: '@rush-temp/ui'
|
name: '@rush-temp/ui'
|
||||||
version: 0.0.0
|
version: 0.0.0
|
||||||
@ -16719,6 +16727,7 @@ packages:
|
|||||||
'@types/jest': 28.1.8
|
'@types/jest': 28.1.8
|
||||||
'@typescript-eslint/eslint-plugin': 5.42.1_d506b9be61cb4ac2646ecbc6e0680464
|
'@typescript-eslint/eslint-plugin': 5.42.1_d506b9be61cb4ac2646ecbc6e0680464
|
||||||
'@typescript-eslint/parser': 5.42.1_eslint@8.27.0+typescript@4.8.4
|
'@typescript-eslint/parser': 5.42.1_eslint@8.27.0+typescript@4.8.4
|
||||||
|
autolinker: 4.0.0
|
||||||
eslint: 8.27.0
|
eslint: 8.27.0
|
||||||
eslint-config-standard-with-typescript: 23.0.0_c9fe9619f50f4e82337a86c3af25e566
|
eslint-config-standard-with-typescript: 23.0.0_c9fe9619f50f4e82337a86c3af25e566
|
||||||
eslint-plugin-import: 2.26.0_eslint@8.27.0
|
eslint-plugin-import: 2.26.0_eslint@8.27.0
|
||||||
|
@ -56,7 +56,10 @@ export const defaultExtensions = [
|
|||||||
Heading.configure({
|
Heading.configure({
|
||||||
levels: headingLevels
|
levels: headingLevels
|
||||||
}),
|
}),
|
||||||
Link.configure({ openOnClick: false }),
|
Link.configure({
|
||||||
|
openOnClick: true,
|
||||||
|
HTMLAttributes: { class: 'cursor-pointer', rel: 'noopener noreferrer', target: '_blank' }
|
||||||
|
}),
|
||||||
...tableExtensions,
|
...tableExtensions,
|
||||||
...taskListExtensions
|
...taskListExtensions
|
||||||
]
|
]
|
||||||
|
@ -38,7 +38,8 @@
|
|||||||
"@hcengineering/core": "^0.6.23",
|
"@hcengineering/core": "^0.6.23",
|
||||||
"just-clone": "~6.2.0",
|
"just-clone": "~6.2.0",
|
||||||
"svelte": "3.55.1",
|
"svelte": "3.55.1",
|
||||||
"fast-equals": "^2.0.3"
|
"fast-equals": "^2.0.3",
|
||||||
|
"autolinker": "4.0.0"
|
||||||
},
|
},
|
||||||
"repository": "https://github.com/hcenginneing/anticrm",
|
"repository": "https://github.com/hcenginneing/anticrm",
|
||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
|
31
packages/ui/src/components/LinkWrapper.svelte
Normal file
31
packages/ui/src/components/LinkWrapper.svelte
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
<!--
|
||||||
|
// Copyright © 2023 Hardcore Engineering Inc.
|
||||||
|
//
|
||||||
|
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License. You may
|
||||||
|
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
//
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
-->
|
||||||
|
<script lang="ts">
|
||||||
|
// This component converts all URLs from the provided string or IntlString to Links.
|
||||||
|
|
||||||
|
import { IntlString, translate } from '@hcengineering/platform'
|
||||||
|
import { replaceURLs } from '../utils'
|
||||||
|
|
||||||
|
export let text: string | undefined = undefined
|
||||||
|
export let label: IntlString | undefined = undefined
|
||||||
|
export let params: Readonly<Record<string, any>> = {}
|
||||||
|
|
||||||
|
$: label && translate(label, params).then((result) => (text = result))
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if text}
|
||||||
|
<!-- "replaceURLs" produces sanitazed string -->
|
||||||
|
{@html replaceURLs(text)}
|
||||||
|
{/if}
|
@ -86,6 +86,7 @@ export { default as Spinner } from './components/Spinner.svelte'
|
|||||||
export { default as Popup } from './components/Popup.svelte'
|
export { default as Popup } from './components/Popup.svelte'
|
||||||
export { default as CircleButton } from './components/CircleButton.svelte'
|
export { default as CircleButton } from './components/CircleButton.svelte'
|
||||||
export { default as Link } from './components/Link.svelte'
|
export { default as Link } from './components/Link.svelte'
|
||||||
|
export { default as LinkWrapper } from './components/LinkWrapper.svelte'
|
||||||
export { default as Like } from './components/Like.svelte'
|
export { default as Like } from './components/Like.svelte'
|
||||||
export { default as TimeSince } from './components/TimeSince.svelte'
|
export { default as TimeSince } from './components/TimeSince.svelte'
|
||||||
export { default as Dropdown } from './components/Dropdown.svelte'
|
export { default as Dropdown } from './components/Dropdown.svelte'
|
||||||
|
@ -17,6 +17,7 @@ import { generateId } from '@hcengineering/core'
|
|||||||
import type { Metadata } from '@hcengineering/platform'
|
import type { Metadata } from '@hcengineering/platform'
|
||||||
import { setMetadata } from '@hcengineering/platform'
|
import { setMetadata } from '@hcengineering/platform'
|
||||||
import { writable } from 'svelte/store'
|
import { writable } from 'svelte/store'
|
||||||
|
import autolinker from 'autolinker'
|
||||||
import { Notification, NotificationPosition, NotificationSeverity, notificationsStore } from '.'
|
import { Notification, NotificationPosition, NotificationSeverity, notificationsStore } from '.'
|
||||||
import { AnyComponent, AnySvelteComponent } from './types'
|
import { AnyComponent, AnySvelteComponent } from './types'
|
||||||
|
|
||||||
@ -128,3 +129,24 @@ export function mouseAttractor (op: () => void, diff = 2): (evt: MouseEvent) =>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Replaces URLs with Links in a given block of text/HTML
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* replaceURLs("Check out google.com")
|
||||||
|
* returns: "Check out <a href='http://google.com' target='_blank' rel='noopener noreferrer'>google.com</a>"
|
||||||
|
*
|
||||||
|
* @export
|
||||||
|
* @param {string} text
|
||||||
|
* @returns {string} string with replaced URLs
|
||||||
|
*/
|
||||||
|
export function replaceURLs (text: string): string {
|
||||||
|
return autolinker.link(text, {
|
||||||
|
urls: true,
|
||||||
|
phone: false,
|
||||||
|
email: false,
|
||||||
|
sanitizeHtml: true,
|
||||||
|
stripPrefix: false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
@ -21,7 +21,7 @@
|
|||||||
import { Vacancy } from '@hcengineering/recruit'
|
import { Vacancy } from '@hcengineering/recruit'
|
||||||
import { FullDescriptionBox } from '@hcengineering/text-editor'
|
import { FullDescriptionBox } from '@hcengineering/text-editor'
|
||||||
import tracker from '@hcengineering/tracker'
|
import tracker from '@hcengineering/tracker'
|
||||||
import { Button, Component, EditBox, Grid, IconMixin, IconMoreH, showPopup } from '@hcengineering/ui'
|
import { Button, Component, EditBox, Grid, IconMixin, IconMoreH, LinkWrapper, showPopup } from '@hcengineering/ui'
|
||||||
import { ContextMenu, DocAttributeBar } from '@hcengineering/view-resources'
|
import { ContextMenu, DocAttributeBar } from '@hcengineering/view-resources'
|
||||||
import { createEventDispatcher } from 'svelte'
|
import { createEventDispatcher } from 'svelte'
|
||||||
import recruit from '../plugin'
|
import recruit from '../plugin'
|
||||||
@ -113,13 +113,11 @@
|
|||||||
>
|
>
|
||||||
<svelte:fragment slot="subtitle">
|
<svelte:fragment slot="subtitle">
|
||||||
{#if object.description}
|
{#if object.description}
|
||||||
{#if object.description.trim().startsWith('http://') || object.description.trim().startsWith('https://')}
|
<div class="flex">
|
||||||
<a href={object.description} class="whitespace-nowrap" target="_blank" rel="noreferrer noopener">
|
<span class="overflow-label" title={object.description}>
|
||||||
{object.description}
|
<LinkWrapper text={object.description} />
|
||||||
</a>
|
</span>
|
||||||
{:else}
|
</div>
|
||||||
{object.description}
|
|
||||||
{/if}
|
|
||||||
{/if}
|
{/if}
|
||||||
</svelte:fragment>
|
</svelte:fragment>
|
||||||
<svelte:fragment slot="attributes" let:direction={dir}>
|
<svelte:fragment slot="attributes" let:direction={dir}>
|
||||||
|
@ -14,13 +14,10 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { getEmbeddedLabel, IntlString } from '@hcengineering/platform'
|
import { getEmbeddedLabel } from '@hcengineering/platform'
|
||||||
import { LabelAndProps, tooltip } from '@hcengineering/ui'
|
import { LabelAndProps, LinkWrapper, tooltip } from '@hcengineering/ui'
|
||||||
import StringEditor from './StringEditor.svelte'
|
|
||||||
|
|
||||||
export let value: string | string[] | undefined
|
export let value: string | string[] | undefined
|
||||||
export let onChange: ((value: string) => void) | undefined = undefined
|
|
||||||
export let placeholder: IntlString = getEmbeddedLabel('')
|
|
||||||
|
|
||||||
$: tooltipParams = getTooltip(value)
|
$: tooltipParams = getTooltip(value)
|
||||||
|
|
||||||
@ -41,11 +38,9 @@
|
|||||||
<span class="lines-limit-2 select-text" use:tooltip={tooltipParams}>
|
<span class="lines-limit-2 select-text" use:tooltip={tooltipParams}>
|
||||||
{#if Array.isArray(value)}
|
{#if Array.isArray(value)}
|
||||||
{#each value as str, i}
|
{#each value as str, i}
|
||||||
<span class:ml-1={i !== 0}>{str}</span>
|
<span class:ml-1={i !== 0}><LinkWrapper text={str} /></span>
|
||||||
{/each}
|
{/each}
|
||||||
{:else if onChange === undefined}
|
{:else if value}
|
||||||
{value ?? ''}
|
<LinkWrapper text={value} />
|
||||||
{:else}
|
|
||||||
<StringEditor {onChange} value={value ?? ''} {placeholder} />
|
|
||||||
{/if}
|
{/if}
|
||||||
</span>
|
</span>
|
||||||
|
@ -15,7 +15,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { Class, Doc, Ref, Space } from '@hcengineering/core'
|
import type { Class, Doc, Ref, Space } from '@hcengineering/core'
|
||||||
import { getClient } from '@hcengineering/presentation'
|
import { getClient } from '@hcengineering/presentation'
|
||||||
import { AnyComponent } from '@hcengineering/ui'
|
import { AnyComponent, LinkWrapper } from '@hcengineering/ui'
|
||||||
import view from '@hcengineering/view'
|
import view from '@hcengineering/view'
|
||||||
import { DocNavLink } from '@hcengineering/view-resources'
|
import { DocNavLink } from '@hcengineering/view-resources'
|
||||||
import plugin from '../plugin'
|
import plugin from '../plugin'
|
||||||
@ -52,13 +52,11 @@
|
|||||||
</div>
|
</div>
|
||||||
{#if description}
|
{#if description}
|
||||||
<span class="ac-header__description">
|
<span class="ac-header__description">
|
||||||
{#if description.trim().startsWith('http://') || description.trim().startsWith('https://')}
|
<div class="flex">
|
||||||
<a href={description} class="whitespace-nowrap" target="_blank" rel="noreferrer noopener">
|
<span class="overflow-label" title={description}>
|
||||||
{description}
|
<LinkWrapper text={description} />
|
||||||
</a>
|
</span>
|
||||||
{:else}
|
</div>
|
||||||
{description}
|
|
||||||
{/if}
|
|
||||||
</span>{/if}
|
</span>{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -49,12 +49,11 @@ test.describe('recruit tests', () => {
|
|||||||
|
|
||||||
await page.click(`text="${last} ${first}"`)
|
await page.click(`text="${last} ${first}"`)
|
||||||
|
|
||||||
|
await expect(page.locator(`text=${first}`).first()).toBeVisible()
|
||||||
|
await expect(page.locator(`text=${last}`).first()).toBeVisible()
|
||||||
|
await expect(page.locator(`text=${loc}`).first()).toBeVisible()
|
||||||
|
|
||||||
const panel = page.locator('.popupPanel')
|
const panel = page.locator('.popupPanel')
|
||||||
|
|
||||||
expect(await panel.locator('[placeholder="First name"]').inputValue()).toEqual(first)
|
|
||||||
expect(await panel.locator('[placeholder="Last name"]').inputValue()).toEqual(last)
|
|
||||||
expect(await panel.locator('[placeholder="Location"]').inputValue()).toEqual(loc)
|
|
||||||
|
|
||||||
await panel.locator('[id="gmail\\:string\\:Email"]').scrollIntoViewIfNeeded()
|
await panel.locator('[id="gmail\\:string\\:Email"]').scrollIntoViewIfNeeded()
|
||||||
await panel.locator('[id="gmail\\:string\\:Email"]').click()
|
await panel.locator('[id="gmail\\:string\\:Email"]').click()
|
||||||
expect(await page.locator('.cover-channel >> input').inputValue()).toEqual(email)
|
expect(await page.locator('.cover-channel >> input').inputValue()).toEqual(email)
|
||||||
|
Loading…
Reference in New Issue
Block a user