facilmap/frontend/src/lib/components/edit-type-dialog/edit-type-dialog.vue

493 wiersze
16 KiB
Vue

<script setup lang="ts">
import { typeValidator, type Field, type ID, type Type, type CRU } from "facilmap-types";
import { canControl, formatFieldName } from "facilmap-utils";
import { getUniqueId, getZodValidator, validateRequired } from "../../utils/utils";
import { mergeTypeObject } from "./edit-type-utils";
import { cloneDeep, isEqual } from "lodash-es";
import { useToasts } from "../ui/toasts/toasts.vue";
import ColourPicker from "../ui/colour-picker.vue";
import ShapePicker from "../ui/shape-picker.vue";
import IconPicker from "../ui/icon-picker.vue";
import RouteMode from "../ui/route-mode.vue";
import Draggable from "vuedraggable";
import FieldInput from "../ui/field-input.vue";
import Icon from "../ui/icon.vue";
import WidthPicker from "../ui/width-picker.vue";
import SizePicker from "../ui/size-picker.vue";
import EditTypeDropdownDialog from "./edit-type-dropdown-dialog.vue";
import { computed, ref, watch } from "vue";
import ModalDialog from "../ui/modal-dialog.vue";
import { showConfirm } from "../ui/alert.vue";
import { injectContextRequired, requireClientContext } from "../facil-map-context-provider/facil-map-context-provider.vue";
import ValidatedField from "../ui/validated-form/validated-field.vue";
import StrokePicker from "../ui/stroke-picker.vue";
import { useI18n } from "../../utils/i18n";
const context = injectContextRequired();
const client = requireClientContext(context);
const toasts = useToasts();
const i18n = useI18n();
const props = defineProps<{
typeId: ID | "createMarkerType" | "createLineType";
}>();
const emit = defineEmits<{
hidden: [];
}>();
const id = getUniqueId("fm-edit-type-dialog");
const isCreate = computed(() => props.typeId === "createMarkerType" || props.typeId === "createLineType");
const originalType = computed(() => {
return typeof props.typeId === "number" ? client.value.types[props.typeId] : undefined;
});
const initialType = computed<Type<CRU.CREATE_VALIDATED | CRU.READ>>(() => {
let type: Type<CRU.CREATE_VALIDATED | CRU.READ>;
if (props.typeId === "createMarkerType") {
type = {
...typeValidator.create.parse({ name: "-", type: "marker" } satisfies Type<CRU.CREATE>),
name: ""
};
} else if (props.typeId === "createLineType") {
type = {
...typeValidator.create.parse({ name: "-", type: "line" } satisfies Type<CRU.CREATE>),
name: ""
};
} else {
type = cloneDeep(originalType.value)!;
}
for(const field of type.fields) {
(field as any).oldName = field.name;
}
return type;
});
const type = ref(cloneDeep(initialType.value));
const editField = ref<Field>();
const modalRef = ref<InstanceType<typeof ModalDialog>>();
const isModified = computed(() => {
return !isEqual(type.value, initialType.value);
});
const resolvedCanControl = computed(() => canControl(type.value, null));
watch(originalType, (newType, oldType) => {
if (oldType && type.value) {
if (!newType) {
modalRef.value?.modal.hide();
// TODO: Show message
} else {
mergeTypeObject(oldType, newType, type.value);
}
}
});
function createField(): void {
type.value.fields.push({ name: "", type: "input", "default": "" });
}
async function deleteField(field: Field): Promise<void> {
if (!await showConfirm({
title: i18n.t("edit-type-dialog.delete-field-title"),
message: i18n.t("edit-type-dialog.delete-field-message", { fieldName: formatFieldName(field.name) }),
variant: "danger",
okLabel: i18n.t("edit-type-dialog.delete-field-button")
}))
return;
var idx = type.value.fields.indexOf(field);
if(idx != -1)
type.value.fields.splice(idx, 1);
}
async function save(): Promise<void> {
toasts.hideToast(`fm${context.id}-edit-type-error`);
try {
if (isCreate.value)
await client.value.addType(type.value);
else
await client.value.editType(type.value as Type);
modalRef.value?.modal.hide();
} catch (err) {
toasts.showErrorToast(
`fm${context.id}-edit-type-error`,
() => (isCreate.value ? i18n.t("edit-type-dialog.create-type-error") : i18n.t("edit-type-dialog.save-type-error")),
err
);
}
}
function editDropdown(field: Field): void {
editField.value = field;
}
function handleUpdateField(field: Field) {
const idx = type.value.fields.indexOf(editField.value!);
if (idx === -1) {
toasts.showErrorToast(
`fm${context.id}-edit-type-dropdown-error`,
() => i18n.t("edit-type-dialog.field-update-error"),
() => i18n.t("edit-type-dialog.field-disappeared-error")
);
}
type.value.fields[idx] = field;
}
function validateFieldName(name: string) {
if (type.value.fields.filter((field) => field.name == name).length > 1) {
return i18n.t("edit-type-dialog.unique-field-name-error");
}
}
</script>
<template>
<ModalDialog
:title="i18n.t('edit-type-dialog.title')"
class="fm-edit-type"
:isModified="isModified"
:isCreate="isCreate"
ref="modalRef"
@submit="$event.waitUntil(save())"
@hidden="emit('hidden')"
>
<div class="row mb-3">
<label :for="`${id}-name-input`" class="col-sm-3 col-form-label">{{i18n.t("edit-type-dialog.name")}}</label>
<ValidatedField
:value="type.name"
:validators="[validateRequired, getZodValidator(typeValidator.update.shape.name)]"
class="col-sm-9 position-relative"
>
<template #default="slotProps">
<input class="form-control" :id="`${id}-name-input`" v-model="type.name" :ref="slotProps.inputRef" />
<div class="invalid-tooltip">
{{slotProps.validationError}}
</div>
</template>
</ValidatedField>
</div>
<div class="row mb-3">
<label :for="`${id}-type-input`" class="col-sm-3 col-form-label">{{i18n.t("edit-type-dialog.type")}}</label>
<div class="col-sm-9">
<select
:id="`${id}-type-input`"
v-model="type.type"
class="form-select"
disabled
>
<option value="marker">{{i18n.t("edit-type-dialog.type-marker")}}</option>
<option value="line">{{i18n.t("edit-type-dialog.type-line")}}</option>
</select>
</div>
</div>
<template v-if="resolvedCanControl.length > 0">
<hr/>
<p class="text-muted">
{{i18n.t("edit-type-dialog.styles-introduction")}}
</p>
<template v-if="resolvedCanControl.includes('colour')">
<div class="row mb-3">
<label :for="`${id}-default-colour-input`" class="col-sm-3 col-form-label">{{i18n.t("edit-type-dialog.default-colour")}}</label>
<div class="col-sm-9">
<div class="row align-items-center">
<div class="col-sm-9">
<ColourPicker
:id="`${id}-default-colour-input`"
v-model="type.defaultColour"
></ColourPicker>
</div>
<div class="col-sm-3">
<div class="form-check">
<input
type="checkbox"
class="form-check-input"
:id="`${id}-default-colour-fixed`"
v-model="type.colourFixed"
/>
<label :for="`${id}-default-colour-fixed`" class="form-check-label">{{i18n.t("edit-type-dialog.fixed")}}</label>
</div>
</div>
</div>
</div>
</div>
</template>
<template v-if="resolvedCanControl.includes('size')">
<div class="row mb-3">
<label :for="`${id}-default-size-input`" class="col-sm-3 col-form-label">{{i18n.t("edit-type-dialog.default-size")}}</label>
<div class="col-sm-9">
<div class="row align-items-center">
<div class="col-sm-9">
<SizePicker
:id="`${id}-default-size-input`"
v-model="type.defaultSize"
class="fm-custom-range-with-label"
></SizePicker>
</div>
<div class="col-sm-3">
<div class="form-check fm-form-check-with-label">
<input
type="checkbox"
class="form-check-input"
:id="`${id}-default-size-fixed`"
v-model="type.sizeFixed"
/>
<label :for="`${id}-default-size-fixed`" class="form-check-label">{{i18n.t("edit-type-dialog.fixed")}}</label>
</div>
</div>
</div>
</div>
</div>
</template>
<template v-if="resolvedCanControl.includes('icon')">
<div class="row mb-3">
<label :for="`${id}-default-icon-input`" class="col-sm-3 col-form-label">{{i18n.t("edit-type-dialog.default-icon")}}</label>
<div class="col-sm-9">
<div class="row align-items-center">
<div class="col-sm-9">
<IconPicker
:id="`${id}-default-icon-input`"
v-model="type.defaultIcon"
></IconPicker>
</div>
<div class="col-sm-3">
<div class="form-check">
<input
type="checkbox"
class="form-check-input"
:id="`${id}-default-icon-fixed`"
v-model="type.iconFixed"
/>
<label :for="`${id}-default-icon-fixed`" class="form-check-label">{{i18n.t("edit-type-dialog.fixed")}}</label>
</div>
</div>
</div>
</div>
</div>
</template>
<template v-if="resolvedCanControl.includes('shape')">
<div class="row mb-3">
<label :for="`${id}-default-shape-input`" class="col-sm-3 col-form-label">{{i18n.t("edit-type-dialog.default-shape")}}</label>
<div class="col-sm-9">
<div class="row align-items-center">
<div class="col-sm-9">
<ShapePicker
:id="`${id}-default-shape-input`"
v-model="type.defaultShape"
></ShapePicker>
</div>
<div class="col-sm-3">
<div class="form-check">
<input
type="checkbox"
class="form-check-input"
:id="`${id}-default-shape-fixed`"
v-model="type.shapeFixed"
/>
<label :for="`${id}-default-shape-fixed`" class="form-check-label">{{i18n.t("edit-type-dialog.fixed")}}</label>
</div>
</div>
</div>
</div>
</div>
</template>
<template v-if="resolvedCanControl.includes('width')">
<div class="row mb-3">
<label :for="`${id}-default-width-input`" class="col-sm-3 col-form-label">{{i18n.t("edit-type-dialog.default-width")}}</label>
<div class="col-sm-9">
<div class="row align-items-center">
<div class="col-sm-9">
<WidthPicker
:id="`${id}-default-width-input`"
v-model="type.defaultWidth"
class="fm-custom-range-with-label"
></WidthPicker>
</div>
<div class="col-sm-3">
<div class="form-check fm-form-check-with-label">
<input
type="checkbox"
class="form-check-input"
:id="`${id}-default-width-fixed`"
v-model="type.widthFixed"
/>
<label :for="`${id}-default-width-fixed`" class="form-check-label">{{i18n.t("edit-type-dialog.fixed")}}</label>
</div>
</div>
</div>
</div>
</div>
</template>
<template v-if="resolvedCanControl.includes('stroke')">
<div class="row mb-3">
<label :for="`${id}-default-stroke-input`" class="col-sm-3 col-form-label">{{i18n.t("edit-type-dialog.default-stroke")}}</label>
<div class="col-sm-9">
<div class="row align-items-center">
<div class="col-sm-9">
<StrokePicker
:id="`${id}-default-stroke-input`"
v-model="type.defaultStroke"
></StrokePicker>
</div>
<div class="col-sm-3">
<div class="form-check fm-form-check-with-label">
<input
type="checkbox"
class="form-check-input"
:id="`${id}-default-stroke-fixed`"
v-model="type.strokeFixed"
/>
<label :for="`${id}-default-stroke-fixed`" class="form-check-label">{{i18n.t("edit-type-dialog.fixed")}}</label>
</div>
</div>
</div>
</div>
</div>
</template>
<template v-if="resolvedCanControl.includes('mode') && context.settings.routing">
<div class="row mb-3">
<label :for="`${id}-default-mode-input`" class="col-sm-3 col-form-label">{{i18n.t("edit-type-dialog.default-route-mode")}}</label>
<div class="col-sm-9">
<div class="row align-items-center">
<div class="col-sm-9">
<RouteMode
:id="`${id}-default-mode-input`"
v-model="type.defaultMode"
></RouteMode>
</div>
<div class="col-sm-3">
<div class="form-check">
<input
type="checkbox"
class="form-check-input"
:id="`${id}-default-mode-fixed`"
v-model="type.modeFixed"
/>
<label :for="`${id}-default-mode-fixed`" class="form-check-label">{{i18n.t("edit-type-dialog.fixed")}}</label>
</div>
</div>
</div>
</div>
</div>
</template>
<hr/>
</template>
<div class="row mb-3">
<label :for="`${id}-show-in-legend-input`" class="col-sm-3 col-form-label">{{i18n.t("edit-type-dialog.legend")}}</label>
<div class="col-sm-9">
<div class="form-check fm-form-check-with-label">
<input
type="checkbox"
class="form-check-input"
:id="`${id}-show-in-legend-input`"
v-model="type.showInLegend"
/>
<label :for="`${id}-show-in-legend-input`" class="form-check-label">{{i18n.t("edit-type-dialog.show-in-legend")}}</label>
</div>
<div class="form-text">
{{i18n.t("edit-type-dialog.show-in-legend-description")}}
</div>
</div>
</div>
<h2>{{i18n.t("edit-type-dialog.fields")}}</h2>
<div class="table-responsive">
<table class="table table-hover table-striped">
<thead>
<tr>
<th style="width: 35%; min-width: 150px">{{i18n.t("edit-type-dialog.field-name")}}</th>
<th style="width: 35%; min-width: 120px">{{i18n.t("edit-type-dialog.field-type")}}</th>
<th style="width: 35%; min-width: 150px">{{i18n.t("edit-type-dialog.field-default-value")}}</th>
<th>{{i18n.t("edit-type-dialog.field-delete")}}</th>
<th></th>
</tr>
</thead>
<Draggable
v-model="type.fields"
tag="tbody"
handle=".fm-drag-handle"
:itemKey="(field: any) => type.fields.indexOf(field)"
>
<template #item="{ element: field }">
<tr>
<ValidatedField
tag="td"
class="position-relative"
:value="field.name"
:validators="[validateRequired, validateFieldName]"
>
<template #default="slotProps">
<input
class="form-control"
v-model="field.name"
:ref="slotProps.inputRef"
/>
<div class="invalid-tooltip">
{{slotProps.validationError}}
</div>
</template>
</ValidatedField>
<td>
<div class="input-group">
<select class="form-select" v-model="field.type">
<option value="input">{{i18n.t("edit-type-dialog.field-type-input")}}</option>
<option value="textarea">{{i18n.t("edit-type-dialog.field-type-textarea")}}</option>
<option value="dropdown">{{i18n.t("edit-type-dialog.field-type-dropdown")}}</option>
<option value="checkbox">{{i18n.t("edit-type-dialog.field-type-checkbox")}}</option>
</select>
<template v-if="['dropdown', 'checkbox'].includes(field.type)">
<button type="button" class="btn btn-secondary" @click="editDropdown(field)">{{i18n.t("edit-type-dialog.field-edit")}}</button>
</template>
</div>
</td>
<td class="text-center align-middle">
<FieldInput :field="field" v-model="field.default" ignore-default></FieldInput>
</td>
<td class="td-buttons">
<button type="button" class="btn btn-secondary" @click="deleteField(field)">{{i18n.t("edit-type-dialog.field-delete")}}</button>
</td>
<td class="td-buttons">
<button type="button" class="btn btn-secondary fm-drag-handle"><Icon icon="resize-vertical" :alt="i18n.t('edit-type-dialog.field-reorder')"></Icon></button>
</td>
</tr>
</template>
</Draggable>
<tfoot>
<tr>
<td colspan="4">
<button type="button" class="btn btn-secondary" @click="createField()"><Icon icon="plus" :alt="i18n.t('edit-type-dialog.field-add')"></Icon></button>
</td>
<td class="move"></td>
</tr>
</tfoot>
</table>
</div>
<EditTypeDropdownDialog
v-if="editField != null"
:type="type"
:field="editField"
@update:field="handleUpdateField($event)"
@hidden="editField = undefined"
></EditTypeDropdownDialog>
</ModalDialog>
</template>