UBERF-7090: Add QMS common components (#5711)

Signed-off-by: Alexey Zinoviev <alexey.zinoviev@xored.com>
This commit is contained in:
Alexey Zinoviev 2024-06-03 16:41:26 +04:00 committed by GitHub
parent 6fcabb9946
commit f11a7f46c2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
20 changed files with 888 additions and 22 deletions

View File

@ -308,6 +308,27 @@
--theme-clockface-min-arrow: conic-gradient(at 50% -10px, rgba(0, 0, 0, 0), rgba(0, 0, 0, 0) 49%, #2F2F3A 50%, rgba(0, 0, 0, 0) 51%, rgba(0, 0, 0, 0) 100%);
--theme-clockface-arrows-holder: radial-gradient(at top center, #2F2F3A, #555555);
--theme-clockface-arrows-shadow: 0 0 1px white;
--theme-dialog-border-color: rgba(255, 255, 255, 0.1);
--theme-dialog-background-color: #2a2938;
--theme-dialog-back-color: #848484;
--theme-icon-stroke: #e8e9e9;
--theme-state-ghost-color: rgba(123, 123, 123, 0.6);
--theme-state-ghost-background-color: rgba(123, 123, 123, 0.1);
--theme-state-ghost-border-color: transparent;
--theme-state-negative-color: #dc5147;
--theme-state-negative-background-color: rgba(220, 81, 71, 0.1);
--theme-state-negative-border-color: rgba(220, 81, 71, 0.15);
--theme-state-positive-color: #139d4a;
--theme-state-positive-background-color: rgba(19, 157, 74, 0.1);
--theme-state-positive-border-color: rgba(19, 157, 74, 0.15);
--theme-state-primary-color: #3070dc;
--theme-state-primary-background-color: rgba(48, 112, 220, 0.1);
--theme-state-primary-border-color: rgba(48, 112, 220, 0.15);
--theme-state-regular-color: #7b7b7b;
--theme-state-regular-background-color: rgba(123, 123, 123, 0.1);
--theme-state-regular-border-color: rgba(123, 123, 123, 0.15);
--theme-wizard-not-visited-color: #34343c;
}
/* Light Theme */
@ -536,4 +557,25 @@
--theme-clockface-min-arrow: conic-gradient(at 50% -10px, rgba(255, 255, 255, 0), rgba(255, 255, 255, 0) 49%, white 50%, rgba(255, 255, 255, 0) 51%, rgba(255, 255, 255, 0) 100%);
--theme-clockface-arrows-holder: radial-gradient(at top center, #eee, #aaa);
--theme-clockface-arrows-shadow: 0 0 1px black;
--theme-icon-stroke: #1f212b;
--theme-dialog-border-color: rgba(0, 0, 0, 0.1);
--theme-dialog-background-color: #ffffff;
--theme-dialog-back-color: #616161;
--theme-state-ghost-color: rgba(123, 123, 123, 0.6);
--theme-state-ghost-background-color: rgba(123, 123, 123, 0.1);
--theme-state-ghost-border-color: transparent;
--theme-state-negative-color: #dc5147;
--theme-state-negative-background-color: rgba(220, 81, 71, 0.1);
--theme-state-negative-border-color: rgba(220, 81, 71, 0.15);
--theme-state-positive-color: #139d4a;
--theme-state-positive-background-color: rgba(19, 157, 74, 0.1);
--theme-state-positive-border-color: rgba(19, 157, 74, 0.15);
--theme-state-primary-color: #3070dc;
--theme-state-primary-background-color: rgba(48, 112, 220, 0.1);
--theme-state-primary-border-color: rgba(48, 112, 220, 0.15);
--theme-state-regular-color: #7b7b7b;
--theme-state-regular-background-color: rgba(123, 123, 123, 0.1);
--theme-state-regular-border-color: rgba(123, 123, 123, 0.15);
--theme-wizard-not-visited-color: #e8e9e9;
}

View File

@ -86,6 +86,9 @@
"NoTimeZonesFound": "No time zones found",
"Selected": "Selected:",
"Spanish": "Spanish",
"Portuguese": "Portuguese"
"Portuguese": "Portuguese",
"Submit": "Submit",
"NextStep": "Next step",
"TypeHere": "Type here..."
}
}

View File

@ -84,6 +84,9 @@
"ThemeDark": "Oscuro",
"ThemeSystem": "Sistema",
"NoTimeZonesFound": "No se encontraron zonas horarias",
"Selected": "Seleccionado:"
"Selected": "Seleccionado:",
"Submit": "Enviar",
"NextStep": "Siguiente paso",
"TypeHere": "Escribe aquí..."
}
}

View File

@ -84,6 +84,9 @@
"ThemeDark": "Escuro",
"ThemeSystem": "Sistema",
"NoTimeZonesFound": "Nenhum fuso horário encontrado",
"Selected": "Selecionado:"
"Selected": "Selecionado:",
"Submit": "Enviar",
"NextStep": "Seguinte passo",
"TypeHere": "Escreva aqui..."
}
}

View File

@ -84,6 +84,9 @@
"ThemeDark": "Тёмная",
"ThemeSystem": "Системная",
"NoTimeZonesFound": "Временные зоны не найдены",
"Selected": "Выбрано:"
"Selected": "Выбрано:",
"Submit": "Отправить",
"NextStep": "Следующий шаг",
"TypeHere": "Вводите здесь..."
}
}

View File

@ -0,0 +1,257 @@
<!--
// Copyright © 2023 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
-->
<script lang="ts">
// Note: migrated from QMS. Should be unified with the platform solutions eventually.
import { createEventDispatcher } from 'svelte'
import type { IntlString } from '@hcengineering/platform'
import { AnySvelteComponent, ButtonKind } from '../types'
import Button from './Button.svelte'
import Label from './Label.svelte'
import Scroller from './Scroller.svelte'
import { resizeObserver } from '../resize'
import ui from '../plugin'
import Close from './icons/Close.svelte'
import Left from './icons/Left.svelte'
export let label: IntlString
export let labelProps: any | undefined = undefined
export let submitLabel: IntlString = ui.string.Submit
export let submitKind: ButtonKind = 'primary'
export let cancelLabel: IntlString = ui.string.Cancel
export let canSubmit: boolean = false
export let shouldSubmitOnEnter: boolean = false
export let shouldCloseOnCancel: boolean = true
export let hasBack: boolean = false
export let isForm: boolean = true
export let embedded: boolean = false
export let width: string | undefined = undefined
export let isFooterBorderHidden = false
export let loading = false
export let noContentPadding = false
export let scrollableContent = true
export let withoutFooter = false
export let closeIcon: AnySvelteComponent = Close
export let shadow: boolean = false
const dispatch = createEventDispatcher()
function submit (): void {
dispatch('submit')
}
function close (): void {
dispatch('close')
}
function cancel (): void {
dispatch('cancel')
if (shouldCloseOnCancel) {
close()
}
}
</script>
<svelte:element
this={isForm ? 'form' : 'div'}
class="root"
class:shadow
class:embedded
on:submit|preventDefault={isForm && shouldSubmitOnEnter ? submit : undefined}
use:resizeObserver={() => {
dispatch('changeContent')
}}
style:width
>
<div class="header">
<slot name="headerLeft">
<div class="headerLeft">
<div class="flex-row-center flex-gap-2">
{#if hasBack}
<div class="back">
<Button
icon={Left}
iconProps={{ size: 'small' }}
size="small"
kind="ghost"
label={ui.string.Back}
on:click={() => dispatch('back')}
/>
</div>
{/if}
<slot name="headerExtra" />
</div>
<span class="label"><Label {label} params={labelProps} /></span>
</div>
</slot>
<slot name="headerRight">
{#if !embedded}
<Button icon={closeIcon} iconProps={{ size: 'medium' }} kind="ghost" size="small" on:click={close} />
{/if}
</slot>
</div>
{#if scrollableContent}
<Scroller>
<div class="content" class:noPadding={noContentPadding}>
{#if !noContentPadding}
<div class="htPadding" />
{/if}
<slot />
{#if !noContentPadding}
<div class="hbPadding" />
{/if}
</div>
</Scroller>
{:else}
<div class="content" class:noPadding={noContentPadding}>
{#if !noContentPadding}
<div class="htPadding" />
{/if}
<slot />
{#if !noContentPadding}
<div class="hbPadding" />
{/if}
</div>
{/if}
{#if !withoutFooter || $$slots.footerExtra || $$slots.footerButtons || $$slots.btnsXtraStart || $$slots.btnsXtraBetween || $$slots.btnsXtraEnd}
<div class="footer tweak-buttons" class:footerWithBorder={!isFooterBorderHidden}>
<slot name="footerExtra">
<div />
</slot>
<div class="footerButtons">
<slot name="footerButtons">
<slot name="btnsXtraStart" />
<Button kind="regular" size="large" label={cancelLabel} on:click={cancel} {loading} />
<slot name="btnsXtraBetween" />
<Button
kind={submitKind}
size="large"
label={submitLabel}
focusIndex={10001}
disabled={!canSubmit}
on:click={submit}
{loading}
/>
<slot name="btnsXtraEnd" />
</slot>
</div>
</div>
{/if}
</svelte:element>
<style lang="scss">
.root {
display: flex;
flex-direction: column;
align-items: stretch;
width: 58.25rem;
max-height: 80vh;
border-radius: 1.25rem;
background-color: var(--theme-dialog-background-color);
&.embedded {
width: 100%;
height: 100%;
max-height: unset;
border-radius: 0;
}
}
.shadow {
box-shadow: var(--theme-popup-shadow);
}
.header {
flex: 0 0 auto;
display: flex;
flex-wrap: nowrap;
justify-content: space-between;
padding: 1.25rem 2rem 0.875rem 2.5rem;
border-bottom: 1px solid var(--theme-dialog-border-color);
}
.headerLeft {
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.back {
:global(button) {
color: var(--theme-dialog-back-color) !important;
padding-left: 0 !important;
}
}
.label {
font-size: 1.25rem;
color: var(--theme-caption-color);
}
// we need these elements as paddings because Scroller doesn't respect css padding
.htPadding {
width: 100%;
height: 1.25rem;
}
.hbPadding {
width: 100%;
height: 2rem;
}
.content {
display: flex;
flex-direction: column;
align-items: stretch;
flex: 1 1 auto;
min-height: 0;
padding: 0 2.5rem 0 2.5rem;
&.noPadding {
padding: 0;
}
}
.footer {
flex: 0 0 auto;
height: 4.875rem;
padding: 1.25rem 2.5rem;
display: flex;
justify-content: space-between;
align-items: center;
&.footerWithBorder {
border-top: 1px solid var(--theme-dialog-border-color);
}
}
.footerButtons {
display: flex;
flex-wrap: nowrap;
align-items: center;
gap: 0.75rem;
}
// TODO: remove when supported on the platform
.tweak-buttons {
:global(button) {
min-width: 6.25rem !important;
}
}
</style>

View File

@ -0,0 +1,96 @@
<!--
// Copyright © 2020 Anticrm Platform Contributors.
//
// 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 { afterUpdate, onMount } from 'svelte'
import type { IntlString } from '@hcengineering/platform'
import { translate } from '@hcengineering/platform'
import { themeStore } from '@hcengineering/theme'
import { DelayedCaller } from '../utils'
import ui from '../plugin'
export let value: string | undefined = undefined
export let placeholder: IntlString = ui.string.TypeHere
export let placeholderParam: any | undefined = undefined
export let disabled: boolean = false
let input: HTMLTextAreaElement
let phTraslate: string = ''
$: void translate(placeholder, placeholderParam ?? {}, $themeStore.language).then((res) => {
phTraslate = res
})
onMount(() => {
const throttle = new DelayedCaller(50)
const observer = new ResizeObserver(() => {
throttle.call(adjustHeight)
})
observer.observe(input)
return () => {
observer.disconnect()
}
})
afterUpdate(adjustHeight)
function adjustHeight (): void {
input.style.height = 'auto'
input.style.height = `${input.scrollHeight + 2}px`
}
export function focus (): void {
input.focus()
}
</script>
<textarea
class="root"
bind:value
bind:this={input}
{disabled}
placeholder={phTraslate}
on:keydown
on:change
on:keydown
on:keypress
on:blur
/>
<style lang="scss">
.root {
width: 100%;
padding: 0.375rem 0.75rem;
min-height: 3.25rem;
font-family: inherit;
font-size: inherit;
line-height: 1.25rem;
color: var(--theme-text-primary-color);
background-color: var(--theme-button-default);
border: 1px solid var(--theme-refinput-border);
border-radius: 0.375rem;
outline: none;
resize: none;
&:focus {
border-color: var(--primary-button-default);
}
&::placeholder {
color: var(--theme-text-placeholder-color);
}
}
</style>

View File

@ -0,0 +1,79 @@
<!--
//
// Copyright © 2024 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 type { IntlString } from '@hcengineering/platform'
import Label from './Label.svelte'
import { StateType } from '../types'
type LabelString = $$Generic<IntlString>
type LabelParams = LabelString extends IntlString<infer Params> ? Params : Record<string, any>
export let type: StateType
export let label: LabelString
export let params: LabelParams = {} as unknown as LabelParams
</script>
<div
class="root"
class:ghost={type === StateType.Ghost}
class:negative={type === StateType.Negative}
class:positive={type === StateType.Positive}
class:primary={type === StateType.Primary}
class:regular={type === StateType.Regular}
>
<Label {label} {params} />
</div>
<style lang="scss">
.root {
border: 1px solid transparent;
border-radius: 0.25rem;
display: flex;
font-size: 0.75rem;
font-weight: 500;
line-height: 1rem;
padding: 0.125rem 0.5rem;
width: max-content;
&.ghost {
background: var(--theme-state-ghost-background-color);
border-color: var(--theme-state-ghost-border-color);
color: var(--theme-state-ghost-color);
}
&.negative {
background: var(--theme-state-negative-background-color);
border-color: var(--theme-state-negative-border-color);
color: var(--theme-state-negative-color);
}
&.positive {
background: var(--theme-state-positive-background-color);
border-color: var(--theme-state-positive-border-color);
color: var(--theme-state-positive-color);
}
&.primary {
background: var(--theme-state-primary-background-color);
border-color: var(--theme-qms-primary-ghost-border-color);
color: var(--theme-state-primary-color);
}
&.regular {
background: var(--theme-state-regular-background-color);
border-color: var(--theme-state-regular-border-color);
color: var(--theme-state-regular-color);
}
}
</style>

View File

@ -0,0 +1,26 @@
<!--
// Copyright © 2023 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
-->
<script lang="ts">
export let size: 'tiny' | 'small' | 'medium' | 'large'
export let fill = 'currentColor'
</script>
<svg class="svg-{size}" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 12 12" {fill}>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M9.58525 3.53975C9.80492 3.75942 9.80492 4.11558 9.58525 4.33525L5.27275 8.64775C5.05308 8.86742 4.69692 8.86742 4.47725 8.64775L2.41475 6.58525C2.19508 6.36558 2.19508 6.00942 2.41475 5.78975C2.63442 5.57008 2.99058 5.57008 3.21025 5.78975L4.875 7.4545L8.78975 3.53975C9.00942 3.32008 9.36558 3.32008 9.58525 3.53975Z"
/>
</svg>

View File

@ -0,0 +1,10 @@
<script lang="ts">
export let size: 'small' | 'medium' | 'large'
</script>
<svg class="svg-{size}" fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
<path
d="M2.6665 8L2.31295 7.64645L1.9594 8L2.31295 8.35355L2.6665 8ZM12.6665 8.5C12.9426 8.5 13.1665 8.27614 13.1665 8C13.1665 7.72386 12.9426 7.5 12.6665 7.5V8.5ZM6.31295 3.64645L2.31295 7.64645L3.02006 8.35355L7.02006 4.35355L6.31295 3.64645ZM2.31295 8.35355L6.31295 12.3536L7.02006 11.6464L3.02006 7.64645L2.31295 8.35355ZM2.6665 8.5H12.6665V7.5H2.6665V8.5Z"
fill="#616161"
/>
</svg>

View File

@ -0,0 +1,105 @@
<!--
// Copyright © 2023 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
-->
<script lang="ts">
import Label from '../Label.svelte'
import { IWizardStep } from '../../types'
import Checkmark from '../icons/Checkmark.svelte'
export let steps: ReadonlyArray<IWizardStep>
export let selectedStep: string
$: selectedIdx = selectedStep !== '' ? steps.findIndex((s) => s.id === selectedStep) : -1
</script>
<div class="root">
{#each steps as step, idx}
{@const isCurrent = idx === selectedIdx}
{@const isPast = idx < selectedIdx}
{#if idx !== 0}
<div class="flex-col-center" style:grid-column="1" style:grid-row={3 * idx}>
<div class="path" class:highlighted={isPast || isCurrent} />
</div>
{/if}
<div
class="circle"
class:filled={!isPast && !isCurrent}
class:filledHighlighted={isPast}
style:grid-column="1"
style:grid-row={3 * idx + 1}
>
{#if isPast}
<div class="checkmark flex-center"><Checkmark size="tiny" /></div>
{/if}
</div>
<div class="label" class:highlighted={isCurrent || isPast} style:grid-column="2" style:grid-row={3 * idx + 1}>
<div class="overflow-label">{idx + 1}. <Label label={step.title} /></div>
</div>
{#if idx !== steps.length - 1}
<div class="flex-col-center" style:grid-column="1" style:grid-row={3 * idx + 2}>
<div class="path" class:highlighted={isPast || isCurrent} />
</div>
{/if}
{/each}
</div>
<style lang="scss">
.root {
display: grid;
grid-template-columns: 1rem 1fr;
grid-auto-rows: auto;
grid-column-gap: 0.5rem;
width: 100%;
}
.circle {
width: 1rem;
height: 1rem;
border-radius: 50%;
border: 2px solid var(--positive-button-default);
&.filledHighlighted {
background: var(--positive-button-default);
}
&.filled {
border-color: var(--theme-wizard-not-visited-color);
background: var(--theme-wizard-not-visited-color);
}
}
.path {
width: 2px;
height: 0.875rem;
border: 1px solid var(--theme-wizard-not-visited-color);
&.highlighted {
border-color: var(--positive-button-default);
}
}
.label {
font-size: 0.8125rem;
line-height: 1rem;
min-width: 0;
&.highlighted {
font-weight: 500;
}
}
.checkmark {
color: var(--theme-button-contrast-color);
}
</style>

View File

@ -0,0 +1,144 @@
<!--
// Copyright © 2023 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
-->
<script lang="ts">
import { createEventDispatcher } from 'svelte'
import { IntlString } from '@hcengineering/platform'
import Button from '../Button.svelte'
import Scroller from '../Scroller.svelte'
import WizardBar from './ModernWizardBar.svelte'
import ModernDialog from '../ModernDialog.svelte'
import { IWizardStep } from '../../types'
import ui from '../../plugin'
import ArrowLeft from '../icons/ArrowLeft.svelte'
import ArrowRight from '../icons/ArrowRight.svelte'
export let loading: boolean = false
export let label: IntlString
export let canSubmit: boolean = true
export let canProceed: boolean = true
export let submitLabel: IntlString
export let steps: ReadonlyArray<IWizardStep>
export let selectedStep: string
const dispatch = createEventDispatcher()
$: selectedIdx = hasSelectedStep() ? steps.findIndex((s) => s.id === selectedStep) : -1
$: hasBack = selectedIdx > 0
$: hasNext = selectedIdx < steps.length - 1
$: hasSubmit = selectedIdx === steps.length - 1
function hasSelectedStep (): boolean {
return selectedStep !== undefined && selectedStep !== ''
}
function handleBack (): void {
if (!hasSelectedStep()) {
return
}
const currIdx = steps.findIndex((s) => s.id === selectedStep)
const newIdx = currIdx > 0 ? currIdx - 1 : 0
dispatch('stepChanged', steps[newIdx].id)
}
function handleNext (): void {
if (!hasSelectedStep()) {
return
}
const currIdx = steps.findIndex((s) => s.id === selectedStep)
const newIdx = currIdx < steps.length - 1 ? currIdx + 1 : steps.length - 1
dispatch('stepChanged', steps[newIdx].id)
}
function handleSubmit (): void {
dispatch('submit')
}
</script>
<ModernDialog {loading} {label} {canSubmit} noContentPadding={true} scrollableContent={false} on:submit on:close>
<div class="root">
<div class="side">
<Scroller>
<WizardBar {steps} {selectedStep} />
</Scroller>
</div>
<div class="content">
<Scroller>
<div class="contentBody">
<slot />
</div>
</Scroller>
</div>
</div>
<div slot="footerExtra">
{#if hasBack}
<Button kind="regular" size="large" label={ui.string.Back} icon={ArrowLeft} on:click={handleBack} {loading} />
{/if}
</div>
<div slot="footerButtons">
{#if hasSubmit}
<Button
kind="positive"
size="large"
label={submitLabel}
disabled={!canSubmit}
{loading}
on:click={handleSubmit}
/>
{/if}
{#if hasNext}
<Button
kind="primary"
size="large"
label={ui.string.NextStep}
iconRight={ArrowRight}
iconRightProps={{ size: 'small' }}
{loading}
disabled={!canProceed}
on:click={handleNext}
/>
{/if}
</div>
</ModernDialog>
<style lang="scss">
.root {
color: var(--theme-text-primary-color);
width: 100%;
min-height: 0;
display: flex;
align-items: stretch;
}
.side {
flex: 0 0 12rem;
padding: 1.5rem;
min-width: 0;
}
.content {
flex: 1 1 0;
min-width: 0;
}
.contentBody {
padding: 1.5rem;
}
</style>

View File

@ -55,6 +55,7 @@ export { default as Button } from './components/Button.svelte'
export { default as ButtonWithDropdown } from './components/ButtonWithDropdown.svelte'
export { default as ButtonGroup } from './components/ButtonGroup.svelte'
export { default as Status } from './components/Status.svelte'
export { default as StateTag } from './components/StateTag.svelte'
export { default as Component } from './components/Component.svelte'
export { default as Icon } from './components/Icon.svelte'
export { default as ActionIcon } from './components/ActionIcon.svelte'
@ -62,6 +63,7 @@ export { default as Toggle } from './components/Toggle.svelte'
export { default as RadioButton } from './components/RadioButton.svelte'
export { default as RadioGroup } from './components/RadioGroup.svelte'
export { default as Dialog } from './components/Dialog.svelte'
export { default as ModernDialog } from './components/ModernDialog.svelte'
export { default as ModernToggle } from './components/ModernToggle.svelte'
export { default as ToggleWithLabel } from './components/ToggleWithLabel.svelte'
export { default as MiniToggle } from './components/MiniToggle.svelte'
@ -77,6 +79,7 @@ export { default as SelectPopup } from './components/SelectPopup.svelte'
export { default as ColorPopup } from './components/ColorPopup.svelte'
export { default as TextArea } from './components/TextArea.svelte'
export { default as TextAreaEditor } from './components/TextAreaEditor.svelte'
export { default as PlainTextEditor } from './components/PlainTextEditor.svelte'
export { default as Section } from './components/Section.svelte'
export { default as DatePicker } from './components/calendar/DatePicker.svelte'
export { default as DateRangePicker } from './components/calendar/DateRangePicker.svelte'
@ -145,6 +148,8 @@ export { default as AccordionItem } from './components/AccordionItem.svelte'
export { default as NotificationToast } from './components/NotificationToast.svelte'
export { default as Hotkey } from './components/Hotkey.svelte'
export { default as HotkeyGroup } from './components/HotkeyGroup.svelte'
export { default as ModernWizardDialog } from './components/wizard/ModernWizardDialog.svelte'
export { default as ModernWizardBar } from './components/wizard/ModernWizardBar.svelte'
export { default as IconAdd } from './components/icons/Add.svelte'
export { default as IconCircleAdd } from './components/icons/CircleAdd.svelte'
@ -167,6 +172,7 @@ export { default as IconExpand } from './components/icons/Expand.svelte'
export { default as IconActivity } from './components/icons/Activity.svelte'
export { default as IconUp } from './components/icons/Up.svelte'
export { default as IconDown } from './components/icons/Down.svelte'
export { default as IconLeft } from './components/icons/Left.svelte'
export { default as IconUpOutline } from './components/icons/UpOutline.svelte'
export { default as IconDownOutline } from './components/icons/DownOutline.svelte'
export { default as IconDropdown } from './components/icons/Dropdown.svelte'
@ -220,6 +226,7 @@ export { default as IconDropdownRight } from './components/icons/DropdownRight.s
export { default as IconKeyCommand } from './components/icons/KeyCommand.svelte'
export { default as IconKeyOption } from './components/icons/KeyOption.svelte'
export { default as IconKeyShift } from './components/icons/KeyShift.svelte'
export { default as IconCheckmark } from './components/icons/Checkmark.svelte'
export { default as PanelInstance } from './components/PanelInstance.svelte'
export { default as Panel } from './components/Panel.svelte'

View File

@ -110,7 +110,10 @@ export const uis = plugin(uiId, {
ThemeDark: '' as IntlString,
ThemeSystem: '' as IntlString,
NoTimeZonesFound: '' as IntlString,
Selected: '' as IntlString
Selected: '' as IntlString,
Submit: '' as IntlString,
NextStep: '' as IntlString,
TypeHere: '' as IntlString
},
metadata: {
DefaultApplication: '' as Metadata<AnyComponent>,

View File

@ -512,3 +512,16 @@ export type MouseTargetEvent = MouseEvent & { currentTarget: EventTarget & HTMLE
export interface ScrollParams {
autoScrolling: boolean
}
export interface IWizardStep<T = string> {
id: T
title: IntlString
}
export enum StateType {
Ghost,
Negative,
Positive,
Primary,
Regular
}

View File

@ -13,20 +13,21 @@
// limitations under the License.
-->
<script lang="ts">
import { createEventDispatcher } from 'svelte'
import contact, { Employee } from '@hcengineering/contact'
import type { Class, Doc, DocumentQuery, IdMap, Ref } from '@hcengineering/core'
import type { IntlString } from '@hcengineering/platform'
import { Label, showPopup, ActionIcon, IconClose, IconAdd, Icon } from '@hcengineering/ui'
import type { IconSize } from '@hcengineering/ui'
import { createEventDispatcher } from 'svelte'
import { getClient } from '@hcengineering/presentation'
import plugin from '../plugin'
import { employeeByIdStore } from '../utils'
import UserInfo from './UserInfo.svelte'
import UsersPopup from './UsersPopup.svelte'
import { getClient } from '@hcengineering/presentation'
export let items: Ref<Employee>[] = []
export let readonlyItems: Ref<Employee>[] = []
export let readonlyItems = new Set<Ref<Employee>>()
export let _class: Ref<Class<Employee>> = contact.mixin.Employee
export let docQuery: DocumentQuery<Employee> | undefined = {}
@ -36,10 +37,10 @@
export let width: string | undefined = undefined
export let readonly: boolean = false
let persons: Employee[] = getPersons(items, $employeeByIdStore)
$: persons = getPersons(items, $employeeByIdStore)
let readonlyPersons: Employee[] = getPersons(readonlyItems, $employeeByIdStore)
$: readonlyPersons = getPersons(readonlyItems, $employeeByIdStore)
$: fixedItems = readonly ? items : items.filter((item) => readonlyItems.has(item))
$: editableItems = readonly ? [] : items.filter((item) => !readonlyItems.has(item))
$: fixedPersons = getPersons(fixedItems, $employeeByIdStore)
$: editablePersons = getPersons(editableItems, $employeeByIdStore)
const client = getClient()
@ -55,7 +56,7 @@
multiSelect: true,
allowDeselect: false,
selectedUsers: items,
ignoreUsers: readonlyItems,
ignoreUsers: fixedItems,
readonly,
filter: (it: Doc) => {
if (client.getHierarchy().hasMixin(it, contact.mixin.Employee)) {
@ -68,18 +69,17 @@
undefined,
(result) => {
if (result != null) {
items = result
dispatch('update', items)
dispatch('update', fixedItems.concat(result))
}
}
)
}
function getPersons (employees: Ref<Employee>[], employeeById: IdMap<Employee>) {
function getPersons (employees: Ref<Employee>[], employeeById: IdMap<Employee>): Employee[] {
return employees.map((p) => employeeById.get(p)).filter((p) => p !== undefined) as Employee[]
}
const removePerson = (removed: Employee) => {
function removePerson (removed: Employee): void {
const newItems = items.filter((it) => it !== removed._id)
dispatch('update', newItems)
}
@ -87,12 +87,12 @@
<div class="flex-col" style:width={width ?? 'auto'}>
<div class="flex-row-center flex-wrap">
{#each readonlyPersons as person}
{#each fixedPersons as person}
<div class="usertag-container gap-1-5">
<UserInfo value={person} {size} />
</div>
{/each}
{#each persons as person}
{#each editablePersons as person}
<div class="usertag-container gap-1-5">
<UserInfo value={person} {size} />
<ActionIcon
@ -110,7 +110,7 @@
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div
class="addButton {size === 'inline' ? 'small' : 'medium'} overflow-label gap-2 cursor-pointer"
class:mt-2={persons.length > 0}
class:mt-2={editablePersons.length > 0}
on:click={addPerson}
>
<span><Label label={actionLabel} /></span>

View File

@ -22,7 +22,9 @@
let request: Request | undefined = undefined
const query = createQuery()
query.query(value._class, { _id: value._id }, (res) => ([request] = res))
query.query(value._class, { _id: value._id }, (res) => {
;[request] = res
})
</script>
{#if request}

View File

@ -21,6 +21,9 @@ import RequestPresenter from './components/RequestPresenter.svelte'
import RequestView from './components/RequestView.svelte'
import NotificationRequestView from './components/NotificationRequestView.svelte'
export { default as RequestStatusPresenter } from './components/RequestStatusPresenter.svelte'
export { default as RequestDetailPopup } from './components/RequestDetailPopup.svelte'
export default async (): Promise<Resources> => ({
activity: {
RequestLabel,

View File

@ -0,0 +1,65 @@
<!--
//
// Copyright © 2023 Hardcore Engineering Inc.
//
-->
<script lang="ts">
import { Class, Doc, DocumentQuery, Ref, Space, WithLookup } from '@hcengineering/core'
import { IntlString, translate } from '@hcengineering/platform'
import { IModeSelector, SearchEdit, ModeSelector, themeStore } from '@hcengineering/ui'
import { ViewOptions, Viewlet, ViewletPreference } from '@hcengineering/view'
import FilterBar from './filter/FilterBar.svelte'
import FilterButton from './filter/FilterButton.svelte'
import ViewletSelector from './ViewletSelector.svelte'
import ViewletSettingButton from './ViewletSettingButton.svelte'
export let viewletQuery: DocumentQuery<Viewlet>
export let viewlet: WithLookup<Viewlet> | undefined
export let viewOptions: ViewOptions | undefined
export let preference: ViewletPreference | undefined
export let loading = true
export let _class: Ref<Class<Doc>>
export let title: IntlString
export let search: string = ''
export let query: DocumentQuery<Doc>
export let modeSelectorProps: IModeSelector | undefined = undefined
export let space: Ref<Space> | undefined = undefined
export let resultQuery: DocumentQuery<Doc>
let label = ''
let searchQuery: DocumentQuery<Doc> = { ...query }
resultQuery = search === '' ? { ...query } : { ...query, $search: search }
$: if (!label && title) {
translate(title, {}, $themeStore.language).then((res) => {
label = res
})
}
$: searchQuery = search === '' ? { ...query } : { ...query, $search: search }
</script>
<div class="ac-header full divide caption-height" class:header-with-mode-selector={modeSelectorProps !== undefined}>
<div class="ac-header__wrap-title">
<span class="ac-header__title">{label}</span>
{#if modeSelectorProps !== undefined}
<ModeSelector props={modeSelectorProps} />
{/if}
</div>
<div class="clear-mins">
<slot name="header-tools" />
</div>
</div>
<div class="ac-header full divide search-start">
<div class="ac-header-full small-gap">
<SearchEdit bind:value={search} />
<div class="buttons-divider" />
<FilterButton {_class} />
</div>
<div class="ac-header-full medium-gap">
<ViewletSelector bind:viewlet bind:preference bind:loading {viewletQuery} />
<ViewletSettingButton bind:viewlet bind:viewOptions />
<slot name="extra" />
</div>
</div>
<FilterBar {_class} query={searchQuery} {space} {viewOptions} on:change={(e) => (resultQuery = { ...e.detail })} />

View File

@ -71,6 +71,7 @@ import UpDownNavigator from './components/UpDownNavigator.svelte'
import ValueSelector from './components/ValueSelector.svelte'
import ViewletContentView from './components/ViewletContentView.svelte'
import ViewletSettingButton from './components/ViewletSettingButton.svelte'
import ViewletPanelHeader from './components/ViewletPanelHeader.svelte'
import ArrayFilter from './components/filter/ArrayFilter.svelte'
import DateFilter from './components/filter/DateFilter.svelte'
import DateFilterPresenter from './components/filter/DateFilterPresenter.svelte'
@ -220,7 +221,8 @@ export {
TreeNode,
UpDownNavigator,
ViewletContentView,
ViewletSettingButton
ViewletSettingButton,
ViewletPanelHeader
}
function PositionElementAlignment (e?: Event): PopupAlignment | undefined {