mirror of
https://github.com/hcengineering/platform.git
synced 2025-04-18 22:38:33 +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
|
||||
'@typescript-eslint/eslint-plugin': ^5.41.0
|
||||
'@typescript-eslint/parser': ^5.41.0
|
||||
autolinker: 4.0.0
|
||||
autoprefixer: ^10.4.14
|
||||
body-parser: ~1.19.1
|
||||
browserslist: 4.21.5
|
||||
@ -641,6 +642,7 @@ dependencies:
|
||||
'@types/xml2js': 0.4.11
|
||||
'@typescript-eslint/eslint-plugin': 5.42.1_d506b9be61cb4ac2646ecbc6e0680464
|
||||
'@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
|
||||
body-parser: 1.19.2
|
||||
browserslist: 4.21.5
|
||||
@ -4267,6 +4269,12 @@ packages:
|
||||
engines: {node: '>=8.0.0'}
|
||||
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:
|
||||
resolution: {integrity: sha512-FQzyfOsTlwVzjHxKEqRIAdJx9niO6VCBCoEwax/VLSoQF29ggECcPuBqUMZ+u8jCZOPSy8b8/8KnuFbp0SaFZQ==}
|
||||
engines: {node: ^10 || ^12 || >=14}
|
||||
@ -16711,7 +16719,7 @@ packages:
|
||||
dev: false
|
||||
|
||||
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
|
||||
name: '@rush-temp/ui'
|
||||
version: 0.0.0
|
||||
@ -16719,6 +16727,7 @@ packages:
|
||||
'@types/jest': 28.1.8
|
||||
'@typescript-eslint/eslint-plugin': 5.42.1_d506b9be61cb4ac2646ecbc6e0680464
|
||||
'@typescript-eslint/parser': 5.42.1_eslint@8.27.0+typescript@4.8.4
|
||||
autolinker: 4.0.0
|
||||
eslint: 8.27.0
|
||||
eslint-config-standard-with-typescript: 23.0.0_c9fe9619f50f4e82337a86c3af25e566
|
||||
eslint-plugin-import: 2.26.0_eslint@8.27.0
|
||||
|
@ -56,7 +56,10 @@ export const defaultExtensions = [
|
||||
Heading.configure({
|
||||
levels: headingLevels
|
||||
}),
|
||||
Link.configure({ openOnClick: false }),
|
||||
Link.configure({
|
||||
openOnClick: true,
|
||||
HTMLAttributes: { class: 'cursor-pointer', rel: 'noopener noreferrer', target: '_blank' }
|
||||
}),
|
||||
...tableExtensions,
|
||||
...taskListExtensions
|
||||
]
|
||||
|
@ -38,7 +38,8 @@
|
||||
"@hcengineering/core": "^0.6.23",
|
||||
"just-clone": "~6.2.0",
|
||||
"svelte": "3.55.1",
|
||||
"fast-equals": "^2.0.3"
|
||||
"fast-equals": "^2.0.3",
|
||||
"autolinker": "4.0.0"
|
||||
},
|
||||
"repository": "https://github.com/hcenginneing/anticrm",
|
||||
"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 CircleButton } from './components/CircleButton.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 TimeSince } from './components/TimeSince.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 { setMetadata } from '@hcengineering/platform'
|
||||
import { writable } from 'svelte/store'
|
||||
import autolinker from 'autolinker'
|
||||
import { Notification, NotificationPosition, NotificationSeverity, notificationsStore } from '.'
|
||||
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 { FullDescriptionBox } from '@hcengineering/text-editor'
|
||||
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 { createEventDispatcher } from 'svelte'
|
||||
import recruit from '../plugin'
|
||||
@ -113,13 +113,11 @@
|
||||
>
|
||||
<svelte:fragment slot="subtitle">
|
||||
{#if object.description}
|
||||
{#if object.description.trim().startsWith('http://') || object.description.trim().startsWith('https://')}
|
||||
<a href={object.description} class="whitespace-nowrap" target="_blank" rel="noreferrer noopener">
|
||||
{object.description}
|
||||
</a>
|
||||
{:else}
|
||||
{object.description}
|
||||
{/if}
|
||||
<div class="flex">
|
||||
<span class="overflow-label" title={object.description}>
|
||||
<LinkWrapper text={object.description} />
|
||||
</span>
|
||||
</div>
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="attributes" let:direction={dir}>
|
||||
|
@ -14,13 +14,10 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { getEmbeddedLabel, IntlString } from '@hcengineering/platform'
|
||||
import { LabelAndProps, tooltip } from '@hcengineering/ui'
|
||||
import StringEditor from './StringEditor.svelte'
|
||||
import { getEmbeddedLabel } from '@hcengineering/platform'
|
||||
import { LabelAndProps, LinkWrapper, tooltip } from '@hcengineering/ui'
|
||||
|
||||
export let value: string | string[] | undefined
|
||||
export let onChange: ((value: string) => void) | undefined = undefined
|
||||
export let placeholder: IntlString = getEmbeddedLabel('')
|
||||
|
||||
$: tooltipParams = getTooltip(value)
|
||||
|
||||
@ -41,11 +38,9 @@
|
||||
<span class="lines-limit-2 select-text" use:tooltip={tooltipParams}>
|
||||
{#if Array.isArray(value)}
|
||||
{#each value as str, i}
|
||||
<span class:ml-1={i !== 0}>{str}</span>
|
||||
<span class:ml-1={i !== 0}><LinkWrapper text={str} /></span>
|
||||
{/each}
|
||||
{:else if onChange === undefined}
|
||||
{value ?? ''}
|
||||
{:else}
|
||||
<StringEditor {onChange} value={value ?? ''} {placeholder} />
|
||||
{:else if value}
|
||||
<LinkWrapper text={value} />
|
||||
{/if}
|
||||
</span>
|
||||
|
@ -15,7 +15,7 @@
|
||||
<script lang="ts">
|
||||
import type { Class, Doc, Ref, Space } from '@hcengineering/core'
|
||||
import { getClient } from '@hcengineering/presentation'
|
||||
import { AnyComponent } from '@hcengineering/ui'
|
||||
import { AnyComponent, LinkWrapper } from '@hcengineering/ui'
|
||||
import view from '@hcengineering/view'
|
||||
import { DocNavLink } from '@hcengineering/view-resources'
|
||||
import plugin from '../plugin'
|
||||
@ -52,13 +52,11 @@
|
||||
</div>
|
||||
{#if description}
|
||||
<span class="ac-header__description">
|
||||
{#if description.trim().startsWith('http://') || description.trim().startsWith('https://')}
|
||||
<a href={description} class="whitespace-nowrap" target="_blank" rel="noreferrer noopener">
|
||||
{description}
|
||||
</a>
|
||||
{:else}
|
||||
{description}
|
||||
{/if}
|
||||
<div class="flex">
|
||||
<span class="overflow-label" title={description}>
|
||||
<LinkWrapper text={description} />
|
||||
</span>
|
||||
</div>
|
||||
</span>{/if}
|
||||
</div>
|
||||
|
||||
|
@ -49,12 +49,11 @@ test.describe('recruit tests', () => {
|
||||
|
||||
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')
|
||||
|
||||
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"]').click()
|
||||
expect(await page.locator('.cover-channel >> input').inputValue()).toEqual(email)
|
||||
|
Loading…
Reference in New Issue
Block a user