From bf6780a32fbaf8a081fd26a60542aaa72b430ec5 Mon Sep 17 00:00:00 2001 From: Anna No Date: Thu, 30 Jun 2022 10:46:46 +0700 Subject: [PATCH] [TSK-225] Tracker: add issue shortlink resolver (Part 2) (#2173) Signed-off-by: Anna --- models/tracker/src/index.ts | 1 + packages/ui/src/types.ts | 25 ++++- plugins/tracker-resources/package.json | 3 +- .../SetParentIssueActionPopup.svelte | 2 +- .../src/components/issues/IssueItem.svelte | 2 +- .../src/components/issues/ParentIssue.svelte | 2 +- .../components/issues/edit/EditIssue.svelte | 4 +- .../issues/edit/SubIssueList.svelte | 2 +- .../issues/edit/SubIssueSelector.svelte | 2 +- .../issues/edit/SubIssuesSelector.svelte | 2 +- plugins/tracker-resources/src/index.ts | 5 +- plugins/tracker-resources/src/issues.ts | 98 +++++++++++++++++++ plugins/tracker-resources/src/utils.ts | 45 +-------- plugins/tracker/src/index.ts | 7 +- .../src/components/Workbench.svelte | 12 +++ plugins/workbench/src/index.ts | 3 +- 16 files changed, 158 insertions(+), 57 deletions(-) create mode 100644 plugins/tracker-resources/src/issues.ts diff --git a/models/tracker/src/index.ts b/models/tracker/src/index.ts index 49cb19cbc1..7dca28a859 100644 --- a/models/tracker/src/index.ts +++ b/models/tracker/src/index.ts @@ -453,6 +453,7 @@ export function createModel (builder: Builder): void { icon: tracker.icon.TrackerApplication, alias: trackerId, hidden: false, + locationResolver: tracker.resolver.Location, navigatorModel: { specials: [ { diff --git a/packages/ui/src/types.ts b/packages/ui/src/types.ts index 91bc72bfe0..841d7528b2 100644 --- a/packages/ui/src/types.ts +++ b/packages/ui/src/types.ts @@ -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 = Resource export type AnyComponent = Resource diff --git a/plugins/tracker-resources/package.json b/plugins/tracker-resources/package.json index 9790a6ab21..892b33d2a0 100644 --- a/plugins/tracker-resources/package.json +++ b/plugins/tracker-resources/package.json @@ -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" } } diff --git a/plugins/tracker-resources/src/components/SetParentIssueActionPopup.svelte b/plugins/tracker-resources/src/components/SetParentIssueActionPopup.svelte index e7c6d865a0..44899bcb81 100644 --- a/plugins/tracker-resources/src/components/SetParentIssueActionPopup.svelte +++ b/plugins/tracker-resources/src/components/SetParentIssueActionPopup.svelte @@ -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[] export let width: 'medium' | 'large' | 'full' = 'large' diff --git a/plugins/tracker-resources/src/components/issues/IssueItem.svelte b/plugins/tracker-resources/src/components/issues/IssueItem.svelte index 6f727c48ba..802bcc998c 100644 --- a/plugins/tracker-resources/src/components/issues/IssueItem.svelte +++ b/plugins/tracker-resources/src/components/issues/IssueItem.svelte @@ -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 $: title = getIssueId(value.$lookup?.space as Team, value) diff --git a/plugins/tracker-resources/src/components/issues/ParentIssue.svelte b/plugins/tracker-resources/src/components/issues/ParentIssue.svelte index 80974bb441..dafc20e1b2 100644 --- a/plugins/tracker-resources/src/components/issues/ParentIssue.svelte +++ b/plugins/tracker-resources/src/components/issues/ParentIssue.svelte @@ -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 diff --git a/plugins/tracker-resources/src/components/issues/edit/EditIssue.svelte b/plugins/tracker-resources/src/components/issues/edit/EditIssue.svelte index 540fd9da36..73b0262966 100644 --- a/plugins/tracker-resources/src/components/issues/edit/EditIssue.svelte +++ b/plugins/tracker-resources/src/components/issues/edit/EditIssue.svelte @@ -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} - + diff --git a/plugins/tracker-resources/src/components/issues/edit/SubIssueList.svelte b/plugins/tracker-resources/src/components/issues/edit/SubIssueList.svelte index c596fb61b8..298170e734 100644 --- a/plugins/tracker-resources/src/components/issues/edit/SubIssueList.svelte +++ b/plugins/tracker-resources/src/components/issues/edit/SubIssueList.svelte @@ -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' diff --git a/plugins/tracker-resources/src/components/issues/edit/SubIssueSelector.svelte b/plugins/tracker-resources/src/components/issues/edit/SubIssueSelector.svelte index 63fbc5ab7f..71cc8cdb92 100644 --- a/plugins/tracker-resources/src/components/issues/edit/SubIssueSelector.svelte +++ b/plugins/tracker-resources/src/components/issues/edit/SubIssueSelector.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 export let team: Team diff --git a/plugins/tracker-resources/src/components/issues/edit/SubIssuesSelector.svelte b/plugins/tracker-resources/src/components/issues/edit/SubIssuesSelector.svelte index 6e5e5212fb..0e03717b65 100644 --- a/plugins/tracker-resources/src/components/issues/edit/SubIssuesSelector.svelte +++ b/plugins/tracker-resources/src/components/issues/edit/SubIssuesSelector.svelte @@ -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 export let currentTeam: Team | undefined diff --git a/plugins/tracker-resources/src/index.ts b/plugins/tracker-resources/src/index.ts index 35c5068b55..97d6f7fc51 100644 --- a/plugins/tracker-resources/src/index.ts +++ b/plugins/tracker-resources/src/index.ts @@ -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 ( @@ -159,5 +159,8 @@ export default async (): Promise => ({ }, actionImpl: { CopyToClipboard: copyToClipboard + }, + resolver: { + Location: resolveLocation } }) diff --git a/plugins/tracker-resources/src/issues.ts b/plugins/tracker-resources/src/issues.ts new file mode 100644 index 0000000000..41d6639e49 --- /dev/null +++ b/plugins/tracker-resources/src/issues.ts @@ -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): Promise { + const object = await client.findOne( + tracker.class.Issue, + { _id: ref as Ref }, + { 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 { + 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 { + 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 { + 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 +} diff --git a/plugins/tracker-resources/src/utils.ts b/plugins/tracker-resources/src/utils.ts index 24f886c446..ab5be1f2c1 100644 --- a/plugins/tracker-resources/src/utils.ts +++ b/plugins/tracker-resources/src/utils.ts @@ -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): Promise { - const object = await client.findOne( - tracker.class.Issue, - { _id: ref as Ref }, - { 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 { - 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) -} diff --git a/plugins/tracker/src/index.ts b/plugins/tracker/src/index.ts index 3f15ff9225..de2afbc6c3 100644 --- a/plugins/tracker/src/index.ts +++ b/plugins/tracker/src/index.ts @@ -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 + }, + resolver: { + Location: '' as Resource<(loc: Location) => Promise> } }) diff --git a/plugins/workbench-resources/src/components/Workbench.svelte b/plugins/workbench-resources/src/components/Workbench.svelte index c2a3039f4e..dd99a2a655 100644 --- a/plugins/workbench-resources/src/components/Workbench.svelte +++ b/plugins/workbench-resources/src/components/Workbench.svelte @@ -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) { diff --git a/plugins/workbench/src/index.ts b/plugins/workbench/src/index.ts index 63a9554302..ce14af1e98 100644 --- a/plugins/workbench/src/index.ts +++ b/plugins/workbench/src/index.ts @@ -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> // Component will be displayed in case navigator model is not defined, or nothing is selected in navigator model component?: AnyComponent