Merge branch 'staging' into develop
Some checks failed
CI / build (push) Has been cancelled
CI / uitest (push) Has been cancelled
CI / uitest-pg (push) Has been cancelled
CI / uitest-qms (push) Has been cancelled
CI / uitest-workspaces (push) Has been cancelled
CI / svelte-check (push) Has been cancelled
CI / formatting (push) Has been cancelled
CI / test (push) Has been cancelled
CI / docker-build (push) Has been cancelled
CI / dist-build (push) Has been cancelled

Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
Andrey Sobolev 2025-02-22 23:57:16 +07:00
commit ce10156fe4
No known key found for this signature in database
GPG Key ID: BD80F68D68D8F7F2
10 changed files with 215 additions and 128 deletions

View File

@ -19,7 +19,7 @@
.underline {
font-weight: 500;
input {
.antiEditBoxInput {
padding: 0.25rem 0.5rem;
background-color: var(--theme-editbox-focus-color);
border-radius: 0.25rem;
@ -43,7 +43,7 @@
&:focus-within::after { content: ''; }
}
input {
.antiEditBoxInput {
margin: 0;
padding: 0;
min-width: 0;
@ -86,7 +86,7 @@
border-radius: .375rem;
border: 1px solid transparent;
input {
.antiEditBoxInput {
color: inherit;
&::placeholder { color: var(--theme-darker-color); }
@ -109,7 +109,7 @@
font-weight: 500;
font-size: 1.5rem;
input {
.antiEditBoxInput {
font: inherit;
&::placeholder {
@ -119,17 +119,17 @@
&:hover input:not(:focus)::placeholder {
color: var(--input-hover-PlaceholderColor);
}
input:focus::placeholder {
.antiEditBoxInput:focus::placeholder {
color: var(--input-focus-PlaceholderColor);
}
&.disabled {
box-shadow: inset 0 0 0 1px var(--input-BorderColor);
&,
input {
.antiEditBoxInput {
cursor: not-allowed;
}
input::placeholder {
.antiEditBoxInput::placeholder {
color: var(--input-PlaceholderColor);
}
}
@ -139,4 +139,27 @@
content: ' *';
color: var(--theme-error-color);
}
.antiEditBoxGridWrapper {
display: grid;
&::after {
content: attr(data-value) " ";
white-space: pre-wrap;
visibility: hidden;
z-index: -1;
}
textarea {
resize: none;
}
&::after, textarea {
font: inherit;
outline: none;
background-color: transparent;
overflow: hidden;
grid-area: 1 / 1 / 2 / 2;
min-height: 1.25rem;
}
}
}

View File

@ -29,7 +29,7 @@
export let value: string | number | undefined = undefined
export let placeholder: IntlString = plugin.string.EditBoxPlaceholder
export let placeholderParam: any | undefined = undefined
export let format: 'text' | 'password' | 'number' = 'text'
export let format: 'text' | 'password' | 'number' | 'text-multiline' = 'text'
export let maxDigitsAfterPoint: number | undefined = undefined
export let kind: EditStyle = 'editbox'
export let autoFocus: boolean = false
@ -43,7 +43,7 @@
const dispatch = createEventDispatcher()
let input: HTMLInputElement
let input: HTMLInputElement | HTMLTextAreaElement
let phTranslate: string = ''
$: {
@ -135,8 +135,28 @@
class:w-full={fullSize}
style:width={maxWidth}
>
{#if format === 'password'}
{#if format === 'text-multiline'}
<div class="antiEditBoxGridWrapper" data-value={value}>
<textarea
rows="1"
class="antiEditBoxInput"
{disabled}
style:width={maxWidth}
bind:this={input}
bind:value
placeholder={phTranslate}
on:input={handleInput}
on:change
on:keydown
on:keypress
on:blur={() => {
dispatch('blur', value)
}}
/>
</div>
{:else if format === 'password'}
<input
class="antiEditBoxInput"
{disabled}
style:width={maxWidth}
id="userPassword"
@ -154,11 +174,11 @@
/>
{:else if format === 'number'}
<input
class="antiEditBoxInput number"
{disabled}
style:width={maxWidth}
bind:this={input}
type="number"
class="number"
bind:value
placeholder={phTranslate}
on:input={handleInput}
@ -171,6 +191,7 @@
/>
{:else}
<input
class="antiEditBoxInput"
{disabled}
style:width={maxWidth}
bind:this={input}

View File

@ -43,6 +43,7 @@
"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"
"ValidateOk": "Form is filled correctly",
"EditAnswers": "Edit answers"
}
}

View File

@ -43,6 +43,7 @@
"SurveySubmitConfirm": "После этого вы больше не сможете изменять ответы. Вы уверены, что хотите завершить опрос сейчас?",
"ValidateFail": "Нет ответов на некоторые обязательные вопросы",
"ValidateInfo": "Так будет выглядеть форма для пользователя. Для проверки анкеты попробуйте вводить ответы и выбирать варианты. Зеленый значок в заголовке покажет, что форма заполнена правильно",
"ValidateOk": "Форма заполнена правильно"
"ValidateOk": "Форма заполнена правильно",
"EditAnswers": "Редактировать ответы"
}
}

View File

@ -60,7 +60,7 @@
}
</script>
<div class="antiSection flex-gap-4">
<div class="antiSection flex-gap-4 poll mb-8">
{#if hasText(object.prompt)}
<div class="antiSection-header">
<span class="antiSection-header__title">
@ -73,10 +73,16 @@
<PollQuestion
bind:this={questionNodes[index]}
bind:isAnswered={isAnswered[index]}
readonly={readonly || object.isCompleted}
{readonly}
on:answered={saveAnswers}
{question}
/>
{/if}
{/each}
</div>
<style>
.poll {
user-select: text;
}
</style>

View File

@ -35,6 +35,10 @@
let object: Poll | undefined = undefined
let canSubmit = false
let requestUpdate = false
$: isCompleted = object?.isCompleted ?? false
$: editable = (!readonly && !isCompleted) || requestUpdate
$: updateObject(_id)
@ -48,19 +52,21 @@
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 })
}
}
)
requestUpdate = false
// 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 })
// }
// }
// )
await getClient().updateDoc(object._class, object.space, object._id, { isCompleted: true })
}
</script>
@ -85,30 +91,36 @@
</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}
{#if editable}
<Button
icon={IconMoreH}
iconProps={{ size: 'medium' }}
kind={'icon'}
on:click={(e) => {
showMenu(e, { object, excludedActions: [view.action.Open] })
icon={survey.icon.Submit}
label={survey.string.SurveySubmit}
kind={'primary'}
disabled={!canSubmit}
showTooltip={{ label: canSubmit ? undefined : survey.string.ValidateFail }}
on:click={submit}
/>
{:else}
<Button
icon={view.icon.Edit}
label={survey.string.EditAnswers}
on:click={() => {
requestUpdate = true
}}
/>
{/if}
<Button
icon={IconMoreH}
iconProps={{ size: 'medium' }}
kind={'icon'}
on:click={(e) => {
showMenu(e, { object, excludedActions: [view.action.Open] })
}}
/>
</svelte:fragment>
<div class="flex-col flex-grow flex-no-shrink">
<EditPoll {object} {readonly} bind:canSubmit />
<EditPoll {object} readonly={!editable} bind:canSubmit />
</div>
</Panel>
{/if}

View File

@ -18,16 +18,16 @@
import { MessageBox, getClient } from '@hcengineering/presentation'
import { Question, QuestionKind, Survey } from '@hcengineering/survey'
import {
ButtonIcon,
EditBox,
Icon,
IconDelete,
SelectPopup,
eventToHTMLElement,
showPopup,
tooltip,
ButtonIcon
tooltip
} from '@hcengineering/ui'
import { createEventDispatcher } from 'svelte'
import { createEventDispatcher, onDestroy } from 'svelte'
import survey from '../plugin'
const client = getClient()
@ -40,40 +40,65 @@
let editQuestion: EditBox
let hovered: boolean = false
$: question = parent?.questions?.[index] as Question
let defaultQuestion: Question = {
name: '',
kind: QuestionKind.STRING,
isMandatory: false,
hasCustomOption: false
}
$: question = (parent?.questions?.[index] as Question) ?? defaultQuestion
$: isNewQuestion = parent?.questions?.[index] === undefined
$: options = question?.options ?? []
$: questionIcon =
question === undefined
? survey.icon.Question
: question.kind === QuestionKind.OPTIONS
? survey.icon.QuestionKindOptions
: question.kind === QuestionKind.OPTION
? survey.icon.QuestionKindOption
: survey.icon.QuestionKindString
$: questionIcon = isNewQuestion
? survey.icon.Question
: question.kind === QuestionKind.OPTIONS
? survey.icon.QuestionKindOptions
: question.kind === QuestionKind.OPTION
? survey.icon.QuestionKindOption
: survey.icon.QuestionKindString
let haveNameChanges = false
let newOption = ''
let newQuestion = ''
onDestroy(() => {
handleExit()
})
function handleExit (): void {
void handleNameChange()
}
async function updateParent (): Promise<void> {
await client.updateDoc(parent._class, parent.space, parent._id, { questions: parent.questions })
}
async function createQuestion (): Promise<void> {
$: if (isNewQuestion && question.name.trim() !== '') {
void createQuestion()
}
function createQuestion (): Promise<void> {
if (parent.questions === undefined) {
parent.questions = []
}
parent.questions.push({
name: newQuestion,
kind: QuestionKind.STRING,
isMandatory: false,
hasCustomOption: false
})
await updateParent()
newQuestion = ''
parent.questions.push({ ...question })
defaultQuestion = { ...defaultQuestion, name: '' }
return updateParent()
}
function handleNameChange (): Promise<void> | void {
if (!haveNameChanges) return
haveNameChanges = false
if (isNewQuestion) return createQuestion()
return changeName()
}
async function changeName (): Promise<void> {
await updateParent()
if (!isNewQuestion) {
await updateParent()
}
}
async function changeKind (kind: QuestionKind): Promise<void> {
@ -348,6 +373,7 @@
}
</script>
<svelte:window on:beforeunload={handleExit} />
<div
bind:this={rootElement}
class="question-container flex-col flex-gap-2"
@ -357,27 +383,33 @@
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div class="flex-row-center flex-gap-3 text-base pr-2" on:click={focusQuestion}>
{#if question === undefined}
<ButtonIcon size={'small'} disabled icon={survey.icon.Question} />
<EditBox
bind:this={editQuestion}
kind={'editbox'}
placeholder={survey.string.QuestionPlaceholder}
bind:value={newQuestion}
on:change={createQuestion}
/>
{#if isNewQuestion}
<div class="self-start">
<ButtonIcon size={'small'} disabled icon={questionIcon} />
</div>
{:else}
<div role="presentation" draggable={!readonly} on:dragstart={rootDragStart} on:dragend={rootDragEnd}>
<div
class="self-start"
role="presentation"
draggable={!readonly}
on:dragstart={rootDragStart}
on:dragend={rootDragEnd}
>
<ButtonIcon size={'small'} disabled={readonly} icon={questionIcon} on:click={showQuestionParams} />
</div>
<EditBox
bind:this={editQuestion}
kind={'editbox'}
disabled={readonly}
placeholder={survey.string.QuestionPlaceholderEmpty}
bind:value={question.name}
on:change={changeName}
/>
{/if}
<EditBox
bind:this={editQuestion}
format={'text-multiline'}
disabled={readonly}
placeholder={survey.string.QuestionPlaceholderEmpty}
bind:value={question.name}
on:input={() => {
haveNameChanges = true
}}
on:change={handleNameChange}
/>
{#if !isNewQuestion}
{#if question.hasCustomOption && question.kind !== QuestionKind.STRING}
<div class="flex-no-shrink" use:tooltip={{ label: survey.string.QuestionTooltipCustomOption }}>
<Icon icon={survey.icon.QuestionHasCustomOption} size={'small'} />
@ -390,7 +422,7 @@
{/if}
{/if}
</div>
{#if question !== undefined && question.kind !== QuestionKind.STRING}
{#if !isNewQuestion && question.kind !== QuestionKind.STRING}
{#each options as option, index (index)}
<div
class="flex-row-center flex-gap-3 option"

View File

@ -29,6 +29,7 @@
export let readonly: boolean = false
$: questions = object?.questions ?? []
$: questionEditSlots = readonly ? questions : [...questions, {}]
async function nameChange (): Promise<void> {
await client.updateDoc(object._class, object.space, object._id, { name: object.name })
@ -114,7 +115,7 @@
<Label label={survey.string.Questions} />
</span>
</div>
{#each questions as question, index}
{#each questionEditSlots as question, index}
<div
role="listitem"
on:dragover={(ev) => {
@ -129,35 +130,22 @@
draggedOverIndex !== draggedIndex &&
draggedOverIndex !== draggedIndex + 1}
>
<EditQuestion
{index}
{readonly}
parent={object}
on:dragStart={() => {
draggedIndex = index
}}
on:dragEnd={() => {
draggedIndex = undefined
draggedOverIndex = undefined
}}
/>
{#key index}
<EditQuestion
{index}
{readonly}
parent={object}
on:dragStart={() => {
draggedIndex = index
}}
on:dragEnd={() => {
draggedIndex = undefined
draggedOverIndex = undefined
}}
/>
{/key}
</div>
{/each}
{#if !readonly}
<div
role="listitem"
on:dragover={(ev) => {
dragOver(ev, questions.length)
}}
on:dragleave={(ev) => {
dragLeave(ev, questions.length)
}}
on:drop={dragDrop}
class:dragged-over={draggedOverIndex === questions.length && draggedIndex !== questions.length - 1}
>
<EditQuestion parent={object} index={-1} />
</div>
{/if}
</div>
{/if}

View File

@ -137,7 +137,7 @@
<div class="question-answer-container flex-col flex-gap-3">
<div class="flex-row-center flex-gap-1 flex-no-shrink">
<span class="text-lg caption-color font-medium">{question.name}</span>
<strong class="text-base caption-color font-medium pre-wrap">{question.name}</strong>
{#if question.isMandatory && !readonly}
<div
class="flex-no-shrink"
@ -151,9 +151,9 @@
{#if readonly}
{#each getReadonlyAnswers() as answer}
{#if answer}
<div class="pl-6">{answer}</div>
<div class="pre-wrap">{answer}</div>
{:else}
<div class="pl-6 content-halfcontent-color">
<div class="content-halfcontent-color">
<Label label={survey.string.NoAnswer} />
</div>
{/if}
@ -180,7 +180,6 @@
{#if selectedOption === customOption}
<div class="pl-6">
<EditBox
kind={'ghost-large'}
bind:value={answer}
placeholder={survey.string.AnswerPlaceholder}
focusable
@ -206,7 +205,6 @@
{#if selectedOptions[customOption]}
<div class="pl-6">
<EditBox
kind={'ghost-large'}
bind:value={answer}
placeholder={survey.string.AnswerPlaceholder}
focusable
@ -218,13 +216,14 @@
{/if}
</div>
{:else}
<EditBox
kind={'ghost-large'}
bind:value={answer}
placeholder={survey.string.AnswerPlaceholder}
focusable
on:change={answerChange}
/>
<div>
<EditBox
format={'text-multiline'}
bind:value={answer}
placeholder={survey.string.AnswerPlaceholder}
on:change={answerChange}
/>
</div>
{/if}
</div>
@ -233,4 +232,7 @@
padding-top: var(--spacing-2);
border-top: 1px solid var(--theme-divider-color);
}
.pre-wrap {
white-space: pre-wrap;
}
</style>

View File

@ -95,7 +95,8 @@ const survey = plugin(surveyId, {
SurveySubmitConfirm: '' as IntlString,
ValidateFail: '' as IntlString,
ValidateInfo: '' as IntlString,
ValidateOk: '' as IntlString
ValidateOk: '' as IntlString,
EditAnswers: '' as IntlString
},
component: {
CreateSurvey: '' as AnyComponent,