diff --git a/packages/viewer/src/api/icon.api.ts b/packages/viewer/src/api/icon.api.ts index 18df91957a..0a014b9f0f 100644 --- a/packages/viewer/src/api/icon.api.ts +++ b/packages/viewer/src/api/icon.api.ts @@ -3,12 +3,7 @@ import axios from 'axios' import { fromString } from 'ol/color' import { getViewerDedicatedServicesBaseUrl } from '@/config/baseUrl.config' -import { - type FeatureStyleColor, - type FeatureStyleSize, - LARGE, - RED, -} from '@/utils/featureStyleUtils' +import { type FeatureStyleColor, LARGE, RED } from '@/utils/featureStyleUtils' /** * Generate an icon URL from its template. If no iconScale is given, the default scale 1 will be @@ -17,19 +12,18 @@ import { * * @returns A full URL to this icon on the service-icons backend */ -export function generateIconURL( - icon: DrawingIcon, - iconColor: FeatureStyleColor = RED, - iconSize: FeatureStyleSize = LARGE -) { +export function generateIconURL(icon: DrawingIcon, iconColor: FeatureStyleColor = RED) { const rgb = fromString(iconColor.fill) - return icon.imageTemplateURL - .replace('{icon_set_name}', icon.iconSetName) - .replace('{icon_name}', icon.name) - .replace('{icon_scale}', iconSize.iconScale + 'x') - .replace('{r}', `${rgb[0]}`) - .replace('{g}', `${rgb[1]}`) - .replace('{b}', `${rgb[2]}`) + return ( + icon.imageTemplateURL + .replace('{icon_set_name}', icon.iconSetName) + .replace('{icon_name}', icon.name) + // we always use the LARGE icon scale and resize the icon with the property in KMLs + .replace('{icon_scale}', LARGE.iconScale + 'x') + .replace('{r}', `${rgb[0]}`) + .replace('{g}', `${rgb[1]}`) + .replace('{b}', `${rgb[2]}`) + ) } /** diff --git a/packages/viewer/src/modules/infobox/components/styling/DrawingStyleMediaLink.vue b/packages/viewer/src/modules/infobox/components/styling/DrawingStyleMediaLink.vue index 7d19edc4d5..3829ba6368 100644 --- a/packages/viewer/src/modules/infobox/components/styling/DrawingStyleMediaLink.vue +++ b/packages/viewer/src/modules/infobox/components/styling/DrawingStyleMediaLink.vue @@ -2,8 +2,10 @@ import { ref } from 'vue' import { useI18n } from 'vue-i18n' +import type { ValidationResult } from '@/utils/composables/useFieldValidation' + import { MediaType } from '@/modules/infobox/DrawingStyleMediaTypes.enum' -import TextInput, { type TextInputValidateResult } from '@/utils/components/TextInput.vue' +import TextInput from '@/utils/components/TextInput.vue' import { isValidUrl } from '@/utils/utils' const { mediaType, urlLabel, descriptionLabel } = defineProps<{ @@ -17,8 +19,8 @@ const emit = defineEmits<{ }>() const { t } = useI18n() -const generatedMediaLink = ref(undefined) -const linkDescription = ref(undefined) +const generatedMediaLink = ref() +const linkDescription = ref() const isFormValid = ref(false) const activateValidation = ref(false) @@ -87,7 +89,7 @@ function validateForm(): boolean { return isFormValid.value } -function onUrlValidate(result: TextInputValidateResult): void { +function onUrlValidate(result: ValidationResult): void { isFormValid.value = result.valid } @@ -117,14 +119,14 @@ function onUrlValidate(result: TextInputValidateResult): void { placeholder="paste_url" :validate="validateUrl" data-cy="drawing-style-media-url" - @keydown.enter="addLink()" + @keydown.enter="addLink" @validate="onUrlValidate" > diff --git a/packages/viewer/src/modules/menu/components/advancedTools/ImportFile/ImportFileLocalTab.vue b/packages/viewer/src/modules/menu/components/advancedTools/ImportFile/ImportFileLocalTab.vue index c5a546b4f3..415e09d875 100644 --- a/packages/viewer/src/modules/menu/components/advancedTools/ImportFile/ImportFileLocalTab.vue +++ b/packages/viewer/src/modules/menu/components/advancedTools/ImportFile/ImportFileLocalTab.vue @@ -4,6 +4,8 @@ import type { ErrorMessage } from '@swissgeo/log/Message' import log from '@swissgeo/log' import { computed, ref } from 'vue' +import type { ValidationResult } from '@/utils/composables/useFieldValidation' + import ImportFileButtons from '@/modules/menu/components/advancedTools/ImportFile/ImportFileButtons.vue' import generateErrorMessageFromErrorType from '@/modules/menu/components/advancedTools/ImportFile/parser/errors/generateErrorMessageFromErrorType.utils' import useImportFile from '@/modules/menu/components/advancedTools/ImportFile/useImportFile.composable' @@ -17,12 +19,12 @@ const { active = false } = defineProps<{ active?: boolean }>() +const selectedFile = defineModel({ default: undefined }) + // Reactive data const loadingFile = ref(false) -const selectedFile = ref() const errorFileLoadingMessage = ref() const isFormValid = ref(false) -const activateValidation = ref(false) const importSuccessMessage = ref('') const buttonState = computed(() => (loadingFile.value ? 'loading' : 'default')) @@ -31,7 +33,6 @@ const buttonState = computed(() => (loadingFile.value ? 'loading' : 'default')) async function loadFile() { importSuccessMessage.value = '' errorFileLoadingMessage.value = undefined - activateValidation.value = true loadingFile.value = true if (isFormValid.value && selectedFile.value) { @@ -52,8 +53,8 @@ async function loadFile() { loadingFile.value = false } -function validateForm(valid: boolean) { - isFormValid.value = valid +function validateForm(validation: ValidationResult) { + isFormValid.value = validation.valid } @@ -74,8 +75,7 @@ function validateForm(valid: boolean) { required :accepted-file-types="acceptedFileTypes" :placeholder="'no_file'" - :activate-validation="activateValidation" - :invalid-marker="!!errorFileLoadingMessage" + :force-invalid="!!errorFileLoadingMessage" :invalid-message="errorFileLoadingMessage?.msg" :invalid-message-extra-params="errorFileLoadingMessage?.params" :valid-message="importSuccessMessage" diff --git a/packages/viewer/src/modules/menu/components/advancedTools/ImportFile/ImportFileOnlineTab.vue b/packages/viewer/src/modules/menu/components/advancedTools/ImportFile/ImportFileOnlineTab.vue index 124ed77b56..63d438279f 100644 --- a/packages/viewer/src/modules/menu/components/advancedTools/ImportFile/ImportFileOnlineTab.vue +++ b/packages/viewer/src/modules/menu/components/advancedTools/ImportFile/ImportFileOnlineTab.vue @@ -4,15 +4,13 @@ import { ErrorMessage, WarningMessage } from '@swissgeo/log/Message' import { type ComponentPublicInstance, computed, onMounted, ref, useTemplateRef, watch } from 'vue' import type { ActionDispatcher } from '@/store/types' +import type { ValidationResult } from '@/utils/composables/useFieldValidation' import ImportFileButtons from '@/modules/menu/components/advancedTools/ImportFile/ImportFileButtons.vue' import generateErrorMessageFromErrorType from '@/modules/menu/components/advancedTools/ImportFile/parser/errors/generateErrorMessageFromErrorType.utils' import useImportFile from '@/modules/menu/components/advancedTools/ImportFile/useImportFile.composable' import useUIStore from '@/store/modules/ui' -import TextInput, { - type TextInputExposed, - type TextInputValidateResult, -} from '@/utils/components/TextInput.vue' +import TextInput, { type TextInputExposed } from '@/utils/components/TextInput.vue' import { isValidUrl } from '@/utils/utils' const dispatcher: ActionDispatcher = { @@ -27,14 +25,14 @@ const uiStore = useUIStore() const { handleFileSource } = useImportFile() -// Reactive data +const fileUrl = defineModel({ default: '' }) + const isLoading = ref(false) -const fileUrlInput = useTemplateRef>('fileUrlInput') -const fileUrl = ref('') const importSuccessMessage = ref('') const errorFileLoadingMessage = ref() const isFormValid = ref(false) -const activateValidation = ref(false) + +const fileUrlInput = useTemplateRef>('fileUrlInput') const buttonState = computed<'loading' | 'default'>(() => (isLoading.value ? 'loading' : 'default')) @@ -54,9 +52,7 @@ onMounted(() => { } }) -// Methods - -function validateUrl(url?: string): TextInputValidateResult { +function validateUrl(url?: string): ValidationResult { if (!url) { return { valid: false, invalidMessage: 'no_url' } } else if (!isValidUrl(url)) { @@ -66,12 +62,11 @@ function validateUrl(url?: string): TextInputValidateResult { } function validateForm() { - activateValidation.value = true return isFormValid.value } -function onUrlValidate(result: TextInputValidateResult) { - isFormValid.value = result.valid +function onUrlValidate(validation: ValidationResult) { + isFormValid.value = validation.valid } function onUrlChange() { @@ -132,8 +127,7 @@ async function loadFile() { required class="mb-2" placeholder="import_file_url_placeholder" - :activate-validation="activateValidation" - :invalid-marker="!!errorFileLoadingMessage" + :force-invalid="!!errorFileLoadingMessage" :invalid-message="errorFileLoadingMessage?.msg" :invalid-message-params="errorFileLoadingMessage?.params" :valid-message="importSuccessMessage" diff --git a/packages/viewer/src/modules/menu/components/help/ReportProblemButton.vue b/packages/viewer/src/modules/menu/components/help/ReportProblemButton.vue index 7e32e9ded5..07bb259e64 100644 --- a/packages/viewer/src/modules/menu/components/help/ReportProblemButton.vue +++ b/packages/viewer/src/modules/menu/components/help/ReportProblemButton.vue @@ -6,6 +6,7 @@ import { computed, type ComputedRef, nextTick, ref, useTemplateRef, watch } from import { useI18n } from 'vue-i18n' import type { ActionDispatcher } from '@/store/types' +import type { ValidationResult } from '@/utils/composables/useFieldValidation' import sendFeedback, { ATTACHMENT_MAX_SIZE, KML_MAX_SIZE } from '@/api/feedback.api' import { getKmlUrl } from '@/api/files.api' @@ -84,7 +85,7 @@ const request = ref({ completed: false, }) const shortLink = ref('') -const activateValidation = ref(false) +const userHasTriedToSubmit = ref(false) const isMessageValid = ref(false) // by default attachment and email are valid as they are optional const isAttachmentValid = ref(true) @@ -118,15 +119,10 @@ watch( { deep: true } ) -// Methods - -function validate() { - activateValidation.value = true - return isFormValid.value -} - async function sendReportProblem() { - if (!validate() && validationResult.value) { + userHasTriedToSubmit.value = true + + if (!isFormValid.value && validationResult.value) { // scrolling down to make sure the message with validation result is visible to the user validationResult.value.scrollIntoView() return @@ -168,7 +164,6 @@ async function sendReportProblem() { } function closeAndCleanForm() { - activateValidation.value = false showReportProblemForm.value = false // reset the state @@ -183,16 +178,16 @@ function closeAndCleanForm() { } } -function onTextValidate(valid: boolean) { - isMessageValid.value = valid +function onTextValidate(validation: ValidationResult) { + isMessageValid.value = validation.valid } -function onAttachmentValidate(valid: boolean) { - isAttachmentValid.value = valid +function onAttachmentValidate(validation: ValidationResult) { + isAttachmentValid.value = validation.valid } -function onEmailValidate(valid: boolean) { - isEmailValid.value = valid +function onEmailValidate(validation: ValidationResult) { + isEmailValid.value = validation.valid } async function generateShortLink() { @@ -282,7 +277,7 @@ function selectItem(dropdownItem: DropdownItem) { class="my-2" :class="{ 'is-valid': feedback.category, - 'is-invalid': !feedback.category && activateValidation, + 'is-invalid': !feedback.category && userHasTriedToSubmit, }" @selectItem="selectItem" /> @@ -308,7 +303,7 @@ function selectItem(dropdownItem: DropdownItem) { :disabled="request.pending" required data-cy="report-problem-text-area" - :activate-validation="activateValidation" + :validate-when-pristine="userHasTriedToSubmit" invalid-message="feedback_empty_warning" @validate="onTextValidate" /> @@ -351,7 +346,7 @@ function selectItem(dropdownItem: DropdownItem) { label="feedback_mail" :disabled="request.pending" :description="'no_email_feedback'" - :activate-validation="activateValidation" + :validate-when-pristine="userHasTriedToSubmit" data-cy="report-problem-email" @validate="onEmailValidate" /> @@ -363,7 +358,7 @@ function selectItem(dropdownItem: DropdownItem) { label="feedback_attachment" :accepted-file-types="acceptedFileTypes" :placeholder="'feedback_placeholder'" - :activate-validation="activateValidation" + :validate-when-pristine="userHasTriedToSubmit" :disabled="request.pending" :max-file-size="ATTACHMENT_MAX_SIZE" data-cy="report-problem-file" @@ -388,7 +383,7 @@ function selectItem(dropdownItem: DropdownItem) { diff --git a/packages/viewer/src/store/modules/drawing/actions/closeDrawing.ts b/packages/viewer/src/store/modules/drawing/actions/closeDrawing.ts index 96983a23f5..db5da37961 100644 --- a/packages/viewer/src/store/modules/drawing/actions/closeDrawing.ts +++ b/packages/viewer/src/store/modules/drawing/actions/closeDrawing.ts @@ -43,7 +43,6 @@ export default async function closeDrawing(this: DrawingStore, dispatcher: Actio await debounceSaveDrawing({ debounceTime: 0, retryOnError: false }) } - if (this.layer.config) { // flagging the layer as not edited anymore (not displayed on the map by the drawing module anymore) if (isOnlineMode(this.onlineMode)) { diff --git a/packages/viewer/src/store/modules/drawing/actions/updateCurrentDrawingFeature.ts b/packages/viewer/src/store/modules/drawing/actions/updateCurrentDrawingFeature.ts index bad7925d92..8cc71b62c2 100644 --- a/packages/viewer/src/store/modules/drawing/actions/updateCurrentDrawingFeature.ts +++ b/packages/viewer/src/store/modules/drawing/actions/updateCurrentDrawingFeature.ts @@ -8,13 +8,7 @@ import { generateIconURL } from '@/api/icon.api' import { DrawingSaveState } from '@/store/modules/drawing/types/DrawingSaveState.enum' import debounceSaveDrawing from '@/store/modules/drawing/utils/debounceSaveDrawing' import useProfileStore from '@/store/modules/profile' -import { - calculateTextOffset, - type FeatureStyleColor, - type FeatureStyleSize, - MEDIUM, - RED, -} from '@/utils/featureStyleUtils' +import { calculateTextOffset } from '@/utils/featureStyleUtils' export default function updateCurrentDrawingFeature( this: DrawingStore, @@ -24,21 +18,24 @@ export default function updateCurrentDrawingFeature( if (this.feature.current) { this.save.state = DrawingSaveState.UnsavedChanges - let needIconUrlRefresh = false - Object.assign(this.feature.current, valuesToUpdate) // keeping values as preferred, if present, so that the next time the user draws, the values are used if (valuesToUpdate.iconSize) { this.edit.preferred.size = valuesToUpdate.iconSize - needIconUrlRefresh = true } if (valuesToUpdate.textSize) { this.edit.preferred.size = valuesToUpdate.textSize } if (valuesToUpdate.fillColor) { this.edit.preferred.color = valuesToUpdate.fillColor - needIconUrlRefresh = true + // refreshing the icon color if present + if (this.feature.current.icon) { + this.feature.current.icon.imageURL = generateIconURL( + this.feature.current.icon, + valuesToUpdate.fillColor + ) + } } if (valuesToUpdate.textColor) { this.edit.preferred.color = valuesToUpdate.textColor @@ -62,18 +59,6 @@ export default function updateCurrentDrawingFeature( } } - if (this.feature.current.icon && needIconUrlRefresh) { - const newIconSize: FeatureStyleSize = - valuesToUpdate.iconSize ?? this.feature.current.iconSize ?? MEDIUM - const newIconColor: FeatureStyleColor = - valuesToUpdate.fillColor ?? this.feature.current.fillColor ?? RED - this.feature.current.icon.imageURL = generateIconURL( - this.feature.current.icon, - newIconColor, - newIconSize - ) - } - const profileStore = useProfileStore() // updating the profile feature if the currently drawn feature is the profile feature if (profileStore.feature?.id === this.feature.current.id) { diff --git a/packages/viewer/src/utils/__tests__/kmlUtils.spec.ts b/packages/viewer/src/utils/__tests__/kmlUtils.spec.ts index db07e1d2d4..dc38e84260 100644 --- a/packages/viewer/src/utils/__tests__/kmlUtils.spec.ts +++ b/packages/viewer/src/utils/__tests__/kmlUtils.spec.ts @@ -12,7 +12,7 @@ import type { EditableFeature } from '@/api/features.api' import { type DrawingIconSet, generateIconURL } from '@/api/icon.api' import { getServiceKmlBaseUrl } from '@/config/baseUrl.config' import { fakeIconSets } from '@/utils/__tests__/legacyKmlUtils.spec' -import { BLUE, EXTRA_LARGE } from '@/utils/featureStyleUtils' +import { BLUE } from '@/utils/featureStyleUtils' import { getIcon, getKmlExtent, @@ -350,7 +350,7 @@ describe('Test KML utils', () => { 'https://fake.image.url/api/icons/sets/default/icons/001-marker@1x-255,0,0.png' ) }) - it('get icon with standard arguments from the set with scale and color', () => { + it('get icon with standard arguments from the set with color', () => { const icon = getIcon( { set: 'default', @@ -363,8 +363,8 @@ describe('Test KML utils', () => { expect(icon).toBeDefined() expect(icon!.name).to.be.equal('001-marker') expect(icon!.iconSetName).to.be.equal('default') - expect(generateIconURL(icon!, BLUE, EXTRA_LARGE)).to.be.equal( - 'https://fake.image.url/api/icons/sets/default/icons/001-marker@1.25x-0,0,255.png' + expect(generateIconURL(icon!, BLUE)).to.be.equal( + 'https://fake.image.url/api/icons/sets/default/icons/001-marker@1x-0,0,255.png' ) }) it('get icon with standard arguments from the babs set', () => { diff --git a/packages/viewer/src/utils/components/EmailInput.vue b/packages/viewer/src/utils/components/EmailInput.vue index 9857883d09..44908e100e 100644 --- a/packages/viewer/src/utils/components/EmailInput.vue +++ b/packages/viewer/src/utils/components/EmailInput.vue @@ -1,12 +1,16 @@ @@ -230,7 +221,10 @@ function onFileSelected(evt: Event): void {
{{ - t(computedInvalidMessage, { + t(validation.invalidMessage, { maxFileSize: maxFileSizeHuman, allowedFormats: acceptedFileTypes.join(', '), ...invalidMessageExtraParams, @@ -254,11 +248,11 @@ function onFileSelected(evt: Event): void { }}
- {{ t(computedValidMessage) }} + {{ t(validMessage) }}
-import { toRefs, useTemplateRef } from 'vue' +import { toRef, useTemplateRef } from 'vue' import { useI18n } from 'vue-i18n' import { useComponentUniqueId } from '@/utils/composables/useComponentUniqueId' -import { useFieldValidation } from '@/utils/composables/useFieldValidation' +import { + useFieldValidation, + type ValidateFunction, + type ValidationResult, +} from '@/utils/composables/useFieldValidation' -const props = withDefaults(defineProps<{ +const props = defineProps<{ /** Label to add above the field */ label?: string /** Description to add below the input */ @@ -28,7 +32,7 @@ const props = withDefaults(defineProps<{ * * NOTE: this props is ignored when activate-validation is false */ - validMarker?: boolean | undefined + forceValid?: boolean /** * Valid message Message that will be added in green below the field once the validation has * been done and the field is valid. @@ -42,7 +46,7 @@ const props = withDefaults(defineProps<{ * * NOTE: this props is ignored when activate-validation is false */ - invalidMarker?: boolean | undefined + forceInvalid?: boolean /** * Invalid message Message that will be added in red below the field once the validation has * been done and the field is invalid. @@ -51,13 +55,7 @@ const props = withDefaults(defineProps<{ * file too big or required empty file) */ invalidMessage?: string - /** - * Mark the field has validated. - * - * As long as the flag is false, no validation is run and no validation marks are set. Also the - * props is-invalid and is-valid are ignored. - */ - activateValidation?: boolean + validateWhenPristine?: boolean /** * Validate function to run when the input changes The function should return an object of type * `{valid: Boolean, invalidMessage: String}`. The `invalidMessage` string should be a @@ -65,42 +63,57 @@ const props = withDefaults(defineProps<{ * * NOTE: this function is called each time the field is modified */ - validate?: ((_value?: string) => { valid: boolean; invalidMessage: string }) | undefined + validate?: ValidateFunction dataCy?: string -}>(), { - validMarker: undefined, - invalidMarker: undefined, -}) - +}>() const { - label, - description, - disabled, - placeholder, - dataCy, -} = toRefs(props) + label = '', + description = '', + disabled = false, + placeholder = '', + validate, + dataCy = '', +} = props -const textAreaInputId = useComponentUniqueId('text-area-input') +// the props passed down to the usFieldValidation need to be converted to refs to keep the reactivity +const required = toRef(props, 'required', false) +const forceValid = toRef(props, 'forceValid', false) +const forceInvalid = toRef(props, 'forceInvalid', false) +const validFieldMessage = toRef(props, 'validMessage', '') +const invalidFieldMessage = toRef(props, 'invalidMessage', '') +const validateWhenPristine = toRef(props, 'validateWhenPristine', false) +const textAreaInputId = useComponentUniqueId('text-area-input') const model = defineModel({ default: '' }) -const emits = defineEmits(['change', 'validate', 'focusin', 'focusout', 'keydown.enter']) + +const emits = defineEmits<{ + change: [value?: string] + validate: [validation: ValidationResult] + focusin: [event: Event] + focusout: [event: Event] + 'keydown.enter': [event: KeyboardEvent] +}>() + const { t } = useI18n() const textAreaElement = useTemplateRef('textAreaElement') -const { - value, - validMarker: computedValidMarker, - invalidMarker: computedInvalidMarker, - validMessage: computedValidMessage, - invalidMessage: computedInvalidMessage, - onFocus, - required: computedRequired, -} = useFieldValidation( - props, +const { validation, onFocus } = useFieldValidation({ model, - emits as (_event: string, ..._args: unknown[]) => void -) + + required, + + validateWhenPristine, + + forceValid, + validFieldMessage, + + forceInvalid, + invalidFieldMessage, + + validate, + emits, +}) function focus(): void { textAreaElement.value?.focus() @@ -117,7 +130,7 @@ defineExpose({ focus })