UBERF-7090: Add QMS plugins (#5716)

Signed-off-by: Alexey Zinoviev <alexey.zinoviev@xored.com>
This commit is contained in:
Alexey Zinoviev
2024-06-03 19:55:54 +04:00
committed by GitHub
parent f11a7f46c2
commit 48e1ca9849
618 changed files with 40225 additions and 7 deletions
@@ -0,0 +1,225 @@
<!--
// 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, onDestroy } from 'svelte'
import { Employee, PersonAccount } from '@hcengineering/contact'
import {
generateId,
getCurrentAccount,
type AttachedData,
type Class,
type Data,
type Ref,
getCollaborativeDoc
} from '@hcengineering/core'
import { MessageBox, getClient } from '@hcengineering/presentation'
import {
AnySvelteComponent,
addNotification,
navigate,
showPopup,
NotificationSeverity,
ModernWizardDialog,
type IWizardStep
} from '@hcengineering/ui'
import { getCurrentLanguage } from '@hcengineering/theme'
import { translate } from '@hcengineering/platform'
import {
type ChangeControl,
type ControlledDocument,
type DocumentCategory,
type DocumentTemplate,
DocumentState,
createChangeControl,
createControlledDocFromTemplate,
DEFAULT_PERIODIC_REVIEW_INTERVAL
} from '@hcengineering/controlled-documents'
import documents from '../../plugin'
import { getProjectDocumentLink } from '../../navigation'
import InfoStep from './steps/InfoStep.svelte'
import LocationStep from './steps/LocationStep.svelte'
import TeamStep from './steps/TeamStep.svelte'
import TemplateStep from './steps/TemplateStep.svelte'
import {
type DocumentWizardStep,
$currentStep as currentStep,
$locationStep as locationStep,
wizardClosed,
currentStepUpdated
} from '../../stores/wizards/create-document'
import FailedToCreateDocument from '../FailedToCreateDocument.svelte'
export let _class: Ref<Class<ControlledDocument>> = documents.class.ControlledDocument
onDestroy(wizardClosed)
const dispatch = createEventDispatcher()
const client = getClient()
const currentUser = getCurrentAccount() as PersonAccount
const steps: IWizardStep<DocumentWizardStep>[] = [
{
id: 'location',
title: documents.string.LocationStepTitle
},
{
id: 'template',
title: documents.string.TemplateStepTitle
},
{
id: 'info',
title: documents.string.InfoStepTitle
},
{
id: 'team',
title: documents.string.TeamStepTitle
}
]
// eslint-disable-next-line no-unused-vars
const stepComponents: { [key in DocumentWizardStep]: AnySvelteComponent } = {
location: LocationStep,
template: TemplateStep,
info: InfoStep,
team: TeamStep
}
const ccRecordId = generateId<ChangeControl>()
const ccRecord: Data<ChangeControl> = {
description: '',
reason: '',
impact: '',
impactedDocuments: []
}
const docObject: AttachedData<ControlledDocument> = {
template: '' as Ref<DocumentTemplate>,
prefix: '',
code: '',
title: '',
labels: 0,
major: 0,
minor: 1,
seqNumber: 0,
commentSequence: 0,
category: '' as Ref<DocumentCategory>,
abstract: '',
author: currentUser.person as Ref<Employee>,
owner: currentUser.person as Ref<Employee>,
state: DocumentState.Draft,
sections: 0,
snapshots: 0,
changeControl: ccRecordId,
content: getCollaborativeDoc(generateId()),
requests: 0,
reviewers: [],
approvers: [],
coAuthors: [],
plannedEffectiveDate: 0,
reviewInterval: DEFAULT_PERIODIC_REVIEW_INTERVAL
}
let canProceed: false // Note: determined by individual steps
const isLoading = false
function getStepComponent (step: DocumentWizardStep): AnySvelteComponent {
return stepComponents[step]
}
function handleStepChanged (e: CustomEvent): void {
currentStepUpdated(e.detail)
}
$: space = $locationStep.space
async function handleSubmit (): Promise<void> {
if ($locationStep.space === undefined || $locationStep.project === undefined) {
return
}
const newDocId = generateId<ControlledDocument>()
const _space = $locationStep.space
const { success } = await createControlledDocFromTemplate(
client,
docObject.template,
newDocId,
docObject,
_space,
$locationStep.project,
$locationStep.parent
)
if (!success) {
addNotification(
await translate(documents.string.CreateDocumentFailed, {}, getCurrentLanguage()),
'',
FailedToCreateDocument,
undefined,
NotificationSeverity.Error
)
dispatch('close')
return
}
await createChangeControl(client, ccRecordId, ccRecord, _space)
const loc = getProjectDocumentLink(newDocId, $locationStep.project)
navigate(loc)
dispatch('close')
}
async function handleClose (): Promise<void> {
showPopup(
MessageBox,
{
label: documents.string.NewDocumentDialogClose,
message: documents.string.NewDocumentCloseNote
},
'top',
(result?: boolean) => {
if (result === true) {
dispatch('close')
}
}
)
}
</script>
<ModernWizardDialog
loading={isLoading}
label={documents.string.NewDocument}
submitLabel={documents.string.CreateDraft}
{canProceed}
{steps}
selectedStep={$currentStep}
on:stepChanged={handleStepChanged}
on:submit={handleSubmit}
on:close={handleClose}
>
<div class="root">
<svelte:component this={getStepComponent($currentStep)} bind:canProceed {docObject} {ccRecord} {space} />
</div>
</ModernWizardDialog>
<style lang="scss">
.root {
height: 29.25rem;
}
</style>
@@ -0,0 +1,242 @@
<!--
// 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, onDestroy } from 'svelte'
import {
ChangeControl,
ControlledDocument,
DEFAULT_PERIODIC_REVIEW_INTERVAL,
DEFAULT_SECTION_TITLE,
DocumentState,
DocumentTemplate,
TEMPLATE_PREFIX,
createChangeControl,
createDocumentTemplate
} from '@hcengineering/controlled-documents'
import { Employee, PersonAccount } from '@hcengineering/contact'
import {
type AttachedData,
type Class,
type Data,
type Ref,
type Mixin,
generateId,
getCollaborativeDoc,
getCurrentAccount
} from '@hcengineering/core'
import { MessageBox, getClient } from '@hcengineering/presentation'
import {
AnySvelteComponent,
addNotification,
navigate,
showPopup,
NotificationSeverity,
ModernWizardDialog,
type IWizardStep
} from '@hcengineering/ui'
import { getCurrentLanguage } from '@hcengineering/theme'
import { translate } from '@hcengineering/platform'
import documents from '../../plugin'
import { getProjectDocumentLink } from '../../navigation'
import InfoStep from './steps/InfoStep.svelte'
import LocationStep from './steps/LocationStep.svelte'
import TeamStep from './steps/TeamStep.svelte'
import {
TemplateWizardStep,
$currentStep as currentStep,
$locationStep as locationStep,
currentStepUpdated,
wizardClosed
} from '../../stores/wizards/create-document'
import FailedToCreateDocument from '../FailedToCreateDocument.svelte'
export let _class: Ref<Class<ControlledDocument>> = documents.class.ControlledDocument
export let _templateMixin: Ref<Mixin<DocumentTemplate>> = documents.mixin.DocumentTemplate
onDestroy(wizardClosed)
const dispatch = createEventDispatcher()
const client = getClient()
const currentUser = getCurrentAccount() as PersonAccount
const steps: IWizardStep<TemplateWizardStep>[] = [
{
id: 'location',
title: documents.string.LocationStepTitle
},
{
id: 'info',
title: documents.string.InfoStepTitle
},
{
id: 'team',
title: documents.string.TeamStepTitle
}
]
// eslint-disable-next-line no-unused-vars
const stepComponents: { [key in TemplateWizardStep]: AnySvelteComponent } = {
location: LocationStep,
info: InfoStep,
team: TeamStep
}
const ccRecordId = generateId<ChangeControl>()
const ccRecord: Data<ChangeControl> = {
description: '',
reason: '',
impact: '',
impactedDocuments: []
}
const docObject: AttachedData<ControlledDocument> & Pick<DocumentTemplate, 'docPrefix'> = {
prefix: TEMPLATE_PREFIX,
title: '',
code: '',
docPrefix: '',
labels: 0,
major: 0,
minor: 1,
commentSequence: 0,
seqNumber: 0,
category: undefined,
abstract: '',
author: currentUser.person as Ref<Employee>,
owner: currentUser.person as Ref<Employee>,
state: DocumentState.Draft,
sections: 0,
snapshots: 0,
changeControl: ccRecordId,
content: getCollaborativeDoc(generateId()),
requests: 0,
reviewers: [],
approvers: [],
coAuthors: [],
plannedEffectiveDate: 0,
reviewInterval: DEFAULT_PERIODIC_REVIEW_INTERVAL
}
let canProceed: false // Note: determined by individual steps
const isLoading = false
function getCurrentStepComponent (currentStep: TemplateWizardStep): AnySvelteComponent {
return stepComponents[currentStep]
}
function handleStepChanged (e: CustomEvent): void {
currentStepUpdated(e.detail)
}
async function handleSubmit (): Promise<void> {
if ($locationStep.space === undefined || $locationStep.project === undefined) {
return
}
const { category } = docObject
if (category === undefined || category === null) return
const newDocId = generateId<ControlledDocument>()
const space = $locationStep.space
const spec = { ...docObject }
delete (spec as any).docPrefix
const { success } = await createDocumentTemplate(
client,
_class,
space,
_templateMixin,
$locationStep.project,
$locationStep.parent,
newDocId,
docObject.docPrefix,
spec,
category,
currentUser.person as Ref<Employee>,
{ title: DEFAULT_SECTION_TITLE }
)
if (!success) {
addNotification(
await translate(documents.string.CreateDocumentTemplateFailed, {}, getCurrentLanguage()),
'',
FailedToCreateDocument,
undefined,
NotificationSeverity.Error
)
dispatch('close')
return
}
await createChangeControl(client, ccRecordId, ccRecord, space)
const loc = getProjectDocumentLink(newDocId, $locationStep.project)
navigate(loc)
dispatch('close')
}
$: space = $locationStep.space
async function handleClose (): Promise<void> {
showPopup(
MessageBox,
{
label: documents.string.NewDocumentDialogClose,
message: documents.string.NewDocumentCloseNote
},
'top',
(result?: boolean) => {
if (result === true) {
dispatch('close')
}
}
)
}
$: currentTemplateStep = $currentStep as TemplateWizardStep
</script>
<ModernWizardDialog
loading={isLoading}
label={documents.string.NewDocumentTemplate}
submitLabel={documents.string.CreateDraft}
{canProceed}
{steps}
selectedStep={currentTemplateStep}
on:stepChanged={handleStepChanged}
on:submit={handleSubmit}
on:close={handleClose}
>
<div class="root">
<svelte:component
this={getCurrentStepComponent(currentTemplateStep)}
bind:canProceed
{docObject}
{ccRecord}
{space}
isTemplate
/>
</div>
</ModernWizardDialog>
<style lang="scss">
.root {
height: 29.25rem;
}
</style>
@@ -0,0 +1,336 @@
<!--
// 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 { DropdownLabels, EditBox, Label, RadioGroup } from '@hcengineering/ui'
import { type AttachedData, type Data } from '@hcengineering/core'
import { translate } from '@hcengineering/platform'
import { createQuery, getClient } from '@hcengineering/presentation'
import {
type Document,
type ChangeControl,
type ControlledDocument,
type DocumentCategory,
type DocumentTemplate,
getDocumentId,
TEMPLATE_PREFIX
} from '@hcengineering/controlled-documents'
import IconWarning from '../../icons/IconWarning.svelte'
import { $infoStep as infoStep, infoStepUpdated } from '../../../stores/wizards/create-document'
import documents from '../../../plugin'
export let docObject: AttachedData<ControlledDocument> & Partial<Pick<DocumentTemplate, 'docPrefix'>>
export let ccRecord: Data<ChangeControl> | undefined = undefined
export let canProceed: boolean
export let isTemplate: boolean = false
const client = getClient()
const newDocCreationIntlString = isTemplate ? documents.string.NewTemplateCreation : documents.string.NewDocCreation
let newDocCreationText = ''
translate(newDocCreationIntlString, {}).then(
(r) => {
newDocCreationText = r
},
(err) => {
console.warn(`Cannot load translation for: ${newDocCreationIntlString}. Error: ${err})`)
}
)
let templatesCodeTitle = ''
translate(documents.string.SysTemplate, {}).then(
(res) => {
templatesCodeTitle = res
templateDocPrefixes[TEMPLATE_PREFIX] = templatesCodeTitle
},
(err) => {
console.warn(`Cannot load translation for: ${documents.string.SysTemplate}. Error: ${err})`)
}
)
const templateDocPrefixesQuery = createQuery()
let templateDocPrefixes: Record<string, string> = {}
templateDocPrefixesQuery.query(
documents.mixin.DocumentTemplate,
{},
(res) => {
templateDocPrefixes = { [TEMPLATE_PREFIX]: templatesCodeTitle }
for (const t of res) {
templateDocPrefixes[t.docPrefix] = t.title
}
},
{
projection: { docPrefix: 1, title: 1 }
}
)
const docCodesQuery = createQuery()
let docCodes: Record<string, string> = {}
let loadingCodes = true
docCodesQuery.query(
documents.class.Document,
{},
(res) => {
docCodes = {}
for (const doc of res) {
if (doc.code === '' || doc.code === undefined) {
continue
}
docCodes[doc.code] = doc.title
}
loadingCodes = false
},
{
projection: { code: 1, title: 1 }
}
)
let customReason = $infoStep.customReason
let abstract: string = docObject?.abstract ?? ''
$: if (docObject !== undefined) {
docObject.abstract = abstract
}
$: if (ccRecord !== undefined && $infoStep.selectedReason === 'newDoc') {
ccRecord.reason = newDocCreationText
}
$: if (ccRecord !== undefined && $infoStep.selectedReason === 'custom') {
ccRecord.reason = customReason
}
$: isCategoryFilled = isTemplate ? docObject.category !== undefined : true
$: isCodeFilled = isTemplate ? docObject.prefix !== '' && docObject.prefix !== undefined : true
$: prefixNotUnique = docObject.docPrefix != null && templateDocPrefixes[docObject.docPrefix] !== undefined
$: codeNotUnique = docObject.code != null && docObject.code !== '' && !isCodeUnique(docObject.code)
$: canProceed =
!!docObject.code &&
!!docObject.title &&
!!ccRecord?.reason &&
isCodeFilled &&
isCategoryFilled &&
!prefixNotUnique &&
!codeNotUnique &&
(!isTemplate || !!docObject.docPrefix)
$: if (docObject !== undefined && docObject.code === '' && !loadingCodes) {
void setInitialCode()
}
async function setInitialCode (): Promise<void> {
let newCodeObj: Pick<Document, 'prefix' | 'seqNumber'> | undefined = undefined
if (isTemplate) {
const seqObj = await client.findOne(documents.class.Sequence, { _id: documents.sequence.Templates })
if (seqObj == null) {
return
}
newCodeObj = { prefix: docObject.prefix, seqNumber: seqObj.sequence + 1 }
} else {
newCodeObj = docObject
}
if (newCodeObj != null) {
let newCode: string
newCode = getDocumentId(newCodeObj)
while (!isCodeUnique(newCode)) {
newCodeObj.seqNumber++
newCode = getDocumentId(newCodeObj)
}
docObject.code = newCode
}
}
function isCodeUnique (code: string): boolean {
return code !== '' && docCodes[code] === undefined
}
const radioItems = [
{
id: 'newDoc',
value: 'newDoc',
labelIntl: isTemplate ? documents.string.NewTemplateCreation : documents.string.NewDocCreation,
labelSize: 'large' as const,
labelGap: 'large' as const,
action: handleSelectedReasonChanged.bind(null, 'newDoc')
},
{
id: 'custom',
value: 'custom',
labelIntl: documents.string.CustomReason,
labelSize: 'large' as const,
labelGap: 'large' as const,
action: handleSelectedReasonChanged.bind(null, 'custom')
}
]
function handleSelectedReasonChanged (selected: 'newDoc' | 'custom'): void {
infoStepUpdated({ ...$infoStep, selectedReason: selected })
}
function handleReasonUpdated (value: string): void {
infoStepUpdated({ ...$infoStep, customReason: value })
}
let categories: DocumentCategory[] = []
$: if (isTemplate) {
void client.findAll(documents.class.DocumentCategory, {}).then((cats) => {
categories = cats
})
}
</script>
{#if ccRecord !== undefined}
<div class="root">
<div class="sectionTitle">
<Label label={isTemplate ? documents.string.TemplateCode : documents.string.DocumentCode} />
</div>
<div class="sectionContent">
<EditBox
placeholder={documents.string.DocumentCodePlaceholder}
disabled={loadingCodes}
bind:value={docObject.code}
kind="large-style"
/>
{#if codeNotUnique}
<div class="error">
<IconWarning size="small" />
<Label label={documents.string.CodeInUse} />
<span class="name">{docCodes[docObject.code]}</span>
</div>
{/if}
</div>
<div class="sectionTitle"><Label label={documents.string.TitleAndDescr} /></div>
<div class="sectionContent">
<EditBox
autoFocus
bind:value={docObject.title}
id="doc-title"
placeholder={isTemplate ? documents.string.NewDocumentTemplate : documents.string.NewDocument}
kind="large-style"
fullSize
on:input={() => {
if (isTemplate) {
docObject.docPrefix =
docObject.title
.split(' ')
.map((it) => it[0]?.toUpperCase())
.filter((it) => it)
.join('') ?? ''
}
}}
/>
</div>
<div>
<EditBox
bind:value={abstract}
id="doc-description"
placeholder={isTemplate ? documents.string.NewTemplatePlaceholder : documents.string.AbstractPlaceholder}
/>
</div>
{#if isTemplate}
<div class="sectionTitle"><Label label={documents.string.DocumentPrefix} /></div>
<div class="sectionContent">
<EditBox
placeholder={documents.string.DocumentPrefixPlaceholder}
bind:value={docObject.docPrefix}
kind="large-style"
/>
{#if prefixNotUnique && docObject.docPrefix !== undefined}
<div class="error">
<IconWarning size="small" />
<Label label={documents.string.PrefixInUse} />
<span class="name">{templateDocPrefixes[docObject.docPrefix]}</span>
</div>
{/if}
</div>
<div class="sectionTitle"><Label label={documents.string.Category} /></div>
<div class="sectionContent">
<DropdownLabels
label={documents.string.Category}
items={categories.map((cat) => ({ id: cat._id, label: cat.title }))}
showDropdownIcon
bind:selected={docObject.category}
/>
</div>
{/if}
<div class="sectionTitle"><Label label={documents.string.Reason} /></div>
<div class="radio">
<RadioGroup items={radioItems} selected={$infoStep.selectedReason} gap="large" />
</div>
{#if $infoStep.selectedReason === 'custom'}
<div class="mt-4">
<EditBox
bind:value={customReason}
id="doc-reason"
placeholder={documents.string.ReasonPlaceholder}
kind="editbox"
fullSize
on:blur={(e) => {
handleReasonUpdated(e.detail)
}}
/>
</div>
{/if}
</div>
{/if}
<style lang="scss">
.root {
height: 100%;
width: 100%;
display: flex;
flex-direction: column;
}
.sectionTitle {
font-size: 0.875rem;
font-weight: 500;
line-height: 1.25rem;
&:not(:first-child) {
margin-top: 2rem;
}
}
.sectionContent {
padding: 1rem 0 0.5rem 0;
}
.radio {
margin-top: 1rem;
font-weight: 500;
font-size: 0.875rem;
line-height: 1.25rem;
overflow-x: visible;
}
.error {
display: flex;
gap: 0.25rem;
margin-top: 0.375rem;
color: var(--negative-button-default);
font-size: 0.75rem;
line-height: 1rem;
}
.name {
font-weight: 500;
}
</style>
@@ -0,0 +1,174 @@
<!--
// Copyright © 2023-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 Doc, type Ref, type Space } from '@hcengineering/core'
import documents, {
type DocumentSpace,
type DocumentSpaceType,
type Project,
type ProjectDocument
} from '@hcengineering/controlled-documents'
import { SpaceSelector, getClient } from '@hcengineering/presentation'
import { Label } from '@hcengineering/ui'
import { permissionsStore } from '@hcengineering/view-resources'
import { $locationStep as locationStep, locationStepUpdated } from '../../../stores/wizards/create-document'
import DocumentParentSelector from '../../hierarchy/DocumentParentSelector.svelte'
import ProjectSelector from '../../project/ProjectSelector.svelte'
import documentsRes from '../../../plugin'
import { getLatestProjectId } from '../../../utils'
export let canProceed: boolean
export let isTemplate: boolean = false
const client = getClient()
const hierarchy = client.getHierarchy()
let spaceRef: Ref<DocumentSpace> | undefined = $locationStep.space
$: locationStepUpdated({ space: spaceRef })
let space: DocumentSpace | undefined
$: void fetchSpace($locationStep.space)
let spaceType: DocumentSpaceType | undefined
$: void fetchSpaceType(space?.type)
let projectRef: Ref<Project> | undefined = $locationStep.project
$: void selectProject(spaceRef)
$: locationStepUpdated({ project: projectRef })
async function selectProject (spaceRef: Ref<DocumentSpace> | undefined): Promise<void> {
projectRef = spaceRef !== undefined ? await getLatestProjectId(spaceRef) : undefined
}
async function fetchSpace (id: Ref<DocumentSpace> | undefined): Promise<void> {
space = id === undefined ? undefined : await client.findOne(documents.class.DocumentSpace, { _id: id })
}
async function fetchSpaceType (id: Ref<DocumentSpaceType> | undefined): Promise<void> {
spaceType = id === undefined ? undefined : await client.findOne(documents.class.DocumentSpaceType, { _id: id }, {})
}
function handleParentSelected (doc: Doc): void {
let parent: Ref<ProjectDocument> | undefined
if (hierarchy.isDerived(doc._class, documents.class.DocumentSpace)) {
parent = undefined
} else if (hierarchy.isDerived(doc._class, documents.class.ProjectDocument)) {
parent = doc._id as Ref<ProjectDocument>
}
locationStepUpdated({ parent })
}
function handleProjectSelected (value: Ref<Project> | undefined): void {
projectRef = value
}
const externalSpaces = hierarchy.getDescendants(documents.class.ExternalSpace)
$: canProceed = $locationStep.space !== undefined && $locationStep.project !== undefined
$: hasParentSelector = $locationStep.space !== documents.space.UnsortedTemplates
$: restrictedSpaces = Object.entries($permissionsStore.ps)
.filter(([, pss]) => !pss.has(documents.permission.CreateDocument))
.map(([s]) => s) as Ref<Space>[]
$: spaceQuery = isTemplate
? { _id: { $nin: restrictedSpaces }, archived: false, _class: { $nin: externalSpaces } }
: { _id: { $nin: restrictedSpaces }, archived: false }
</script>
<div class="root">
<div class="sectionTitle"><Label label={documentsRes.string.Space} /></div>
<div class="flex-row-center flex-no-shrink flex-gap-4">
<div class="space">
<SpaceSelector
_class={documents.class.DocumentSpace}
query={spaceQuery}
bind:space={spaceRef}
label={documentsRes.string.Space}
width="100%"
justify="left"
autoSelect={true}
/>
</div>
{#if space && spaceType && spaceType.projects}
<div class="space">
<ProjectSelector
value={projectRef}
space={space._id}
kind={'no-border'}
size={'small'}
justify="left"
showReadonly={false}
on:change={(e) => {
handleProjectSelected(e.detail)
}}
/>
</div>
{/if}
</div>
{#if hasParentSelector}
<div class="sectionTitle"><Label label={documents.string.Parent} /></div>
<div class="parentText"><Label label={documentsRes.string.SelectParent} /></div>
<div class="parentSelector">
{#if space}
<DocumentParentSelector
{space}
project={projectRef}
selected={$locationStep.parent}
collapsedPrefix="locationStep"
on:selected={(e) => {
handleParentSelected(e.detail)
}}
/>
{/if}
</div>
{/if}
</div>
<style lang="scss">
.root {
height: 100%;
width: 100%;
display: flex;
flex-direction: column;
}
.sectionTitle {
font-size: 0.875rem;
font-weight: 500;
line-height: 1.25rem;
&:not(:first-child) {
margin-top: 3rem;
}
}
.space {
width: 12.5rem;
margin-top: 0.5rem;
}
.parentText {
font-size: 0.6875rem;
line-height: 1rem;
}
.parentSelector {
margin-top: 0.5rem;
}
</style>
@@ -0,0 +1,52 @@
<!--
// 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 { Data, Ref, TypedSpace } from '@hcengineering/core'
import { Employee } from '@hcengineering/contact'
import { type ControlledDocument } from '@hcengineering/controlled-documents'
import DocTeam from '../../document/DocTeam.svelte'
export let space: Ref<TypedSpace>
export let docObject: Data<ControlledDocument> | undefined = undefined
async function handleUpdate ({
detail
}: {
detail: { type: 'reviewers' | 'approvers', users: Ref<Employee>[] }
}): Promise<void> {
if (docObject === undefined) {
return
}
const { type, users } = detail
docObject[type] = users
}
</script>
{#if docObject !== undefined}
<div class="root">
<DocTeam controlledDoc={docObject} {space} on:update={handleUpdate} />
</div>
{/if}
<style lang="scss">
.root {
height: 100%;
width: 100%;
}
</style>
@@ -0,0 +1,218 @@
<!--
// 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 { slide } from 'svelte/transition'
import { Data, Ref, WithLookup } from '@hcengineering/core'
import { IconCollapseArrow, RadioButton } from '@hcengineering/ui'
import { getClient } from '@hcengineering/presentation'
import documents, {
type ControlledDocument,
type DocumentCategory,
type DocumentSpace,
type DocumentTemplate,
DocumentState
} from '@hcengineering/controlled-documents'
import { $templateStep as templateStep, templateStepUpdated } from '../../../stores/wizards/create-document'
export let docObject: Data<ControlledDocument> | undefined = undefined
export let canProceed: boolean
const client = getClient()
let docSpaces: Ref<DocumentSpace>[] = []
$: void client
.findAll(
documents.class.DocumentSpace,
{},
{
projection: {
_id: 1
}
}
)
.then((res) => {
docSpaces = res.map((s) => s._id)
})
let templatesByCategory: Record<string, WithLookup<DocumentTemplate>[]>
let categoriesById: Record<Ref<DocumentCategory>, DocumentCategory>
$: if (docSpaces.length > 0) {
void client
.findAll(
documents.mixin.DocumentTemplate,
{
space: { $in: docSpaces },
state: DocumentState.Effective
},
{
lookup: {
category: documents.class.DocumentCategory
}
}
)
.then((res) => {
const newTemplatesByCategory: typeof templatesByCategory = {}
const newCategoriesById: typeof categoriesById = {}
for (const doc of res) {
const category = doc.$lookup?.category
const categoryId = category?._id ?? 'unassigned'
if (categoryId !== 'unassigned' && category !== undefined && newCategoriesById[categoryId] === undefined) {
newCategoriesById[categoryId] = category
}
if (newTemplatesByCategory[categoryId] === undefined) {
newTemplatesByCategory[categoryId] = []
}
newTemplatesByCategory[categoryId]?.push(doc)
}
templatesByCategory = newTemplatesByCategory
categoriesById = newCategoriesById
})
}
$: canProceed = docObject?.template !== undefined && docObject.template !== ''
$: categoriesIds = categoriesById !== undefined ? (Object.keys(categoriesById) as Ref<DocumentCategory>[]) : []
function handleExpanderToggled (id: Ref<DocumentCategory>): void {
const categories = { ...$templateStep.collapsedCategories }
if (categories[id] === undefined) {
categories[id] = 'x'
} else {
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
delete categories[id]
}
templateStepUpdated({ ...$templateStep, collapsedCategories: categories })
}
function handleTemplateSelected (tmp: Ref<DocumentTemplate>, prefix: string, seqNumber: number): void {
if (docObject === undefined) {
return
}
docObject.template = tmp
docObject.seqNumber = seqNumber
docObject.prefix = prefix
docObject.code = ''
}
</script>
{#if docObject !== undefined}
<div class="root antiAccordion">
{#each categoriesIds as catId}
{@const expanded = $templateStep.collapsedCategories[catId] === undefined}
<div class="category">
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div
class="catHeader"
on:click={() => {
handleExpanderToggled(catId)
}}
>
<div class="expander" class:expanded>
<IconCollapseArrow size="small" />
</div>
<span>{categoriesById[catId]?.title}</span>
</div>
{#if expanded}
<div class="templates" transition:slide|local>
{#each templatesByCategory[catId] as tmp}
<RadioButton
value={tmp._id}
group={docObject.template}
action={() => {
handleTemplateSelected(tmp._id, tmp.docPrefix, tmp.sequence + 1)
}}
>
<div class="template">
<div class="tmpHeader">{`${tmp.docPrefix} (${tmp.title})`}</div>
{#if tmp.abstract}
<div class="tmpDescr">{tmp.abstract}</div>
{/if}
</div>
</RadioButton>
{/each}
</div>
{/if}
</div>
{/each}
</div>
{/if}
<style lang="scss">
.root {
height: 100%;
width: 100%;
}
.category {
display: flex;
flex-direction: column;
align-items: flex-start;
&:not(:first-child) {
margin-top: 1.5rem;
}
}
.catHeader {
display: flex;
align-items: center;
font-weight: 500;
cursor: pointer;
}
.expander {
transform-origin: center;
transition: transform 0.15s ease-in-out;
&.expanded {
transform: rotate(90deg);
}
}
.templates {
padding: 0 1rem;
align-self: stretch;
display: flex;
flex-direction: column;
}
.template {
margin-left: 0.625rem;
padding: 1.5rem 0;
border-bottom: 1px solid var(--theme-divider-color);
display: flex;
flex-direction: column;
}
.tmpHeader {
font-weight: 500;
font-size: 0.875rem;
line-height: 1.25rem;
}
.tmpDescr {
color: var(--theme-dark-color);
line-height: 1.25rem;
}
</style>