mirror of
https://github.com/hcengineering/platform.git
synced 2025-05-30 12:20:00 +00:00
show polls in panel, add surveys to vacancies and candidates (#7176)
Signed-off-by: Nikolay Chunosov <Chunosov.N@gmail.com>
This commit is contained in:
parent
8bfeba7303
commit
123ae25b68
@ -18,7 +18,7 @@ import {
|
||||
Account,
|
||||
IndexKind,
|
||||
type CollaborativeDoc,
|
||||
type Collection as Array,
|
||||
type Collection,
|
||||
type Domain,
|
||||
type Markup,
|
||||
type Ref,
|
||||
@ -28,7 +28,7 @@ import {
|
||||
type Timestamp
|
||||
} from '@hcengineering/core'
|
||||
import {
|
||||
Collection,
|
||||
Collection as TypeCollection,
|
||||
Hidden,
|
||||
Index,
|
||||
Mixin,
|
||||
@ -70,7 +70,9 @@ export class TVacancy extends TProject implements Vacancy {
|
||||
@Index(IndexKind.FullText)
|
||||
fullDescription!: CollaborativeDoc
|
||||
|
||||
@Prop(Collection(attachment.class.Attachment), attachment.string.Attachments, { shortLabel: attachment.string.Files })
|
||||
@Prop(TypeCollection(attachment.class.Attachment), attachment.string.Attachments, {
|
||||
shortLabel: attachment.string.Files
|
||||
})
|
||||
attachments?: number
|
||||
|
||||
@Prop(TypeDate(), recruit.string.Due, recruit.icon.Calendar)
|
||||
@ -83,13 +85,16 @@ export class TVacancy extends TProject implements Vacancy {
|
||||
@Prop(TypeRef(contact.class.Organization), recruit.string.Company, { icon: contact.icon.Company })
|
||||
company?: Ref<Organization>
|
||||
|
||||
@Prop(Collection(chunter.class.ChatMessage), chunter.string.Comments)
|
||||
@Prop(TypeCollection(chunter.class.ChatMessage), chunter.string.Comments)
|
||||
comments?: number
|
||||
|
||||
@Prop(TypeString(), recruit.string.Vacancy)
|
||||
@Index(IndexKind.FullText)
|
||||
@Hidden()
|
||||
number!: number
|
||||
|
||||
@Prop(TypeCollection(survey.class.Poll), survey.string.Polls)
|
||||
polls?: Collection<Poll>
|
||||
}
|
||||
|
||||
@Mixin(recruit.mixin.Candidate, contact.class.Person)
|
||||
@ -99,7 +104,7 @@ export class TCandidate extends TPerson implements Candidate {
|
||||
@Index(IndexKind.FullText)
|
||||
title?: string
|
||||
|
||||
@Prop(Collection(recruit.class.Applicant), recruit.string.Applications, {
|
||||
@Prop(TypeCollection(recruit.class.Applicant), recruit.string.Applications, {
|
||||
shortLabel: recruit.string.ApplicationsShort
|
||||
})
|
||||
applications?: number
|
||||
@ -114,26 +119,29 @@ export class TCandidate extends TPerson implements Candidate {
|
||||
@Index(IndexKind.FullText)
|
||||
source?: string
|
||||
|
||||
@Prop(Collection(tags.class.TagReference, recruit.string.SkillLabel), recruit.string.SkillsLabel, {
|
||||
@Prop(TypeCollection(tags.class.TagReference, recruit.string.SkillLabel), recruit.string.SkillsLabel, {
|
||||
icon: recruit.icon.Skills,
|
||||
schema: '3'
|
||||
})
|
||||
skills?: number
|
||||
|
||||
@Prop(Collection(recruit.class.Review, recruit.string.Review), recruit.string.Reviews)
|
||||
@Prop(TypeCollection(recruit.class.Review, recruit.string.Review), recruit.string.Reviews)
|
||||
reviews?: number
|
||||
|
||||
@Prop(
|
||||
Collection(recruit.class.ApplicantMatch, getEmbeddedLabel('Vacancy match')),
|
||||
TypeCollection(recruit.class.ApplicantMatch, getEmbeddedLabel('Vacancy match')),
|
||||
getEmbeddedLabel('Vacancy Matches')
|
||||
)
|
||||
vacancyMatch?: number
|
||||
|
||||
@Prop(TypeCollection(survey.class.Poll), survey.string.Polls)
|
||||
polls?: Collection<Poll>
|
||||
}
|
||||
|
||||
@Mixin(recruit.mixin.VacancyList, contact.class.Organization)
|
||||
@UX(recruit.string.VacancyList, recruit.icon.RecruitApplication, 'CM', 'name')
|
||||
export class TVacancyList extends TOrganization implements VacancyList {
|
||||
@Prop(Collection(recruit.class.Vacancy), recruit.string.Vacancies)
|
||||
@Prop(TypeCollection(recruit.class.Vacancy), recruit.string.Vacancies)
|
||||
vacancies!: number
|
||||
}
|
||||
|
||||
@ -162,8 +170,8 @@ export class TApplicant extends TTask implements Applicant {
|
||||
@Index(IndexKind.Indexed)
|
||||
declare status: Ref<Status>
|
||||
|
||||
@Prop(Collection(survey.class.Poll), survey.string.Polls)
|
||||
polls?: Array<Poll>
|
||||
@Prop(TypeCollection(survey.class.Poll), survey.string.Polls)
|
||||
polls?: Collection<Poll>
|
||||
}
|
||||
|
||||
@Model(recruit.class.ApplicantMatch, core.class.AttachedDoc, DOMAIN_TASK)
|
||||
@ -211,7 +219,7 @@ export class TReview extends TEvent implements Review {
|
||||
@Prop(TypeRef(contact.class.Organization), recruit.string.Company, { icon: contact.icon.Company })
|
||||
company?: Ref<Organization>
|
||||
|
||||
@Prop(Collection(recruit.class.Opinion), recruit.string.Opinions)
|
||||
@Prop(TypeCollection(recruit.class.Opinion), recruit.string.Opinions)
|
||||
opinions?: number
|
||||
}
|
||||
|
||||
@ -225,10 +233,12 @@ export class TOpinion extends TAttachedDoc implements Opinion {
|
||||
@Prop(TypeRef(recruit.class.Review), recruit.string.Review)
|
||||
declare attachedTo: Ref<Review>
|
||||
|
||||
@Prop(Collection(attachment.class.Attachment), attachment.string.Attachments, { shortLabel: attachment.string.Files })
|
||||
@Prop(TypeCollection(attachment.class.Attachment), attachment.string.Attachments, {
|
||||
shortLabel: attachment.string.Files
|
||||
})
|
||||
attachments?: number
|
||||
|
||||
@Prop(Collection(chunter.class.ChatMessage), chunter.string.Comments)
|
||||
@Prop(TypeCollection(chunter.class.ChatMessage), chunter.string.Comments)
|
||||
comments?: number
|
||||
|
||||
@Prop(TypeMarkup(), recruit.string.Description)
|
||||
|
@ -38,6 +38,7 @@
|
||||
"@hcengineering/platform": "^0.6.11",
|
||||
"@hcengineering/survey": "^0.6.0",
|
||||
"@hcengineering/survey-resources": "^0.6.0",
|
||||
"@hcengineering/ui": "^0.6.15"
|
||||
"@hcengineering/ui": "^0.6.15",
|
||||
"@hcengineering/view": "^0.6.13"
|
||||
}
|
||||
}
|
||||
|
@ -18,7 +18,7 @@ import { AccountRole } from '@hcengineering/core'
|
||||
import { type Builder } from '@hcengineering/model'
|
||||
import core from '@hcengineering/model-core'
|
||||
import chunter from '@hcengineering/model-chunter'
|
||||
import view, { type Viewlet } from '@hcengineering/model-view'
|
||||
import view, { createAction, type Viewlet } from '@hcengineering/model-view'
|
||||
import workbench from '@hcengineering/model-workbench'
|
||||
import { surveyId } from '@hcengineering/survey'
|
||||
import { TPoll, TSurvey } from './types'
|
||||
@ -79,6 +79,14 @@ export function createModel (builder: Builder): void {
|
||||
survey.viewlet.TableSurvey
|
||||
)
|
||||
|
||||
builder.mixin(survey.class.Survey, core.class.Class, view.mixin.ObjectTitle, {
|
||||
titleProvider: survey.function.SurveyTitleProvider
|
||||
})
|
||||
|
||||
builder.mixin(survey.class.Survey, core.class.Class, view.mixin.LinkProvider, {
|
||||
encode: survey.function.GetSurveyLink
|
||||
})
|
||||
|
||||
builder.mixin(survey.class.Survey, core.class.Class, view.mixin.ObjectPanel, {
|
||||
component: survey.component.EditSurveyPanel
|
||||
})
|
||||
@ -100,20 +108,58 @@ export function createModel (builder: Builder): void {
|
||||
{
|
||||
attachTo: survey.class.Poll,
|
||||
descriptor: view.viewlet.Table,
|
||||
config: ['', 'modifiedOn'],
|
||||
config: ['', 'isCompleted', 'modifiedOn'],
|
||||
configOptions: {
|
||||
hiddenKeys: ['name', 'survey', 'results'],
|
||||
hiddenKeys: ['name', 'survey', 'questions'],
|
||||
sortable: true
|
||||
}
|
||||
},
|
||||
survey.viewlet.TablePoll
|
||||
)
|
||||
|
||||
builder.mixin(survey.class.Poll, core.class.Class, view.mixin.ObjectTitle, {
|
||||
titleProvider: survey.function.PollTitleProvider
|
||||
})
|
||||
|
||||
builder.mixin(survey.class.Poll, core.class.Class, view.mixin.LinkProvider, {
|
||||
encode: survey.function.GetPollLink
|
||||
})
|
||||
|
||||
builder.mixin(survey.class.Poll, core.class.Class, view.mixin.ObjectPanel, {
|
||||
component: survey.component.EditPollPanel
|
||||
})
|
||||
|
||||
builder.mixin(survey.class.Poll, core.class.Class, view.mixin.ObjectPresenter, {
|
||||
presenter: survey.component.PollPresenter
|
||||
})
|
||||
|
||||
builder.mixin(survey.class.Poll, core.class.Class, activity.mixin.ActivityDoc, {})
|
||||
|
||||
builder.createDoc(activity.class.ActivityExtension, core.space.Model, {
|
||||
ofClass: survey.class.Poll,
|
||||
components: { input: chunter.component.ChatMessageInput }
|
||||
})
|
||||
|
||||
builder.mixin(survey.class.Poll, core.class.Class, view.mixin.CollectionEditor, {
|
||||
editor: survey.component.PollCollection
|
||||
})
|
||||
|
||||
createAction(
|
||||
builder,
|
||||
{
|
||||
action: survey.actionImpl.DeletePoll,
|
||||
label: workbench.string.Delete,
|
||||
icon: view.icon.Delete,
|
||||
input: 'any',
|
||||
category: survey.category.Survey,
|
||||
target: survey.class.Poll,
|
||||
context: {
|
||||
mode: ['context', 'browser'],
|
||||
group: 'remove'
|
||||
},
|
||||
visibilityTester: view.function.CanDeleteObject,
|
||||
override: [view.action.Delete]
|
||||
},
|
||||
survey.action.DeletePoll
|
||||
)
|
||||
}
|
||||
|
@ -14,7 +14,18 @@
|
||||
//
|
||||
|
||||
import { IndexKind, type Domain, type Ref } from '@hcengineering/core'
|
||||
import { ArrOf, Hidden, Index, Model, Prop, TypeRecord, TypeRef, TypeString, UX } from '@hcengineering/model'
|
||||
import {
|
||||
ArrOf,
|
||||
Hidden,
|
||||
Index,
|
||||
Model,
|
||||
Prop,
|
||||
TypeBoolean,
|
||||
TypeRecord,
|
||||
TypeRef,
|
||||
TypeString,
|
||||
UX
|
||||
} from '@hcengineering/model'
|
||||
import core, { TAttachedDoc, TDoc } from '@hcengineering/model-core'
|
||||
import { getEmbeddedLabel } from '@hcengineering/platform'
|
||||
import { type Poll, type Question, type Survey } from '@hcengineering/survey'
|
||||
@ -48,7 +59,10 @@ export class TPoll extends TAttachedDoc implements Poll {
|
||||
@Prop(TypeString(), survey.string.Prompt)
|
||||
prompt!: string
|
||||
|
||||
@Prop(ArrOf(TypeRecord()), getEmbeddedLabel('Answers'))
|
||||
@Prop(ArrOf(TypeRecord()), getEmbeddedLabel('Questions'))
|
||||
@Hidden()
|
||||
results?: { question: string, answer: string[] }[]
|
||||
questions?: Question[]
|
||||
|
||||
@Prop(TypeBoolean(), survey.string.Completed)
|
||||
isCompleted?: boolean
|
||||
}
|
||||
|
@ -54,6 +54,8 @@
|
||||
"@hcengineering/presentation": "^0.6.3",
|
||||
"@hcengineering/recruit": "^0.6.29",
|
||||
"@hcengineering/rekoni": "^0.6.0",
|
||||
"@hcengineering/survey": "^0.6.0",
|
||||
"@hcengineering/survey-resources": "^0.6.0",
|
||||
"@hcengineering/tags": "^0.6.16",
|
||||
"@hcengineering/tags-resources": "^0.6.0",
|
||||
"@hcengineering/task": "^0.6.20",
|
||||
|
@ -21,6 +21,7 @@
|
||||
import { getResource } from '@hcengineering/platform'
|
||||
import presentation, { createQuery, getClient } from '@hcengineering/presentation'
|
||||
import { Vacancy } from '@hcengineering/recruit'
|
||||
import survey from '@hcengineering/survey'
|
||||
import tracker from '@hcengineering/tracker'
|
||||
import { Button, Component, EditBox, IconMixin, IconMoreH, Label } from '@hcengineering/ui'
|
||||
import view from '@hcengineering/view'
|
||||
@ -202,6 +203,9 @@
|
||||
<div class="w-full mt-6">
|
||||
<VacancyApplications objectId={object._id} {readonly} />
|
||||
</div>
|
||||
<div class="w-full mt-6">
|
||||
<Component is={survey.component.PollCollection} props={{ object, label: survey.string.Polls }} />
|
||||
</div>
|
||||
<div class="w-full mt-6">
|
||||
<Component is={tracker.component.RelatedIssuesSection} props={{ object, label: tracker.string.RelatedIssues }} />
|
||||
</div>
|
||||
|
@ -10,6 +10,8 @@ import {
|
||||
type Vacancy,
|
||||
type VacancyList
|
||||
} from '@hcengineering/recruit'
|
||||
import { type Poll } from '@hcengineering/survey'
|
||||
import { generatePollLocation } from '@hcengineering/survey-resources'
|
||||
import { getCurrentResolvedLocation, getPanelURI, type Location, type ResolvedLocation } from '@hcengineering/ui'
|
||||
import view from '@hcengineering/view'
|
||||
import { accessDeniedStore } from '@hcengineering/view-resources'
|
||||
@ -34,6 +36,10 @@ export async function resolveLocation (loc: Location): Promise<ResolvedLocation
|
||||
return undefined
|
||||
}
|
||||
|
||||
if (loc.path[3] === 'poll') {
|
||||
return await generatePollLocation(loc, loc.path[4] as Ref<Poll>)
|
||||
}
|
||||
|
||||
const shortLink = loc.path[3]
|
||||
|
||||
// shortlink
|
||||
|
@ -38,6 +38,7 @@ export interface Vacancy extends Project {
|
||||
company?: Ref<Organization>
|
||||
comments?: number
|
||||
number: number
|
||||
polls?: Collection<Poll>
|
||||
}
|
||||
|
||||
/** @public */
|
||||
@ -54,6 +55,7 @@ export interface Candidate extends Person {
|
||||
source?: string
|
||||
skills?: number
|
||||
reviews?: number
|
||||
polls?: Collection<Poll>
|
||||
}
|
||||
|
||||
/** @public */
|
||||
|
@ -24,4 +24,17 @@
|
||||
<symbol id="question" viewBox="0 0 16 16">
|
||||
<path d="M4.475 5.458c-.284 0-.514-.237-.47-.517C4.28 3.24 5.576 2 7.825 2c2.25 0 3.767 1.36 3.767 3.215 0 1.344-.665 2.288-1.79 2.973-1.1.659-1.414 1.118-1.414 2.01v.03a.5.5 0 01-.5.5h-.77a.5.5 0 01-.5-.495l-.003-.2c-.043-1.221.477-2.001 1.645-2.712 1.03-.632 1.397-1.135 1.397-2.028 0-.979-.758-1.698-1.926-1.698-1.009 0-1.71.529-1.938 1.402-.066.254-.278.461-.54.461h-.777zM7.496 14c.622 0 1.095-.474 1.095-1.09 0-.618-.473-1.092-1.095-1.092-.606 0-1.087.474-1.087 1.091S6.89 14 7.496 14z" />
|
||||
</symbol>
|
||||
<symbol id="submit" viewBox="0 0 32 32">
|
||||
<path d="M4 8C4 5.79086 5.79086 4 8 4H21C21.5523 4 22 4.44772 22 5C22 5.55228 21.5523 6 21 6H8C6.89543 6 6 6.89543 6 8V24C6 25.1046 6.89543 26 8 26H24C25.1046 26 26 25.1046 26 24V17C26 16.4477 26.4477 16 27 16C27.5523 16 28 16.4477 28 17V24C28 26.2091 26.2091 28 24 28H8C5.79086 28 4 26.2091 4 24V8ZM29.7071 6.29289C30.0976 6.68342 30.0976 7.31658 29.7071 7.70711L17.7071 19.7071C17.3166 20.0976 16.6834 20.0976 16.2929 19.7071L10.2929 13.7071C9.90237 13.3166 9.90237 12.6834 10.2929 12.2929C10.6834 11.9024 11.3166 11.9024 11.7071 12.2929L17 17.5858L28.2929 6.29289C28.6834 5.90237 29.3166 5.90237 29.7071 6.29289Z" />
|
||||
</symbol>
|
||||
<symbol id="validate-ok" viewBox="0 0 1024 1024">
|
||||
<path d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm193.5 301.7l-210.6 292a31.8 31.8 0 01-51.7 0L318.5 484.9c-3.8-5.3 0-12.7 6.5-12.7h46.9c10.2 0 19.9 4.9 25.9 13.3l71.2 98.8 157.2-218c6-8.3 15.6-13.3 25.9-13.3H699c6.5 0 10.3 7.4 6.5 12.7z" />
|
||||
</symbol>
|
||||
<symbol id="validate-fail" viewBox="0 0 24 24">
|
||||
<path d="M16.707 2.293A.996.996 0 0016 2H8a.996.996 0 00-.707.293l-5 5A.996.996 0 002 8v8c0 .266.105.52.293.707l5 5A.996.996 0 008 22h8c.266 0 .52-.105.707-.293l5-5A.996.996 0 0022 16V8a.996.996 0 00-.293-.707l-5-5zM13 17h-2v-2h2v2zm0-4h-2V7h2v6z" />
|
||||
</symbol>
|
||||
<symbol id="info" viewBox="0 0 16 16">
|
||||
<path d="M7 4.75c0-.412.338-.75.75-.75h.5c.412 0 .75.338.75.75v.5c0 .412-.338.75-.75.75h-.5A.753.753 0 017 5.25v-.5zM10 12H6v-1h1V8H6V7h3v4h1z" />
|
||||
<path d="M8 0a8 8 0 100 16A8 8 0 008 0zm0 14.5a6.5 6.5 0 110-13 6.5 6.5 0 010 13z" />
|
||||
</symbol>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 4.3 KiB |
@ -6,9 +6,12 @@
|
||||
"Application": "Survey",
|
||||
"Close": "Close",
|
||||
"Control": "Control",
|
||||
"Completed": "Completed",
|
||||
"CreatePoll": "Make a survey",
|
||||
"CreateSurvey": "Survey",
|
||||
"DeleteOption": "Delete Option",
|
||||
"DeletePoll": "Delete Survey",
|
||||
"DeletePollConfirm": "Are you sure you want to delete the survey?",
|
||||
"DeleteQuestion": "Delete Question",
|
||||
"DeleteQuestionConfirm": "Are you sure you want to delete the question?",
|
||||
"Name": "Title",
|
||||
@ -27,12 +30,19 @@
|
||||
"QuestionKindOptions": "Select several options",
|
||||
"QuestionIsMandatory": "Is mandatory",
|
||||
"QuestionHasCustomOption": "Has custom option",
|
||||
"QuestionOptionPlaceholder": "Add an option",
|
||||
"QuestionPlaceholder": "Add a question",
|
||||
"QuestionEmptyPlaceholder": "Provide question text",
|
||||
"QuestionPlaceholderEmpty": "Provide question text",
|
||||
"QuestionPlaceholderOption": "Add an option",
|
||||
"QuestionTooltipMandatory": "Answer is required",
|
||||
"QuestionTooltipCustomOption": "User can provide their own option",
|
||||
"Survey": "Survey Form",
|
||||
"Surveys": "Survey Forms",
|
||||
"SurveyEdit": "Edit Form",
|
||||
"SurveyPreview": "Preview Form",
|
||||
"SurveySubmit": "Submit"
|
||||
"SurveySubmit": "Submit",
|
||||
"SurveySubmitConfirm": "You will not be able to change answers after that. Are you sure you want to submit now?",
|
||||
"ValidateFail": "Some required questions are not answered",
|
||||
"ValidateInfo": "This is how the form will look like for an user. Try to type answers and select options to test your survey. A green icon in the header above shows the form is filled properly",
|
||||
"ValidateOk": "Form is filled correctly"
|
||||
}
|
||||
}
|
||||
|
@ -6,9 +6,12 @@
|
||||
"Application": "Опросник",
|
||||
"Close": "Закрыть",
|
||||
"Control": "Управление",
|
||||
"Completed": "Завершен",
|
||||
"CreatePoll": "Провести опрос",
|
||||
"CreateSurvey": "Анкета",
|
||||
"DeleteOption": "Удалить вариант",
|
||||
"DeletePoll": "Удалить опрос",
|
||||
"DeletePollConfirm": "Уверены, что хотите удалить опрос?",
|
||||
"DeleteQuestion": "Удалить вопрос",
|
||||
"DeleteQuestionConfirm": "Уверены, что хотите удалить вопрос?",
|
||||
"Name": "Заголовок",
|
||||
@ -27,12 +30,19 @@
|
||||
"QuestionKindOptions": "Выбор нескольких вариантов",
|
||||
"QuestionIsMandatory": "Обязательный вопрос",
|
||||
"QuestionHasCustomOption": "Есть свой вариант",
|
||||
"QuestionOptionPlaceholder": "Добавить вариант",
|
||||
"QuestionPlaceholder": "Добавить вопрос",
|
||||
"QuestionEmptyPlaceholder": "Введите текст вопроса",
|
||||
"QuestionPlaceholderEmpty": "Введите текст вопроса",
|
||||
"QuestionPlaceholderOption": "Добавить вариант",
|
||||
"QuestionTooltipMandatory": "Ответ обязателен",
|
||||
"QuestionTooltipCustomOption": "Пользователь может предложить свой вариант",
|
||||
"Survey": "Анкета",
|
||||
"Surveys": "Анкеты",
|
||||
"SurveyEdit": "Редактировать форму",
|
||||
"SurveyPreview": "Просмотр формы",
|
||||
"SurveySubmit": "Отправить"
|
||||
"SurveySubmit": "Завершить",
|
||||
"SurveySubmitConfirm": "После этого вы больше не сможете изменять ответы. Вы уверены, что хотите завершить опрос сейчас?",
|
||||
"ValidateFail": "Нет ответов на некоторые обязательные вопросы",
|
||||
"ValidateInfo": "Так будет выглядеть форма для пользователя. Для проверки анкеты попробуйте вводить ответы и выбирать варианты. Зеленый значок в заголовке покажет, что форма заполнена правильно",
|
||||
"ValidateOk": "Форма заполнена правильно"
|
||||
}
|
||||
}
|
||||
|
@ -19,13 +19,17 @@ import survey, { surveyId } from '@hcengineering/survey'
|
||||
const icons = require('../assets/icons.svg') as string // eslint-disable-line
|
||||
loadMetadata(survey.icon, {
|
||||
Application: `${icons}#application`,
|
||||
Info: `${icons}#info`,
|
||||
Poll: `${icons}#page`,
|
||||
Question: `${icons}#question`,
|
||||
QuestionKindString: `${icons}#textline`,
|
||||
QuestionKindOption: `${icons}#radio`,
|
||||
QuestionKindOptions: `${icons}#checkbox`,
|
||||
Survey: `${icons}#application`,
|
||||
Submit: `${icons}#submit`,
|
||||
QuestionIsMandatory: `${icons}#asterisk`,
|
||||
QuestionHasCustomOption: `${icons}#star`
|
||||
QuestionHasCustomOption: `${icons}#star`,
|
||||
ValidateFail: `${icons}#validate-fail`,
|
||||
ValidateOk: `${icons}#validate-ok`
|
||||
})
|
||||
addStringsLoader(surveyId, async (lang: string) => await import(`../lang/${lang}.json`))
|
||||
|
90
plugins/survey-resources/src/components/EditPoll.svelte
Normal file
90
plugins/survey-resources/src/components/EditPoll.svelte
Normal file
@ -0,0 +1,90 @@
|
||||
<!--
|
||||
//
|
||||
// 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 { getClient } from '@hcengineering/presentation'
|
||||
import { Question, QuestionKind, Poll, PollData } from '@hcengineering/survey'
|
||||
import PollQuestion from './PollQuestion.svelte'
|
||||
import { hasText } from '../utils'
|
||||
|
||||
const client = getClient()
|
||||
|
||||
export let object: Poll | PollData
|
||||
export let canSubmit: boolean = false
|
||||
export let readonly: boolean = false
|
||||
|
||||
const questionNodes: PollQuestion[] = []
|
||||
const isAnswered: boolean[] = []
|
||||
|
||||
$: updateCanSubmit(isAnswered)
|
||||
|
||||
function updateCanSubmit (isAnswered: boolean[]): void {
|
||||
canSubmit = isAnswered.every((yes) => yes)
|
||||
}
|
||||
|
||||
function isQuestionValid (question: Question): boolean {
|
||||
if (!hasText(question.name)) {
|
||||
return false
|
||||
}
|
||||
if (question.kind === QuestionKind.OPTION || question.kind === QuestionKind.OPTIONS) {
|
||||
if (question.options === undefined || question.options === null || question.options.length === 0) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
function isPreviewMode (): boolean {
|
||||
return (object as Poll)._id === undefined
|
||||
}
|
||||
|
||||
async function saveAnswers (): Promise<void> {
|
||||
if (isPreviewMode()) {
|
||||
return
|
||||
}
|
||||
const poll = object as Poll
|
||||
await client.updateDoc(poll._class, poll.space, poll._id, { questions: object.questions })
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="antiSection">
|
||||
{#if hasText(object.prompt)}
|
||||
<div class="antiSection-header">
|
||||
<span class="antiSection-header__title">
|
||||
{object.prompt}
|
||||
</span>
|
||||
</div>
|
||||
{/if}
|
||||
{#each object.questions ?? [] as question, index}
|
||||
{#if isQuestionValid(question)}
|
||||
<div class="question">
|
||||
<PollQuestion
|
||||
bind:this={questionNodes[index]}
|
||||
bind:isAnswered={isAnswered[index]}
|
||||
readonly={readonly || object.isCompleted}
|
||||
on:answered={saveAnswers}
|
||||
{question}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.question {
|
||||
margin-top: 1.25em;
|
||||
}
|
||||
</style>
|
114
plugins/survey-resources/src/components/EditPollPanel.svelte
Normal file
114
plugins/survey-resources/src/components/EditPollPanel.svelte
Normal file
@ -0,0 +1,114 @@
|
||||
<!--
|
||||
//
|
||||
// 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 { Ref } from '@hcengineering/core'
|
||||
import { Panel } from '@hcengineering/panel'
|
||||
import { MessageBox, createQuery, getClient } from '@hcengineering/presentation'
|
||||
import { Poll } from '@hcengineering/survey'
|
||||
import { Button, IconMoreH, showPopup } from '@hcengineering/ui'
|
||||
import view from '@hcengineering/view'
|
||||
import { DocNavLink, ParentsNavigator, showMenu } from '@hcengineering/view-resources'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import EditPoll from './EditPoll.svelte'
|
||||
import survey from '../plugin'
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
const query = createQuery()
|
||||
|
||||
export let _id: Ref<Poll>
|
||||
export let embedded: boolean = false
|
||||
export let readonly: boolean = false
|
||||
|
||||
let object: Poll | undefined = undefined
|
||||
let canSubmit = false
|
||||
|
||||
$: updateObject(_id)
|
||||
|
||||
function updateObject (_id: Ref<Poll>): void {
|
||||
query.query(survey.class.Poll, { _id }, (result) => {
|
||||
object = result[0]
|
||||
})
|
||||
}
|
||||
|
||||
async function submit (): Promise<void> {
|
||||
if (object === undefined) {
|
||||
return
|
||||
}
|
||||
showPopup(
|
||||
MessageBox,
|
||||
{
|
||||
label: survey.string.SurveySubmit,
|
||||
message: survey.string.SurveySubmitConfirm
|
||||
},
|
||||
undefined,
|
||||
async (result?: boolean) => {
|
||||
if (result === true && object !== undefined) {
|
||||
await getClient().updateDoc(object._class, object.space, object._id, { isCompleted: true })
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if object}
|
||||
<Panel
|
||||
isHeader={false}
|
||||
isSub={false}
|
||||
isAside={true}
|
||||
{embedded}
|
||||
{object}
|
||||
on:open
|
||||
on:close={() => {
|
||||
dispatch('close')
|
||||
}}
|
||||
withoutInput={readonly}
|
||||
>
|
||||
<svelte:fragment slot="title">
|
||||
{#if !embedded}<ParentsNavigator element={object} />{/if}
|
||||
<DocNavLink noUnderline {object}>
|
||||
<div class="title">{object.name}</div>
|
||||
</DocNavLink>
|
||||
</svelte:fragment>
|
||||
|
||||
<svelte:fragment slot="utils">
|
||||
{#if !readonly}
|
||||
{#if !(object.isCompleted ?? false)}
|
||||
<Button
|
||||
icon={survey.icon.Submit}
|
||||
label={survey.string.SurveySubmit}
|
||||
kind={'primary'}
|
||||
disabled={!canSubmit}
|
||||
showTooltip={{ label: canSubmit ? undefined : survey.string.ValidateFail }}
|
||||
on:click={submit}
|
||||
/>
|
||||
{/if}
|
||||
<Button
|
||||
icon={IconMoreH}
|
||||
iconProps={{ size: 'medium' }}
|
||||
kind={'icon'}
|
||||
on:click={(e) => {
|
||||
showMenu(e, { object, excludedActions: [view.action.Open] })
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
|
||||
<div class="flex-col flex-grow flex-no-shrink">
|
||||
<EditPoll {object} {readonly} bind:canSubmit />
|
||||
</div>
|
||||
</Panel>
|
||||
{/if}
|
@ -17,7 +17,16 @@
|
||||
<script lang="ts">
|
||||
import { MessageBox, getClient } from '@hcengineering/presentation'
|
||||
import { Question, QuestionKind, Survey } from '@hcengineering/survey'
|
||||
import { Button, EditBox, IconDelete, SelectPopup, eventToHTMLElement, showPopup } from '@hcengineering/ui'
|
||||
import {
|
||||
Button,
|
||||
EditBox,
|
||||
Icon,
|
||||
IconDelete,
|
||||
SelectPopup,
|
||||
eventToHTMLElement,
|
||||
showPopup,
|
||||
tooltip
|
||||
} from '@hcengineering/ui'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import survey from '../plugin'
|
||||
|
||||
@ -342,10 +351,20 @@
|
||||
<div class="text">
|
||||
<EditBox
|
||||
disabled={readonly}
|
||||
placeholder={survey.string.QuestionEmptyPlaceholder}
|
||||
placeholder={survey.string.QuestionPlaceholderEmpty}
|
||||
bind:value={question.name}
|
||||
on:change={changeName}
|
||||
/>
|
||||
{#if question.hasCustomOption && question.kind !== QuestionKind.STRING}
|
||||
<span class="question-param-icon" use:tooltip={{ label: survey.string.QuestionTooltipCustomOption }}>
|
||||
<Icon icon={survey.icon.QuestionHasCustomOption} size="medium" fill="var(--theme-won-color)" />
|
||||
</span>
|
||||
{/if}
|
||||
{#if question.isMandatory}
|
||||
<span class="question-param-icon" use:tooltip={{ label: survey.string.QuestionTooltipMandatory }}>
|
||||
<Icon icon={survey.icon.QuestionIsMandatory} size="medium" fill="var(--theme-urgent-color)" />
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
@ -390,7 +409,7 @@
|
||||
<div class="text">
|
||||
<EditBox
|
||||
disabled={readonly}
|
||||
placeholder={survey.string.QuestionOptionPlaceholder}
|
||||
placeholder={survey.string.QuestionPlaceholderOption}
|
||||
bind:value={options[index]}
|
||||
on:change={async () => {
|
||||
await changeOption(index)
|
||||
@ -415,7 +434,7 @@
|
||||
>
|
||||
<Button noFocus={true} icon={survey.icon.Question} kind="list" size="small" />
|
||||
<div class="text">
|
||||
<EditBox placeholder={survey.string.QuestionOptionPlaceholder} bind:value={newOption} on:change={addOption} />
|
||||
<EditBox placeholder={survey.string.QuestionPlaceholderOption} bind:value={newOption} on:change={addOption} />
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
@ -443,6 +462,7 @@
|
||||
margin-left: 1em;
|
||||
margin-right: 1em;
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
}
|
||||
.option {
|
||||
display: flex;
|
||||
@ -459,4 +479,7 @@
|
||||
transition: box-shadow 0.1s ease-in;
|
||||
box-shadow: 0 -3px 0 0 var(--primary-button-outline);
|
||||
}
|
||||
.question-param-icon {
|
||||
margin-left: 0.5em;
|
||||
}
|
||||
</style>
|
||||
|
@ -17,9 +17,8 @@
|
||||
<script lang="ts">
|
||||
import { getClient } from '@hcengineering/presentation'
|
||||
import { Survey } from '@hcengineering/survey'
|
||||
import { Button, EditBox, FocusHandler, Label, createFocusManager, showPopup } from '@hcengineering/ui'
|
||||
import { EditBox, FocusHandler, Label, createFocusManager } from '@hcengineering/ui'
|
||||
import EditQuestion from './EditQuestion.svelte'
|
||||
import SurveyForm from './SurveyForm.svelte'
|
||||
import survey from '../plugin'
|
||||
|
||||
const manager = createFocusManager()
|
||||
@ -27,7 +26,6 @@
|
||||
|
||||
export let object: Survey
|
||||
export let readonly: boolean = false
|
||||
export let showPeviewButton = true
|
||||
|
||||
$: questions = object?.questions ?? []
|
||||
|
||||
@ -39,10 +37,6 @@
|
||||
await client.updateDoc(object._class, object.space, object._id, { prompt: object.prompt })
|
||||
}
|
||||
|
||||
export function previewSurveyForm (): void {
|
||||
showPopup(SurveyForm, { source: object }, 'top')
|
||||
}
|
||||
|
||||
let draggedIndex: number | undefined = undefined
|
||||
let draggedOverIndex: number | undefined = undefined
|
||||
|
||||
@ -96,9 +90,6 @@
|
||||
<div class="flex-grow flex-col">
|
||||
<div class="name">
|
||||
<EditBox disabled={readonly} placeholder={survey.string.Name} bind:value={object.name} on:change={nameChange} />
|
||||
{#if showPeviewButton}
|
||||
<Button icon={survey.icon.Poll} label={survey.string.SurveyPreview} on:click={previewSurveyForm} />
|
||||
{/if}
|
||||
</div>
|
||||
<div class="prompt">
|
||||
<EditBox
|
||||
|
@ -19,12 +19,14 @@
|
||||
import { Panel } from '@hcengineering/panel'
|
||||
import { createQuery } from '@hcengineering/presentation'
|
||||
import { Survey } from '@hcengineering/survey'
|
||||
import { Button, IconMoreH } from '@hcengineering/ui'
|
||||
import { Button, Icon, IconMoreH, Label, tooltip } from '@hcengineering/ui'
|
||||
import view from '@hcengineering/view'
|
||||
import { DocNavLink, showMenu } from '@hcengineering/view-resources'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import EditSurvey from './EditSurvey.svelte'
|
||||
import EditPoll from './EditPoll.svelte'
|
||||
import survey from '../plugin'
|
||||
import { makePollData } from '../utils'
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
const query = createQuery()
|
||||
@ -34,9 +36,11 @@
|
||||
export let readonly: boolean = false
|
||||
|
||||
let object: Survey | undefined = undefined
|
||||
let editor: EditSurvey
|
||||
let preview = false
|
||||
let canSubmit = false
|
||||
|
||||
$: updateObject(_id)
|
||||
$: poll = preview && object !== undefined ? makePollData(object) : undefined
|
||||
|
||||
function updateObject (_id: Ref<Survey>): void {
|
||||
query.query(survey.class.Survey, { _id }, (result) => {
|
||||
@ -65,14 +69,25 @@
|
||||
</svelte:fragment>
|
||||
|
||||
<svelte:fragment slot="utils">
|
||||
<Button
|
||||
icon={survey.icon.Poll}
|
||||
label={survey.string.SurveyPreview}
|
||||
on:click={() => {
|
||||
editor.previewSurveyForm()
|
||||
}}
|
||||
/>
|
||||
{#if !readonly}
|
||||
{#if preview}
|
||||
{#if canSubmit}
|
||||
<span use:tooltip={{ label: survey.string.ValidateOk }}>
|
||||
<Icon size="x-large" icon={survey.icon.ValidateOk} fill="var(--theme-won-color)" />
|
||||
</span>
|
||||
{:else}
|
||||
<span use:tooltip={{ label: survey.string.ValidateFail }}>
|
||||
<Icon size="x-large" icon={survey.icon.ValidateFail} iconProps={{ opacity: 0.75 }} />
|
||||
</span>
|
||||
{/if}
|
||||
{/if}
|
||||
<Button
|
||||
icon={preview ? survey.icon.Survey : survey.icon.Poll}
|
||||
label={preview ? survey.string.SurveyEdit : survey.string.SurveyPreview}
|
||||
on:click={() => {
|
||||
preview = !preview
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
icon={IconMoreH}
|
||||
iconProps={{ size: 'medium' }}
|
||||
@ -85,7 +100,19 @@
|
||||
</svelte:fragment>
|
||||
|
||||
<div class="flex-col flex-grow flex-no-shrink">
|
||||
<EditSurvey bind:this={editor} {object} {readonly} showPeviewButton={false} />
|
||||
{#if preview}
|
||||
{#if poll !== undefined}
|
||||
<div class="antiSection-empty solid flex-row mt-3">
|
||||
<Icon icon={survey.icon.Info} size="large" />
|
||||
<span class="content-dark-color" style="margin-left:1em">
|
||||
<Label label={survey.string.ValidateInfo} />
|
||||
</span>
|
||||
</div>
|
||||
<EditPoll object={poll} bind:canSubmit />
|
||||
{/if}
|
||||
{:else}
|
||||
<EditSurvey {object} {readonly} />
|
||||
{/if}
|
||||
</div>
|
||||
</Panel>
|
||||
{/if}
|
||||
|
@ -16,13 +16,14 @@
|
||||
-->
|
||||
<script lang="ts">
|
||||
import type { Class, Doc, Ref, Space } from '@hcengineering/core'
|
||||
import { getClient } from '@hcengineering/presentation'
|
||||
import { Survey } from '@hcengineering/survey'
|
||||
import { Button, IconAdd, Label, Section, showPopup } from '@hcengineering/ui'
|
||||
import { Viewlet, ViewletPreference } from '@hcengineering/view'
|
||||
import { Table, ViewletSelector, ViewletSettingButton } from '@hcengineering/view-resources'
|
||||
import SurveyForm from './SurveyForm.svelte'
|
||||
import { Button, IconAdd, Label, Section, navigate, showPopup } from '@hcengineering/ui'
|
||||
import view, { Viewlet, ViewletPreference } from '@hcengineering/view'
|
||||
import { Table, ViewletSelector, ViewletSettingButton, getObjectLinkFragment } from '@hcengineering/view-resources'
|
||||
import SurveyPopup from './SurveyPopup.svelte'
|
||||
import survey from '../plugin'
|
||||
import { makePollData } from '../utils'
|
||||
|
||||
export let objectId: Ref<Doc>
|
||||
export let space: Ref<Space>
|
||||
@ -34,13 +35,29 @@
|
||||
let viewlet: Viewlet | undefined
|
||||
let preference: ViewletPreference | undefined
|
||||
|
||||
function createPoll (ev: MouseEvent): void {
|
||||
showPopup(SurveyPopup, {}, ev.target as HTMLElement, (result) => {
|
||||
function selectSurvey (ev: MouseEvent): void {
|
||||
showPopup(SurveyPopup, {}, ev.target as HTMLElement, async (result) => {
|
||||
if (result != null) {
|
||||
showPopup(SurveyForm, { source: result as Survey, objectId, space, _class }, 'top')
|
||||
await createPoll(result as Survey)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async function createPoll (source: Survey): Promise<void> {
|
||||
const client = getClient()
|
||||
const pollId = await client.addCollection(survey.class.Poll, space, objectId, _class, 'polls', makePollData(source))
|
||||
|
||||
const poll = await client.findOne(survey.class.Survey, { _id: pollId })
|
||||
if (poll === undefined) {
|
||||
console.error(`Could not find just created poll ${pollId}.`)
|
||||
return
|
||||
}
|
||||
|
||||
const hierarchy = client.getHierarchy()
|
||||
const panel = hierarchy.classHierarchyMixin(poll._class as Ref<Class<Doc>>, view.mixin.ObjectPanel)
|
||||
const loc = await getObjectLinkFragment(hierarchy, poll, {}, panel?.component)
|
||||
navigate(loc)
|
||||
}
|
||||
</script>
|
||||
|
||||
<Section label={survey.string.Polls} icon={survey.icon.Poll}>
|
||||
@ -55,7 +72,7 @@
|
||||
/>
|
||||
<ViewletSettingButton kind={'tertiary'} bind:viewlet />
|
||||
{#if !readonly}
|
||||
<Button id={survey.string.CreatePoll} icon={IconAdd} kind={'ghost'} on:click={createPoll} />
|
||||
<Button id={survey.string.CreatePoll} icon={IconAdd} kind={'ghost'} on:click={selectSurvey} />
|
||||
{/if}
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
@ -78,7 +95,7 @@
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
{#if !readonly}
|
||||
<span class="over-underline content-color" on:click={createPoll}>
|
||||
<span class="over-underline content-color" on:click={selectSurvey}>
|
||||
<Label label={survey.string.CreatePoll} />
|
||||
</span>
|
||||
{/if}
|
||||
|
@ -1,41 +0,0 @@
|
||||
<!--
|
||||
//
|
||||
// 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 survey, { Poll } from '@hcengineering/survey'
|
||||
import { Label, showPopup } from '@hcengineering/ui'
|
||||
import PollResult from './PollResult.svelte'
|
||||
import { hasText } from '../utils'
|
||||
|
||||
export let value: Poll | undefined | null
|
||||
export let overflowLabel = true
|
||||
|
||||
function showPollResult (): void {
|
||||
showPopup(PollResult, { poll: value })
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if value}
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<span class="over-underline content-color" class:overflow-label={overflowLabel} on:click={showPollResult}>
|
||||
{#if hasText(value.name)}
|
||||
{value.name}
|
||||
{:else}
|
||||
<Label label={survey.string.NoName} />
|
||||
{/if}
|
||||
</span>
|
||||
{/if}
|
242
plugins/survey-resources/src/components/PollQuestion.svelte
Normal file
242
plugins/survey-resources/src/components/PollQuestion.svelte
Normal file
@ -0,0 +1,242 @@
|
||||
<!--
|
||||
//
|
||||
// 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 { generateId } from '@hcengineering/core'
|
||||
import { EditBox, Icon, Label, tooltip } from '@hcengineering/ui'
|
||||
import { AnsweredQuestion, QuestionKind } from '@hcengineering/survey'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import survey from '../plugin'
|
||||
import { hasText } from '../utils'
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
export let question: AnsweredQuestion
|
||||
export let isAnswered: boolean = false
|
||||
export let readonly: boolean = false
|
||||
|
||||
let answer = ''
|
||||
let selectedOption: number | undefined
|
||||
const selectedOptions: boolean[] = []
|
||||
const customOption = -1
|
||||
|
||||
$: id = generateId()
|
||||
$: showAnswers(question)
|
||||
$: updateIsAnswered(question, selectedOption, selectedOptions)
|
||||
|
||||
function showAnswers (question: AnsweredQuestion): void {
|
||||
if (question.kind === QuestionKind.STRING) {
|
||||
answer = question.answer ?? ''
|
||||
} else if (question.kind === QuestionKind.OPTION) {
|
||||
selectedOption = question.answers?.[0]
|
||||
if ((question.answers === undefined || question.answers === null) && typeof question.answer === 'string') {
|
||||
selectedOption = customOption
|
||||
answer = question.answer
|
||||
}
|
||||
} else if (question.kind === QuestionKind.OPTIONS) {
|
||||
question.answers?.forEach((index) => {
|
||||
selectedOptions[index] = true
|
||||
})
|
||||
if (typeof question.answer === 'string') {
|
||||
selectedOptions[customOption] = true
|
||||
answer = question.answer
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function updateIsAnswered (
|
||||
question: AnsweredQuestion,
|
||||
selectedOption: number | undefined,
|
||||
selectedOptions: boolean[]
|
||||
): void {
|
||||
if (!question.isMandatory) {
|
||||
isAnswered = true
|
||||
return
|
||||
}
|
||||
if (question.kind === QuestionKind.STRING) {
|
||||
isAnswered = hasText(answer)
|
||||
} else if (question.kind === QuestionKind.OPTION) {
|
||||
isAnswered = selectedOption === customOption ? hasText(answer) : selectedOption !== undefined
|
||||
} else if (question.kind === QuestionKind.OPTIONS) {
|
||||
isAnswered = selectedOptions[customOption] ? hasText(answer) : selectedOptions.some((on) => on)
|
||||
}
|
||||
}
|
||||
|
||||
function answerChange (): void {
|
||||
question.answer = hasText(answer) ? answer : undefined
|
||||
dispatch('answered')
|
||||
}
|
||||
|
||||
function optionChange (): void {
|
||||
if (selectedOption === undefined) {
|
||||
question.answer = undefined
|
||||
question.answers = undefined
|
||||
} else if (selectedOption === customOption) {
|
||||
question.answer = answer
|
||||
question.answers = undefined
|
||||
} else {
|
||||
question.answer = undefined
|
||||
question.answers = [selectedOption]
|
||||
}
|
||||
dispatch('answered')
|
||||
}
|
||||
|
||||
function optionsChange (): void {
|
||||
const answers: number[] = []
|
||||
selectedOptions.forEach((on, index) => {
|
||||
if (on) {
|
||||
answers.push(index)
|
||||
}
|
||||
})
|
||||
question.answers = answers.length > 0 ? answers : undefined
|
||||
question.answer = selectedOptions[customOption] ? answer : undefined
|
||||
dispatch('answered')
|
||||
}
|
||||
|
||||
function getReadonlyAnswers (): string[] {
|
||||
if (question.kind === QuestionKind.STRING) {
|
||||
return [answer.trim()]
|
||||
}
|
||||
if (question.kind === QuestionKind.OPTION) {
|
||||
if (selectedOption === undefined) {
|
||||
return []
|
||||
}
|
||||
if (selectedOption === customOption) {
|
||||
return [answer.trim()]
|
||||
}
|
||||
return [question.options?.[selectedOption] ?? '']
|
||||
}
|
||||
if (question.kind === QuestionKind.OPTIONS) {
|
||||
const answers: string[] = []
|
||||
question.options?.forEach((option, index) => {
|
||||
if (selectedOptions[index]) {
|
||||
answers.push(option)
|
||||
}
|
||||
})
|
||||
if (selectedOptions[customOption]) {
|
||||
answers.push(answer.trim())
|
||||
}
|
||||
return answers
|
||||
}
|
||||
return []
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="antiSection">
|
||||
<div class="antiSection-header">
|
||||
<span class="antiSection-header__title" style="display:flex">
|
||||
{question.name}
|
||||
{#if question.isMandatory && !readonly}
|
||||
<span style="margin-left:0.25em" use:tooltip={{ label: survey.string.QuestionTooltipMandatory }}>
|
||||
<Icon icon={survey.icon.QuestionIsMandatory} size="tiny" fill="var(--theme-urgent-color)" />
|
||||
</span>
|
||||
{/if}
|
||||
</span>
|
||||
</div>
|
||||
{#if readonly}
|
||||
{#each getReadonlyAnswers() as answer}
|
||||
{#if answer}
|
||||
<div class="answer">{answer}</div>
|
||||
{:else}
|
||||
<div class="answer empty">
|
||||
<Label label={survey.string.NoAnswer} />
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
{:else if question.kind === QuestionKind.OPTION}
|
||||
{#each question.options ?? [] as option, i}
|
||||
<div class="option">
|
||||
<input type="radio" id={`${id}-${i}`} value={i} bind:group={selectedOption} on:change={optionChange} />
|
||||
<label class="option__label" for={`${id}-${i}`}>
|
||||
{option}
|
||||
</label>
|
||||
</div>
|
||||
{/each}
|
||||
{#if question.hasCustomOption}
|
||||
<div class="option">
|
||||
<input
|
||||
type="radio"
|
||||
id={`${id}-custom`}
|
||||
value={customOption}
|
||||
bind:group={selectedOption}
|
||||
on:change={optionChange}
|
||||
/>
|
||||
<label class="option__label" for={`${id}-custom`}>
|
||||
<Label label={survey.string.AnswerCustomOption} />
|
||||
</label>
|
||||
{#if selectedOption === customOption}
|
||||
<div class="option__custom">
|
||||
<EditBox bind:value={answer} on:change={answerChange} placeholder={survey.string.AnswerPlaceholder} />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
{:else if question.kind === QuestionKind.OPTIONS}
|
||||
{#each question.options ?? [] as option, i}
|
||||
<div class="option">
|
||||
<input type="checkbox" id={`${id}-${i}`} bind:checked={selectedOptions[i]} on:change={optionsChange} />
|
||||
<label class="option__label" for={`${id}-${i}`}>
|
||||
{option}
|
||||
</label>
|
||||
</div>
|
||||
{/each}
|
||||
{#if question.hasCustomOption}
|
||||
<div class="option">
|
||||
<input
|
||||
type="checkbox"
|
||||
id={`${id}-custom`}
|
||||
bind:checked={selectedOptions[customOption]}
|
||||
on:change={optionsChange}
|
||||
/>
|
||||
<label class="option__label" for={`${id}-custom`}>
|
||||
<Label label={survey.string.AnswerCustomOption} />
|
||||
</label>
|
||||
{#if selectedOptions[customOption]}
|
||||
<div class="option__custom">
|
||||
<EditBox bind:value={answer} on:change={answerChange} placeholder={survey.string.AnswerPlaceholder} />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
{:else}
|
||||
<div class="option">
|
||||
<EditBox bind:value={answer} on:change={answerChange} placeholder={survey.string.AnswerPlaceholder} />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.option {
|
||||
margin-left: 1em;
|
||||
margin-top: 0.5em;
|
||||
}
|
||||
.option__label {
|
||||
cursor: pointer;
|
||||
margin-left: 0.25em;
|
||||
}
|
||||
.option__custom {
|
||||
margin-left: 2em;
|
||||
margin-top: 0.5em;
|
||||
}
|
||||
.answer {
|
||||
margin-left: 2em;
|
||||
margin-top: 0.5em;
|
||||
|
||||
&.empty {
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
</style>
|
@ -1,100 +0,0 @@
|
||||
<!--
|
||||
//
|
||||
// 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 { Card } from '@hcengineering/presentation'
|
||||
import { Poll } from '@hcengineering/survey'
|
||||
import { FocusHandler, Label, createFocusManager } from '@hcengineering/ui'
|
||||
import { createEventDispatcher, onMount } from 'svelte'
|
||||
import survey from '../plugin'
|
||||
import { hasText } from '../utils'
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
const manager = createFocusManager()
|
||||
|
||||
export let poll: Poll
|
||||
|
||||
onMount(() => {
|
||||
dispatch('open', {})
|
||||
})
|
||||
</script>
|
||||
|
||||
<FocusHandler {manager} />
|
||||
|
||||
<Card
|
||||
label={survey.string.Survey}
|
||||
canSave={true}
|
||||
okLabel={survey.string.Close}
|
||||
okAction={() => {
|
||||
dispatch('close')
|
||||
}}
|
||||
on:close={() => {
|
||||
dispatch('close')
|
||||
}}
|
||||
on:changeContent
|
||||
>
|
||||
<svelte:fragment slot="title">
|
||||
{#if hasText(poll.name)}
|
||||
{poll.name}
|
||||
{:else}
|
||||
<Label label={survey.string.NoName} />
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
|
||||
<div class="antiSection">
|
||||
{#if hasText(poll.prompt)}
|
||||
<div class="antiSection-header">
|
||||
<span class="antiSection-header__title">
|
||||
{poll.prompt}
|
||||
</span>
|
||||
</div>
|
||||
{/if}
|
||||
{#each poll.results ?? [] as result}
|
||||
<div class="antiSection question">
|
||||
<div class="antiSection-header">
|
||||
<span class="antiSection-header__title">
|
||||
{result.question}
|
||||
</span>
|
||||
</div>
|
||||
{#if result.answer === undefined || result.answer === null || result.answer.length === 0}
|
||||
<div class="answer empty">
|
||||
<Label label={survey.string.NoAnswer} />
|
||||
</div>
|
||||
{:else}
|
||||
{#each result.answer as answer}
|
||||
<div class="answer">
|
||||
{answer}
|
||||
</div>
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<style lang="scss">
|
||||
.question {
|
||||
margin-top: 1.25em;
|
||||
}
|
||||
.answer {
|
||||
margin-left: 2em;
|
||||
margin-top: 0.5em;
|
||||
|
||||
&.empty {
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
</style>
|
@ -1,127 +0,0 @@
|
||||
<!--
|
||||
//
|
||||
// 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 { Class, Doc, Ref, Space } from '@hcengineering/core'
|
||||
import { Card, getClient } from '@hcengineering/presentation'
|
||||
import { FocusHandler, Label, createFocusManager } from '@hcengineering/ui'
|
||||
import { Question, QuestionKind, Survey } from '@hcengineering/survey'
|
||||
import { createEventDispatcher, onMount } from 'svelte'
|
||||
import SurveyFormQuestion from './SurveyFormQuestion.svelte'
|
||||
import survey from '../plugin'
|
||||
import { hasText } from '../utils'
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
const manager = createFocusManager()
|
||||
|
||||
export let source: Survey
|
||||
export let objectId: Ref<Doc>
|
||||
export let space: Ref<Space>
|
||||
export let _class: Ref<Class<Doc>>
|
||||
|
||||
const questionNodes: SurveyFormQuestion[] = []
|
||||
|
||||
function isQuestionValid (question: Question): boolean {
|
||||
if (!hasText(question.name)) {
|
||||
return false
|
||||
}
|
||||
if (question.kind === QuestionKind.OPTION || question.kind === QuestionKind.OPTIONS) {
|
||||
if (question.options === undefined || question.options.length === 0) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
function isPreview (): boolean {
|
||||
return objectId === undefined || space === undefined || _class === undefined
|
||||
}
|
||||
|
||||
function canSave (): boolean {
|
||||
return source.questions !== undefined && source.questions.length > 0
|
||||
// TODO: validate answers
|
||||
}
|
||||
|
||||
async function saveAnswers (): Promise<void> {
|
||||
if (!isPreview() && canSave()) {
|
||||
await getClient().addCollection(survey.class.Poll, space, objectId, _class, 'polls', {
|
||||
survey: source._id,
|
||||
name: source.name,
|
||||
prompt: source.prompt,
|
||||
results: (source.questions ?? []).map((q, i) => {
|
||||
let answer: string[] = []
|
||||
const node = questionNodes[i]
|
||||
if (node !== undefined) {
|
||||
answer = node.getAnswer()
|
||||
}
|
||||
return {
|
||||
question: q.name,
|
||||
answer
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
dispatch('close')
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
dispatch('open', {})
|
||||
})
|
||||
</script>
|
||||
|
||||
<FocusHandler {manager} />
|
||||
|
||||
<Card
|
||||
label={survey.string.Survey}
|
||||
canSave={isPreview() || canSave()}
|
||||
okLabel={isPreview() ? survey.string.Close : survey.string.SurveySubmit}
|
||||
okAction={saveAnswers}
|
||||
on:close={() => {
|
||||
dispatch('close')
|
||||
}}
|
||||
on:changeContent
|
||||
>
|
||||
<svelte:fragment slot="title">
|
||||
{#if hasText(source.name)}
|
||||
{source.name}
|
||||
{:else}
|
||||
<Label label={survey.string.NoName} />
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
|
||||
<div class="antiSection">
|
||||
{#if hasText(source.prompt)}
|
||||
<div class="antiSection-header">
|
||||
<span class="antiSection-header__title">
|
||||
{source.prompt}
|
||||
</span>
|
||||
</div>
|
||||
{/if}
|
||||
{#each source.questions ?? [] as question, index}
|
||||
{#if isQuestionValid(question)}
|
||||
<div class="question">
|
||||
<SurveyFormQuestion bind:this={questionNodes[index]} {question} />
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<style lang="scss">
|
||||
.question {
|
||||
margin-top: 1.25em;
|
||||
}
|
||||
</style>
|
@ -1,132 +0,0 @@
|
||||
<!--
|
||||
//
|
||||
// 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 { generateId } from '@hcengineering/core'
|
||||
import { EditBox, Label } from '@hcengineering/ui'
|
||||
import { Question, QuestionKind } from '@hcengineering/survey'
|
||||
import survey from '../plugin'
|
||||
|
||||
export let question: Question
|
||||
|
||||
$: id = generateId()
|
||||
|
||||
let answer = ''
|
||||
let selectedOption: number
|
||||
const selectedOptions: Record<number, boolean> = {}
|
||||
const customOption = -1
|
||||
|
||||
export function getAnswer (): string[] {
|
||||
const answers = []
|
||||
if (question.kind === QuestionKind.STRING) {
|
||||
if (answer.trim().length > 0) {
|
||||
answers.push(answer)
|
||||
}
|
||||
} else if (question.kind === QuestionKind.OPTION) {
|
||||
if (question.options !== undefined && selectedOption !== undefined) {
|
||||
if (selectedOption === customOption) {
|
||||
answers.push(answer)
|
||||
} else {
|
||||
answers.push(question.options[selectedOption])
|
||||
}
|
||||
}
|
||||
} else if (question.kind === QuestionKind.OPTIONS) {
|
||||
if (question.options !== undefined) {
|
||||
for (let i = 0; i < question.options.length; i++) {
|
||||
if (selectedOptions[i]) {
|
||||
answers.push(question.options[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
if (selectedOptions[customOption]) {
|
||||
answers.push(answer)
|
||||
}
|
||||
}
|
||||
return answers
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="antiSection">
|
||||
<div class="antiSection-header">
|
||||
<span class="antiSection-header__title">
|
||||
{question.name}
|
||||
</span>
|
||||
</div>
|
||||
{#if question.kind === QuestionKind.OPTION}
|
||||
{#each question.options ?? [] as option, i}
|
||||
<div class="option">
|
||||
<input type="radio" id={`${id}-${i}`} value={i} bind:group={selectedOption} />
|
||||
<label class="option__label" for={`${id}-${i}`}>
|
||||
{option}
|
||||
</label>
|
||||
</div>
|
||||
{/each}
|
||||
{#if question.hasCustomOption}
|
||||
<div class="option">
|
||||
<input id="custom" type="radio" value={customOption} bind:group={selectedOption} />
|
||||
<label class="option__label" for="custom">
|
||||
<Label label={survey.string.AnswerCustomOption} />
|
||||
</label>
|
||||
{#if selectedOption === customOption}
|
||||
<div class="option__custom">
|
||||
<EditBox bind:value={answer} placeholder={survey.string.AnswerPlaceholder} />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
{:else if question.kind === QuestionKind.OPTIONS}
|
||||
{#each question.options ?? [] as option, i}
|
||||
<div class="option">
|
||||
<input id={`${id}-${i}`} type="checkbox" bind:checked={selectedOptions[i]} />
|
||||
<label class="option__label" for={`${id}-${i}`}>
|
||||
{option}
|
||||
</label>
|
||||
</div>
|
||||
{/each}
|
||||
{#if question.hasCustomOption}
|
||||
<div class="option">
|
||||
<input id="custom" type="checkbox" bind:checked={selectedOptions[customOption]} />
|
||||
<label class="option__label" for="custom">
|
||||
<Label label={survey.string.AnswerCustomOption} />
|
||||
</label>
|
||||
{#if selectedOptions[customOption]}
|
||||
<div class="option__custom">
|
||||
<EditBox bind:value={answer} placeholder={survey.string.AnswerPlaceholder} />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
{:else}
|
||||
<div class="option">
|
||||
<EditBox bind:value={answer} placeholder={survey.string.AnswerPlaceholder} />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.option {
|
||||
margin-left: 1em;
|
||||
margin-top: 0.5em;
|
||||
}
|
||||
.option__label {
|
||||
cursor: pointer;
|
||||
margin-left: 0.25em;
|
||||
}
|
||||
.option__custom {
|
||||
margin-left: 2em;
|
||||
margin-top: 0.5em;
|
||||
}
|
||||
</style>
|
@ -15,13 +15,13 @@
|
||||
//
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { getEmbeddedLabel } from '@hcengineering/platform'
|
||||
import { DocNavLink, ObjectMention } from '@hcengineering/view-resources'
|
||||
import { ObjectPresenterType } from '@hcengineering/view'
|
||||
import { Icon, tooltip } from '@hcengineering/ui'
|
||||
import survey, { Survey } from '@hcengineering/survey'
|
||||
import { Icon, Label } from '@hcengineering/ui'
|
||||
import survey, { Survey, Poll } from '@hcengineering/survey'
|
||||
import { hasText } from '../utils'
|
||||
|
||||
export let value: Survey | undefined | null
|
||||
export let value: Survey | Poll | undefined | null
|
||||
export let inline: boolean = false
|
||||
export let disabled: boolean = false
|
||||
export let accent: boolean = false
|
||||
@ -38,16 +38,26 @@
|
||||
<ObjectMention object={value} {disabled} {accent} {noUnderline} {colorInherit} onClick={onEdit} />
|
||||
{:else if type === 'link'}
|
||||
<DocNavLink object={value} onClick={onEdit} {disabled} {noUnderline} {colorInherit} {accent} noOverflow>
|
||||
<div class="flex-presenter" style:max-width={maxWidth} use:tooltip={{ label: getEmbeddedLabel(value.name) }}>
|
||||
<div class="icon"><Icon icon={survey.icon.Survey} size={'small'} /></div>
|
||||
<div class="flex-presenter" style:max-width={maxWidth}>
|
||||
<div class="icon">
|
||||
<Icon icon={value._class === survey.class.Survey ? survey.icon.Survey : survey.icon.Poll} size={'small'} />
|
||||
</div>
|
||||
<span class="ap-label" class:overflow-label={overflowLabel} class:colorInherit class:fs-bold={accent}>
|
||||
{value.name}
|
||||
{#if hasText(value.name)}
|
||||
{value.name}
|
||||
{:else}
|
||||
<Label label={survey.string.NoName} />
|
||||
{/if}
|
||||
</span>
|
||||
</div>
|
||||
</DocNavLink>
|
||||
{:else if type === 'text'}
|
||||
<span class:overflow-label={overflowLabel} use:tooltip={{ label: getEmbeddedLabel(value.name) }}>
|
||||
{value.name}
|
||||
<span class:overflow-label={overflowLabel}>
|
||||
{#if hasText(value.name)}
|
||||
{value.name}
|
||||
{:else}
|
||||
<Label label={survey.string.NoName} />
|
||||
{/if}
|
||||
</span>
|
||||
{/if}
|
||||
{/if}
|
||||
|
@ -15,22 +15,41 @@
|
||||
|
||||
import { type Resources } from '@hcengineering/platform'
|
||||
import CreateSurvey from './components/CreateSurvey.svelte'
|
||||
import EditPollPanel from './components/EditPollPanel.svelte'
|
||||
import EditSurveyPanel from './components/EditSurveyPanel.svelte'
|
||||
import PollCollection from './components/PollCollection.svelte'
|
||||
import PollPresenter from './components/PollPresenter.svelte'
|
||||
import SurveyPresenter from './components/SurveyPresenter.svelte'
|
||||
|
||||
import { resolveLocation } from './utils'
|
||||
import {
|
||||
deletePoll,
|
||||
getPollLink,
|
||||
getSurveyLink,
|
||||
pollTitleProvider,
|
||||
resolveLocation,
|
||||
surveyTitleProvider
|
||||
} from './utils'
|
||||
|
||||
export * from './utils'
|
||||
|
||||
export default async (): Promise<Resources> => ({
|
||||
component: {
|
||||
CreateSurvey,
|
||||
EditPollPanel,
|
||||
EditSurveyPanel,
|
||||
PollCollection,
|
||||
PollPresenter,
|
||||
PollPresenter: SurveyPresenter,
|
||||
SurveyPresenter
|
||||
},
|
||||
resolver: {
|
||||
Location: resolveLocation
|
||||
},
|
||||
function: {
|
||||
GetPollLink: getPollLink,
|
||||
GetSurveyLink: getSurveyLink,
|
||||
PollTitleProvider: pollTitleProvider,
|
||||
SurveyTitleProvider: surveyTitleProvider
|
||||
},
|
||||
actionImpl: {
|
||||
DeletePoll: deletePoll
|
||||
}
|
||||
})
|
||||
|
@ -13,7 +13,29 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import { mergeIds } from '@hcengineering/platform'
|
||||
import type { Client, Doc, Ref } from '@hcengineering/core'
|
||||
import { type Resource, mergeIds } from '@hcengineering/platform'
|
||||
import survey, { surveyId } from '@hcengineering/survey'
|
||||
import type { Location, ResolvedLocation } from '@hcengineering/ui'
|
||||
import type { Action, ActionCategory, ViewAction } from '@hcengineering/view'
|
||||
|
||||
export default mergeIds(surveyId, survey, {})
|
||||
export default mergeIds(surveyId, survey, {
|
||||
resolver: {
|
||||
Location: '' as Resource<(loc: Location) => Promise<ResolvedLocation | undefined>>
|
||||
},
|
||||
function: {
|
||||
GetPollLink: '' as Resource<(doc: Doc, props: Record<string, any>) => Promise<Location>>,
|
||||
GetSurveyLink: '' as Resource<(doc: Doc, props: Record<string, any>) => Promise<Location>>,
|
||||
PollTitleProvider: '' as Resource<(client: Client, ref: Ref<Doc>, doc?: Doc) => Promise<string>>,
|
||||
SurveyTitleProvider: '' as Resource<(client: Client, ref: Ref<Doc>, doc?: Doc) => Promise<string>>
|
||||
},
|
||||
actionImpl: {
|
||||
DeletePoll: '' as ViewAction
|
||||
},
|
||||
action: {
|
||||
DeletePoll: '' as Ref<Action<Doc, any>>
|
||||
},
|
||||
category: {
|
||||
Survey: '' as Ref<ActionCategory>
|
||||
}
|
||||
})
|
||||
|
@ -1,51 +1,172 @@
|
||||
import { type Class, type Doc, type Ref } from '@hcengineering/core'
|
||||
import { getClient } from '@hcengineering/presentation'
|
||||
import { type Location, type ResolvedLocation, getPanelURI } from '@hcengineering/ui'
|
||||
//
|
||||
// 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.
|
||||
//
|
||||
|
||||
import { clone, type Class, type Client, type Doc, type Ref } from '@hcengineering/core'
|
||||
import survey, { surveyId, type Poll, type PollData, type Survey } from '@hcengineering/survey'
|
||||
import { getClient, MessageBox } from '@hcengineering/presentation'
|
||||
import {
|
||||
type Location,
|
||||
type ResolvedLocation,
|
||||
getCurrentResolvedLocation,
|
||||
getPanelURI,
|
||||
showPopup
|
||||
} from '@hcengineering/ui'
|
||||
import { accessDeniedStore } from '@hcengineering/view-resources'
|
||||
import view from '@hcengineering/view'
|
||||
import survey, { surveyId, type Survey } from '@hcengineering/survey'
|
||||
|
||||
export function hasText (value: string | undefined | null): boolean {
|
||||
return typeof value === 'string' && value.trim().length > 0
|
||||
}
|
||||
|
||||
export async function generateLocation (loc: Location, id: Ref<Survey>): Promise<ResolvedLocation | undefined> {
|
||||
const client = getClient()
|
||||
export function makePollData (source: Survey): PollData {
|
||||
return {
|
||||
survey: source._id,
|
||||
name: source.name,
|
||||
prompt: source.prompt,
|
||||
questions: clone(source.questions),
|
||||
isCompleted: false
|
||||
}
|
||||
}
|
||||
|
||||
const surv = await client.findOne(survey.class.Survey, { _id: id })
|
||||
if (surv === undefined) {
|
||||
export async function generateSurveyLocation (loc: Location, id: Ref<Survey>): Promise<ResolvedLocation | undefined> {
|
||||
const client = getClient()
|
||||
const doc = await client.findOne(survey.class.Survey, { _id: id })
|
||||
if (doc === undefined) {
|
||||
accessDeniedStore.set(true)
|
||||
console.error(`Could not find document ${id}.`)
|
||||
console.error(`Could not find survey ${id}.`)
|
||||
return undefined
|
||||
}
|
||||
|
||||
const appComponent = loc.path[0] ?? ''
|
||||
const workspace = loc.path[1] ?? ''
|
||||
|
||||
const objectPanel = client.getHierarchy().classHierarchyMixin(surv._class as Ref<Class<Doc>>, view.mixin.ObjectPanel)
|
||||
const objectPanel = client.getHierarchy().classHierarchyMixin(doc._class as Ref<Class<Doc>>, view.mixin.ObjectPanel)
|
||||
const component = objectPanel?.component ?? view.component.EditDoc
|
||||
|
||||
return {
|
||||
loc: {
|
||||
path: [appComponent, workspace, surveyId],
|
||||
fragment: getPanelURI(component, surv._id, surv._class, 'content')
|
||||
path: [appComponent, workspace],
|
||||
fragment: getPanelURI(component, doc._id, doc._class, 'content')
|
||||
},
|
||||
defaultLocation: {
|
||||
path: [appComponent, workspace, surveyId],
|
||||
fragment: getPanelURI(component, surv._id, surv._class, 'content')
|
||||
path: [appComponent, workspace, surveyId, 'surveys'],
|
||||
fragment: getPanelURI(component, doc._id, doc._class, 'content')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function generatePollLocation (loc: Location, id: Ref<Poll>): Promise<ResolvedLocation | undefined> {
|
||||
const client = getClient()
|
||||
const doc = await client.findOne(survey.class.Poll, { _id: id })
|
||||
if (doc === undefined) {
|
||||
accessDeniedStore.set(true)
|
||||
console.error(`Could not find poll ${id}.`)
|
||||
return undefined
|
||||
}
|
||||
|
||||
const appComponent = loc.path[0] ?? ''
|
||||
const workspace = loc.path[1] ?? ''
|
||||
const appId = loc.path[2] ?? ''
|
||||
|
||||
const objectPanel = client.getHierarchy().classHierarchyMixin(doc._class as Ref<Class<Doc>>, view.mixin.ObjectPanel)
|
||||
const component = objectPanel?.component ?? view.component.EditDoc
|
||||
|
||||
return {
|
||||
loc: {
|
||||
path: [appComponent, workspace],
|
||||
fragment: getPanelURI(component, doc._id, doc._class, 'content')
|
||||
},
|
||||
defaultLocation: {
|
||||
path: [appComponent, workspace, appId],
|
||||
fragment: getPanelURI(component, doc._id, doc._class, 'content')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function resolveLocation (loc: Location): Promise<ResolvedLocation | undefined> {
|
||||
// if (loc.path[2] !== surveyId) {
|
||||
// return undefined
|
||||
// }
|
||||
if (loc.path[2] !== surveyId) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
// const id = loc.path[3] as Ref<Survey>
|
||||
// if (id !== undefined) {
|
||||
// return await generateLocation(loc, id)
|
||||
// }
|
||||
if (loc.path[3] === 'surveys') {
|
||||
return undefined
|
||||
}
|
||||
|
||||
return undefined
|
||||
if (loc.path[3] === 'poll') {
|
||||
return await generatePollLocation(loc, loc.path[4] as Ref<Poll>)
|
||||
}
|
||||
|
||||
return await generateSurveyLocation(loc, loc.path[3] as Ref<Survey>)
|
||||
}
|
||||
|
||||
export async function getSurveyLink (doc: Doc): Promise<Location> {
|
||||
const loc = getCurrentResolvedLocation()
|
||||
loc.path.length = 2
|
||||
loc.fragment = undefined
|
||||
loc.query = undefined
|
||||
loc.path[2] = surveyId
|
||||
loc.path[3] = doc._id
|
||||
return loc
|
||||
}
|
||||
|
||||
export async function getPollLink (doc: Doc): Promise<Location> {
|
||||
const loc = getCurrentResolvedLocation()
|
||||
loc.path.length = 3
|
||||
loc.fragment = undefined
|
||||
loc.query = undefined
|
||||
// loc.path[2] is the app id from where the link is generated
|
||||
loc.path[3] = 'poll'
|
||||
loc.path[4] = doc._id
|
||||
return loc
|
||||
}
|
||||
|
||||
export async function pollTitleProvider (client: Client, ref: Ref<Doc>, doc?: Doc): Promise<string> {
|
||||
const obj = await client.findOne(survey.class.Poll, { _id: ref as Ref<Poll> })
|
||||
if (obj !== undefined && hasText(obj.name)) {
|
||||
return obj.name
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
export async function surveyTitleProvider (client: Client, ref: Ref<Doc>, doc?: Doc): Promise<string> {
|
||||
const obj = await client.findOne(survey.class.Survey, { _id: ref as Ref<Survey> })
|
||||
if (obj !== undefined && hasText(obj.name)) {
|
||||
return obj.name
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
export async function deletePoll (poll: Poll): Promise<void> {
|
||||
showPopup(
|
||||
MessageBox,
|
||||
{
|
||||
label: survey.string.DeletePoll,
|
||||
message: survey.string.DeletePollConfirm
|
||||
},
|
||||
undefined,
|
||||
async (result?: boolean) => {
|
||||
if (result === true) {
|
||||
await getClient().removeCollection(
|
||||
poll._class,
|
||||
poll.space,
|
||||
poll._id,
|
||||
poll.attachedTo,
|
||||
poll.attachedToClass,
|
||||
'polls'
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
@ -14,9 +14,9 @@
|
||||
//
|
||||
|
||||
import { Class, Doc, Ref, Space } from '@hcengineering/core'
|
||||
import { plugin, IntlString, type Asset, type Plugin, type Resource } from '@hcengineering/platform'
|
||||
import { plugin, IntlString, type Asset, type Plugin } from '@hcengineering/platform'
|
||||
import { Viewlet } from '@hcengineering/view'
|
||||
import { type AnyComponent, type ResolvedLocation } from '@hcengineering/ui'
|
||||
import { AnyComponent } from '@hcengineering/ui'
|
||||
import { Poll, Survey } from './types'
|
||||
|
||||
export * from './types'
|
||||
@ -30,6 +30,7 @@ const survey = plugin(surveyId, {
|
||||
},
|
||||
icon: {
|
||||
Application: '' as Asset,
|
||||
Info: '' as Asset,
|
||||
Poll: '' as Asset,
|
||||
Question: '' as Asset,
|
||||
QuestionKindString: '' as Asset,
|
||||
@ -37,7 +38,10 @@ const survey = plugin(surveyId, {
|
||||
QuestionKindOptions: '' as Asset,
|
||||
QuestionIsMandatory: '' as Asset,
|
||||
QuestionHasCustomOption: '' as Asset,
|
||||
Survey: '' as Asset
|
||||
Submit: '' as Asset,
|
||||
Survey: '' as Asset,
|
||||
ValidateOk: '' as Asset,
|
||||
ValidateFail: '' as Asset
|
||||
},
|
||||
space: {
|
||||
Survey: '' as Ref<Space>
|
||||
@ -52,9 +56,12 @@ const survey = plugin(surveyId, {
|
||||
Application: '' as IntlString,
|
||||
Close: '' as IntlString,
|
||||
Control: '' as IntlString,
|
||||
Completed: '' as IntlString,
|
||||
CreatePoll: '' as IntlString,
|
||||
CreateSurvey: '' as IntlString,
|
||||
DeleteOption: '' as IntlString,
|
||||
DeletePoll: '' as IntlString,
|
||||
DeletePollConfirm: '' as IntlString,
|
||||
DeleteQuestion: '' as IntlString,
|
||||
DeleteQuestionConfirm: '' as IntlString,
|
||||
Name: '' as IntlString,
|
||||
@ -75,16 +82,24 @@ const survey = plugin(surveyId, {
|
||||
QuestionIsMandatory: '' as IntlString,
|
||||
QuestionHasCustomOption: '' as IntlString,
|
||||
QuestionOptions: '' as IntlString,
|
||||
QuestionOptionPlaceholder: '' as IntlString,
|
||||
QuestionPlaceholder: '' as IntlString,
|
||||
QuestionEmptyPlaceholder: '' as IntlString,
|
||||
QuestionPlaceholderEmpty: '' as IntlString,
|
||||
QuestionPlaceholderOption: '' as IntlString,
|
||||
QuestionTooltipMandatory: '' as IntlString,
|
||||
QuestionTooltipCustomOption: '' as IntlString,
|
||||
Survey: '' as IntlString,
|
||||
Surveys: '' as IntlString,
|
||||
SurveyEdit: '' as IntlString,
|
||||
SurveyPreview: '' as IntlString,
|
||||
SurveySubmit: '' as IntlString
|
||||
SurveySubmit: '' as IntlString,
|
||||
SurveySubmitConfirm: '' as IntlString,
|
||||
ValidateFail: '' as IntlString,
|
||||
ValidateInfo: '' as IntlString,
|
||||
ValidateOk: '' as IntlString
|
||||
},
|
||||
component: {
|
||||
CreateSurvey: '' as AnyComponent,
|
||||
EditPollPanel: '' as AnyComponent,
|
||||
EditSurveyPanel: '' as AnyComponent,
|
||||
PollCollection: '' as AnyComponent,
|
||||
PollPresenter: '' as AnyComponent,
|
||||
@ -93,9 +108,6 @@ const survey = plugin(surveyId, {
|
||||
viewlet: {
|
||||
TableSurvey: '' as Ref<Viewlet>,
|
||||
TablePoll: '' as Ref<Viewlet>
|
||||
},
|
||||
resolver: {
|
||||
Location: '' as Resource<(loc: Location) => Promise<ResolvedLocation | undefined>>
|
||||
}
|
||||
})
|
||||
|
||||
|
@ -38,10 +38,19 @@ export interface Question {
|
||||
hasCustomOption: boolean
|
||||
}
|
||||
|
||||
export interface AnsweredQuestion extends Question {
|
||||
answer?: string
|
||||
answers?: number[]
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export interface Poll extends AttachedDoc {
|
||||
export interface PollData {
|
||||
survey: Ref<Survey>
|
||||
name: string
|
||||
prompt: string
|
||||
results?: { question: string, answer: string[] }[]
|
||||
questions?: AnsweredQuestion[]
|
||||
isCompleted?: boolean
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export interface Poll extends PollData, AttachedDoc {}
|
||||
|
Loading…
Reference in New Issue
Block a user