Move services to public (#6156)

Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
Andrey Sobolev
2024-07-28 14:55:43 +07:00
committed by Andrey Sobolev
parent 9aad4a4561
commit ddecae80dd
578 changed files with 109084 additions and 390 deletions
@@ -0,0 +1,42 @@
<script lang="ts">
import { Ref, Space } from '@hcengineering/core'
import ui, { ModernButton } from '@hcengineering/ui'
import { GithubProject } from '@hcengineering/github'
import github from '../plugin'
import { githubAuth, githubProjects, onAuthorize } from './utils'
import { getMetadata } from '@hcengineering/platform'
export let space: Ref<Space>
export let kind: 'primary' | 'secondary' | 'tertiary' | 'negative' = 'secondary'
export let readonly: boolean = false
$: auth = $githubAuth
$: spaceObj = $githubProjects.get(space as Ref<GithubProject>)
</script>
{#if spaceObj !== undefined}
{#if auth === undefined || auth.login === '' || auth.error != null}
{#if !readonly}
<ModernButton
label={github.string.Authorize}
labelParams={{ title: getMetadata(ui.metadata.PlatformTitle) }}
icon={github.icon.Github}
kind={'primary'}
size={'small'}
on:click={() => {
void onAuthorize()
}}
/>
{/if}
{:else}
<ModernButton
icon={github.icon.Github}
label={github.string.AuthorizeAs}
labelParams={{ login: auth.login }}
disabled={true}
{kind}
size={'small'}
/>
{/if}
{/if}
@@ -0,0 +1,8 @@
<script lang="ts">
import { Integration } from '@hcengineering/setting'
import Connect from './Connect.svelte'
export let integration: Integration
</script>
<Connect bind:integration on:close />
@@ -0,0 +1,174 @@
<!--
//
// Copyright © 2023 Hardcore Engineering Inc.
//GithubIntegrationsGithubIntegrations
-->
<script lang="ts">
import GithubIntegrations from './GithubIntegerations.svelte'
import GithubPersonProfile from './GithubPersonProfile.svelte'
import { Analytics } from '@hcengineering/analytics'
import { WithLookup, getCurrentAccount } from '@hcengineering/core'
import { getEmbeddedLabel, getMetadata, translate } from '@hcengineering/platform'
import presentation, { Card, HTMLViewer, NavLink, createQuery } from '@hcengineering/presentation'
import { Integration } from '@hcengineering/setting'
import tracker, { Project } from '@hcengineering/tracker'
import ui, { Button, Label, Loading, TabItem, TabList, location, ticker } from '@hcengineering/ui'
import { GithubAuthentication, GithubIntegration } from '@hcengineering/github'
import { createEventDispatcher } from 'svelte'
import github from '../plugin'
import { onAuthorize } from './utils'
export let integration: Integration
const dispatch = createEventDispatcher()
const query = createQuery()
const authQuery = createQuery()
const projectsQuery = createQuery()
let loading = true
let integrations: WithLookup<GithubIntegration>[] = []
let auth: GithubAuthentication | undefined
let projects: Project[] = []
query.query(
github.class.GithubIntegration,
{},
(res) => {
integrations = res
loading = false
},
{
lookup: {
_id: {
repositories: github.class.GithubIntegrationRepository
}
}
}
)
authQuery.query(github.class.GithubAuthentication, {}, (res) => {
;[auth] = res
})
projectsQuery.query(tracker.class.Project, {}, (res) => {
projects = res
})
function save (): void {
dispatch('close', { value: auth?.login ?? '-' })
}
function onConnect (): void {
const state = btoa(
JSON.stringify({
accountId: getCurrentAccount()._id,
op: 'installation',
workspace: $location.path[1],
token: getMetadata(presentation.metadata.Token)
})
)
Analytics.handleEvent('Install github app clicked')
const githubApp = getMetadata(github.metadata.GithubApplication) ?? ''
window.open(`https://github.com/apps/${githubApp}/installations/new?state=${state}`)
}
const tabs: TabItem[] = [
{
id: 'personal',
labelIntl: getEmbeddedLabel('Your Github account')
},
{
id: 'installations',
labelIntl: getEmbeddedLabel('Github Repositories')
}
]
let selectedTab: string = tabs[0].id
$: loading = $ticker - (auth?.authRequestTime ?? 0) < 5000
</script>
<Card
label={github.string.GithubDesc}
okAction={save}
canSave={true}
okLabel={presentation.string.Ok}
on:close={() => dispatch('close')}
on:changeContent
>
{#if loading}
<Loading />
{:else}
<TabList
items={tabs}
bind:selected={selectedTab}
kind={'plain'}
on:select={(result) => {
selectedTab = result.detail.id
}}
/>
<div class="flex flex-grow mt-4">
{#if selectedTab === 'personal'}
<div class="flex-row flex-grow p-3">
{#if auth}
<GithubPersonProfile {auth} />
{:else}
<Label label={github.string.PleaseAuthorizeAs} params={{ title: getMetadata(ui.metadata.PlatformTitle) }} />
{/if}
</div>
{:else if selectedTab === 'installations'}
<div class="flex flex-col flex-grow">
<GithubIntegrations {integrations} {projects}></GithubIntegrations>
<div>
{#if integrations.length === 0}
<div class="flex-grow flex-col">
<div class="">
{#await translate(github.string.NoIntegrationsConfigured, { appName: getMetadata(github.metadata.GithubApplication), title: getMetadata(ui.metadata.PlatformTitle) }) then msg}
<HTMLViewer value={msg} />
{/await}
<div class="underline">
<NavLink href={'https://github.com/settings/installations/'} noUnderline={false}>
<b><Label label={github.string.Uninstall} /></b>
</NavLink>
</div>
</div>
</div>
{/if}
</div>
</div>
{/if}
</div>
{/if}
<svelte:fragment slot="footer">
{#if selectedTab === 'personal'}
{#if auth !== undefined && $ticker - (auth?.authRequestTime ?? 0) > 0 && auth.login === ''}
<div class="fs-title" style:color={'red'}>
<Label label={github.string.PleaseRetry} />
</div>
{/if}
<Button
label={auth !== undefined ? github.string.ReAuthorize : github.string.Authorize}
labelParams={{ title: getMetadata(ui.metadata.PlatformTitle) }}
{loading}
on:click={() => onAuthorize()}
size={'large'}
kind={'primary'}
/>
{:else if selectedTab === 'installations'}
<Button
label={integrations.length === 0 ? github.string.InstallApp : github.string.Configure}
labelParams={{ title: getMetadata(ui.metadata.PlatformTitle) }}
on:click={onConnect}
size={'large'}
kind={'primary'}
/>
{/if}
</svelte:fragment>
</Card>
<style lang="scss">
.bordered {
border: 1px dashed var(--theme-divider-color);
}
</style>
@@ -0,0 +1,116 @@
<!--
//
// Copyright © 2023 Hardcore Engineering Inc.
//
-->
<script lang="ts">
import { Account, Ref } from '@hcengineering/core'
import ui, { Label, Location, Spinner, location } from '@hcengineering/ui'
import { onDestroy } from 'svelte'
import github from '../plugin'
import { sendGHServiceRequest } from './utils'
import { getMetadata } from '@hcengineering/platform'
let autoClose = 10
let promise: Promise<void> | undefined
let interval: any
let installationId: number | undefined
let showSelector = false
async function createIntegration (loc: Location): Promise<void> {
if (loc.query?.error != null) {
window.close()
return
}
installationId = parseInt(loc.query?.installation_id ?? '-1')
const state = loc.query?.state
const code = loc.query?.code
const setupAction = loc.query?.setup_action
if (state == null) {
// we need to show a list of workspaces available to install application ito.
if (setupAction === 'install') {
// Show error
showSelector = true
}
return
}
function doAutoClose (): void {
autoClose = 3
clearInterval(interval)
interval = setInterval(() => {
autoClose = autoClose - 1
if (autoClose === 0) {
clearInterval(interval)
window.close()
}
}, 1000)
}
const rawState = JSON.parse(atob(state))
const {
accountId,
op,
workspace,
token
}: { accountId: Ref<Account>, workspace: string, op: string, token: string } = rawState
if (op === 'installation') {
if (installationId == null || setupAction === null) {
window.close()
return
}
promise = sendGHServiceRequest('installation', {
installationId,
workspace,
accountId,
token
}).then(doAutoClose)
}
if (code !== null) {
promise = sendGHServiceRequest('auth', {
code,
state,
workspace,
accountId
}).then(doAutoClose)
}
}
onDestroy(
location.subscribe((loc) => {
void createIntegration(loc)
})
)
</script>
{#if showSelector}
<div class="flex flex-center flex-col h-full w-full">
<span class="text-lg">
<Label label={github.string.SelectWorkspaceToInstallApp} />
</span>
<span class="text-lg">
<Label label={github.string.SelectWorkspaceToInstallAppMsg} />
</span>
<a href={`https://github.com/settings/installations/${installationId}`}>
<Label label={github.string.Configure} params={{ title: getMetadata(ui.metadata.PlatformTitle) }} />
</a>
</div>
{:else}
<div class="flex flex-center fs-title text-center items-center h-full w-full">
{#await promise}
<div class="flex flex-row-center flex-center flex-grow">
<Spinner />
<div class="ml-1">
<Label label={github.string.Processing} />
</div>
</div>
{:then}
<Label label={github.string.AutoClose} params={{ time: autoClose }} />
{/await}
</div>
{/if}
@@ -0,0 +1,160 @@
<script lang="ts">
import { Analytics } from '@hcengineering/analytics'
import core, { ClassifierKind, Ref, WithLookup, generateId } from '@hcengineering/core'
import { getEmbeddedLabel, getMetadata, translate } from '@hcengineering/platform'
import { getClient } from '@hcengineering/presentation'
import task, { TaskType, updateProjectType, type TaskStatusFactory } from '@hcengineering/task'
import tracker, { Project, createStatesData } from '@hcengineering/tracker'
import ui, {
Button,
IconChevronDown,
PaletteColorIndexes,
getEventPopupPositionElement,
showPopup
} from '@hcengineering/ui'
import DropdownLabelsPopup from '@hcengineering/ui/src/components/DropdownLabelsPopup.svelte'
import { GithubIntegration, GithubIntegrationRepository, githubPullRequestStates } from '@hcengineering/github'
import github from '../plugin'
export let integration: WithLookup<GithubIntegration>
export let repository: GithubIntegrationRepository
export let projects: Project[] = []
/**
* @public
*/
export const baseIssueTaskStatuses: TaskStatusFactory[] = [
{
category: task.statusCategory.UnStarted,
statuses: [['Backlog', PaletteColorIndexes.Cloud, tracker.status.Backlog]]
},
{
category: task.statusCategory.Active,
statuses: [
['Coding', PaletteColorIndexes.Porpoise, tracker.status.Coding],
['Under review', PaletteColorIndexes.Cerulean, tracker.status.UnderReview]
]
},
{ category: task.statusCategory.Won, statuses: [['Done', PaletteColorIndexes.Grass, tracker.status.Done]] },
{
category: task.statusCategory.Lost,
statuses: [['Canceled', PaletteColorIndexes.Coin, tracker.status.Canceled]]
}
]
const client = getClient()
async function assignRepository (project: Ref<Project>): Promise<void> {
if (project === undefined) {
return
}
const projectInst = projects.find((it) => it._id === project)
if (projectInst === undefined) {
return
}
Analytics.handleEvent('Connect project to github')
if (!client.getHierarchy().hasMixin(projectInst, github.mixin.GithubProject)) {
// We need to add GithubProject mixin
const mixinId = await getClient().createDoc(core.class.Mixin, core.space.Model, {
extends: github.mixin.GithubIssue,
kind: ClassifierKind.MIXIN,
label: getEmbeddedLabel(projectInst.name),
hidden: false,
icon: github.icon.Github
})
await getClient().createMixin(
projectInst._id,
tracker.class.Project,
core.space.Space,
github.mixin.GithubProject,
{
integration: integration._id,
repositories: [],
mixinClass: mixinId,
mappings: []
}
)
}
// Check if issue and pull request are missing, we need to add them, and mark both of them as system to prevent deletion.
const issueId: Ref<TaskType> = generateId()
await updateProjectType(client, projectInst.type, [
{
_id: issueId,
descriptor: tracker.descriptors.Issue,
kind: 'both',
name: await translate(tracker.string.Issue, {}),
ofClass: tracker.class.Issue,
statusCategories: baseIssueTaskStatuses.map((it) => it.category),
statusClass: tracker.class.IssueStatus,
icon: tracker.icon.Issue,
color: 0,
allowedAsChildOf: [issueId],
factory: createStatesData(baseIssueTaskStatuses)
},
{
_id: generateId(),
descriptor: github.descriptors.PullRequest,
kind: 'both',
name: await translate(github.string.PullRequest, {}),
ofClass: github.class.GithubPullRequest,
statusCategories: githubPullRequestStates.map((it) => it.category),
statusClass: tracker.class.IssueStatus,
icon: tracker.icon.Issue,
color: 0,
allowedAsChildOf: [issueId],
factory: createStatesData(githubPullRequestStates)
}
])
const githubProject = client.getHierarchy().as(projectInst, github.mixin.GithubProject)
void getClient().update(githubProject, {
$push: { repositories: repository._id }
})
void getClient().update(repository, { githubProject: githubProject._id, enabled: true })
}
$: allowedProjects = projects.filter(
(it) =>
(client.getHierarchy().asIf(it, github.mixin.GithubProject)?.integration ?? integration._id) === integration._id
)
async function selectProject (event: MouseEvent): Promise<void> {
showPopup(
DropdownLabelsPopup,
{
enableSearch: allowedProjects.length > 5,
items: [
...allowedProjects.map((it) => ({ id: `${it._id}`, label: it.name })),
{ id: '#', label: await translate(tracker.string.NewProject, {}) }
]
},
getEventPopupPositionElement(event),
(result) => {
if (result != null) {
if (result === '#') {
showPopup(tracker.component.CreateProject, {}, 'center', (prj) => {
if (prj != null) {
void assignRepository(prj)
}
})
} else {
void assignRepository(result)
}
}
}
)
}
</script>
<Button
size={'medium'}
kind={'primary'}
label={github.string.LinkToProject}
labelParams={{ title: getMetadata(ui.metadata.PlatformTitle) }}
on:click={selectProject}
iconRight={IconChevronDown}
/>
@@ -0,0 +1,15 @@
<!--
// Copyright © 2023 Hardcore Engineering Inc.
//
-->
<script lang="ts">
import { GithubPullRequest } from '@hcengineering/github'
import PullRequestDiff from './PullRequestDiff.svelte'
export let object: GithubPullRequest
export let embedded = false
</script>
<div class="mt-6">
<PullRequestDiff pullRequest={object} />
</div>
@@ -0,0 +1,14 @@
<!--
// Copyright © 2023 Hardcore Engineering Inc.
//
-->
<script lang="ts">
export let size: 'small' | 'medium' | 'large'
const fill: string = 'currentColor'
</script>
<svg xmlns="http://www.w3.org/2000/svg" class="svg-{size}" {fill} viewBox="0 0 24 24">
<path
d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"
/>
</svg>
@@ -0,0 +1,56 @@
<script lang="ts">
import GithubRepositories from './GithubRepositories.svelte'
import { WithLookup } from '@hcengineering/core'
import { getClient } from '@hcengineering/presentation'
import { Project } from '@hcengineering/tracker'
import { Scroller } from '@hcengineering/ui'
import { GithubIntegration } from '@hcengineering/github'
import github from '../plugin'
export let integrations: WithLookup<GithubIntegration>[] = []
export let projects: Project[] = []
const client = getClient()
$: githubProjects = client.getHierarchy().asIfArray(projects, github.mixin.GithubProject)
</script>
{#if integrations.length > 0}
<Scroller shrink={false}>
<div class="mt-4 ml-4 mb-4 flex-grow h-90">
{#each integrations as gi}
{@const giprj = githubProjects.filter((it) => it.integration === gi._id)}
<div class="flex flex-col mb-4">
<!-- svelte-ignore a11y-missing-attribute -->
<GithubRepositories integration={gi} giProjects={giprj} {projects} />
</div>
{/each}
</div>
</Scroller>
{/if}
<style lang="scss">
.bordered {
border: 1px dashed var(--theme-divider-color);
}
.repository-card {
border: 1px solid var(--theme-divider-color);
border-radius: 8px;
margin: 0.25rem;
padding: 1rem;
// height: 7rem;
}
.visibility {
border: 1px solid var(--theme-divider-color);
border-radius: 2em;
padding: 0 7px;
height: fit-content;
}
.lcolor-pin {
border-radius: 50%;
width: 12px;
height: 12px;
background: var(--theme-divider-color);
}
</style>
@@ -0,0 +1,99 @@
<script lang="ts">
import { Ref, WithLookup } from '@hcengineering/core'
import { getEmbeddedLabel } from '@hcengineering/platform'
import { NavLink, createQuery, getClient, isAdminUser } from '@hcengineering/presentation'
import { Issue } from '@hcengineering/tracker'
import { ButtonKind, showPopup } from '@hcengineering/ui'
import Button from '@hcengineering/ui/src/components/Button.svelte'
import Icon from '@hcengineering/ui/src/components/Icon.svelte'
import { DocSyncInfo, GithubIntegrationRepository, GithubProject } from '@hcengineering/github'
import github from '../plugin'
import MarkdownDescriptionDiff from './MarkdownDescriptionDiff.svelte'
import RepositoryPresenterRefEditor from './RepositoryPresenterRefEditor.svelte'
import { githubProjects, integrationRepositories } from './utils'
export let space: Ref<GithubProject>
export let kind: ButtonKind = 'regular'
export let readonly: boolean = false
export let value: WithLookup<Issue>
const client = getClient()
$: ghProject = $githubProjects.get(space)
$: ghIssue = client.getHierarchy().asIf(value, github.mixin.GithubIssue)
$: repository = ghIssue?.repository !== undefined ? $integrationRepositories.get(ghIssue.repository) : undefined
async function assignRepository (repository: Ref<GithubIntegrationRepository>): Promise<void> {
if (ghIssue !== undefined) {
// We just need to assign the repository to the issue
await getClient().update(ghIssue, { repository })
} else {
// We need to create a new issue
await getClient().createMixin(value._id, value._class, value.space, github.mixin.GithubIssue, {
repository,
url: '',
githubNumber: 0
})
}
}
const syncInfoQuery = createQuery()
let docSyncInfo: DocSyncInfo | undefined
$: if (isAdminUser()) {
syncInfoQuery.query(github.class.DocSyncInfo, { _id: value._id as any }, (info) => {
;[docSyncInfo] = info
})
}
</script>
{#if ghIssue !== undefined || ghProject !== undefined}
<div class="ml-2">
{#if ghIssue?.repository == null}
<RepositoryPresenterRefEditor
label={github.string.CreateGithubIssue}
kind={'regular'}
showIcon={true}
{space}
disabled={readonly}
onChange={(val) => {
if (val !== undefined) {
void assignRepository(val)
}
}}
/>
{:else if ghIssue.url !== ''}
<div class="flex flex-row-center">
{#if repository !== undefined}
<Icon icon={github.icon.Github} size={'small'} />
<span class="ml-1">
{repository?.name}
</span>
{/if}
<NavLink
disabled={readonly}
href={ghIssue.url}
accent={true}
noOverflow={true}
onClick={() => {
window.open(ghIssue.url, '_blank')
}}
>
<span class="ml-2">
#{ghIssue.githubNumber}
</span>
</NavLink>
</div>
{/if}
</div>
{#if isAdminUser() && docSyncInfo?.markdown !== undefined}
<div class="ml-2">
<Button
label={getEmbeddedLabel('Diff')}
on:click={(evt) => {
showPopup(MarkdownDescriptionDiff, { issue: docSyncInfo }, 'center')
}}
/>
</div>
{/if}
{/if}
@@ -0,0 +1,21 @@
<script lang="ts">
import { Ref } from '@hcengineering/core'
import { Icon, Label } from '@hcengineering/ui'
import { GithubIntegrationRepository } from '@hcengineering/github'
import github from '../plugin'
import { integrationRepositories } from './utils'
export let repository: Ref<GithubIntegrationRepository> | undefined = undefined
$: repositoryValue = repository != null ? $integrationRepositories.get(repository) : undefined
</script>
<div class="flex-row-center">
{#if repositoryValue}
<Label label={github.string.IssueRepositoryTarget} />
<div class="ml-2 mr-2">
<Icon icon={github.icon.Github} size={'small'} />
</div>
{repositoryValue.name}
{/if}
</div>
@@ -0,0 +1,203 @@
<script lang="ts">
import { DocumentUpdate, Ref } from '@hcengineering/core'
import { IntlString, getEmbeddedLabel } from '@hcengineering/platform'
import { getClient } from '@hcengineering/presentation'
import tracker, { Project, ProjectTargetPreference } from '@hcengineering/tracker'
import {
Button,
ButtonWithDropdown,
Icon,
IconDropdown,
Label,
SelectPopup,
eventToHTMLElement,
showPopup,
type SelectPopupValueType
} from '@hcengineering/ui'
import { GithubIntegrationRepository } from '@hcengineering/github'
import { Writable } from 'svelte/store'
import github from '../plugin'
import { integrationRepositories } from './utils'
export let state: Writable<Record<string, any>>
export let okProcessing: boolean = false
export let canSave: boolean = true
export let okLabel: IntlString | undefined
export let space: Project | undefined
export let popupPlaceholder: IntlString = github.string.Repository
export let handleOkClick: () => void
export let preferences: ProjectTargetPreference[] = []
$: githubProject =
space !== undefined && getClient().getHierarchy().hasMixin(space, github.mixin.GithubProject)
? getClient().getHierarchy().as(space, github.mixin.GithubProject)
: undefined
let repository: Ref<GithubIntegrationRepository> | undefined
let selectedRepository: GithubIntegrationRepository | undefined
$: spacePreferences = preferences.find((it) => it.attachedTo === space?._id)
$: if (spacePreferences !== undefined) {
repository = spacePreferences.props?.find((it) => it.key === github.class.GithubIntegrationRepository)?.value
}
$: rawRepositories = Array.from($integrationRepositories.values()).filter(
(it) => it.githubProject === githubProject?._id
)
const handleSelectedRepositoryIdUpdated = async (
newRepositoryId: Ref<GithubIntegrationRepository> | null | undefined,
components: GithubIntegrationRepository[]
): Promise<void> => {
if (newRepositoryId === null || newRepositoryId === undefined) {
selectedRepository = undefined
return
}
selectedRepository = components.find((it) => it._id === newRepositoryId)
}
function performOK (repository: Ref<GithubIntegrationRepository> | undefined): void {
$state.repository = repository ?? undefined
handleOkClick()
}
$: void handleSelectedRepositoryIdUpdated(repository, rawRepositories)
function getRepositoryInfo (rawComponents: GithubIntegrationRepository[]): SelectPopupValueType[] {
return [
...rawComponents.map((p) => ({
id: p._id,
icon: github.icon.Github,
label: getEmbeddedLabel(p.name),
props: {
value: p
}
})),
{
id: '#',
label: github.string.WithoutRepository
}
]
}
let repositories: SelectPopupValueType[] = []
$: repositories = getRepositoryInfo(rawRepositories)
function updateProjectPreferences (
space: Ref<Project>,
spacePreferences: ProjectTargetPreference | undefined,
repository: Ref<GithubIntegrationRepository> | undefined
): void {
if (spacePreferences !== undefined) {
const data: DocumentUpdate<ProjectTargetPreference> = {
usedOn: Date.now()
}
const value = (spacePreferences.props ?? []).find((it) => it.key === github.class.GithubIntegrationRepository)
if (value === undefined) {
data.props = [
...(spacePreferences.props ?? []),
{ key: github.class.GithubIntegrationRepository, value: repository }
]
} else if (value.value !== repository) {
// no value
value.value = repository
data.props = spacePreferences.props
}
void getClient().update(spacePreferences, data)
} else {
void getClient().createDoc(tracker.class.ProjectTargetPreference, space, {
attachedTo: space,
usedOn: Date.now(),
props: [
{
key: github.class.GithubIntegrationRepository as string,
value: repository
}
]
})
}
}
const handleRepositoryEditorOpened = async (event: MouseEvent): Promise<void> => {
event.stopPropagation()
if (!canSave) {
return
}
if (repository != null) {
performOK(repository)
return
}
showPopup(
SelectPopup,
{ value: repositories, placeholder: popupPlaceholder, searchable: false },
eventToHTMLElement(event),
(evt) => {
if (evt !== undefined) {
if (evt === '#') {
performOK(undefined)
} else {
repository = evt
if (repository != null && space !== undefined) {
updateProjectPreferences(space._id, spacePreferences, repository)
}
performOK(repository)
}
}
}
)
}
</script>
{#if githubProject !== undefined && rawRepositories.length > 0}
<ButtonWithDropdown
kind={'primary'}
size={'large'}
dropdownItems={repositories}
disabled={!canSave}
label={okLabel}
dropdownIcon={IconDropdown}
loading={okProcessing}
on:click={handleRepositoryEditorOpened}
on:dropdown-selected={(ev) => {
if (ev.detail != null) {
if (ev.detail === '#') {
performOK(undefined)
} else {
repository = ev.detail
if (repository != null && space !== undefined) {
updateProjectPreferences(space._id, spacePreferences, repository)
}
}
}
}}
>
<svelte:fragment slot="content">
<div class="ml-1">
<Label label={github.string.RepositoryIn} />
</div>
{#if selectedRepository !== undefined}
<div class="flex-row-center">
<div class="p-1">
<Icon icon={github.icon.Github} size={'small'} />
</div>
{selectedRepository.name}
</div>
{/if}
</svelte:fragment>
</ButtonWithDropdown>
{:else}
<Button
loading={okProcessing}
focusIndex={10001}
disabled={!canSave}
label={okLabel}
kind={'primary'}
size={'large'}
on:click={() => {
performOK(repository)
}}
/>
{/if}
@@ -0,0 +1,146 @@
<script lang="ts">
import contact from '@hcengineering/contact'
import { NavLink } from '@hcengineering/presentation'
import tracker from '@hcengineering/tracker'
import { Icon } from '@hcengineering/ui'
import { GithubAuthentication } from '@hcengineering/github'
import github from '../plugin'
export let auth: GithubAuthentication
</script>
<div class="flex flex-row">
<div class="flex flex-col">
<div class="">
<img src={auth?.avatar} width="64" height={'64'} alt={auth.name ?? ''} />
</div>
{#if auth?.name}
<div class="p1 fs-title text-lg">
{auth.name}
</div>
{/if}
<div class="p1 text-base">
<NavLink href={auth?.url ?? ''}>{auth.login}</NavLink>
</div>
{#if auth}
<div class="flex-row-center mt-4 text-sm no-word-wrap">
<Icon icon={contact.icon.Person} size={'small'} />
<span class="ml-1">
followers <b>{auth.followers}</b>
</span>
{#if auth.following}
<div class="ml-1">
⋅ following <b>{auth.following}</b>
</div>
{/if}
</div>
<div class="flex-row-center mt-4 text-sm no-word-wrap">
<Icon icon={github.icon.GithubRepository} size={'small'} />
<span class="ml-1">
repositories <b>{auth.repositories}</b>
</span>
<div class="ml-1">
⋅ starred <b>{auth.starredRepositories}</b>
</div>
</div>
<div class="flex-row-center mt-4 text-sm no-word-wrap">
<Icon icon={tracker.icon.Issue} size={'small'} />
<span class="ml-1">
open <b>{auth.openIssues}</b>
</span>
<div class="ml-1">
⋅ closed <b>{auth.closedIssues}</b>
</div>
</div>
<div class="flex-row-center mt-4 text-sm no-word-wrap">
<Icon icon={github.icon.PullRequest} size={'small'} />
<span class="ml-1">
open <b>{auth.openPRs}</b>
</span>
<div class="ml-1">
⋅ merged <b>{auth.mergedPRs}</b>
</div>
<!-- <div class="ml-1">
⋅ closed <b>{auth.closedPRs}</b>
</div> -->
</div>
{#if auth.bio}
<div class="p1 mt-4 no-word-wrap infoCard">
bio <b class="ml-1">{auth.bio}</b>
</div>
{/if}
{#if auth.blog}
<div class="flex-row-center mt-4 text-sm no-word-wrap infoCard">
blog <b class="ml-1">{auth.blog}</b>
</div>
{/if}
{#if auth.company}
<div class="flex-row-center mt-4 text-sm no-word-wrap infoCard">
company <b class="ml-1">{auth.company}</b>
</div>
{/if}
{#if auth.email}
<div class="flex-row-center mt-4 text-sm infoCard">
email <b class="ml-1">{auth.email}</b>
</div>
{/if}
{#if auth.location}
<div class="flex-row-center mt-4 text-sm infoCard">
location <b class="ml-1">{auth.location}</b>
</div>
{/if}
<div class="flex-row-center mt-4 text-sm">
<Icon icon={github.icon.Github} size={'small'} />
<span class="ml-1">
organizations <b class="ml-1">{auth.organizations?.totalCount ?? 0}</b>
</span>
</div>
{/if}
</div>
<div class="flex flex-col ml-4 flex-grow">
<div class="flex-grow" style:overflow={'auto'}>
{#each auth?.organizations?.nodes ?? [] as organization}
<div class="org-card mt-2 flex-grow">
<div class="flex flex-row-center">
{#if organization.avatarUrl}
<div class="mr-2">
<img src={organization.avatarUrl} width="32" height={'32'} alt={organization.name} />
</div>
{/if}
<div class="flex-row-center flex-no-wrap">
<NavLink href={organization.url}>{organization.name}</NavLink>
</div>
</div>
<div class="mt-2">
{organization.description ?? ''}
</div>
</div>
{/each}
</div>
</div>
{#if auth.error}
{auth.error}
{/if}
</div>
<style lang="scss">
.bordered {
border: 1px dashed var(--theme-divider-color);
}
.org-card {
border: 1px solid var(--theme-divider-color);
border-radius: 8px;
padding: 1rem;
}
.infoCard {
overflow: hidden;
text-overflow: ellipsis;
}
</style>
@@ -0,0 +1,317 @@
<script lang="ts">
import { AttachedDoc, Ref, WithLookup } from '@hcengineering/core'
import { getMetadata } from '@hcengineering/platform'
import presentation, { NavLink, getClient, isAdminUser } from '@hcengineering/presentation'
import MessageBox from '@hcengineering/presentation/src/components/MessageBox.svelte'
import tracker, { Project } from '@hcengineering/tracker'
import ui, {
Action,
Button,
Expandable,
Icon,
IconColStar,
IconMoreV,
Label,
Menu,
SearchEdit,
TimeSince,
getEventPositionElement,
showPopup
} from '@hcengineering/ui'
import { ObjectPresenter } from '@hcengineering/view-resources'
import { GithubIntegration, GithubIntegrationRepository, GithubProject } from '@hcengineering/github'
import github from '../plugin'
import ConnectProject from './ConnectProject.svelte'
import { githubLanguageColors } from './languageColors'
import { sendGHServiceRequest } from './utils'
export let integration: WithLookup<GithubIntegration>
export let projects: Project[] = []
export let giProjects: GithubProject[] = []
const client = getClient()
const asRepos = (docs: AttachedDoc[]) => docs as GithubIntegrationRepository[]
let search: string = ''
let limit = 5
async function disconnect (prj: GithubProject, repository: GithubIntegrationRepository): Promise<void> {
// We need to disable repository first
await client.update(repository, {
enabled: false,
githubProject: null
})
await client.update(prj, {
$pull: { repositories: repository._id }
})
// // We need to delete all issues related to repository
// const ops = client.apply('cleanup:' + repository._id)
// const issuesQuery = await client.findAll(
// github.mixin.GithubIssue,
// {
// space: prj._id as Ref<Project>,
// repository: repository._id
// },
// { projection: { _id: 1, _class: 1 } }
// )
// for (const i of issuesQuery) {
// await ops.removeDoc(i._class, prj._id, i._id)
// }
// const docInfo = await client.findAll(
// github.class.DocSyncInfo,
// {
// space: prj._id as Ref<Project>,
// repository: repository._id
// },
// { projection: { _id: 1, _class: 1 } }
// )
// for (const i of docInfo) {
// await ops.removeDoc(i._class, prj._id, i._id)
// }
// await ops.commit()
}
async function onDisconnect (
event: MouseEvent,
prj: GithubProject,
repository: GithubIntegrationRepository
): Promise<void> {
const issuesQuery = await client.findAll(
github.mixin.GithubIssue,
{
space: prj._id as Ref<Project>,
repository: repository._id
},
{ total: true, limit: 1 }
)
showPopup(
MessageBox,
{
label: github.string.UnlinkRepository,
message: github.string.UnlinkMessage,
params: { repositoryName: repository.name, prjName: prj.name, total: issuesQuery.total },
richMessage: true
},
undefined,
async (res) => {
if (res === true) {
void disconnect(prj, repository)
}
}
)
}
$: repos = asRepos(integration.$lookup?.repositories ?? [])
.filter((it) => it.name.toLowerCase().includes(search))
.sort((a, b) => {
const aprj = giProjects.find((it) => it.repositories.includes(a._id))
const bprj = giProjects.find((it) => it.repositories.includes(b._id))
if (aprj !== undefined && bprj === undefined) {
return -1
}
if (bprj !== undefined && aprj === undefined) {
return 1
}
return (b.updatedAt ?? 0) - (a.updatedAt ?? 0)
})
async function showMenu (evt: MouseEvent, prj: GithubProject, repository: GithubIntegrationRepository): Promise<void> {
if (isAdminUser()) {
const actions: Action[] = [
{
label: !repository.enabled ? github.string.Enable : github.string.Disable,
action: async (props: any, ev: Event) => {
void client.update(repository, { enabled: !repository.enabled })
}
}
]
showPopup(
Menu,
{
actions
},
getEventPositionElement(evt),
() => {}
)
}
}
</script>
<Expandable expanded={true}>
<svelte:fragment slot="title">
<span class="fs-title flex-row-center flex-between flex-grow flex">
<div class="ml-2 mr-2">
<img class="svg-large" src={integration.name.replace('github.com', 'avatars.githubusercontent.com')} />
</div>
{#if integration.name.length > 0}
{integration.name}
{#if (integration.type ?? '') !== ''}
({integration.type})
{/if}
{:else}
<Label label={github.string.ConnectionPending} />
{/if}
{#if !integration.alive}
<Label label={github.string.Closed} />
{/if}
</span>
</svelte:fragment>
<svelte:fragment slot="tools">
<Button
kind={'dangerous'}
label={github.string.RemoveInstallation}
on:click={() => {
showPopup(
MessageBox,
{
label: github.string.UnlinkInstallationTitle,
message: github.string.UnlinkInstallation,
params: {},
richMessage: true
},
undefined,
async (res) => {
if (res !== null) {
await sendGHServiceRequest('installation-remove', {
installationId: integration.installationId,
token: getMetadata(presentation.metadata.Token) ?? ''
})
}
}
)
}}
/>
</svelte:fragment>
<div class="flex flex-row-center flex-between m-0-5">
<SearchEdit bind:value={search} width={'100%'} />
</div>
{#each repos.slice(0, limit) as repository}
{@const prj = giProjects.find((it) => it.repositories.includes(repository._id))}
<div
class="repository-card flex-col m-0-5"
class:selected={prj !== undefined}
class:disabled={prj !== undefined && !repository.enabled}
>
<div class="flex flex-row-center flex-between">
<div class="flex-row-center">
<NavLink href={repository.htmlURL}>{repository.name}</NavLink>
<div class="ml-2 visibility">
{repository.visibility}
</div>
{#if !repository.enabled && prj !== undefined}
<div class="ml-2 visibility">
<Label label={github.string.Disabled} />
</div>
{/if}
</div>
<div class="flex flex-row-center">
{#if prj !== undefined}
<div class="mr-2">
<Label label={github.string.LinkedWith} />
</div>
<ObjectPresenter _class={prj._class} objectId={prj._id} value={prj} />
<div class="ml-2">
<Button
kind={'dangerous'}
label={github.string.UnlinkFromProject}
size={'medium'}
on:click={(evt) => {
void onDisconnect(evt, prj, repository)
}}
/>
</div>
<div class="ml-2">
<Button
icon={IconMoreV}
size={'small'}
on:click={(evt) => {
void showMenu(evt, prj, repository)
}}
/>
</div>
{:else}
<ConnectProject {integration} {repository} {projects} />
{/if}
</div>
</div>
<div class="mt-2">
{repository.description ?? ''}
</div>
<div class="flex flex-row-center mt-4">
{#if repository.language != null}
<div class="flex-row-center mr-4">
<div class="lcolor-pin mr-1" style:background-color={githubLanguageColors[repository.language] ?? ''} />
{repository.language}
</div>
{/if}
<div class="flex-row-center">
<Icon icon={IconColStar} fill={'none'} size={'small'} />
<span class="ml-1">{repository.stargazers ?? 0}</span>
</div>
<div class="flex-row-center ml-4">
<Icon icon={github.icon.Forks} size={'small'} />
<span class="ml-1">{repository.forks ?? 0}</span>
</div>
<div class="flex-row-center ml-4">
<Icon icon={tracker.icon.Issue} size={'small'} />
<span class="ml-1">{repository.openIssues ?? 0}</span>
</div>
{#if repository.updatedAt !== undefined}
<div class="flex-row-center ml-4">
<Label label={github.string.Updated} />
<span class="ml-2">
<TimeSince value={repository.updatedAt} />
</span>
</div>
{/if}
</div>
</div>
{/each}
{#if repos.length > limit}
<Button
label={ui.string.ShowMore}
on:click={() => {
limit = limit + 10
}}
/>
{/if}
</Expandable>
<style lang="scss">
.bordered {
border: 1px dashed var(--theme-divider-color);
}
.repository-card {
border: 1px solid var(--theme-divider-color);
border-radius: 8px;
padding: 1rem;
&.selected {
background-color: var(--theme-divider-color);
}
&.disabled {
border-color: red;
}
}
.visibility {
border: 1px solid var(--theme-divider-color);
border-radius: 2em;
padding: 0 7px;
height: fit-content;
}
.lcolor-pin {
border-radius: 50%;
width: 12px;
height: 12px;
background: var(--theme-divider-color);
}
</style>
@@ -0,0 +1,61 @@
<script lang="ts">
import { getEmbeddedLabel } from '@hcengineering/platform'
import { Card, getClient } from '@hcengineering/presentation'
import { DocSyncInfo } from '@hcengineering/github'
export let issue: DocSyncInfo
function allowEdit (): void {
void getClient().update(issue, {
isDescriptionLocked: false,
needSync: ''
})
}
</script>
<Card
okLabel={getEmbeddedLabel('Unlock')}
label={getEmbeddedLabel('Description merge conflict')}
on:close
on:changeContent
okAction={allowEdit}
fullSize={true}
canSave={true}
>
<div class="flex flex-row flex-grow">
<div class="flex-row" style={'width: 50%'} style:overflow={'auto'}>
Github Markdown value:
<div class="proseCodeBlock select-text flex-row" style={'text-wrap: wrap;'}>
{#each (issue?.external.body ?? '').split('\n') as line, i}
<div class="flex">
<div class="line">
{i + 1}
</div>
{line}
</div>
{/each}
</div>
</div>
<div class="flex-row" style={'width: 50%'} style:overflow={'auto'}>
Platform markdown value:
<div class="proseCodeBlock select-text flex-col" style={'text-wrap: wrap;'}>
{#each (issue?.markdown ?? '').split('\n') as line, i}
<div class="flex">
<div class="line">
{i + 1}
</div>
{line}
</div>
{/each}
</div>
</div>
</div>
</Card>
<style lang="scss">
.line {
border-right: 1px solid #ddd;
padding: 0 0.5em;
margin-right: 0.5em;
color: #888;
}
</style>
@@ -0,0 +1,141 @@
<!--
// Copyright © 2023 Hardcore Engineering Inc.
//
-->
<script lang="ts">
import { PersonAccount } from '@hcengineering/contact'
import { getCurrentAccount } from '@hcengineering/core'
import { createQuery, getClient, getFileUrl } from '@hcengineering/presentation'
import { Button, Chevron, Component, ExpandCollapse, Label } from '@hcengineering/ui'
import diffview from '@hcengineering/diffview'
import { GithubPatch, GithubPullRequest, GithubPullRequestReview } from '@hcengineering/github'
import github from '../plugin'
export let pullRequest: GithubPullRequest
const me = getCurrentAccount() as PersonAccount
let isCollapsed = true
let patch: GithubPatch | undefined
const patchQuery = createQuery()
$: patchQuery.query(github.class.GithubPatch, { attachedTo: pullRequest._id }, (res) => {
;[patch] = res
})
let patchText: string = ''
$: if (patch !== undefined) {
void fetch(getFileUrl(patch.file, patch.name))
.then((data) => data.text())
.then((text) => {
patchText = text
})
}
$: hasPatch = patch !== undefined && patchText !== ''
let review: GithubPullRequestReview | undefined
const reviewQuery = createQuery()
$: reviewQuery.query(
github.class.GithubPullRequestReview,
{
attachedTo: pullRequest._id,
author: me.person
},
(res) => {
;[review] = res
}
)
$: viewedFiles = review?.files ?? []
const client = getClient()
async function handleFileViewed (fileName: string, sha: string, viewed: boolean): Promise<void> {
const current = await client.findOne(github.class.GithubPullRequestReview, {
attachedTo: pullRequest._id,
author: me.person
})
const files = current?.files ?? []
const index = files.findIndex((file) => file.fileName === fileName && file.sha === sha)
if (index !== -1) {
files.splice(index, 1)
}
if (viewed) {
files.push({ fileName, sha })
}
if (current) {
await client.update(current, { files })
} else {
await client.addCollection(
github.class.GithubPullRequestReview,
pullRequest.space,
pullRequest._id,
github.class.GithubPullRequest,
'reviewsVisual',
{ author: me.person, files }
)
}
}
</script>
<div class="mt-6">
{#if hasPatch}
<div class="flex-between mb-1">
<Button
width="min-content"
kind="ghost"
on:click={() => {
isCollapsed = !isCollapsed
}}
>
<svelte:fragment slot="content">
<Chevron
size={'small'}
expanded={!isCollapsed}
outline
fill={'var(--caption-color)'}
marginRight={'.375rem'}
/>
<Label label={github.string.ChangedFiles} params={{ files: pullRequest.files }} />
</svelte:fragment>
</Button>
</div>
{#if !isCollapsed}
<ExpandCollapse isExpanded={!isCollapsed}>
<div class="list" class:collapsed={isCollapsed}>
<Component
is={diffview.component.DiffView}
props={{ patch: patchText, viewed: viewedFiles }}
on:change={(evt) => {
const { fileName, sha, viewed } = evt.detail
handleFileViewed(fileName, sha, viewed)
}}
/>
</div>
</ExpandCollapse>
{/if}
{/if}
</div>
<style lang="scss">
.list {
padding-top: 0.75rem;
border-top: 1px solid var(--divider-color);
&.collapsed {
padding-top: 1px;
border-top: none;
}
}
</style>
@@ -0,0 +1,44 @@
<script lang="ts">
import { getClient } from '@hcengineering/presentation'
import { Issue } from '@hcengineering/tracker'
import { ButtonSize, Icon, Label } from '@hcengineering/ui'
import { GithubPullRequest, GithubPullRequestState } from '@hcengineering/github'
import github from '../plugin'
import PullRequestReviewDecisionValuePresenter from './presenters/PullRequestReviewDecisionValuePresenter.svelte'
export let value: Issue
export let size: ButtonSize = 'medium'
export let small = false
$: pr = getClient().getHierarchy().isDerived(value?._class, github.class.GithubPullRequest)
? (value as GithubPullRequest)
: undefined
</script>
{#if pr}
{#if pr.state === GithubPullRequestState.open}
<div class="ml-4">
<PullRequestReviewDecisionValuePresenter value={pr.reviewDecision} {small} />
</div>
{#if pr.mergeable === 'CONFLICTING' && !small}
<div class="ml-4">
<Label label={github.string.Conflict} />
</div>
{/if}
{:else if pr.state === GithubPullRequestState.merged}
<div class:ml-4={!small} class="flex-row-center">
<Icon icon={github.icon.PullRequestMerged} size={'small'} />
{#if !small}
<Label label={github.string.PRMerged} />
{/if}
</div>
{:else if pr.state === GithubPullRequestState.closed}
<div class:ml-4={!small} class="flex-row-center">
<Icon icon={github.icon.PullRequestClosed} size={'small'} />
{#if !small}
<Label label={github.string.PRClosed} />
{/if}
</div>
{/if}
{/if}
@@ -0,0 +1,84 @@
<!--
// Copyright © 2023 Hardcore Engineering Inc.
//
-->
<script lang="ts">
import { DocumentQuery, Ref } from '@hcengineering/core'
import { createEventDispatcher } from 'svelte'
import { IntlString } from '@hcengineering/platform'
import { createQuery } from '@hcengineering/presentation'
import tracker, { Issue, Project } from '@hcengineering/tracker'
import { IModeSelector, resolvedLocationStore } from '@hcengineering/ui'
import task from '@hcengineering/task'
import { GithubProject, GithubPullRequest } from '@hcengineering/github'
import PullRequestsView from './PullRequestsView.svelte'
export let currentSpace: Ref<GithubProject> | undefined = undefined
export let baseQuery: DocumentQuery<GithubPullRequest> = {}
export let title: IntlString
export let config: [string, IntlString, object][]
const dispatch = createEventDispatcher()
let query: DocumentQuery<GithubPullRequest> | undefined = undefined
let modeSelectorProps: IModeSelector | undefined = undefined
const archivedProjectQuery = createQuery()
let archived: Ref<Project>[] = []
archivedProjectQuery.query(
tracker.class.Project,
{ archived: true },
(res) => {
archived = res.map((it) => it._id)
},
{ projection: { _id: 1 } }
)
$: spaceQuery = currentSpace ? { space: currentSpace } : { space: { $nin: archived as Ref<GithubProject>[] } }
const activeStatusQuery = createQuery()
let active: DocumentQuery<Issue>
$: all = { ...spaceQuery }
$: activeStatusQuery.query(
tracker.class.IssueStatus,
{
category: { $nin: [task.statusCategory.Won, task.statusCategory.Lost] }
},
(result) => {
active = { status: { $in: result.map(({ _id }) => _id) }, ...spaceQuery }
}
)
const closedStatusQuery = createQuery()
let closed: DocumentQuery<GithubPullRequest> = {}
$: closedStatusQuery.query(
tracker.class.IssueStatus,
{ category: { $in: [task.statusCategory.Won, task.statusCategory.Lost] } },
(result) => {
closed = { status: { $in: result.map(({ _id }) => _id) }, ...spaceQuery }
}
)
$: queries = { all, active, closed }
$: mode = $resolvedLocationStore.query?.mode ?? undefined
$: if (mode === undefined || (queries as any)[mode] === undefined) {
;[[mode]] = config
}
$: if (mode !== undefined) {
query = { ...((queries as any)[mode] ?? {}) }
modeSelectorProps = {
config,
mode,
onChange: (newMode: string) => dispatch('action', { mode: newMode })
}
}
</script>
{#if query !== undefined && modeSelectorProps !== undefined}
{#key query && currentSpace}
<PullRequestsView {query} space={currentSpace} {title} {modeSelectorProps} />
{/key}
{/if}
@@ -0,0 +1,95 @@
<script lang="ts">
import { DocumentQuery, Ref, Space, WithLookup } from '@hcengineering/core'
import { IntlString, translate } from '@hcengineering/platform'
import { Button, IModeSelector, IconDetails, IconDetailsFilled, themeStore } from '@hcengineering/ui'
import { ViewOptions, Viewlet } from '@hcengineering/view'
import { FilterBar, SpaceHeader, ViewletContentView, ViewletSettingButton } from '@hcengineering/view-resources'
import { GithubPullRequest } from '@hcengineering/github'
import github from '../plugin'
export let space: Ref<Space> | undefined = undefined
export let query: DocumentQuery<GithubPullRequest> = {}
export let title: IntlString | undefined = undefined
export let label: string = ''
export let panelWidth: number = 0
export let modeSelectorProps: IModeSelector | undefined = undefined
let viewlet: WithLookup<Viewlet> | undefined = undefined
const viewlets: WithLookup<Viewlet>[] = []
let viewOptions: ViewOptions | undefined
let search = ''
let searchQuery: DocumentQuery<GithubPullRequest> = { ...query }
function updateSearchQuery (search: string): void {
searchQuery = search === '' ? { ...query } : { ...query, $search: search }
}
$: if (query) updateSearchQuery(search)
let resultQuery: DocumentQuery<GithubPullRequest> = { ...searchQuery }
$: if (!label && title) {
void translate(title, {}, $themeStore.language).then((res) => {
label = res
})
}
let asideFloat: boolean = false
let asideShown: boolean = true
$: if (panelWidth < 900 && !asideFloat) asideFloat = true
$: if (panelWidth >= 900 && asideFloat) {
asideFloat = false
asideShown = false
}
let docWidth: number
let docSize: boolean = false
$: if (docWidth <= 900 && !docSize) docSize = true
$: if (docWidth > 900 && docSize) docSize = false
</script>
<SpaceHeader
bind:viewlet
bind:search
_class={github.class.GithubPullRequest}
showLabelSelector={$$slots.label_selector}
{viewlets}
{label}
{space}
{modeSelectorProps}
>
<svelte:fragment slot="label_selector">
<slot name="label_selector" />
</svelte:fragment>
<svelte:fragment slot="extra">
<ViewletSettingButton bind:viewOptions bind:viewlet />
{#if asideFloat && $$slots.aside}
<div class="buttons-divider" />
<Button
icon={asideShown ? IconDetailsFilled : IconDetails}
kind={'ghost'}
size={'medium'}
selected={asideShown}
on:click={() => {
asideShown = !asideShown
}}
/>
{/if}
</svelte:fragment>
</SpaceHeader>
{#if viewlet && viewOptions}
<FilterBar
_class={github.class.GithubPullRequest}
query={searchQuery}
{space}
{viewOptions}
on:change={(e) => (resultQuery = e.detail)}
/>
<slot name="afterHeader" />
<div class="popupPanel rowContent">
{#if viewlet}
<ViewletContentView _class={github.class.GithubPullRequest} {viewlet} query={resultQuery} {space} {viewOptions} />
{/if}
{#if $$slots.aside !== undefined && asideShown}
<div class="popupPanel-body__aside" class:shown={asideShown}>
<slot name="aside" />
</div>
{/if}
</div>
{/if}
@@ -0,0 +1,60 @@
<!--
// Copyright © 2022 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">
import { WithLookup } from '@hcengineering/core'
import { translate } from '@hcengineering/platform'
import { Icon, themeStore } from '@hcengineering/ui'
import { GithubIntegrationRepository } from '@hcengineering/github'
import github from '../plugin'
export let value: WithLookup<GithubIntegrationRepository> | undefined
export let shouldShowAvatar = true
export let onClick: (() => void) | undefined = undefined
export let disabled: boolean = false
export let inline: boolean = false
export let accent: boolean = false
export let noUnderline = false
export let kind: 'list' | undefined = undefined
let label: string
$: if (value !== undefined) {
label = value.name
} else {
translate(github.string.NoRepository, {}, $themeStore.language)
.then((r) => {
label = r
})
.catch((err) => {
console.error(err)
})
}
$: disabled = disabled || value === undefined
</script>
<div class="flex-row-center">
<span class="flex-presenter flex-row-center" class:list={kind === 'list'}>
<div class="flex-row-center">
{#if shouldShowAvatar}
<div class="icon ml-2">
<Icon icon={github.icon.Github} size={'small'} />
</div>
{/if}
<span title={label} class="nowrap" class:no-underline={disabled || noUnderline} class:fs-bold={accent}>
{label}
</span>
</div>
</span>
</div>
@@ -0,0 +1,33 @@
<!--
//
// Copyright © 2023 Hardcore Engineering Inc.
//
-->
<script lang="ts">
import { Ref } from '@hcengineering/core'
import { getEmbeddedLabel } from '@hcengineering/platform'
import { ButtonKind, ButtonSize } from '@hcengineering/ui'
import { HyperlinkEditor } from '@hcengineering/view-resources'
import github, { GithubIntegrationRepository } from '@hcengineering/github'
import { integrationRepositories } from './utils'
export let value: Ref<GithubIntegrationRepository>
export let kind: ButtonKind | undefined = undefined
export let size: ButtonSize = 'small'
export let justify: 'left' | 'center' = 'center'
export let width: string | undefined = 'fit-content'
$: repository = $integrationRepositories.get(value)
</script>
<HyperlinkEditor
value={repository?.repository?.html_url}
placeholder={getEmbeddedLabel(repository?.name ?? '')}
title={repository?.name ?? ''}
readonly
icon={github.icon.Github}
{kind}
{size}
{justify}
{width}
/>
@@ -0,0 +1,128 @@
<!--
//
// Copyright © 2023 Hardcore Engineering Inc.
//
-->
<script lang="ts">
import { Ref } from '@hcengineering/core'
import { IntlString, getEmbeddedLabel } from '@hcengineering/platform'
import {
Button,
ButtonKind,
ButtonSize,
Icon,
IconChevronDown,
LabelAndProps,
SelectPopup,
SelectPopupValueType,
eventToHTMLElement,
showPopup
} from '@hcengineering/ui'
import { HyperlinkEditor } from '@hcengineering/view-resources'
import { GithubIntegrationRepository, GithubProject } from '@hcengineering/github'
import github from '../plugin'
import { integrationRepositories } from './utils'
export let value: Ref<GithubIntegrationRepository> | undefined = undefined
export let space: Ref<GithubProject>
export let kind: ButtonKind | undefined = undefined
export let size: ButtonSize = 'small'
export let justify: 'left' | 'center' = 'center'
export let width: string | undefined = 'fit-content'
export let onChange: (value: Ref<GithubIntegrationRepository> | undefined) => void
export let disabled: boolean
export let popupPlaceholder: IntlString = github.string.Repository
export let focusIndex: number | undefined = undefined
export let showTooltip: LabelAndProps | undefined = undefined
export let label: IntlString = github.string.AssignRepository
export let showIcon: boolean = false
$: repository = $integrationRepositories.get(value)
let selectedRepository: GithubIntegrationRepository | undefined
$: rawComponents = Array.from($integrationRepositories.values()).filter((it) => it.githubProject === space)
const handleSelectedRepositoryIdUpdated = async (
newRepositoryId: Ref<GithubIntegrationRepository> | null | undefined,
components: GithubIntegrationRepository[]
): Promise<void> => {
if (newRepositoryId === null || newRepositoryId === undefined) {
selectedRepository = undefined
return
}
selectedRepository = components.find((it) => it._id === newRepositoryId)
}
$: void handleSelectedRepositoryIdUpdated(value, rawComponents)
function getRepositoryInfo (
rawComponents: GithubIntegrationRepository[],
sp: GithubIntegrationRepository | undefined
): SelectPopupValueType[] {
return [
...rawComponents.map((p) => ({
id: p._id,
icon: github.icon.Github,
label: getEmbeddedLabel(p.name),
props: {
value: p
}
}))
]
}
let components: SelectPopupValueType[] = []
$: components = getRepositoryInfo(rawComponents, selectedRepository)
const handleRepositoryEditorOpened = async (event: MouseEvent): Promise<void> => {
event.stopPropagation()
if (disabled) {
return
}
showPopup(
SelectPopup,
{ value: components, placeholder: popupPlaceholder, searchable: false },
eventToHTMLElement(event),
onChange
)
}
</script>
{#if value == null}
<Button
{kind}
{justify}
{size}
{focusIndex}
{showTooltip}
{disabled}
{label}
icon={showIcon ? github.icon.Github : undefined}
iconRight={IconChevronDown}
on:click={handleRepositoryEditorOpened}
>
<svelte:fragment slot="content">
{#if selectedRepository !== undefined}
<div class="p-1">
<Icon icon={github.icon.Github} size={'small'} />
</div>
{selectedRepository.name}
{/if}
</svelte:fragment>
</Button>
{:else}
<HyperlinkEditor
value={repository?.htmlURL ?? ''}
placeholder={getEmbeddedLabel(repository?.name ?? '')}
title={repository?.name ?? ''}
readonly
icon={github.icon.Github}
{kind}
{size}
{justify}
{width}
/>
{/if}
@@ -0,0 +1,111 @@
<!--
// Copyright © 2024 Hardcore Engineering Inc.
//
-->
<script lang="ts">
import { Ref } from '@hcengineering/core'
import { IntlString, getEmbeddedLabel } from '@hcengineering/platform'
import type { ButtonKind, ButtonShape, ButtonSize, LabelAndProps, SelectPopupValueType } from '@hcengineering/ui'
import { ButtonWithDropdown, Icon, IconDropdown, SelectPopup, eventToHTMLElement, showPopup } from '@hcengineering/ui'
import { GithubIntegrationRepository, GithubProject } from '@hcengineering/github'
import github from '../plugin'
import { integrationRepositories } from './utils'
export let value: Ref<GithubIntegrationRepository> | null | undefined
export let githubProject: Ref<GithubProject>
// export let shouldShowLabel: boolean = true
export let isEditable: boolean = true
export let onChange: ((newRepositoryId: Ref<GithubIntegrationRepository> | undefined) => void) | undefined = undefined
export let popupPlaceholder: IntlString = github.string.Repository
export let kind: ButtonKind = 'no-border'
export let size: ButtonSize = 'small'
export let justify: 'left' | 'center' = 'center'
export let focusIndex: number | undefined = undefined
export let showTooltip: LabelAndProps | undefined = undefined
let selectedRepository: GithubIntegrationRepository | undefined
$: rawComponents = Array.from($integrationRepositories.values()).filter((it) => it.githubProject === githubProject)
const handleSelectedRepositoryIdUpdated = async (
newRepositoryId: Ref<GithubIntegrationRepository> | null | undefined,
components: GithubIntegrationRepository[]
): Promise<void> => {
if (newRepositoryId === null || newRepositoryId === undefined) {
selectedRepository = undefined
return
}
selectedRepository = components.find((it) => it._id === newRepositoryId)
}
$: void handleSelectedRepositoryIdUpdated(value, rawComponents)
function getRepositoryInfo (
rawComponents: GithubIntegrationRepository[],
sp: GithubIntegrationRepository | undefined
): SelectPopupValueType[] {
return [
...rawComponents.map((p) => ({
id: p._id,
icon: github.icon.Github,
label: getEmbeddedLabel(p.name),
props: {
value: p
}
})),
{
id: null,
label: github.string.WithoutRepository
}
]
}
let components: SelectPopupValueType[] = []
$: components = getRepositoryInfo(rawComponents, selectedRepository)
const handleRepositoryEditorOpened = async (event: MouseEvent): Promise<void> => {
event.stopPropagation()
if (!isEditable) {
return
}
showPopup(
SelectPopup,
{ value: components, placeholder: popupPlaceholder, searchable: false },
eventToHTMLElement(event),
onChange
)
}
</script>
<ButtonWithDropdown
{kind}
{justify}
{size}
{focusIndex}
showTooltipMain={showTooltip}
dropdownItems={components}
disabled={!isEditable}
label={github.string.NoRepository}
dropdownIcon={IconDropdown}
on:click={handleRepositoryEditorOpened}
on:dropdown-selected={(ev) => {
if (ev.detail != null) {
value = ev.detail
}
}}
>
<svelte:fragment slot="content">
{#if selectedRepository !== undefined}
<div class="p-1">
<Icon icon={github.icon.Github} size={'small'} />
</div>
{selectedRepository.name}
{/if}
</svelte:fragment>
</ButtonWithDropdown>
@@ -0,0 +1,200 @@
export const githubLanguageColors: Record<string, string> = {
ABAP: '#E8274B',
ActionScript: '#882B0F',
Ada: '#02f88c',
Agda: '#315665',
'AGS Script': '#B9D9FF',
Alloy: '#64C800',
AMPL: '#E6EFBB',
ANTLR: '#9DC3FF',
'API Blueprint': '#2ACCA8',
APL: '#5A8164',
Arc: '#aa2afe',
Arduino: '#bd79d1',
ASP: '#6a40fd',
AspectJ: '#a957b0',
Assembly: '#6E4C13',
ATS: '#1ac620',
AutoHotkey: '#6594b9',
AutoIt: '#1C3552',
BlitzMax: '#cd6400',
Boo: '#d4bec1',
Brainfuck: '#2F2530',
'C Sharp': '#178600',
C: '#555555',
Chapel: '#8dc63f',
Cirru: '#ccccff',
Clarion: '#db901e',
Clean: '#3F85AF',
Click: '#E4E6F3',
Clojure: '#db5855',
CoffeeScript: '#244776',
'ColdFusion CFC': '#ed2cd6',
ColdFusion: '#ed2cd6',
'Common Lisp': '#3fb68b',
'Component Pascal': '#b0ce4e',
cpp: '#f34b7d',
Crystal: '#776791',
CSS: '#563d7c',
D: '#ba595e',
Dart: '#00B4AB',
Diff: '#88dddd',
DM: '#447265',
Dogescript: '#cca760',
Dylan: '#6c616e',
E: '#ccce35',
Eagle: '#814C05',
eC: '#913960',
ECL: '#8a1267',
edn: '#db5855',
Eiffel: '#946d57',
Elixir: '#6e4a7e',
Elm: '#60B5CC',
'Emacs Lisp': '#c065db',
EmberScript: '#FFF4F3',
Erlang: '#B83998',
'F#': '#b845fc',
Factor: '#636746',
Fancy: '#7b9db4',
Fantom: '#dbded5',
FLUX: '#88ccff',
Forth: '#341708',
FORTRAN: '#4d41b1',
FreeMarker: '#0050b2',
Frege: '#00cafe',
'Game Maker Language': '#8fb200',
Glyph: '#e4cc98',
Gnuplot: '#f0a9f0',
Go: '#375eab',
Golo: '#88562A',
Gosu: '#82937f',
'Grammatical Framework': '#79aa7a',
Groovy: '#e69f56',
Handlebars: '#01a9d6',
Harbour: '#0e60e3',
Haskell: '#29b544',
Haxe: '#df7900',
HTML: '#e44b23',
Hy: '#7790B2',
IDL: '#a3522f',
Io: '#a9188d',
Ioke: '#078193',
Isabelle: '#FEFE00',
J: '#9EEDFF',
Java: '#b07219',
JavaScript: '#f1e05a',
JFlex: '#DBCA00',
JSONiq: '#40d47e',
Julia: '#a270ba',
'Jupyter Notebook': '#DA5B0B',
Kotlin: '#F18E33',
KRL: '#28431f',
Lasso: '#999999',
Latte: '#A8FF97',
Lex: '#DBCA00',
LFE: '#004200',
LiveScript: '#499886',
LOLCODE: '#cc9900',
LookML: '#652B81',
LSL: '#3d9970',
Lua: '#000080',
Makefile: '#427819',
Mask: '#f97732',
Matlab: '#bb92ac',
Max: '#c4a79c',
MAXScript: '#00a6a6',
Mercury: '#ff2b2b',
Metal: '#8f14e9',
Mirah: '#c7a938',
MTML: '#b7e1f4',
NCL: '#28431f',
Nemerle: '#3d3c6e',
nesC: '#94B0C7',
NetLinx: '#0aa0ff',
'NetLinx+ERB': '#747faa',
NetLogo: '#ff6375',
NewLisp: '#87AED7',
Nimrod: '#37775b',
Nit: '#009917',
Nix: '#7e7eff',
Nu: '#c9df40',
'Objective-C': '#438eff',
'Objective-C++': '#6866fb',
'Objective-J': '#ff0c5a',
OCaml: '#3be133',
Omgrofl: '#cabbff',
ooc: '#b0b77e',
Opal: '#f7ede0',
Oxygene: '#cdd0e3',
Oz: '#fab738',
Pan: '#cc0000',
Papyrus: '#6600cc',
Parrot: '#f3ca0a',
Pascal: '#b0ce4e',
PAWN: '#dbb284',
Perl: '#0298c3',
Perl6: '#0000fb',
PHP: '#4F5D95',
PigLatin: '#fcd7de',
Pike: '#005390',
PLSQL: '#dad8d8',
PogoScript: '#d80074',
Processing: '#0096D8',
Prolog: '#74283c',
'Propeller Spin': '#7fa2a7',
Puppet: '#302B6D',
'Pure Data': '#91de79',
PureBasic: '#5a6986',
PureScript: '#1D222D',
Python: '#3572A5',
QML: '#44a51c',
R: '#198ce7',
Racket: '#22228f',
'Ragel in Ruby Host': '#9d5200',
RAML: '#77d9fb',
Rebol: '#358a5b',
Red: '#ee0000',
"Ren'Py": '#ff7f7f',
Rouge: '#cc0088',
Ruby: '#701516',
Rust: '#dea584',
SaltStack: '#646464',
SAS: '#B34936',
Scala: '#DC322F',
Scheme: '#1e4aec',
Self: '#0579aa',
Shell: '#89e051',
Shen: '#120F14',
Slash: '#007eff',
Slim: '#ff8f77',
Smalltalk: '#596706',
SourcePawn: '#5c7611',
SQF: '#3F3F3F',
Squirrel: '#800000',
Stan: '#b2011d',
'Standard ML': '#dc566d',
SuperCollider: '#46390b',
Swift: '#ffac45',
SystemVerilog: '#DAE1C2',
Tcl: '#e4cc98',
TeX: '#3D6117',
Turing: '#45f715',
TypeScript: '#2b7489',
'Unified Parallel C': '#4e3617',
'Unity3D Asset': '#ab69a1',
UnrealScript: '#a54c4d',
Vala: '#fbe5cd',
Verilog: '#b2b7f8',
VHDL: '#adb2cb',
VimL: '#199f4b',
'Visual Basic': '#945db7',
Volt: '#1F1F1F',
Vue: '#2c3e50',
'Web Ontology Language': '#9cc9dd',
wisp: '#7582D1',
X10: '#4B6BEF',
xBase: '#403a40',
XC: '#99DA07',
XQuery: '#5232e7',
Zephir: '#118f9e'
}
@@ -0,0 +1,27 @@
<script lang="ts">
import { Issue } from '@hcengineering/tracker'
import { getClient } from '@hcengineering/presentation'
import { HyperlinkEditor } from '@hcengineering/view-resources'
import github from '../../plugin'
import { integrationRepositories } from '../utils'
export let value: Issue
$: ghIssue = getClient().getHierarchy().asIf(value, github.mixin.GithubIssue)
$: repository = ghIssue?.repository !== undefined ? $integrationRepositories.get(ghIssue?.repository) : undefined
</script>
{#if ghIssue !== undefined && ghIssue.url !== '' && repository !== undefined}
<div class="flex flex-row-center">
<HyperlinkEditor
readonly
icon={github.icon.Github}
kind={'ghost'}
value={ghIssue.url}
placeholder={github.string.Issue}
title={`${repository.name}`}
/>
</div>
{/if}
@@ -0,0 +1,112 @@
<!--
// Copyright © 2023 Hardcore Engineering Inc.
//
-->
<script lang="ts">
import { Ref, WithLookup, groupByArray } from '@hcengineering/core'
import {
GithubPullRequestReviewState,
GithubReview,
GithubReviewComment,
GithubReviewThread
} from '@hcengineering/github'
import { ActivityMessageHeader, ActivityMessageTemplate } from '@hcengineering/activity-resources'
import { Person, PersonAccount } from '@hcengineering/contact'
import { personAccountByIdStore, personByIdStore } from '@hcengineering/contact-resources'
import { IntlString } from '@hcengineering/platform'
import { MessageViewer, createQuery } from '@hcengineering/presentation'
import { Component, PaletteColorIndexes, getPlatformColor, themeStore } from '@hcengineering/ui'
import diffview from '@hcengineering/diffview'
import github from '../../plugin'
import ReviewCommentPresenter from './ReviewCommentPresenter.svelte'
import { isEmptyMarkup } from '@hcengineering/text'
export let value: WithLookup<GithubReview>
export let showNotify: boolean = false
export let isHighlighted: boolean = false
export let isSelected: boolean = false
export let shouldScroll: boolean = false
export let embedded: boolean = false
export let onClick: (() => void) | undefined = undefined
$: personAccount = $personAccountByIdStore.get((value?.createdBy ?? value?.modifiedBy) as Ref<PersonAccount>)
$: person = $personByIdStore.get(personAccount?.person as Ref<Person>)
function getCommentFromState (value?: GithubPullRequestReviewState): {
label: IntlString
color?: number
} {
switch (value) {
case GithubPullRequestReviewState.Approved:
return { label: github.string.ReviewApproved, color: PaletteColorIndexes.Grass }
case GithubPullRequestReviewState.ChangesRequested:
return { label: github.string.ReviewChangesRequested, color: PaletteColorIndexes.Sunshine }
case GithubPullRequestReviewState.Commented:
return { label: github.string.ReviewCommented }
case GithubPullRequestReviewState.Dismissed:
return { label: github.string.ReviewDismissed, color: PaletteColorIndexes.Coin }
case GithubPullRequestReviewState.Pending:
default:
return { label: github.string.ReviewPending }
}
}
$: presentationState = getCommentFromState(value?.state)
</script>
{#if value?.state !== GithubPullRequestReviewState.Commented || (value?.state === GithubPullRequestReviewState.Commented && (value?.body?.length ?? 0) > 0 && !isEmptyMarkup(value?.body ?? ''))}
<div
class:review={presentationState.color !== undefined}
style:border-color={presentationState.color !== undefined
? getPlatformColor(presentationState.color, $themeStore.dark)
: undefined}
>
<ActivityMessageTemplate
message={value}
parentMessage={undefined}
{person}
{showNotify}
{isHighlighted}
{isSelected}
{shouldScroll}
{embedded}
viewlet={undefined}
{onClick}
>
<svelte:fragment slot="header">
<ActivityMessageHeader
message={value}
{person}
object={undefined}
parentObject={undefined}
isEdited={false}
label={presentationState.label}
/>
</svelte:fragment>
<svelte:fragment slot="content">
<div class="flex-row-center">
<div class="customContent">
<MessageViewer message={value.body} />
</div>
</div>
</svelte:fragment>
</ActivityMessageTemplate>
</div>
{/if}
<style lang="scss">
.review {
border: 1px solid;
border-radius: 0.5rem;
margin-top: 0.25rem;
margin-bottom: 0.25rem;
}
.customContent {
display: flex;
flex-wrap: wrap;
column-gap: 0.625rem;
row-gap: 0.625rem;
}
</style>
@@ -0,0 +1,190 @@
<!--
// Copyright © 2023 Hardcore Engineering Inc.
//
-->
<script lang="ts">
import core, { Account, Ref, WithLookup, getCurrentAccount } from '@hcengineering/core'
import { GithubPullRequest, GithubReviewComment, GithubReviewThread } from '@hcengineering/github'
import { ActivityMessageHeader, ActivityMessageTemplate } from '@hcengineering/activity-resources'
import { Person, PersonAccount } from '@hcengineering/contact'
import { EmployeePresenter, personAccountByIdStore, personByIdStore } from '@hcengineering/contact-resources'
import { getEmbeddedLabel } from '@hcengineering/platform'
import { createQuery, getClient } from '@hcengineering/presentation'
import { ReferenceInput } from '@hcengineering/text-editor-resources'
import { Button, Component, Label, PaletteColorIndexes, getPlatformColor, themeStore } from '@hcengineering/ui'
import diffview from '@hcengineering/diffview'
import github from '../../plugin'
import ReviewCommentPresenter from './ReviewCommentPresenter.svelte'
import { githubConfiguration } from '../../configuration'
export let value: WithLookup<GithubReviewThread>
export let showNotify: boolean = false
export let isHighlighted: boolean = false
export let isSelected: boolean = false
export let shouldScroll: boolean = false
export let embedded: boolean = false
export let onClick: (() => void) | undefined = undefined
$: personAccount = $personAccountByIdStore.get((value?.createdBy ?? value?.modifiedBy) as Ref<PersonAccount>)
$: person = $personByIdStore.get(personAccount?.person as Ref<Person>)
const commentsQuery = createQuery()
let comments: GithubReviewComment[] = []
$: commentsQuery.query(
github.class.GithubReviewComment,
{ attachedTo: value.attachedTo as Ref<GithubPullRequest>, reviewThreadId: value.threadId },
(res) => {
comments = res
}
)
let expanded = !value.isResolved
async function onMessage (event: CustomEvent<string>): Promise<void> {
await getClient().addCollection(
github.class.GithubReviewComment,
value.space,
value.attachedTo,
value.attachedToClass,
'reviewComments',
{
body: event.detail,
diffHunk: '',
includesCreatedEdit: false,
isMinimized: false,
line: 0,
minimizedReason: null,
originalLine: 0,
originalStartLine: 0,
outdated: false,
path: value.path,
reviewThreadId: value.threadId,
reviewUrl: '',
startLine: 0,
url: ''
}
)
}
async function changeResolution (): Promise<void> {
await getClient().update(value, { isResolved: !value.isResolved, resolvedBy: null })
}
const toRefPersonAccount = (account: Ref<Account>): Ref<PersonAccount> => account as Ref<PersonAccount>
const toRefPerson = (account?: Ref<Person>): Ref<Person> => account as Ref<Person>
</script>
<div
class:reviewUnresolved={!value.isResolved}
style:border-color={!value.isResolved ? getPlatformColor(PaletteColorIndexes.Orange, $themeStore.dark) : undefined}
>
<ActivityMessageTemplate
message={value}
parentMessage={undefined}
{person}
{showNotify}
{isHighlighted}
{isSelected}
{shouldScroll}
{embedded}
viewlet={undefined}
{onClick}
>
<svelte:fragment slot="header">
<ActivityMessageHeader
message={value}
{person}
object={undefined}
parentObject={undefined}
isEdited={false}
label={getEmbeddedLabel('reviewed')}
/>
</svelte:fragment>
<svelte:fragment slot="content">
<div class="file-content">
{#if comments.length > 0}
<Component
is={diffview.component.InlineDiffView}
props={{
patch: comments?.[0]?.diffHunk ?? '',
fileName: value.path,
expandable: value.isResolved,
expanded,
onExpand: (value) => {
expanded = value
}
}}
/>
{/if}
{#if expanded}
<div class="ml-4">
{#each comments as comment}
<ReviewCommentPresenter {comment} />
{/each}
</div>
<ReferenceInput showSend={true} showHeader showActions on:message={onMessage} />
<div class="p-2 flex-row-center flex-grow">
{#if githubConfiguration.ResolveThreadSupported}
<Button
label={value.isResolved
? getEmbeddedLabel('Unresolve conversation')
: getEmbeddedLabel('Resolve conversation')}
on:click={changeResolution}
/>
{/if}
{#if value.isResolved && value.resolvedBy != null}
{@const resolveAccount = $personAccountByIdStore.get(toRefPersonAccount(value.resolvedBy))}
{@const resolvePerson = $personByIdStore.get(toRefPerson(resolveAccount?.person))}
{#if resolvePerson !== undefined}
<div class="flex-row-center ml-4">
<Label label={getEmbeddedLabel('resolved by')} />
<div class="content ml-2 clear-mins">
<div class="header clear-mins">
{#if resolvePerson}
<EmployeePresenter value={resolvePerson} shouldShowAvatar={true} />
{:else}
<div class="strong">
<Label label={core.string.System} />
</div>
{/if}
</div>
</div>
</div>
{/if}
{/if}
</div>
{/if}
</div>
</svelte:fragment>
</ActivityMessageTemplate>
</div>
<style lang="scss">
.reviewUnresolved {
border: 1px solid;
border-radius: 0.5rem;
margin-top: 0.25rem;
margin-bottom: 0.25rem;
}
.customContent {
display: flex;
flex-wrap: wrap;
column-gap: 0.625rem;
row-gap: 0.625rem;
}
.file-content {
border: 1px solid var(--theme-divider-color);
border-top: 0;
border-bottom-left-radius: 0.25rem;
border-bottom-right-radius: 0.25rem;
overflow-y: hidden;
}
.file-info {
border-top: 1px solid var(--theme-divider-color);
font-weight: 600;
direction: rtl;
text-align: left;
}
</style>
@@ -0,0 +1,16 @@
<script lang="ts">
import { Label } from '@hcengineering/ui'
import { PullRequestMergeable } from '@hcengineering/github'
import github from '../../plugin'
export let value: PullRequestMergeable
export let accent = false
</script>
<div class="p-1" class:fs-bold={accent}>
{#if value === PullRequestMergeable.CONFLICTING}
<Label label={github.string.Conflict} />
{:else if value === PullRequestMergeable.MERGEABLE}
<Label label={github.string.ReadyForMerge} />
{/if}
</div>
@@ -0,0 +1,42 @@
<!--
// Copyright © 2023 Hardcore Engineering Inc.
//
-->
<script lang="ts">
import { createQuery, getClient } from '@hcengineering/presentation'
import { Issue } from '@hcengineering/tracker'
import { GithubIssue, GithubProject, GithubPullRequest } from '@hcengineering/github'
import github from '../../plugin'
export let value: GithubPullRequest
const client = getClient()
const spaceQuery = createQuery()
let currentProject: GithubProject | undefined = undefined
$: spaceQuery.query(github.mixin.GithubProject, { _id: value.space }, (res) => {
;[currentProject] = res
})
$: ghIssue = client.getHierarchy().hasMixin(value, github.mixin.GithubIssue)
? client.getHierarchy().as<Issue, GithubIssue>(value, github.mixin.GithubIssue)
: undefined
</script>
{#if value}
<div class="flex-col">
<div class="flex-row-center crop-presenter">
<span class="font-medium mr-2 whitespace-nowrap clear-mins">{value.identifier}</span>
<span class="overflow-label">
{currentProject?.name}
</span>
</div>
<span class="overflow-label mt-10px">
{value.title}
{#if ghIssue}
# {ghIssue.githubNumber}
{/if}
</span>
</div>
{/if}
@@ -0,0 +1,29 @@
<!--
//
// Copyright © 2023 Hardcore Engineering Inc.
//
-->
<script lang="ts">
import { WithLookup } from '@hcengineering/core'
import { IssuePresenter } from '@hcengineering/tracker-resources'
import github, { GithubPullRequest } from '@hcengineering/github'
export let value: WithLookup<GithubPullRequest>
export let disabled = false
export let onClick: (() => void) | undefined = undefined
export let shouldShowAvatar: boolean = false
export let noUnderline = false
export let inline = false
export let kind: 'list' | undefined = undefined
</script>
<IssuePresenter
{value}
{disabled}
{onClick}
{shouldShowAvatar}
{noUnderline}
{inline}
{kind}
icon={github.icon.PullRequest}
/>
@@ -0,0 +1,52 @@
<script lang="ts">
import { Asset, IntlString } from '@hcengineering/platform'
import { AnySvelteComponent, Icon, Label, PaletteColorIndexes, getPlatformColor, themeStore } from '@hcengineering/ui'
import { GithubReviewDecisionState } from '@hcengineering/github'
import { ComponentType } from 'svelte'
import github from '../../plugin'
export let value: GithubReviewDecisionState
export let small = false
const labels: Record<
GithubReviewDecisionState,
{ label: IntlString, color: number, icon: Asset | AnySvelteComponent | ComponentType }
> = {
[GithubReviewDecisionState.Approved]: {
icon: github.icon.PullRequest,
label: github.string.ReviewApproved,
color: PaletteColorIndexes.Grass
},
[GithubReviewDecisionState.ChangesRequested]: {
icon: github.icon.PullRequest,
label: github.string.ReviewChangesRequested,
color: PaletteColorIndexes.Firework
},
[GithubReviewDecisionState.ReviewRequired]: {
icon: github.icon.PullRequest,
label: github.string.ReviewPending,
color: PaletteColorIndexes.Blueberry
}
}
$: label = labels[value]
</script>
<div
class={!small ? 'p-1 border-radius-1' : ''}
style:background-color={!small ? getPlatformColor(label.color, $themeStore.dark) : undefined}
style:color={'white'}
>
<div class="flex-row-center no-word-wrap">
<Icon
icon={label.icon}
size={'small'}
fill={small ? getPlatformColor(label.color, $themeStore.dark) : 'currentColor'}
/>
{#if !small}
<div class="ml-1">
<Label label={label.label} />
</div>
{/if}
</div>
</div>
@@ -0,0 +1,18 @@
<script lang="ts">
import { Label } from '@hcengineering/ui'
import { GithubPullRequestState } from '@hcengineering/github'
import github from '../../plugin'
export let value: GithubPullRequestState
export let accent = false
</script>
<div class="p-1" class:fs-bold={accent}>
{#if value === GithubPullRequestState.open}
<Label label={github.string.PROpen} />
{:else if value === GithubPullRequestState.merged}
<Label label={github.string.PRMerged} />
{:else if value === GithubPullRequestState.closed}
<Label label={github.string.PRClosed} />
{/if}
</div>
@@ -0,0 +1,17 @@
<!--
//
// Copyright © 2023 Hardcore Engineering Inc.
//
-->
<script lang="ts">
import { Icon } from '@hcengineering/ui'
import { GithubIntegrationRepository } from '@hcengineering/github'
import github from '../../plugin'
export let value: GithubIntegrationRepository
</script>
<div class="flex-row-center">
<Icon icon={github.icon.GithubRepository} size={'small'} />
<span class="ml-1">{value.name}</span>
</div>
@@ -0,0 +1,58 @@
<script lang="ts">
import { Person, PersonAccount } from '@hcengineering/contact'
import {
EmployeePresenter,
SystemAvatar,
personAccountByIdStore,
personByIdStore
} from '@hcengineering/contact-resources'
import Avatar from '@hcengineering/contact-resources/src/components/Avatar.svelte'
import core, { Ref, getDisplayTime } from '@hcengineering/core'
import { MessageViewer } from '@hcengineering/presentation'
import { Label } from '@hcengineering/ui'
import { GithubReviewComment } from '@hcengineering/github'
export let comment: GithubReviewComment
$: personAccount = $personAccountByIdStore.get((comment?.createdBy ?? comment?.modifiedBy) as Ref<PersonAccount>)
$: person = $personByIdStore.get(personAccount?.person as Ref<Person>)
</script>
{#if comment}
<div>
<div class="flex-row-center">
<div class="min-w-6 mt-1">
{#if $$slots.icon}
<slot name="icon" />
{:else if person}
<Avatar size="tiny" {person} name={person.name} />
{:else}
<SystemAvatar size="tiny" />
{/if}
</div>
<div class="header clear-mins flex-row-center">
{#if person}
<EmployeePresenter value={person} shouldShowAvatar={false} />
{:else}
<div class="strong">
<Label label={core.string.System} />
</div>
{/if}
<span class="text-sm ml-2">{getDisplayTime(comment.createdOn ?? 0)}</span>
</div>
</div>
<div class="customContent p-2">
<MessageViewer message={comment.body} />
</div>
</div>
{/if}
<style lang="scss">
.customContent {
display: flex;
flex-wrap: wrap;
column-gap: 0.625rem;
row-gap: 0.625rem;
}
</style>
@@ -0,0 +1,21 @@
<!--
//
// Copyright © 2023 Hardcore Engineering Inc.
//
-->
<script lang="ts">
import { WithLookup } from '@hcengineering/core'
import { TitlePresenter } from '@hcengineering/tracker-resources'
import { GithubPullRequest } from '@hcengineering/github'
export let value: WithLookup<GithubPullRequest>
export let shouldUseMargin: boolean = false
export let showParent = true
export let kind: 'list' | undefined = undefined
export let onClick: (() => void) | undefined = undefined
export let disabled = false
</script>
{#if value}
<TitlePresenter {value} {shouldUseMargin} {showParent} {kind} {onClick} {disabled} />
{/if}
@@ -0,0 +1,111 @@
import { Analytics } from '@hcengineering/analytics'
import core, { concatLink, getCurrentAccount, toIdMap, type IdMap } from '@hcengineering/core'
import { PlatformError, getMetadata, unknownError } from '@hcengineering/platform'
import presentation, { createQuery, getClient } from '@hcengineering/presentation'
import { location } from '@hcengineering/ui'
import {
makeQuery,
type GithubAuthentication,
type GithubIntegrationRepository,
type GithubProject
} from '@hcengineering/github'
import { get, writable } from 'svelte/store'
import github from '../plugin'
export async function onAuthorize (login?: string): Promise<void> {
const meId = getCurrentAccount()._id
const state = btoa(
JSON.stringify({
accountId: meId,
workspace: get(location).path[1],
token: getMetadata(presentation.metadata.Token),
op: 'authorize'
})
)
const client = getClient()
const config = await client.findOne(github.class.GithubAuthentication, {})
if (config !== undefined) {
await client.remove(config)
}
await client.createDoc<GithubAuthentication>(github.class.GithubAuthentication, core.space.Workspace, {
attachedTo: meId,
login: '',
error: null,
authRequestTime: Date.now(),
createdAt: new Date(),
followers: 0,
following: 0,
nodeId: '',
updatedAt: new Date(),
url: '',
repositories: 0,
organizations: { totalCount: 0, nodes: [] },
closedIssues: 0,
openIssues: 0,
mergedPRs: 0,
openPRs: 0,
closedPRs: 0,
repositoryDiscussions: 0,
starredRepositories: 0
})
Analytics.handleEvent('Authorize github clicked')
const url =
'https://github.com/login/oauth/authorize?' +
makeQuery({
client_id: getMetadata(github.metadata.GithubClientID),
login: '',
state,
allow_signup: 'true'
})
window.open(url)
}
const repositoryQuery = createQuery(true)
/**
* @public
*/
export const integrationRepositories = writable<IdMap<GithubIntegrationRepository>>(new Map())
repositoryQuery.query(github.class.GithubIntegrationRepository, {}, (res) => {
integrationRepositories.set(toIdMap(res))
})
const projectQuery = createQuery(true)
/**
* @public
*/
export const githubProjects = writable<IdMap<GithubProject>>(new Map())
projectQuery.query(github.mixin.GithubProject, {}, (res) => {
githubProjects.set(toIdMap(res))
})
const authQuery = createQuery(true)
export const githubAuth = writable<GithubAuthentication | undefined>(undefined)
authQuery.query(github.class.GithubAuthentication, {}, (res) => {
githubAuth.set(res.shift())
})
/**
* @public
*/
export async function sendGHServiceRequest (path: string, args: Record<string, any>): Promise<any> {
const githubURL = getMetadata(github.metadata.GithubURL)
if (githubURL === undefined) {
// We could try use recognition service to find some document properties.
throw new PlatformError(unknownError('Github integration is not configured'))
}
return await fetch(concatLink(concatLink(githubURL, '/api/v1/'), path), {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(args)
})
}