mirror of
https://github.com/hcengineering/platform.git
synced 2026-10-01 05:55:09 +02:00
UBERF-7090: Add QMS plugins (#5716)
Signed-off-by: Alexey Zinoviev <alexey.zinoviev@xored.com>
This commit is contained in:
+225
@@ -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>
|
||||
+242
@@ -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>
|
||||
+336
@@ -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>
|
||||
+174
@@ -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>
|
||||
+52
@@ -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>
|
||||
+218
@@ -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>
|
||||
Reference in New Issue
Block a user