mirror of
https://github.com/hcengineering/platform.git
synced 2025-06-01 21:31:04 +00:00
[TSK-225] Tracker: add issue shortlink resolver (Part 2) (#2173)
Signed-off-by: Anna <anna.no@xored.com>
This commit is contained in:
parent
94ee6f5afe
commit
bf6780a32f
@ -453,6 +453,7 @@ export function createModel (builder: Builder): void {
|
||||
icon: tracker.icon.TrackerApplication,
|
||||
alias: trackerId,
|
||||
hidden: false,
|
||||
locationResolver: tracker.resolver.Location,
|
||||
navigatorModel: {
|
||||
specials: [
|
||||
{
|
||||
|
@ -12,7 +12,6 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import type { Asset, IntlString } from '@anticrm/platform'
|
||||
import { /* Metadata, Plugin, plugin, */ Resource /*, Service */ } from '@anticrm/platform'
|
||||
import { /* getContext, */ SvelteComponent } from 'svelte'
|
||||
@ -26,6 +25,30 @@ export interface Location {
|
||||
fragment?: string // a value of fragment
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if locations are equal.
|
||||
*/
|
||||
export function areLocationsEqual (loc1: Location, loc2: Location): boolean {
|
||||
if (loc1 === loc2) {
|
||||
return true
|
||||
}
|
||||
if (loc1.fragment !== loc2.fragment) {
|
||||
return false
|
||||
}
|
||||
if (loc1.path.length !== loc2.path.length) {
|
||||
return false
|
||||
}
|
||||
if (loc1.path.findIndex((v, i) => v !== loc2.path[i]) >= 0) {
|
||||
return false
|
||||
}
|
||||
const keys1 = Object.keys(loc1.query ?? {})
|
||||
const keys2 = Object.keys(loc2.query ?? {})
|
||||
if (keys1.length !== keys2.length) {
|
||||
return false
|
||||
}
|
||||
return keys1.findIndex((k) => loc1.query?.[k] !== loc2.query?.[k]) < 0
|
||||
}
|
||||
|
||||
export type AnySvelteComponent = typeof SvelteComponent
|
||||
export type Component<C extends AnySvelteComponent> = Resource<C>
|
||||
export type AnyComponent = Resource<AnySvelteComponent>
|
||||
|
@ -51,6 +51,7 @@
|
||||
"@anticrm/text-editor": "~0.6.0",
|
||||
"@anticrm/panel": "~0.6.0",
|
||||
"@anticrm/kanban": "~0.6.0",
|
||||
"@anticrm/attachment-resources": "~0.6.0"
|
||||
"@anticrm/attachment-resources": "~0.6.0",
|
||||
"@anticrm/workbench": "~0.6.1"
|
||||
}
|
||||
}
|
||||
|
@ -19,7 +19,7 @@
|
||||
import { Icon } from '@anticrm/ui'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import tracker from '../plugin'
|
||||
import { getIssueId } from '../utils'
|
||||
import { getIssueId } from '../issues'
|
||||
|
||||
export let value: Issue | AttachedData<Issue> | Issue[]
|
||||
export let width: 'medium' | 'large' | 'full' = 'large'
|
||||
|
@ -18,7 +18,7 @@
|
||||
import type { Issue, Team } from '@anticrm/tracker'
|
||||
import { Icon } from '@anticrm/ui'
|
||||
import tracker from '../../plugin'
|
||||
import { getIssueId } from '../../utils'
|
||||
import { getIssueId } from '../../issues'
|
||||
|
||||
export let value: WithLookup<Issue>
|
||||
$: title = getIssueId(value.$lookup?.space as Team, value)
|
||||
|
@ -18,7 +18,7 @@
|
||||
import { Spinner, IconClose, tooltip } from '@anticrm/ui'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import tracker from '../../plugin'
|
||||
import { getIssueId } from '../../utils'
|
||||
import { getIssueId } from '../../issues'
|
||||
|
||||
export let issue: Issue
|
||||
|
||||
|
@ -36,7 +36,7 @@
|
||||
import { ContextMenu } from '@anticrm/view-resources'
|
||||
import { createEventDispatcher, onDestroy, onMount } from 'svelte'
|
||||
import tracker from '../../../plugin'
|
||||
import { getIssueId } from '../../../utils'
|
||||
import { generateIssueShortLink, getIssueId } from '../../../issues'
|
||||
import IssueStatusActivity from '../IssueStatusActivity.svelte'
|
||||
import ControlPanel from './ControlPanel.svelte'
|
||||
import CopyToClipboard from './CopyToClipboard.svelte'
|
||||
@ -269,7 +269,7 @@
|
||||
{#if issueId}{issueId}{/if}
|
||||
</span>
|
||||
<svelte:fragment slot="actions">
|
||||
<CopyToClipboard issueUrl={window.location.href} {issueId} />
|
||||
<CopyToClipboard issueUrl={generateIssueShortLink(issueId)} {issueId} />
|
||||
</svelte:fragment>
|
||||
|
||||
<svelte:fragment slot="custom-attributes">
|
||||
|
@ -26,7 +26,7 @@
|
||||
} from '@anticrm/view-resources'
|
||||
import { showPanel, showPopup } from '@anticrm/ui'
|
||||
import tracker from '../../../plugin'
|
||||
import { getIssueId } from '../../../utils'
|
||||
import { getIssueId } from '../../../issues'
|
||||
import Circles from '../../icons/Circles.svelte'
|
||||
import AssigneeEditor from '../AssigneeEditor.svelte'
|
||||
import DueDateEditor from '../DueDateEditor.svelte'
|
||||
|
@ -28,7 +28,7 @@
|
||||
Spinner
|
||||
} from '@anticrm/ui'
|
||||
import tracker from '../../../plugin'
|
||||
import { getIssueId } from '../../../utils'
|
||||
import { getIssueId } from '../../../issues'
|
||||
|
||||
export let issue: WithLookup<Issue>
|
||||
export let team: Team
|
||||
|
@ -19,7 +19,7 @@
|
||||
import { Button, closeTooltip, ProgressCircle, SelectPopup, showPanel, showPopup } from '@anticrm/ui'
|
||||
import { updateFocus } from '@anticrm/view-resources'
|
||||
import tracker from '../../../plugin'
|
||||
import { getIssueId } from '../../../utils'
|
||||
import { getIssueId } from '../../../issues'
|
||||
|
||||
export let issue: WithLookup<Issue>
|
||||
export let currentTeam: Team | undefined
|
||||
|
@ -57,7 +57,7 @@ import SetParentIssueActionPopup from './components/SetParentIssueActionPopup.sv
|
||||
import Views from './components/views/Views.svelte'
|
||||
import KanbanView from './components/issues/KanbanView.svelte'
|
||||
import tracker from './plugin'
|
||||
import { copyToClipboard, getIssueId, getIssueTitle } from './utils'
|
||||
import { copyToClipboard, getIssueId, getIssueTitle, resolveLocation } from './issues'
|
||||
import CreateIssue from './components/CreateIssue.svelte'
|
||||
|
||||
export async function queryIssue<D extends Issue> (
|
||||
@ -159,5 +159,8 @@ export default async (): Promise<Resources> => ({
|
||||
},
|
||||
actionImpl: {
|
||||
CopyToClipboard: copyToClipboard
|
||||
},
|
||||
resolver: {
|
||||
Location: resolveLocation
|
||||
}
|
||||
})
|
||||
|
98
plugins/tracker-resources/src/issues.ts
Normal file
98
plugins/tracker-resources/src/issues.ts
Normal file
@ -0,0 +1,98 @@
|
||||
import { Doc, Ref, TxOperations } from '@anticrm/core'
|
||||
import { getClient } from '@anticrm/presentation'
|
||||
import { Issue, Team, trackerId } from '@anticrm/tracker'
|
||||
import { getPanelURI, Location } from '@anticrm/ui'
|
||||
import workbench from '@anticrm/workbench'
|
||||
import tracker from './plugin'
|
||||
|
||||
export function getIssueId (team: Team, issue: Issue): string {
|
||||
return `${team.identifier}-${issue.number}`
|
||||
}
|
||||
|
||||
export function isIssueId (shortLink: string): boolean {
|
||||
return /^\w+-\d+$/.test(shortLink)
|
||||
}
|
||||
|
||||
export async function getIssueTitle (client: TxOperations, ref: Ref<Doc>): Promise<string> {
|
||||
const object = await client.findOne(
|
||||
tracker.class.Issue,
|
||||
{ _id: ref as Ref<Issue> },
|
||||
{ lookup: { space: tracker.class.Team } }
|
||||
)
|
||||
if (object?.$lookup?.space === undefined) throw new Error(`Issue Team not found, _id: ${ref}`)
|
||||
return getIssueId(object.$lookup.space, object)
|
||||
}
|
||||
|
||||
export function generateIssuePanelUri (issue: Issue): string {
|
||||
return getPanelURI(tracker.component.EditIssue, issue._id, issue._class, 'content')
|
||||
}
|
||||
|
||||
export async function copyToClipboard (object: Issue, ev: Event, { type }: { type: string }): Promise<void> {
|
||||
const client = getClient()
|
||||
let text: string
|
||||
switch (type) {
|
||||
case 'id':
|
||||
text = await getIssueTitle(client, object._id)
|
||||
break
|
||||
case 'title':
|
||||
text = object.title
|
||||
break
|
||||
case 'link':
|
||||
// TODO: fix when short link is available
|
||||
text = `${window.location.href}#${generateIssuePanelUri(object)}`
|
||||
break
|
||||
default:
|
||||
return
|
||||
}
|
||||
await navigator.clipboard.writeText(text)
|
||||
}
|
||||
|
||||
export function generateIssueShortLink (issueId: string): string {
|
||||
return `${window.location.host}/${workbench.component.WorkbenchApp}/${trackerId}/${issueId}`
|
||||
}
|
||||
|
||||
export async function generateIssueLocation (loc: Location, issueId: string): Promise<Location | undefined> {
|
||||
const tokens = issueId.split('-')
|
||||
if (tokens.length < 2) {
|
||||
return undefined
|
||||
}
|
||||
const teamId = tokens[0]
|
||||
const issueNumber = Number(tokens[1])
|
||||
const client = getClient()
|
||||
const team = await client.findOne(tracker.class.Team, { identifier: teamId })
|
||||
if (team === undefined) {
|
||||
console.error(
|
||||
`Could not find team ${teamId}. Make sure you are in correct workspace and the team was not deleted or renamed.`
|
||||
)
|
||||
return undefined
|
||||
}
|
||||
const issue = await client.findOne(tracker.class.Issue, { number: issueNumber, space: team._id })
|
||||
if (issue === undefined) {
|
||||
console.error(`Could not find issue ${issueId}.`)
|
||||
return undefined
|
||||
}
|
||||
const appComponent = loc.path[0] ?? ''
|
||||
return {
|
||||
path: [appComponent, trackerId, team._id, 'issues'],
|
||||
fragment: generateIssuePanelUri(issue)
|
||||
}
|
||||
}
|
||||
|
||||
export async function resolveLocation (loc: Location): Promise<Location | undefined> {
|
||||
const app = loc.path.length > 1 ? loc.path[1] : undefined
|
||||
if (app !== trackerId) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const shortLink = loc.path.length > 2 ? loc.path[2] : undefined
|
||||
if (shortLink === undefined || shortLink === null) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
// issue shortlink
|
||||
if (isIssueId(shortLink)) {
|
||||
return await generateIssueLocation(loc, shortLink)
|
||||
}
|
||||
|
||||
return undefined
|
||||
}
|
@ -14,10 +14,9 @@
|
||||
//
|
||||
|
||||
import contact, { Employee, formatName } from '@anticrm/contact'
|
||||
import { Doc, DocumentQuery, Ref, SortingOrder, TxOperations } from '@anticrm/core'
|
||||
import { DocumentQuery, Ref, SortingOrder, TxOperations } from '@anticrm/core'
|
||||
import { TypeState } from '@anticrm/kanban'
|
||||
import { Asset, IntlString, translate } from '@anticrm/platform'
|
||||
import { getClient } from '@anticrm/presentation'
|
||||
import {
|
||||
Issue,
|
||||
IssuesDateModificationPeriod,
|
||||
@ -27,13 +26,7 @@ import {
|
||||
ProjectStatus,
|
||||
Team
|
||||
} from '@anticrm/tracker'
|
||||
import {
|
||||
AnyComponent,
|
||||
AnySvelteComponent,
|
||||
getMillisecondsInMonth,
|
||||
getPanelURI,
|
||||
MILLISECONDS_IN_WEEK
|
||||
} from '@anticrm/ui'
|
||||
import { AnyComponent, AnySvelteComponent, getMillisecondsInMonth, MILLISECONDS_IN_WEEK } from '@anticrm/ui'
|
||||
import tracker from './plugin'
|
||||
import { defaultPriorities, defaultProjectStatuses, issuePriorities } from './types'
|
||||
|
||||
@ -346,10 +339,6 @@ export function getCategories (
|
||||
return existingCategories
|
||||
}
|
||||
|
||||
export function getIssueId (team: Team, issue: Issue): string {
|
||||
return `${team.identifier}-${issue.number}`
|
||||
}
|
||||
|
||||
export async function getKanbanStatuses (
|
||||
client: TxOperations,
|
||||
groupBy: IssuesGrouping,
|
||||
@ -460,33 +449,3 @@ export async function getKanbanStatuses (
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
export async function getIssueTitle (client: TxOperations, ref: Ref<Doc>): Promise<string> {
|
||||
const object = await client.findOne(
|
||||
tracker.class.Issue,
|
||||
{ _id: ref as Ref<Issue> },
|
||||
{ lookup: { space: tracker.class.Team } }
|
||||
)
|
||||
if (object?.$lookup?.space === undefined) throw new Error(`Issue Team not found, _id: ${ref}`)
|
||||
return getIssueId(object.$lookup.space, object)
|
||||
}
|
||||
|
||||
export async function copyToClipboard (object: Issue, ev: Event, { type }: { type: string }): Promise<void> {
|
||||
const client = getClient()
|
||||
let text: string
|
||||
switch (type) {
|
||||
case 'id':
|
||||
text = await getIssueTitle(client, object._id)
|
||||
break
|
||||
case 'title':
|
||||
text = object.title
|
||||
break
|
||||
case 'link':
|
||||
// TODO: fix when short link is available
|
||||
text = `${window.location.href}#${getPanelURI(tracker.component.EditIssue, object._id, object._class, 'content')}`
|
||||
break
|
||||
default:
|
||||
return
|
||||
}
|
||||
await navigator.clipboard.writeText(text)
|
||||
}
|
||||
|
@ -16,9 +16,9 @@
|
||||
import { Employee } from '@anticrm/contact'
|
||||
import type { AttachedDoc, Class, Doc, Markup, Ref, Space, Timestamp, Type } from '@anticrm/core'
|
||||
import { Action, ActionCategory } from '@anticrm/view'
|
||||
import type { Asset, IntlString, Plugin } from '@anticrm/platform'
|
||||
import type { Asset, IntlString, Plugin, Resource } from '@anticrm/platform'
|
||||
import { plugin } from '@anticrm/platform'
|
||||
import { AnyComponent } from '@anticrm/ui'
|
||||
import { AnyComponent, Location } from '@anticrm/ui'
|
||||
import type { TagCategory } from '@anticrm/tags'
|
||||
|
||||
/**
|
||||
@ -288,5 +288,8 @@ export default plugin(trackerId, {
|
||||
},
|
||||
team: {
|
||||
DefaultTeam: '' as Ref<Team>
|
||||
},
|
||||
resolver: {
|
||||
Location: '' as Resource<(loc: Location) => Promise<Location | undefined>>
|
||||
}
|
||||
})
|
||||
|
@ -31,6 +31,7 @@
|
||||
Label,
|
||||
location,
|
||||
Location,
|
||||
areLocationsEqual,
|
||||
navigate,
|
||||
PanelInstance,
|
||||
Popup,
|
||||
@ -169,6 +170,17 @@
|
||||
navigatorModel = currentApplication?.navigatorModel
|
||||
}
|
||||
|
||||
// resolve short links
|
||||
if (currentApplication?.locationResolver) {
|
||||
const resolver = await getResource(currentApplication.locationResolver)
|
||||
const resolvedLocation = await resolver?.(loc)
|
||||
if (resolvedLocation && !areLocationsEqual(loc, resolvedLocation)) {
|
||||
// make sure not to go into infinite loop here
|
||||
navigate(resolvedLocation)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if (space === undefined) {
|
||||
const last = localStorage.getItem(`platform_last_loc_${app}`)
|
||||
if (last !== null) {
|
||||
|
@ -16,7 +16,7 @@
|
||||
import type { Class, Doc, Mixin, Obj, Ref, Space } from '@anticrm/core'
|
||||
import type { Asset, IntlString, Metadata, Plugin, Resource } from '@anticrm/platform'
|
||||
import { plugin } from '@anticrm/platform'
|
||||
import { AnyComponent } from '@anticrm/ui'
|
||||
import { AnyComponent, Location } from '@anticrm/ui'
|
||||
import { ViewAction } from '@anticrm/view'
|
||||
|
||||
/**
|
||||
@ -28,6 +28,7 @@ export interface Application extends Doc {
|
||||
icon: Asset
|
||||
hidden: boolean
|
||||
navigatorModel?: NavigatorModel
|
||||
locationResolver?: Resource<(loc: Location) => Promise<Location | undefined>>
|
||||
|
||||
// Component will be displayed in case navigator model is not defined, or nothing is selected in navigator model
|
||||
component?: AnyComponent
|
||||
|
Loading…
Reference in New Issue
Block a user