@webitel/ui-sdk 3.0.3 → 3.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/ui-sdk.common.js +46 -43
- package/dist/ui-sdk.common.js.map +1 -1
- package/dist/ui-sdk.css +1 -1
- package/dist/ui-sdk.umd.js +49 -46
- package/dist/ui-sdk.umd.js.map +1 -1
- package/dist/ui-sdk.umd.min.js +1 -1
- package/dist/ui-sdk.umd.min.js.map +1 -1
- package/package.json +4 -1
- package/src/components/molecules/wt-datepicker/wt-datepicker.vue +2 -2
- package/src/components/molecules/wt-select/wt-select.vue +1 -1
- package/src/components/organisms/wt-table-column-select/wt-table-column-select.vue +3 -3
- package/src/locale/en/en.js +12 -0
- package/src/locale/ru/ru.js +12 -0
- package/src/locale/ua/ua.js +12 -0
- package/src/mixins/validationMixin/validationMixin.js +12 -12
- package/src/modules/AuditForm/components/__tests__/audit-form-question-read-wrapper.spec.js +14 -0
- package/src/modules/AuditForm/components/__tests__/audit-form-question-write-wrapper.spec.js +59 -0
- package/src/modules/AuditForm/components/__tests__/audit-form-question.spec.js +14 -0
- package/src/modules/AuditForm/components/__tests__/audit-form.spec.js +66 -0
- package/src/modules/AuditForm/components/audit-form-question-read-wrapper.vue +100 -0
- package/src/modules/AuditForm/components/audit-form-question-write-wrapper.vue +151 -0
- package/src/modules/AuditForm/components/audit-form-question.vue +102 -0
- package/src/modules/AuditForm/components/audit-form.vue +142 -0
- package/src/modules/AuditForm/components/questions/__tests__/audit-form-question-options.spec.js +46 -0
- package/src/modules/AuditForm/components/questions/__tests__/audit-form-question-score.spec.js +30 -0
- package/src/modules/AuditForm/components/questions/audit-form-question-options.vue +124 -0
- package/src/modules/AuditForm/components/questions/audit-form-question-score.vue +89 -0
- package/src/modules/AuditForm/composables/useDestroyableSortable.js +66 -0
- package/src/modules/AuditForm/schemas/AuditFormQuestionOptionsSchema.js +11 -0
- package/src/modules/AuditForm/schemas/AuditFormQuestionSchema.js +8 -0
- package/src/modules/AuditForm/schemas/AuditFormQuestionScoreSchema.js +6 -0
- package/src/scripts/__tests__/updateObject.spec.js +19 -0
- package/src/scripts/updateObject.js +6 -0
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<component
|
|
3
|
+
class="audit-form-question"
|
|
4
|
+
:class="[
|
|
5
|
+
`audit-form-question--mode-${mode}`
|
|
6
|
+
]"
|
|
7
|
+
v-clickaway="saveQuestion"
|
|
8
|
+
:is="component"
|
|
9
|
+
:question="question"
|
|
10
|
+
:result="result"
|
|
11
|
+
:v="v$"
|
|
12
|
+
:disable-dragging="mode === 'fill'"
|
|
13
|
+
:disable-delete="disableDelete"
|
|
14
|
+
@copy="emits('copy')"
|
|
15
|
+
@delete="emits('delete')"
|
|
16
|
+
@activate="activateQuestion"
|
|
17
|
+
@change:question="emits('update:question', $event)"
|
|
18
|
+
@change:result="emits('update:result', $event)"
|
|
19
|
+
></component>
|
|
20
|
+
</template>
|
|
21
|
+
|
|
22
|
+
<script setup>
|
|
23
|
+
import { useVuelidate } from '@vuelidate/core';
|
|
24
|
+
import { required } from '@vuelidate/validators';
|
|
25
|
+
import { computed, ref, toRefs } from 'vue';
|
|
26
|
+
import vClickaway from '../../../directives/clickaway/clickaway';
|
|
27
|
+
import QuestionWrite from './audit-form-question-write-wrapper.vue';
|
|
28
|
+
import QuestionRead from './audit-form-question-read-wrapper.vue';
|
|
29
|
+
|
|
30
|
+
const props = defineProps({
|
|
31
|
+
question: {
|
|
32
|
+
type: Object,
|
|
33
|
+
required: true,
|
|
34
|
+
},
|
|
35
|
+
result: {
|
|
36
|
+
type: Object,
|
|
37
|
+
},
|
|
38
|
+
mode: {
|
|
39
|
+
type: String,
|
|
40
|
+
},
|
|
41
|
+
disableDelete: {
|
|
42
|
+
type: Boolean,
|
|
43
|
+
default: true,
|
|
44
|
+
},
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
const emits = defineEmits([
|
|
48
|
+
'copy',
|
|
49
|
+
'delete',
|
|
50
|
+
'update:question',
|
|
51
|
+
'update:result',
|
|
52
|
+
]);
|
|
53
|
+
|
|
54
|
+
const QuestionState = {
|
|
55
|
+
SAVED: 'saved',
|
|
56
|
+
EDIT: 'edit',
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const state = ref(QuestionState.SAVED);
|
|
60
|
+
|
|
61
|
+
const { question } = toRefs(props);
|
|
62
|
+
|
|
63
|
+
// validate only "create" mode
|
|
64
|
+
const v$ = useVuelidate(computed(() => (
|
|
65
|
+
(props.mode === 'create')
|
|
66
|
+
? {
|
|
67
|
+
question: {
|
|
68
|
+
text: { required },
|
|
69
|
+
},
|
|
70
|
+
$autoDirty: true,
|
|
71
|
+
} : {})), { question });
|
|
72
|
+
|
|
73
|
+
const component = computed(() => {
|
|
74
|
+
if (props.mode === 'create') {
|
|
75
|
+
if (state.value === QuestionState.SAVED) return QuestionRead;
|
|
76
|
+
return QuestionWrite;
|
|
77
|
+
}
|
|
78
|
+
return QuestionRead;
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
function saveQuestion() {
|
|
82
|
+
state.value = QuestionState.SAVED;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function activateQuestion() {
|
|
86
|
+
if (props.mode !== 'create') return;
|
|
87
|
+
state.value = QuestionState.EDIT;
|
|
88
|
+
}
|
|
89
|
+
</script>
|
|
90
|
+
|
|
91
|
+
<style lang="scss" scoped>
|
|
92
|
+
.audit-form-question {
|
|
93
|
+
padding: var(--spacing-sm);
|
|
94
|
+
background: var(--main-color);
|
|
95
|
+
border-radius: var(--border-radius);
|
|
96
|
+
box-shadow: var(--elevation-1);
|
|
97
|
+
|
|
98
|
+
&--mode-create.audit-form-question-read {
|
|
99
|
+
cursor: pointer;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
</style>
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<section class="audit-form">
|
|
3
|
+
<div
|
|
4
|
+
class="audit-form__sortable-wrapper"
|
|
5
|
+
ref="sortableWrapper"
|
|
6
|
+
v-if="!reloadSortable"
|
|
7
|
+
>
|
|
8
|
+
<audit-form-question
|
|
9
|
+
v-for="(question, key) of questions"
|
|
10
|
+
:key="key"
|
|
11
|
+
:question="question"
|
|
12
|
+
:result="(result && result[key]) ? result[key] : null"
|
|
13
|
+
:mode="mode"
|
|
14
|
+
:disable-delete="questions.length <= 1"
|
|
15
|
+
@copy="copyQuestion({ question, key })"
|
|
16
|
+
@delete="deleteQuestion({ question, key})"
|
|
17
|
+
@update:question="handleQuestionUpdate({ key, value: $event })"
|
|
18
|
+
@update:result="handleResultUpdate({ key, value: $event })"
|
|
19
|
+
></audit-form-question>
|
|
20
|
+
</div>
|
|
21
|
+
<wt-button
|
|
22
|
+
class="audit-form__add-button"
|
|
23
|
+
v-if="mode === 'create'"
|
|
24
|
+
:disabled="isInvalidForm"
|
|
25
|
+
@click="addQuestion"
|
|
26
|
+
>{{ $t('webitelUI.auditForm.addQuestion') }}
|
|
27
|
+
</wt-button>
|
|
28
|
+
</section>
|
|
29
|
+
</template>
|
|
30
|
+
|
|
31
|
+
<script setup>
|
|
32
|
+
import cloneDeep from 'lodash/cloneDeep';
|
|
33
|
+
import {
|
|
34
|
+
watch, watchEffect, ref, computed,
|
|
35
|
+
} from 'vue';
|
|
36
|
+
import { useVuelidate } from '@vuelidate/core';
|
|
37
|
+
import { useDestroyableSortable } from '../composables/useDestroyableSortable';
|
|
38
|
+
import AuditFormQuestion from './audit-form-question.vue';
|
|
39
|
+
import WtButton from '../../../components/atoms/wt-button/wt-button.vue';
|
|
40
|
+
import { generateQuestionSchema } from '../schemas/AuditFormQuestionSchema';
|
|
41
|
+
|
|
42
|
+
const props = defineProps({
|
|
43
|
+
mode: {
|
|
44
|
+
type: String,
|
|
45
|
+
required: true,
|
|
46
|
+
/*
|
|
47
|
+
* Available options: ['create', 'fill']
|
|
48
|
+
* */
|
|
49
|
+
},
|
|
50
|
+
questions: {
|
|
51
|
+
type: Array,
|
|
52
|
+
required: true,
|
|
53
|
+
},
|
|
54
|
+
result: {
|
|
55
|
+
type: Array,
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
const emit = defineEmits([
|
|
60
|
+
'update:questions',
|
|
61
|
+
'update:result',
|
|
62
|
+
'update:validation',
|
|
63
|
+
]);
|
|
64
|
+
|
|
65
|
+
const v$ = useVuelidate();
|
|
66
|
+
|
|
67
|
+
const isInvalidForm = computed(() => !!v$.value.$errors.length);
|
|
68
|
+
|
|
69
|
+
function addQuestion({ index, question } = {}) {
|
|
70
|
+
const questions = [...props.questions];
|
|
71
|
+
const newQuestion = question || generateQuestionSchema();
|
|
72
|
+
if (index != null) questions.splice(index, 0, newQuestion);
|
|
73
|
+
else questions.push(newQuestion);
|
|
74
|
+
emit('update:questions', questions);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function handleQuestionUpdate({ key, value }) {
|
|
78
|
+
const questions = [...props.questions];
|
|
79
|
+
questions[key] = value;
|
|
80
|
+
emit('update:questions', questions);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function copyQuestion({ question, key }) {
|
|
84
|
+
const questions = [...props.questions];
|
|
85
|
+
questions.splice(key + 1, 0, cloneDeep(question));
|
|
86
|
+
emit('update:questions', questions);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function deleteQuestion({ key }) {
|
|
90
|
+
const questions = [...props.questions];
|
|
91
|
+
questions.splice(key, 1);
|
|
92
|
+
emit('update:questions', questions);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function changeQuestionsOrder({ oldIndex, newIndex }) {
|
|
96
|
+
const questions = [...props.questions];
|
|
97
|
+
const [el] = questions.splice(oldIndex, 1);
|
|
98
|
+
questions.splice(newIndex, 0, el);
|
|
99
|
+
emit('update:questions', questions);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function handleResultUpdate({ key, value }) {
|
|
103
|
+
const result = [...props.result];
|
|
104
|
+
result[key] = value;
|
|
105
|
+
emit('update:result', result);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function initResult() {
|
|
109
|
+
const result = props.questions.map(() => null);
|
|
110
|
+
emit('update:result', result);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const sortableWrapper = ref(null);
|
|
114
|
+
|
|
115
|
+
const { reloadSortable } = useDestroyableSortable(sortableWrapper, {
|
|
116
|
+
handle: '.audit-form-question-read__drag-icon',
|
|
117
|
+
disabled: props.mode !== 'create',
|
|
118
|
+
onEnd: ({ newIndex, oldIndex }) => {
|
|
119
|
+
if (newIndex === oldIndex) return;
|
|
120
|
+
changeQuestionsOrder({ oldIndex, newIndex });
|
|
121
|
+
},
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
watch(v$, () => emit('update:validation', v$));
|
|
125
|
+
watchEffect(initResult);
|
|
126
|
+
</script>
|
|
127
|
+
|
|
128
|
+
<style lang="scss" scoped>
|
|
129
|
+
.audit-form {
|
|
130
|
+
display: flex;
|
|
131
|
+
flex-direction: column;
|
|
132
|
+
gap: var(--spacing-sm);
|
|
133
|
+
|
|
134
|
+
&__sortable-wrapper {
|
|
135
|
+
display: contents;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
.audit-form__add-button {
|
|
140
|
+
align-self: flex-end;
|
|
141
|
+
}
|
|
142
|
+
</style>
|
package/src/modules/AuditForm/components/questions/__tests__/audit-form-question-options.spec.js
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { shallowMount, mount } from '@vue/test-utils';
|
|
2
|
+
import AuditFormQuestionOptions from '../audit-form-question-options.vue';
|
|
3
|
+
|
|
4
|
+
describe('AuditFormQuestionOptions', () => {
|
|
5
|
+
it('renders a component', () => {
|
|
6
|
+
const wrapper = shallowMount(AuditFormQuestionOptions, {
|
|
7
|
+
props: {
|
|
8
|
+
question: {},
|
|
9
|
+
},
|
|
10
|
+
});
|
|
11
|
+
expect(wrapper.isVisible()).toBe(true);
|
|
12
|
+
});
|
|
13
|
+
it('adds new question option at "add" button click', () => {
|
|
14
|
+
const wrapper = shallowMount(AuditFormQuestionOptions, {
|
|
15
|
+
props: {
|
|
16
|
+
question: { options: [{}] },
|
|
17
|
+
mode: 'write',
|
|
18
|
+
},
|
|
19
|
+
});
|
|
20
|
+
wrapper.findComponent('.audit-form-question-options-write__add-button').vm.$emit('click');
|
|
21
|
+
expect(wrapper.emitted()['change:question'][0][0].options.length).toBe(2);
|
|
22
|
+
});
|
|
23
|
+
it('deletes existing question option at "delete" icon-btn click', () => {
|
|
24
|
+
const wrapper = mount(AuditFormQuestionOptions, {
|
|
25
|
+
props: {
|
|
26
|
+
question: { options: [{}] },
|
|
27
|
+
mode: 'write',
|
|
28
|
+
},
|
|
29
|
+
});
|
|
30
|
+
const deleteBtn = wrapper.findComponent({ name: 'wt-icon-btn' });
|
|
31
|
+
expect(deleteBtn.props().icon).toBe('bucket');
|
|
32
|
+
deleteBtn.vm.$emit('click');
|
|
33
|
+
expect(wrapper.emitted()['change:question'][0][0].options.length).toBe(0);
|
|
34
|
+
});
|
|
35
|
+
it('emits result change with selected radio option score', () => {
|
|
36
|
+
const score = 11;
|
|
37
|
+
const wrapper = shallowMount(AuditFormQuestionOptions, {
|
|
38
|
+
props: {
|
|
39
|
+
question: { options: [{ score }] },
|
|
40
|
+
mode: 'read',
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
wrapper.findComponent({ name: 'wt-radio' }).vm.$emit('input', { score });
|
|
44
|
+
expect(wrapper.emitted()['change:result'][0][0]).toEqual({ score });
|
|
45
|
+
});
|
|
46
|
+
});
|
package/src/modules/AuditForm/components/questions/__tests__/audit-form-question-score.spec.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { shallowMount } from '@vue/test-utils';
|
|
2
|
+
import AuditFormQuestionScore from '../audit-form-question-score.vue';
|
|
3
|
+
|
|
4
|
+
describe('AuditFormQuestionScore', () => {
|
|
5
|
+
it('renders a component', () => {
|
|
6
|
+
const wrapper = shallowMount(AuditFormQuestionScore, {
|
|
7
|
+
props: {
|
|
8
|
+
question: {
|
|
9
|
+
min: 0,
|
|
10
|
+
max: 10,
|
|
11
|
+
},
|
|
12
|
+
},
|
|
13
|
+
});
|
|
14
|
+
expect(wrapper.isVisible()).toBe(true);
|
|
15
|
+
});
|
|
16
|
+
it('emits result change with selected radio score', () => {
|
|
17
|
+
const min = 8;
|
|
18
|
+
const wrapper = shallowMount(AuditFormQuestionScore, {
|
|
19
|
+
props: {
|
|
20
|
+
question: {
|
|
21
|
+
min,
|
|
22
|
+
max: 10,
|
|
23
|
+
},
|
|
24
|
+
mode: 'read',
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
wrapper.findComponent({ name: 'wt-radio' }).vm.$emit('input');
|
|
28
|
+
expect(wrapper.emitted()['change:result'][0][0]).toEqual({ score: min });
|
|
29
|
+
});
|
|
30
|
+
});
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<article class="audit-form-question-options">
|
|
3
|
+
<div
|
|
4
|
+
v-if="mode === 'write'"
|
|
5
|
+
class="audit-form-question-options-write"
|
|
6
|
+
>
|
|
7
|
+
<div
|
|
8
|
+
v-for="({ text, score }, key) of question.options"
|
|
9
|
+
:key="key"
|
|
10
|
+
class="audit-form-question-options-write-row"
|
|
11
|
+
>
|
|
12
|
+
<wt-input
|
|
13
|
+
:value="text"
|
|
14
|
+
:label="$tc('webitelUI.auditForm.option', 1)"
|
|
15
|
+
@input="updateQuestion({ path: `options[${key}].text`, value: $event })"
|
|
16
|
+
></wt-input>
|
|
17
|
+
<wt-input
|
|
18
|
+
:value="score"
|
|
19
|
+
:label="$tc('webitelUI.auditForm.score', 1)"
|
|
20
|
+
type="number"
|
|
21
|
+
@input="updateQuestion({ path: `options[${key}].score`, value: $event })"
|
|
22
|
+
></wt-input>
|
|
23
|
+
<wt-tooltip>
|
|
24
|
+
<template v-slot:activator>
|
|
25
|
+
<wt-icon-btn
|
|
26
|
+
icon="bucket"
|
|
27
|
+
@click="deleteQuestionOption({ key })"
|
|
28
|
+
></wt-icon-btn>
|
|
29
|
+
</template>
|
|
30
|
+
{{ $t('reusable.delete') }}
|
|
31
|
+
</wt-tooltip>
|
|
32
|
+
</div>
|
|
33
|
+
<wt-button
|
|
34
|
+
class="audit-form-question-options-write__add-button"
|
|
35
|
+
@click="addQuestionOption"
|
|
36
|
+
>{{ $t('reusable.add') }}
|
|
37
|
+
</wt-button>
|
|
38
|
+
</div>
|
|
39
|
+
<div
|
|
40
|
+
v-else-if="mode === 'read'"
|
|
41
|
+
class="audit-form-question-options-read"
|
|
42
|
+
>
|
|
43
|
+
<wt-radio
|
|
44
|
+
v-for="({ text, score }) of question.options"
|
|
45
|
+
:key="score"
|
|
46
|
+
:label="text"
|
|
47
|
+
:value="score"
|
|
48
|
+
:selected="result ? result.score : result"
|
|
49
|
+
@input="emit('change:result', { score })"
|
|
50
|
+
></wt-radio>
|
|
51
|
+
</div>
|
|
52
|
+
<div v-else>Unknown mode: {{ mode }}</div>
|
|
53
|
+
</article>
|
|
54
|
+
</template>
|
|
55
|
+
|
|
56
|
+
<script setup>
|
|
57
|
+
import updateObject from '../../../../scripts/updateObject';
|
|
58
|
+
import WtTooltip from '../../../../components/atoms/wt-tooltip/wt-tooltip.vue';
|
|
59
|
+
import WtIconBtn from '../../../../components/molecules/wt-icon-btn/wt-icon-btn.vue';
|
|
60
|
+
import WtInput from '../../../../components/molecules/wt-input/wt-input.vue';
|
|
61
|
+
import WtButton from '../../../../components/atoms/wt-button/wt-button.vue';
|
|
62
|
+
import WtRadio from '../../../../components/molecules/wt-radio/wt-radio.vue';
|
|
63
|
+
import { generateOption } from '../../schemas/AuditFormQuestionOptionsSchema';
|
|
64
|
+
|
|
65
|
+
const props = defineProps({
|
|
66
|
+
question: {
|
|
67
|
+
type: Object,
|
|
68
|
+
required: true,
|
|
69
|
+
},
|
|
70
|
+
result: {
|
|
71
|
+
type: Object,
|
|
72
|
+
},
|
|
73
|
+
mode: {
|
|
74
|
+
// options: ['read', 'write']
|
|
75
|
+
type: String,
|
|
76
|
+
default: 'read',
|
|
77
|
+
},
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
const emit = defineEmits([
|
|
81
|
+
'change:question',
|
|
82
|
+
'change:result',
|
|
83
|
+
]);
|
|
84
|
+
|
|
85
|
+
function updateQuestion({ path, value }) {
|
|
86
|
+
emit('change:question', updateObject({ obj: props.question, path, value }));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function addQuestionOption() {
|
|
90
|
+
const options = [...props.question.options, generateOption()];
|
|
91
|
+
return updateQuestion({ path: 'options', value: options });
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function deleteQuestionOption({ key }) {
|
|
95
|
+
const options = [...props.question.options];
|
|
96
|
+
options.splice(key, 1);
|
|
97
|
+
return updateQuestion({ path: 'options', value: options });
|
|
98
|
+
}
|
|
99
|
+
</script>
|
|
100
|
+
|
|
101
|
+
<style lang="scss" scoped>
|
|
102
|
+
.audit-form-question-options-write {
|
|
103
|
+
display: flex;
|
|
104
|
+
flex-direction: column;
|
|
105
|
+
gap: var(--spacing-sm);
|
|
106
|
+
|
|
107
|
+
&__add-button {
|
|
108
|
+
align-self: flex-start;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
.audit-form-question-options-write-row {
|
|
113
|
+
display: grid;
|
|
114
|
+
grid-template-columns: 3fr 1fr 24px;
|
|
115
|
+
gap: var(--spacing-sm);
|
|
116
|
+
align-items: center;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
.audit-form-question-options-read {
|
|
120
|
+
display: flex;
|
|
121
|
+
flex-direction: column;
|
|
122
|
+
gap: var(--spacing-sm);
|
|
123
|
+
}
|
|
124
|
+
</style>
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<article class="audit-form-question-score">
|
|
3
|
+
<div
|
|
4
|
+
v-if="mode === 'write'"
|
|
5
|
+
class="audit-form-question-score-write"
|
|
6
|
+
>
|
|
7
|
+
<wt-input
|
|
8
|
+
:value="question.min"
|
|
9
|
+
type="number"
|
|
10
|
+
@input="updateQuestion({ path: 'min', value: $event })"
|
|
11
|
+
></wt-input>
|
|
12
|
+
<wt-input
|
|
13
|
+
:value="question.max"
|
|
14
|
+
type="number"
|
|
15
|
+
@input="updateQuestion({ path: 'max', value: $event })"
|
|
16
|
+
></wt-input>
|
|
17
|
+
</div>
|
|
18
|
+
<div
|
|
19
|
+
v-else-if="mode === 'read'"
|
|
20
|
+
class="audit-form-question-score-read"
|
|
21
|
+
>
|
|
22
|
+
<wt-radio
|
|
23
|
+
v-for="(value) of scoreRange"
|
|
24
|
+
:key="value"
|
|
25
|
+
:label="`${value}`"
|
|
26
|
+
:value="value"
|
|
27
|
+
:selected="result ? result.score : result"
|
|
28
|
+
@input="emit('change:result', { score: value })"
|
|
29
|
+
></wt-radio>
|
|
30
|
+
</div>
|
|
31
|
+
</article>
|
|
32
|
+
</template>
|
|
33
|
+
|
|
34
|
+
<script setup>
|
|
35
|
+
import { computed } from 'vue';
|
|
36
|
+
import updateObject from '../../../../scripts/updateObject';
|
|
37
|
+
import WtRadio from '../../../../components/molecules/wt-radio/wt-radio.vue';
|
|
38
|
+
import WtInput from '../../../../components/molecules/wt-input/wt-input.vue';
|
|
39
|
+
|
|
40
|
+
const props = defineProps({
|
|
41
|
+
question: {
|
|
42
|
+
type: Object,
|
|
43
|
+
required: true,
|
|
44
|
+
},
|
|
45
|
+
result: {
|
|
46
|
+
type: Object,
|
|
47
|
+
},
|
|
48
|
+
mode: {
|
|
49
|
+
// options: ['read', 'write']
|
|
50
|
+
type: String,
|
|
51
|
+
default: 'read',
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
const emit = defineEmits([
|
|
56
|
+
'change:question',
|
|
57
|
+
'change:result',
|
|
58
|
+
]);
|
|
59
|
+
|
|
60
|
+
const scoreRange = computed(() => {
|
|
61
|
+
if (props.question.min > props.question.max) return [];
|
|
62
|
+
const result = [];
|
|
63
|
+
let i = +props.question.min;
|
|
64
|
+
do {
|
|
65
|
+
result.push(i);
|
|
66
|
+
i += 1;
|
|
67
|
+
} while (i <= props.question.max);
|
|
68
|
+
return result;
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
function updateQuestion({ path, value }) {
|
|
72
|
+
emit('change:question', updateObject({ obj: props.question, path, value }));
|
|
73
|
+
}
|
|
74
|
+
</script>
|
|
75
|
+
|
|
76
|
+
<style lang="scss" scoped>
|
|
77
|
+
.audit-form-question-score-write {
|
|
78
|
+
display: grid;
|
|
79
|
+
grid-template-columns: 100px 100px;
|
|
80
|
+
gap: var(--spacing-sm);
|
|
81
|
+
margin-right: calc(var(--spacing-sm) + 24px); // icon offset
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
.audit-form-question-score-read {
|
|
85
|
+
display: flex;
|
|
86
|
+
flex-wrap: nowrap;
|
|
87
|
+
gap: var(--spacing-sm);
|
|
88
|
+
}
|
|
89
|
+
</style>
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import Sortable from 'sortablejs';
|
|
2
|
+
import {
|
|
3
|
+
ref,
|
|
4
|
+
nextTick,
|
|
5
|
+
onMounted,
|
|
6
|
+
onUnmounted,
|
|
7
|
+
} from 'vue';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @param elRef
|
|
11
|
+
* @param options
|
|
12
|
+
* @returns {{reloadSortable: Ref<UnwrapRef<boolean>>}}
|
|
13
|
+
*
|
|
14
|
+
* @description initializes Sortable on passed element ref and reinitializes it after each reorder
|
|
15
|
+
*
|
|
16
|
+
* WHY TO REINITIALIZE SORTABLE AFTER EACH ARRAY REORDER?
|
|
17
|
+
* Sortable and Vue both changing DOM so that there are collisions
|
|
18
|
+
* because both Vue and Sortable try to put element to the position. So element is put incorrectly.
|
|
19
|
+
*
|
|
20
|
+
* There are 2 vue packages for sortable (vue 2 and vue 3), but I had encountered issues when tried to use them.
|
|
21
|
+
* Also, it seems to me that they aren't supported now :(
|
|
22
|
+
*
|
|
23
|
+
* So that I decided to reinitialize Sortable each time order changes to represent it correctly.
|
|
24
|
+
* Bad decision, but I haven't come up with a better one
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
// eslint-disable-next-line import/prefer-default-export
|
|
28
|
+
export function useDestroyableSortable(elRef, options) {
|
|
29
|
+
let sortable = null;
|
|
30
|
+
|
|
31
|
+
const reloadSortable = ref(false);
|
|
32
|
+
|
|
33
|
+
function destroySortable() {
|
|
34
|
+
return sortable.destroy();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function initSortable() {
|
|
38
|
+
const replaceSortable = async () => {
|
|
39
|
+
if (!sortable) return;
|
|
40
|
+
destroySortable();
|
|
41
|
+
reloadSortable.value = true;
|
|
42
|
+
await nextTick();
|
|
43
|
+
reloadSortable.value = false;
|
|
44
|
+
await nextTick();
|
|
45
|
+
initSortable();
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
sortable = Sortable.create(elRef.value, {
|
|
49
|
+
...options,
|
|
50
|
+
onEnd: async (e) => {
|
|
51
|
+
if (options.onEnd) options.onEnd(e);
|
|
52
|
+
await replaceSortable();
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
onMounted(() => {
|
|
58
|
+
initSortable();
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
onUnmounted(() => {
|
|
62
|
+
destroySortable();
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
return { reloadSortable };
|
|
66
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import updateObject from '../updateObject';
|
|
2
|
+
|
|
3
|
+
describe('updateObject', () => {
|
|
4
|
+
it('doesn\'t mutate original object', () => {
|
|
5
|
+
const original = { jest: 'jest' };
|
|
6
|
+
updateObject({ obj: original, path: 'jest', value: '123' });
|
|
7
|
+
expect(original.jest).toBe('jest');
|
|
8
|
+
});
|
|
9
|
+
it('changes shallow value', () => {
|
|
10
|
+
const original = { jest: 'jest' };
|
|
11
|
+
const result = { jest: 'huest' };
|
|
12
|
+
expect(updateObject({ obj: original, path: 'jest', value: 'huest' })).toEqual(result);
|
|
13
|
+
});
|
|
14
|
+
it('changes deep value', () => {
|
|
15
|
+
const original = { jest: { deep: 'jest' } };
|
|
16
|
+
const result = { jest: { deep: 'huest' } };
|
|
17
|
+
expect(updateObject({ obj: original, path: 'jest.deep', value: 'huest' })).toEqual(result);
|
|
18
|
+
});
|
|
19
|
+
});
|