kopia lustrzana https://github.com/FacilMap/facilmap
493 wiersze
16 KiB
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> |