Merge branch 'staging' into develop

Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
Andrey Sobolev 2025-02-06 11:33:40 +07:00
commit 86494e7fd1
No known key found for this signature in database
GPG Key ID: BD80F68D68D8F7F2
31 changed files with 291 additions and 163 deletions

View File

@ -13,7 +13,7 @@
"DESKTOP_UPDATES_CHANNEL": "front", "DESKTOP_UPDATES_CHANNEL": "front",
"FILES_URL": "https://dl.hc.engineering/blob/:workspace/:blobId", "FILES_URL": "https://dl.hc.engineering/blob/:workspace/:blobId/:filename",
"GITHUB_APP": "huly-github-staging", "GITHUB_APP": "huly-github-staging",

View File

@ -349,7 +349,7 @@ function defineResource (builder: Builder): void {
key: '', key: '',
presenter: drive.component.ResourcePresenter, presenter: drive.component.ResourcePresenter,
label: drive.string.Name, label: drive.string.Name,
sortingKey: 'name' sortingKey: 'title'
}, },
'$lookup.file.size', '$lookup.file.size',
'comments', 'comments',
@ -366,7 +366,7 @@ function defineResource (builder: Builder): void {
} }
} as FindOptions<Resource>, } as FindOptions<Resource>,
configOptions: { configOptions: {
hiddenKeys: ['name', 'parent', 'path', 'file', 'versions'], hiddenKeys: ['title', 'parent', 'path', 'file', 'versions'],
sortable: true sortable: true
} }
}, },
@ -393,7 +393,7 @@ function defineResource (builder: Builder): void {
viewOptions: { viewOptions: {
groupBy: [], groupBy: [],
orderBy: [ orderBy: [
['name', SortingOrder.Ascending], ['title', SortingOrder.Ascending],
['$lookup.file.size', SortingOrder.Ascending], ['$lookup.file.size', SortingOrder.Ascending],
['$lookup.file.modifiedOn', SortingOrder.Descending] ['$lookup.file.modifiedOn', SortingOrder.Descending]
], ],
@ -404,14 +404,14 @@ function defineResource (builder: Builder): void {
key: '', key: '',
presenter: drive.component.ResourcePresenter, presenter: drive.component.ResourcePresenter,
label: drive.string.Name, label: drive.string.Name,
sortingKey: 'name' sortingKey: 'title'
}, },
'$lookup.file.size', '$lookup.file.size',
'$lookup.file.modifiedOn', '$lookup.file.modifiedOn',
'createdBy' 'createdBy'
], ],
configOptions: { configOptions: {
hiddenKeys: ['name', 'parent', 'path', 'file', 'versions'], hiddenKeys: ['title', 'parent', 'path', 'file', 'versions'],
sortable: true sortable: true
}, },
/* eslint-disable @typescript-eslint/consistent-type-assertions */ /* eslint-disable @typescript-eslint/consistent-type-assertions */

View File

@ -275,3 +275,19 @@ export interface Storage {
export interface FulltextStorage { export interface FulltextStorage {
searchFulltext: (query: SearchQuery, options: SearchOptions) => Promise<SearchResult> searchFulltext: (query: SearchQuery, options: SearchOptions) => Promise<SearchResult>
} }
export function shouldShowArchived<T extends Doc> (
query: DocumentQuery<T>,
options: FindOptions<T> | undefined
): boolean {
if (options?.showArchived !== undefined) {
return options.showArchived
}
if (query._id !== undefined && typeof query._id === 'string') {
return true
}
if (query.space !== undefined && typeof query.space === 'string') {
return true
}
return false
}

View File

@ -95,13 +95,17 @@
case 'disabled': case 'disabled':
return { _id: { $nin: ignoreObjects, ..._idExtra } } return { _id: { $nin: ignoreObjects, ..._idExtra } }
case 'fulltext': case 'fulltext':
return { $search: search, _id: { $nin: ignoreObjects, ..._idExtra } } return search !== ''
? { $search: search, _id: { $nin: ignoreObjects, ..._idExtra } }
: { _id: { $nin: ignoreObjects, ..._idExtra } }
case 'spotlight': case 'spotlight':
return extraItems.length > 0 return extraItems.length > 0
? { _id: { $in: extraItems, $nin: ignoreObjects } } ? { _id: { $in: extraItems, $nin: ignoreObjects } }
: { _id: { $nin: ignoreObjects, ..._idExtra } } : { _id: { $nin: ignoreObjects, ..._idExtra } }
default: default:
return { [searchField]: { $like: '%' + search + '%' }, _id: { $nin: ignoreObjects, ..._idExtra } } return search !== ''
? { [searchField]: { $like: '%' + search + '%' }, _id: { $nin: ignoreObjects, ..._idExtra } }
: { _id: { $nin: ignoreObjects, ..._idExtra } }
} }
})() })()
} }

View File

@ -15,7 +15,7 @@
<script lang="ts"> <script lang="ts">
// import { Doc } from '@hcengineering/core' // import { Doc } from '@hcengineering/core'
import type { Blob, Ref } from '@hcengineering/core' import type { Blob, Ref } from '@hcengineering/core'
import { Button, Dialog, Label, Spinner } from '@hcengineering/ui' import { Button, Dialog, EmbeddedPDF, Label, Spinner } from '@hcengineering/ui'
import { createEventDispatcher, onMount } from 'svelte' import { createEventDispatcher, onMount } from 'svelte'
import presentation, { getFileUrl } from '..' import presentation, { getFileUrl } from '..'
import ActionContext from './ActionContext.svelte' import ActionContext from './ActionContext.svelte'
@ -45,22 +45,9 @@
}) })
let download: HTMLAnchorElement let download: HTMLAnchorElement
$: srcRef = file !== undefined ? getFileUrl(file, name) : undefined $: src = file !== undefined ? getFileUrl(file, name) : undefined
$: isImage = contentType !== undefined && contentType.startsWith('image/') $: isImage = contentType !== undefined && contentType.startsWith('image/')
let frame: HTMLIFrameElement | undefined = undefined
// eslint-disable-next-line @typescript-eslint/prefer-optional-chain
$: if (css !== undefined && frame !== undefined && frame !== null) {
frame.onload = () => {
const head = frame?.contentDocument?.querySelector('head')
// eslint-disable-next-line @typescript-eslint/prefer-optional-chain
if (css !== undefined && head !== undefined && head !== null) {
head.appendChild(document.createElement('style')).textContent = css
}
}
}
</script> </script>
<ActionContext context={{ mode: 'browser' }} /> <ActionContext context={{ mode: 'browser' }} />
@ -85,41 +72,37 @@
</svelte:fragment> </svelte:fragment>
<svelte:fragment slot="utils"> <svelte:fragment slot="utils">
{#await srcRef then src} {#if !isLoading && src !== ''}
{#if !isLoading && src !== ''} <a class="no-line" href={src} download={name} bind:this={download}>
<a class="no-line" href={src} download={name} bind:this={download}> <Button
<Button icon={Download}
icon={Download} kind={'ghost'}
kind={'ghost'} on:click={() => {
on:click={() => { download.click()
download.click() }}
}} showTooltip={{ label: presentation.string.Download }}
showTooltip={{ label: presentation.string.Download }} />
/> </a>
</a> {/if}
{/if}
{/await}
</svelte:fragment> </svelte:fragment>
{#await srcRef then src} {#if !isLoading}
{#if !isLoading} {#if src === '' || src === undefined}
{#if src === '' || src === undefined}
<div class="centered">
<Label label={presentation.string.FailedToPreview} />
</div>
{:else if isImage}
<div class="pdfviewer-content img">
<img class="img-fit" {src} alt="" />
</div>
{:else}
<iframe bind:this={frame} class="pdfviewer-content" src={src + '#view=FitH&navpanes=0'} title="" />
{/if}
{:else}
<div class="centered"> <div class="centered">
<Spinner size="medium" /> <Label label={presentation.string.FailedToPreview} />
</div> </div>
{:else if isImage}
<div class="pdfviewer-content img">
<img class="img-fit" {src} alt="" />
</div>
{:else}
<EmbeddedPDF {src} {name} {css} fit />
{/if} {/if}
{/await} {:else}
<div class="centered">
<Spinner size="medium" />
</div>
{/if}
</Dialog> </Dialog>
<style lang="scss"> <style lang="scss">

View File

@ -54,6 +54,7 @@ import core, {
getObjectValue, getObjectValue,
matchQuery, matchQuery,
reduceCalls, reduceCalls,
shouldShowArchived,
toFindResult toFindResult
} from '@hcengineering/core' } from '@hcengineering/core'
import { PlatformError } from '@hcengineering/platform' import { PlatformError } from '@hcengineering/platform'
@ -519,8 +520,7 @@ export class LiveQuery implements WithTx, Client {
if (q.options?.lookup !== undefined) { if (q.options?.lookup !== undefined) {
options.lookup = q.options?.lookup options.lookup = q.options?.lookup
} }
const showArchived: boolean = const showArchived = shouldShowArchived(q.query, q.options)
options?.showArchived ?? (q.query._id !== undefined && typeof q.query._id === 'string')
options.showArchived = showArchived options.showArchived = showArchived
const docIdKey = _id + JSON.stringify(options ?? {}) + q._class const docIdKey = _id + JSON.stringify(options ?? {}) + q._class

View File

@ -0,0 +1,88 @@
<!--
// Copyright © 2025 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 { onDestroy } from 'svelte'
import Loading from './Loading.svelte'
export let src: string
export let name: string
export let fit: boolean = false
export let css: string | undefined = undefined
let iframeSrc: string | undefined = undefined
async function loadFile (src: string): Promise<void> {
if (iframeSrc !== undefined) {
URL.revokeObjectURL(iframeSrc)
iframeSrc = undefined
}
const response = await fetch(src)
const blob = await response.blob()
iframeSrc = URL.createObjectURL(blob)
}
$: void loadFile(src)
let iframe: HTMLIFrameElement | undefined = undefined
// eslint-disable-next-line @typescript-eslint/prefer-optional-chain
$: if (css !== undefined && iframe !== undefined && iframe !== null) {
iframe.onload = () => {
const head = iframe?.contentDocument?.querySelector('head')
// eslint-disable-next-line @typescript-eslint/prefer-optional-chain
if (css !== undefined && head !== undefined && head !== null) {
head.appendChild(document.createElement('style')).textContent = css
}
}
if (iframe.contentDocument !== undefined) {
const style = iframe.contentDocument?.querySelector('head style')
if (style != null) {
style.textContent = css
}
}
}
onDestroy(() => {
if (iframeSrc !== undefined) {
URL.revokeObjectURL(iframeSrc)
}
})
</script>
{#if iframeSrc}
<iframe bind:this={iframe} class:fit src={iframeSrc + '#view=FitH&navpanes=0'} title={name} on:load />
{:else}
<Loading />
{/if}
<style lang="scss">
iframe {
width: 100%;
border: none;
&.fit {
min-height: 100%;
}
&:not(.fit) {
height: 80vh;
min-height: 20rem;
}
}
</style>

View File

@ -280,6 +280,7 @@ export { default as CodeForm } from './components/CodeForm.svelte'
export { default as CodeInput } from './components/CodeInput.svelte' export { default as CodeInput } from './components/CodeInput.svelte'
export { default as TimeLeft } from './components/TimeLeft.svelte' export { default as TimeLeft } from './components/TimeLeft.svelte'
export { default as SectionEmpty } from './components/SectionEmpty.svelte' export { default as SectionEmpty } from './components/SectionEmpty.svelte'
export { default as EmbeddedPDF } from './components/EmbeddedPDF.svelte'
export { default as Dock } from './components/Dock.svelte' export { default as Dock } from './components/Dock.svelte'

View File

@ -86,6 +86,7 @@
bind:viewlet bind:viewlet
bind:preference bind:preference
bind:loading bind:loading
ignoreFragment
viewletQuery={{ viewletQuery={{
attachTo: contact.class.Contact, attachTo: contact.class.Contact,
descriptor: view.viewlet.Table descriptor: view.viewlet.Table

View File

@ -7,7 +7,7 @@
import { type Blob, type Ref } from '@hcengineering/core' import { type Blob, type Ref } from '@hcengineering/core'
import { getMetadata } from '@hcengineering/platform' import { getMetadata } from '@hcengineering/platform'
import presentation, { BlobMetadata, getFileUrl } from '@hcengineering/presentation' import presentation, { BlobMetadata, getFileUrl } from '@hcengineering/presentation'
import { Spinner, themeStore } from '@hcengineering/ui' import { EmbeddedPDF, Spinner, themeStore } from '@hcengineering/ui'
import { convertToHTML } from '@hcengineering/print' import { convertToHTML } from '@hcengineering/print'
export let value: Ref<Blob> export let value: Ref<Blob>
@ -55,7 +55,6 @@
--scrollbar-bar-color: #e0e0e0; --scrollbar-bar-color: #e0e0e0;
--scrollbar-bar-hover: #90959d; --scrollbar-bar-hover: #90959d;
` `
let oldColors = colors
$: css = ` $: css = `
* { * {
@ -262,29 +261,6 @@
border-radius: 2px; border-radius: 2px;
} }
` `
let frame: HTMLIFrameElement | undefined = undefined
// eslint-disable-next-line @typescript-eslint/prefer-optional-chain
$: if (css !== undefined && frame !== undefined && frame !== null) {
frame.onload = () => {
const head = frame?.contentDocument?.querySelector('head')
// eslint-disable-next-line @typescript-eslint/prefer-optional-chain
if (css !== undefined && head !== undefined && head !== null) {
head.appendChild(document.createElement('style')).textContent = css
oldColors = colors
}
}
}
$: if (oldColors !== colors && css !== undefined && frame != null) {
const style = frame?.contentDocument?.querySelector('head style')
if (style != null) {
style.textContent = css
oldColors = colors
}
}
</script> </script>
{#if src} {#if src}
@ -293,7 +269,7 @@
<Spinner size="medium" /> <Spinner size="medium" />
</div> </div>
{:else} {:else}
<iframe bind:this={frame} src={src + '#view=FitH&navpanes=0'} class="w-full h-full" title={name} /> <EmbeddedPDF {src} {name} {css} />
{/if} {/if}
{/if} {/if}

View File

@ -236,6 +236,7 @@
isRegular: true, isRegular: true,
disableLink: true disableLink: true
}} }}
searchField={'code'}
excluded={excludedChangeControl} excluded={excludedChangeControl}
kind={'regular'} kind={'regular'}
size={'small'} size={'small'}

View File

@ -58,6 +58,7 @@
<Table <Table
_class={recruit.class.Applicant} _class={recruit.class.Applicant}
config={preference?.config ?? viewlet.config} config={preference?.config ?? viewlet.config}
options={{ showArchived: true }}
query={{ attachedTo: objectId, ...(viewlet?.baseQuery ?? {}) }} query={{ attachedTo: objectId, ...(viewlet?.baseQuery ?? {}) }}
loadingProps={{ length: applications }} loadingProps={{ length: applications }}
{readonly} {readonly}

View File

@ -35,6 +35,7 @@
_class={recruit.class.Applicant} _class={recruit.class.Applicant}
config={['', '$lookup.space.name', '$lookup.space.company', 'status']} config={['', '$lookup.space.name', '$lookup.space.company', 'status']}
query={{ attachedTo: value._id }} query={{ attachedTo: value._id }}
options={{ showArchived: true }}
loadingProps={{ length: value.applications ?? 0 }} loadingProps={{ length: value.applications ?? 0 }}
/> />
</div> </div>

View File

@ -198,6 +198,7 @@
bind:loading bind:loading
bind:viewlet bind:viewlet
bind:preference bind:preference
ignoreFragment
viewletQuery={{ viewletQuery={{
attachTo: recruit.mixin.VacancyList, attachTo: recruit.mixin.VacancyList,
descriptor: { $in: [view.viewlet.Table, view.viewlet.List] } descriptor: { $in: [view.viewlet.Table, view.viewlet.List] }

View File

@ -146,6 +146,7 @@
bind:loading bind:loading
bind:viewlet bind:viewlet
bind:preference bind:preference
ignoreFragment
viewletQuery={{ viewletQuery={{
attachTo: recruit.class.Vacancy, attachTo: recruit.class.Vacancy,
descriptor: { $in: [view.viewlet.Table, view.viewlet.List] } descriptor: { $in: [view.viewlet.Table, view.viewlet.List] }

View File

@ -15,7 +15,7 @@
<script lang="ts"> <script lang="ts">
import { Doc, Ref } from '@hcengineering/core' import { Doc, Ref } from '@hcengineering/core'
import presentation from '@hcengineering/presentation' import presentation from '@hcengineering/presentation'
import { Button, Icon, IconAdd, Label, showPopup, Scroller } from '@hcengineering/ui' import { Button, Icon, IconAdd, Label, Scroller, showPopup } from '@hcengineering/ui'
import view, { BuildModelKey } from '@hcengineering/view' import view, { BuildModelKey } from '@hcengineering/view'
import { Table } from '@hcengineering/view-resources' import { Table } from '@hcengineering/view-resources'
import recruit from '../plugin' import recruit from '../plugin'
@ -66,6 +66,7 @@
_class={recruit.class.Vacancy} _class={recruit.class.Vacancy}
{config} {config}
query={{ company: objectId }} query={{ company: objectId }}
options={{ showArchived: true }}
{readonly} {readonly}
loadingProps={{ length: vacancies ?? 0 }} loadingProps={{ length: vacancies ?? 0 }}
/> />

View File

@ -1,12 +1,22 @@
<script lang="ts"> <script lang="ts">
import core, { Association, Class, Data, Doc, Ref } from '@hcengineering/core' import core, { Association, Class, Data, Doc, Ref } from '@hcengineering/core'
import { createQuery } from '@hcengineering/presentation' import { createQuery, getClient, MessageBox } from '@hcengineering/presentation'
import { Button, Header, Breadcrumb, Separator, defineSeparators, twoPanelsSeparators } from '@hcengineering/ui' import {
Button,
Header,
Breadcrumb,
Separator,
defineSeparators,
twoPanelsSeparators,
IconDelete,
showPopup
} from '@hcengineering/ui'
import settings from '../plugin' import settings from '../plugin'
import view from '@hcengineering/view' import view from '@hcengineering/view-resources/src/plugin'
import AssociationEditor from './AssociationEditor.svelte' import AssociationEditor from './AssociationEditor.svelte'
const query = createQuery() const query = createQuery()
const client = getClient()
let selected: Association | Data<Association> | undefined let selected: Association | Data<Association> | undefined
@ -26,12 +36,37 @@
} }
} }
function isAssociation (data: Data<Association> | Association | undefined): data is Association {
return (data as Association)?._id !== undefined
}
defineSeparators('workspaceSettings', twoPanelsSeparators) defineSeparators('workspaceSettings', twoPanelsSeparators)
async function remove (val: Association | Data<Association> | undefined): Promise<void> {
if (isAssociation(val)) {
showPopup(MessageBox, {
label: view.string.DeleteObject,
message: view.string.DeleteObjectConfirm,
params: { count: 1 },
dangerous: true,
action: async () => {
selected = undefined
await client.remove(val)
}
})
}
}
</script> </script>
<div class="hulyComponent"> <div class="hulyComponent">
<Header adaptive={'disabled'}> <Header adaptive={'disabled'}>
<Breadcrumb icon={settings.icon.Relations} label={core.string.Relations} size={'large'} isCurrent /> <Breadcrumb icon={settings.icon.Relations} label={core.string.Relations} size={'large'} isCurrent />
<svelte:fragment slot="actions">
{#if isAssociation(selected)}
<Button icon={IconDelete} label={view.string.Delete} kind={'dangerous'} on:click={() => remove(selected)} />
{/if}
</svelte:fragment>
</Header> </Header>
<div class="hulyComponent-content__container columns"> <div class="hulyComponent-content__container columns">

View File

@ -13,7 +13,7 @@
// limitations under the License. // limitations under the License.
--> -->
<script lang="ts"> <script lang="ts">
import { Class, Doc, DocumentQuery, FindOptions, mergeQueries, Ref, Space, WithLookup } from '@hcengineering/core' import { Class, Doc, DocumentQuery, mergeQueries, Ref, Space, WithLookup } from '@hcengineering/core'
import { IntlString } from '@hcengineering/platform' import { IntlString } from '@hcengineering/platform'
import { createQuery } from '@hcengineering/presentation' import { createQuery } from '@hcengineering/presentation'
import { Project, ProjectType, ProjectTypeDescriptor } from '@hcengineering/task' import { Project, ProjectType, ProjectTypeDescriptor } from '@hcengineering/task'
@ -97,6 +97,7 @@
bind:viewlet bind:viewlet
bind:preference bind:preference
bind:viewlets bind:viewlets
ignoreFragment
viewletQuery={{ viewletQuery={{
attachTo: _class, attachTo: _class,
variant: { $exists: false }, variant: { $exists: false },

View File

@ -15,9 +15,9 @@
<script lang="ts"> <script lang="ts">
import { Analytics } from '@hcengineering/analytics' import { Analytics } from '@hcengineering/analytics'
import { AccountRole, Ref, Space, getCurrentAccount, hasAccountRole } from '@hcengineering/core' import { AccountRole, Ref, Space, getCurrentAccount, hasAccountRole } from '@hcengineering/core'
import { MultipleDraftController, getClient } from '@hcengineering/presentation' import { MultipleDraftController, createQuery, getClient } from '@hcengineering/presentation'
import { TrackerEvents } from '@hcengineering/tracker' import { TrackerEvents } from '@hcengineering/tracker'
import { ButtonWithDropdown, IconAdd, IconDropdown, SelectPopupValueType, showPopup } from '@hcengineering/ui' import { Button, ButtonWithDropdown, IconAdd, IconDropdown, SelectPopupValueType, showPopup } from '@hcengineering/ui'
import view from '@hcengineering/view' import view from '@hcengineering/view'
import { onDestroy } from 'svelte' import { onDestroy } from 'svelte'
@ -44,6 +44,14 @@
}) })
} }
const query = createQuery()
let projectExists = false
query.query(tracker.class.Project, {}, (res) => {
projectExists = res.length > 0
})
$: label = draftExists || !closed ? tracker.string.ResumeDraft : tracker.string.NewIssue $: label = draftExists || !closed ? tracker.string.ResumeDraft : tracker.string.NewIssue
$: dropdownItems = hasAccountRole(getCurrentAccount(), AccountRole.User) $: dropdownItems = hasAccountRole(getCurrentAccount(), AccountRole.User)
? [ ? [
@ -82,30 +90,45 @@
</script> </script>
<div class="antiNav-subheader"> <div class="antiNav-subheader">
<ButtonWithDropdown {#if projectExists}
icon={IconAdd} <ButtonWithDropdown
justify={'left'} icon={IconAdd}
kind={'primary'} justify={'left'}
{label} kind={'primary'}
on:click={newIssue} {label}
{dropdownItems} on:click={newIssue}
dropdownIcon={IconDropdown} {dropdownItems}
on:dropdown-selected={(ev) => { dropdownIcon={IconDropdown}
dropdownItemSelected(ev.detail) on:dropdown-selected={(ev) => {
}} dropdownItemSelected(ev.detail)
mainButtonId={'new-issue'} }}
showTooltipMain={{ mainButtonId={'new-issue'}
direction: 'bottom', showTooltipMain={{
label, direction: 'bottom',
keys label,
}} keys
> }}
<div slot="content" class="draft-circle-container"> >
{#if draftExists} <div slot="content" class="draft-circle-container">
<div class="draft-circle" /> {#if draftExists}
{/if} <div class="draft-circle" />
</div> {/if}
</ButtonWithDropdown> </div>
</ButtonWithDropdown>
{:else}
<Button
icon={IconAdd}
justify="left"
kind="primary"
label={tracker.string.CreateProject}
width="100%"
on:click={() => {
showPopup(tracker.component.CreateProject, {}, 'top', () => {
closed = true
})
}}
/>
{/if}
</div> </div>
<style lang="scss"> <style lang="scss">

View File

@ -326,25 +326,14 @@ async function deleteProject (project: Project | undefined): Promise<void> {
}) })
} else { } else {
const anyIssue = await client.findOne(tracker.class.Issue, { space: project._id }) const anyIssue = await client.findOne(tracker.class.Issue, { space: project._id })
if (anyIssue !== undefined) { showPopup(MessageBox, {
showPopup(MessageBox, { label: tracker.string.ArchiveProjectName,
label: tracker.string.ArchiveProjectName, labelProps: { name: project.name },
labelProps: { name: project.name }, message: anyIssue !== undefined ? tracker.string.ProjectHasIssues : tracker.string.ArchiveProjectConfirm,
message: tracker.string.ProjectHasIssues, action: async () => {
action: async () => { await client.update(project, { archived: true })
await client.update(project, { archived: true }) }
} })
})
} else {
showPopup(MessageBox, {
label: tracker.string.ArchiveProjectName,
labelProps: { name: project.name },
message: tracker.string.ArchiveProjectConfirm,
action: async () => {
await client.update(project, { archived: true })
}
})
}
} }
} }
} }

View File

@ -42,11 +42,12 @@
(res) => { (res) => {
configurationRaw = res configurationRaw = res
configurationsLoading = false configurationsLoading = false
loading = configurationsLoading || preferencesLoading
} }
) )
} }
function fetchPreferences (viewlet: Viewlet): void { function fetchPreferences (configurationRaw: Viewlet[]): void {
preferencesLoading = preferenceQuery.query( preferencesLoading = preferenceQuery.query(
view.class.ViewletPreference, view.class.ViewletPreference,
{ {
@ -56,6 +57,7 @@
(res) => { (res) => {
preference = res preference = res
preferencesLoading = false preferencesLoading = false
loading = configurationsLoading || preferencesLoading
} }
) )
} }
@ -81,7 +83,7 @@
} }
$: fetchConfigurations(viewlet) $: fetchConfigurations(viewlet)
$: fetchPreferences(viewlet) $: fetchPreferences(configurationRaw)
$: updateConfiguration(configurationRaw, preference) $: updateConfiguration(configurationRaw, preference)
@ -90,6 +92,8 @@
{#if viewlet?.$lookup?.descriptor?.component} {#if viewlet?.$lookup?.descriptor?.component}
{#if loading} {#if loading}
{configurationsLoading}
{preferencesLoading}
<Loading /> <Loading />
{:else} {:else}
<Component <Component

View File

@ -12,6 +12,7 @@
export let preference: ViewletPreference | undefined = undefined export let preference: ViewletPreference | undefined = undefined
export let loading = true export let loading = true
export let hidden = false export let hidden = false
export let ignoreFragment = false
const query = createQuery() const query = createQuery()
@ -29,11 +30,11 @@
} }
) )
let key = makeViewletKey() let key = makeViewletKey(undefined, ignoreFragment)
onDestroy( onDestroy(
resolvedLocationStore.subscribe((loc) => { resolvedLocationStore.subscribe((loc) => {
key = makeViewletKey(loc) key = makeViewletKey(loc, ignoreFragment)
}) })
) )

View File

@ -15,25 +15,11 @@
<script lang="ts"> <script lang="ts">
import { type Blob, type Ref } from '@hcengineering/core' import { type Blob, type Ref } from '@hcengineering/core'
import { getFileUrl } from '@hcengineering/presentation' import { getFileUrl } from '@hcengineering/presentation'
import { EmbeddedPDF } from '@hcengineering/ui'
export let value: Ref<Blob> export let value: Ref<Blob>
export let name: string export let name: string
export let fit: boolean = false export let fit: boolean = false
</script> </script>
<iframe class:fit src={getFileUrl(value, name) + '#view=FitH&navpanes=0'} title={name} /> <EmbeddedPDF src={getFileUrl(value, name)} {name} {fit} />
<style lang="scss">
iframe {
width: 100%;
border: none;
&.fit {
min-height: 100%;
}
&:not(.fit) {
height: 80vh;
min-height: 20rem;
}
}
</style>

View File

@ -743,11 +743,11 @@ export function categorizeFields (
return result return result
} }
export function makeViewletKey (loc?: Location): string { export function makeViewletKey (loc?: Location, ignoreFragment = false): string {
loc = loc != null ? { path: loc.path, fragment: loc.fragment } : getCurrentResolvedLocation() loc = loc != null ? { path: loc.path, fragment: loc.fragment } : getCurrentResolvedLocation()
loc.query = undefined loc.query = undefined
if (loc.fragment != null && loc.fragment !== '') { if (!ignoreFragment && loc.fragment != null && loc.fragment !== '') {
const props = decodeURIComponent(loc.fragment).split('|') const props = decodeURIComponent(loc.fragment).split('|')
if (props.length >= 3) { if (props.length >= 3) {
const [panel, , _class] = props const [panel, , _class] = props

View File

@ -95,7 +95,7 @@
{#if space} {#if space}
<Header hideActions={createItemDialog === undefined}> <Header hideActions={createItemDialog === undefined}>
<svelte:fragment slot="beforeTitle"> <svelte:fragment slot="beforeTitle">
<ViewletSelector {viewletQuery} bind:viewlet bind:viewlets /> <ViewletSelector {viewletQuery} ignoreFragment bind:viewlet bind:viewlets />
<ViewletSettingButton bind:viewOptions bind:viewlet /> <ViewletSettingButton bind:viewOptions bind:viewlet />
</svelte:fragment> </svelte:fragment>

View File

@ -114,6 +114,7 @@
bind:viewlet bind:viewlet
bind:preference bind:preference
bind:viewlets bind:viewlets
ignoreFragment
viewletQuery={{ viewletQuery={{
attachTo: _class, attachTo: _class,
variant: { $exists: false }, variant: { $exists: false },

View File

@ -758,6 +758,11 @@ export async function backup (
backupInfo.domainHashes = {} backupInfo.domainHashes = {}
} }
if (backupInfo.domainHashes === undefined) {
// Migration
backupInfo.domainHashes = {}
}
let lastTx: Tx | undefined let lastTx: Tx | undefined
let lastTxChecked = false let lastTxChecked = false

View File

@ -43,6 +43,8 @@ import core, {
WorkspaceEvent, WorkspaceEvent,
clone, clone,
generateId, generateId,
shouldShowArchived,
systemAccountEmail,
toFindResult, toFindResult,
type SessionData, type SessionData,
type PersonUuid type PersonUuid
@ -537,7 +539,7 @@ export class SpaceSecurityMiddleware extends BaseMiddleware implements Middlewar
const account = ctx.contextData.account const account = ctx.contextData.account
const isSpace = this.context.hierarchy.isDerived(_class, core.class.Space) const isSpace = this.context.hierarchy.isDerived(_class, core.class.Space)
const field = this.getKey(domain) const field = this.getKey(domain)
const showArchived: boolean = options?.showArchived ?? (query._id !== undefined && typeof query._id === 'string') const showArchived: boolean = shouldShowArchived(query, options)
let clientFilterSpaces: Set<Ref<Space>> | undefined let clientFilterSpaces: Set<Ref<Space>> | undefined

View File

@ -43,6 +43,7 @@ import core, {
type Ref, type Ref,
type ReverseLookups, type ReverseLookups,
type SessionData, type SessionData,
shouldShowArchived,
type SortingQuery, type SortingQuery,
type StorageIterator, type StorageIterator,
systemAccountUuid, systemAccountUuid,
@ -662,7 +663,7 @@ abstract class PostgresAdapterBase implements DbAdapter {
const select = `SELECT ${this.getProjection(vars, domain, options?.projection, joins, options?.associations)} FROM ${domain}` const select = `SELECT ${this.getProjection(vars, domain, options?.projection, joins, options?.associations)} FROM ${domain}`
const showArchived = options?.showArchived ?? (query._id !== undefined && typeof query._id === 'string') const showArchived = shouldShowArchived(query, options)
const secJoin = this.addSecurity(vars, query, showArchived, domain, ctx.contextData) const secJoin = this.addSecurity(vars, query, showArchived, domain, ctx.contextData)
if (secJoin !== undefined) { if (secJoin !== undefined) {
sqlChunks.push(secJoin) sqlChunks.push(secJoin)
@ -683,7 +684,7 @@ abstract class PostgresAdapterBase implements DbAdapter {
let total = options?.total === true ? 0 : -1 let total = options?.total === true ? 0 : -1
if (options?.total === true) { if (options?.total === true) {
const pvars = new ValuesVariables() const pvars = new ValuesVariables()
const showArchived = options?.showArchived ?? (query._id !== undefined && typeof query._id === 'string') const showArchived = shouldShowArchived(query, options)
const secJoin = this.addSecurity(pvars, query, showArchived, domain, ctx.contextData) const secJoin = this.addSecurity(pvars, query, showArchived, domain, ctx.contextData)
const totalChunks: string[] = [] const totalChunks: string[] = []
if (secJoin !== undefined) { if (secJoin !== undefined) {
@ -860,7 +861,12 @@ abstract class PostgresAdapterBase implements DbAdapter {
if (key === 'data') { if (key === 'data') {
obj[p] = { ...obj[p], ...row[column] } obj[p] = { ...obj[p], ...row[column] }
} else { } else {
if (key === 'attachedTo' && row[column] === 'NULL') { if (key === 'createdOn' || key === 'modifiedOn') {
const val = Number.parseInt(row[column])
obj[p][key] = Number.isNaN(val) ? null : val
} else if (key === '%hash%') {
continue
} else if (key === 'attachedTo' && row[column] === 'NULL') {
continue continue
} else { } else {
obj[p][key] = row[column] === 'NULL' ? null : row[column] obj[p][key] = row[column] === 'NULL' ? null : row[column]

View File

@ -216,7 +216,7 @@ export abstract class IssueSyncManagerBase {
const target: IssueSyncTarget | undefined = const target: IssueSyncTarget | undefined =
milestone !== undefined milestone !== undefined
? { ? {
mappings: milestone.mappings, mappings: milestone.mappings ?? [],
project: prj, project: prj,
target: milestone target: milestone
} }
@ -1167,7 +1167,7 @@ export abstract class IssueSyncManagerBase {
} }
return { return {
project, project,
mappings: milestone.mappings, mappings: milestone.mappings ?? [],
target: milestone, target: milestone,
prjData: external.projectItems.nodes.find((it) => it.project.id === milestone.projectNodeId) prjData: external.projectItems.nodes.find((it) => it.project.id === milestone.projectNodeId)
} }
@ -1178,7 +1178,7 @@ export abstract class IssueSyncManagerBase {
getProjectIssueTarget (project: GithubProject, external?: IssueExternalData): IssueSyncTarget { getProjectIssueTarget (project: GithubProject, external?: IssueExternalData): IssueSyncTarget {
return { return {
project, project,
mappings: project.mappings, mappings: project.mappings ?? [],
target: project, target: project,
prjData: external?.projectItems.nodes.find((it) => it.project.id === project.projectNodeId) prjData: external?.projectItems.nodes.find((it) => it.project.id === project.projectNodeId)
} }

View File

@ -610,7 +610,7 @@ export class ProjectsSyncManager implements DocSyncManager {
okit: Octokit okit: Octokit
): Promise<{ projectStructure: GithubProjectV2, wasUpdates: boolean, mappings: GithubFieldMapping[] }> { ): Promise<{ projectStructure: GithubProjectV2, wasUpdates: boolean, mappings: GithubFieldMapping[] }> {
let projectStructure = await this.queryProjectStructure(integration, target) let projectStructure = await this.queryProjectStructure(integration, target)
let mappings = target.mappings let mappings = target.mappings ?? []
if (projectStructure === undefined) { if (projectStructure === undefined) {
if (this.client.getHierarchy().isDerived(tracker.class.Project, target._class)) { if (this.client.getHierarchy().isDerived(tracker.class.Project, target._class)) {
@ -653,7 +653,7 @@ export class ProjectsSyncManager implements DocSyncManager {
const mHash = JSON.stringify(mappings) const mHash = JSON.stringify(mappings)
// Create any platform field into matching github field // Create any platform field into matching github field
for (const [, f] of allFields.entries()) { for (const [, f] of allFields.entries()) {
const existingField = (mappings ?? []).find((it) => it._id === f._id) const existingField = mappings.find((it) => it._id === f._id)
if (f.hidden === true) { if (f.hidden === true) {
continue continue
} }