2 Commits

Author SHA1 Message Date
3705b8daed feat(model-types): allow adding custom fields in restricted mode
When a category has linked items (pieces, components, products),
enable restricted mode instead of blocking all edits:
- Allow adding new custom fields
- Lock existing fields from modification or deletion
- Hide add buttons for products, pieces, and subcomponents
- Display informative message about restricted mode

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-29 19:53:56 +01:00
Matthieu
202b964b24 chore(branding): update navbar logo and app name 2026-01-25 22:31:40 +01:00
10 changed files with 203 additions and 28 deletions

View File

@@ -297,15 +297,17 @@
</ul> </ul>
</div> </div>
<div class="flex items-center space-x-3"> <div class="flex items-center space-x-3">
<div class="avatar placeholder"> <div class="avatar">
<div <div class="w-14">
class="bg-primary text-primary-content rounded-lg w-10 grid place-items-center" <img
> :src="logoSrc"
<IconLucideBoxes class="w-6 h-6" aria-hidden="true" /> alt="Logo Malio"
class="h-full w-full object-contain"
/>
</div> </div>
</div> </div>
<NuxtLink to="/" class="btn btn-ghost text-xl"> <NuxtLink to="/" class="btn btn-ghost text-xl">
Inventaire Pro Inventory
</NuxtLink> </NuxtLink>
</div> </div>
</div> </div>
@@ -705,13 +707,13 @@ import { useRoute, navigateTo, useRuntimeConfig } from "#imports";
import { useProfileSession } from "~/composables/useProfileSession"; import { useProfileSession } from "~/composables/useProfileSession";
import IconLucideMenu from "~icons/lucide/menu"; import IconLucideMenu from "~icons/lucide/menu";
import IconLucideSettings from "~icons/lucide/settings"; import IconLucideSettings from "~icons/lucide/settings";
import IconLucideBoxes from "~icons/lucide/boxes";
import IconLucidePlus from "~icons/lucide/plus"; import IconLucidePlus from "~icons/lucide/plus";
import IconLucideCpu from "~icons/lucide/cpu"; import IconLucideCpu from "~icons/lucide/cpu";
import IconLucideFilePlus from "~icons/lucide/file-plus"; import IconLucideFilePlus from "~icons/lucide/file-plus";
import IconLucideMapPin from "~icons/lucide/map-pin"; import IconLucideMapPin from "~icons/lucide/map-pin";
import IconLucideChevronRight from "~icons/lucide/chevron-right"; import IconLucideChevronRight from "~icons/lucide/chevron-right";
import IconLucideLogOut from "~icons/lucide/log-out"; import IconLucideLogOut from "~icons/lucide/log-out";
import logoSrc from "~/assets/LOGO_CARRE_BLANC.png";
// État du modal des paramètres d'affichage // État du modal des paramètres d'affichage
const displaySettingsOpen = ref(false); const displaySettingsOpen = ref(false);

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

View File

@@ -10,6 +10,7 @@
:locked-type-label="displayedRootTypeLabel" :locked-type-label="displayedRootTypeLabel"
:allow-subcomponents="allowSubcomponents" :allow-subcomponents="allowSubcomponents"
:max-subcomponent-depth="maxSubcomponentDepth" :max-subcomponent-depth="maxSubcomponentDepth"
:restricted-mode="restrictedMode"
is-root is-root
/> />
</div> </div>
@@ -55,6 +56,10 @@ const props = defineProps({
type: Number, type: Number,
default: Infinity, default: Infinity,
}, },
restrictedMode: {
type: Boolean,
default: false,
},
}) })
const emit = defineEmits(['update:modelValue']) const emit = defineEmits(['update:modelValue'])

View File

@@ -7,10 +7,10 @@
Produits inclus par défaut Produits inclus par défaut
</h3> </h3>
<p class="text-xs text-base-content/70"> <p class="text-xs text-base-content/70">
Ces produits safficheront lors de la création dune pièce basée sur cette catégorie. Ces produits s'afficheront lors de la création d'une pièce basée sur cette catégorie.
</p> </p>
</div> </div>
<button type="button" class="btn btn-outline btn-xs" @click="addProduct"> <button v-if="!restrictedMode" type="button" class="btn btn-outline btn-xs" @click="addProduct">
<IconLucidePlus class="w-3 h-3 mr-2" aria-hidden="true" /> <IconLucidePlus class="w-3 h-3 mr-2" aria-hidden="true" />
Ajouter Ajouter
</button> </button>
@@ -35,6 +35,7 @@
<select <select
v-model="product.typeProductId" v-model="product.typeProductId"
class="select select-bordered select-xs" class="select select-bordered select-xs"
:disabled="isProductLocked(product)"
@change="handleProductTypeSelect(product)" @change="handleProductTypeSelect(product)"
> >
<option value=""> <option value="">
@@ -51,12 +52,22 @@
</div> </div>
</div> </div>
<button <button
v-if="!isProductLocked(product)"
type="button" type="button"
class="btn btn-error btn-xs btn-square" class="btn btn-error btn-xs btn-square"
@click="removeProduct(index)" @click="removeProduct(index)"
> >
<IconLucideTrash class="w-4 h-4" aria-hidden="true" /> <IconLucideTrash class="w-4 h-4" aria-hidden="true" />
</button> </button>
<div v-else class="tooltip tooltip-left" data-tip="Ce produit ne peut pas être supprimé car des éléments utilisent cette catégorie">
<button
type="button"
class="btn btn-ghost btn-xs btn-square opacity-30 cursor-not-allowed"
disabled
>
<IconLucideTrash class="w-4 h-4" aria-hidden="true" />
</button>
</div>
</div> </div>
</li> </li>
</ul> </ul>
@@ -107,8 +118,9 @@
type="text" type="text"
class="input input-bordered input-xs" class="input input-bordered input-xs"
placeholder="Nom du champ" placeholder="Nom du champ"
:disabled="isFieldLocked(field)"
> >
<select v-model="field.type" class="select select-bordered select-xs"> <select v-model="field.type" class="select select-bordered select-xs" :disabled="isFieldLocked(field)">
<option value="text"> <option value="text">
Texte Texte
</option> </option>
@@ -128,7 +140,7 @@
</div> </div>
<div class="flex items-center gap-2 text-xs"> <div class="flex items-center gap-2 text-xs">
<input v-model="field.required" type="checkbox" class="checkbox checkbox-xs"> <input v-model="field.required" type="checkbox" class="checkbox checkbox-xs" :disabled="isFieldLocked(field)">
Obligatoire Obligatoire
</div> </div>
@@ -137,16 +149,27 @@
v-model="field.optionsText" v-model="field.optionsText"
class="textarea textarea-bordered textarea-xs h-20" class="textarea textarea-bordered textarea-xs h-20"
placeholder="Option 1&#10;Option 2" placeholder="Option 1&#10;Option 2"
:disabled="isFieldLocked(field)"
/> />
</div> </div>
<button <button
v-if="!isFieldLocked(field)"
type="button" type="button"
class="btn btn-error btn-xs btn-square" class="btn btn-error btn-xs btn-square"
@click="removeField(index)" @click="removeField(index)"
> >
<IconLucideTrash class="w-4 h-4" aria-hidden="true" /> <IconLucideTrash class="w-4 h-4" aria-hidden="true" />
</button> </button>
<div v-else class="tooltip tooltip-left" data-tip="Ce champ ne peut pas être supprimé car des éléments utilisent cette catégorie">
<button
type="button"
class="btn btn-ghost btn-xs btn-square opacity-30 cursor-not-allowed"
disabled
>
<IconLucideTrash class="w-4 h-4" aria-hidden="true" />
</button>
</div>
</div> </div>
</li> </li>
</ul> </ul>
@@ -181,6 +204,7 @@ type EditorProduct = {
const props = defineProps<{ const props = defineProps<{
modelValue?: PieceModelStructure | null modelValue?: PieceModelStructure | null
restrictedMode?: boolean
}>() }>()
const emit = defineEmits<{ const emit = defineEmits<{
@@ -330,6 +354,19 @@ const fields = ref<EditorField[]>(hydrateFields(props.modelValue))
const products = ref<EditorProduct[]>(hydrateProducts(props.modelValue)) const products = ref<EditorProduct[]>(hydrateProducts(props.modelValue))
const restState = ref<Record<string, unknown>>(extractRest(props.modelValue)) const restState = ref<Record<string, unknown>>(extractRest(props.modelValue))
const initialFieldUids = ref<Set<string>>(new Set(fields.value.map(f => f.uid)))
const initialProductUids = ref<Set<string>>(new Set(products.value.map(p => p.uid)))
const isFieldLocked = (field: EditorField): boolean => {
return props.restrictedMode === true && initialFieldUids.value.has(field.uid)
}
const isProductLocked = (product: EditorProduct): boolean => {
return props.restrictedMode === true && initialProductUids.value.has(product.uid)
}
const restrictedMode = computed(() => props.restrictedMode === true)
const applyOrderIndex = (list: EditorField[]): EditorField[] => const applyOrderIndex = (list: EditorField[]): EditorField[] =>
list.map((field, index) => ({ list.map((field, index) => ({
...field, ...field,
@@ -438,6 +475,8 @@ watch(
products.value = hydrateProducts(value) products.value = hydrateProducts(value)
products.value.forEach((product) => updateProductTypeMetadata(product)) products.value.forEach((product) => updateProductTypeMetadata(product))
lastEmitted = incomingSerialized lastEmitted = incomingSerialized
initialFieldUids.value = new Set(fields.value.map(f => f.uid))
initialProductUids.value = new Set(products.value.map(p => p.uid))
}, },
{ deep: true }, { deep: true },
) )

View File

@@ -17,6 +17,7 @@
<select <select
v-model="node.typeComposantId" v-model="node.typeComposantId"
class="select select-bordered select-sm w-full" class="select select-bordered select-sm w-full"
:disabled="isLocked"
@change="handleComponentTypeSelect(node)" @change="handleComponentTypeSelect(node)"
> >
<option value=""> <option value="">
@@ -42,6 +43,7 @@
type="text" type="text"
class="input input-bordered input-xs" class="input input-bordered input-xs"
placeholder="Alias du sous-composant" placeholder="Alias du sous-composant"
:disabled="isLocked"
/> />
</div> </div>
</template> </template>
@@ -52,13 +54,18 @@
</template> </template>
</div> </div>
<button <button
v-if="!isRoot" v-if="!isRoot && !isLocked"
type="button" type="button"
class="btn btn-error btn-xs btn-square" class="btn btn-error btn-xs btn-square"
@click="emit('remove')" @click="emit('remove')"
> >
<IconLucideTrash class="w-4 h-4" aria-hidden="true" /> <IconLucideTrash class="w-4 h-4" aria-hidden="true" />
</button> </button>
<div v-else-if="!isRoot && isLocked" class="tooltip tooltip-left" data-tip="Ce sous-composant ne peut pas être supprimé">
<button type="button" class="btn btn-ghost btn-xs btn-square opacity-30 cursor-not-allowed" disabled>
<IconLucideTrash class="w-4 h-4" aria-hidden="true" />
</button>
</div>
</div> </div>
<div class="px-4 py-4 space-y-5"> <div class="px-4 py-4 space-y-5">
@@ -107,8 +114,9 @@
type="text" type="text"
class="input input-bordered input-xs" class="input input-bordered input-xs"
placeholder="Nom du champ" placeholder="Nom du champ"
:disabled="isCustomFieldLocked(index)"
/> />
<select v-model="field.type" class="select select-bordered select-xs"> <select v-model="field.type" class="select select-bordered select-xs" :disabled="isCustomFieldLocked(index)">
<option value="text">Texte</option> <option value="text">Texte</option>
<option value="number">Nombre</option> <option value="number">Nombre</option>
<option value="select">Liste</option> <option value="select">Liste</option>
@@ -117,7 +125,7 @@
</select> </select>
</div> </div>
<div class="flex items-center gap-2 text-xs"> <div class="flex items-center gap-2 text-xs">
<input v-model="field.required" type="checkbox" class="checkbox checkbox-xs" /> <input v-model="field.required" type="checkbox" class="checkbox checkbox-xs" :disabled="isCustomFieldLocked(index)" />
Obligatoire Obligatoire
</div> </div>
<textarea <textarea
@@ -125,15 +133,26 @@
v-model="field.optionsText" v-model="field.optionsText"
class="textarea textarea-bordered textarea-xs h-20" class="textarea textarea-bordered textarea-xs h-20"
placeholder="Option 1&#10;Option 2" placeholder="Option 1&#10;Option 2"
:disabled="isCustomFieldLocked(index)"
></textarea> ></textarea>
</div> </div>
<button <button
v-if="!isCustomFieldLocked(index)"
type="button" type="button"
class="btn btn-error btn-xs btn-square" class="btn btn-error btn-xs btn-square"
@click="removeCustomField(index)" @click="removeCustomField(index)"
> >
<IconLucideTrash class="w-4 h-4" aria-hidden="true" /> <IconLucideTrash class="w-4 h-4" aria-hidden="true" />
</button> </button>
<div v-else class="tooltip tooltip-left" data-tip="Ce champ ne peut pas être supprimé car des éléments utilisent cette catégorie">
<button
type="button"
class="btn btn-ghost btn-xs btn-square opacity-30 cursor-not-allowed"
disabled
>
<IconLucideTrash class="w-4 h-4" aria-hidden="true" />
</button>
</div>
</div> </div>
</div> </div>
</div> </div>
@@ -144,7 +163,7 @@
<h4 :class="headingClass"> <h4 :class="headingClass">
{{ isRoot ? 'Produits inclus par défaut' : 'Produits' }} {{ isRoot ? 'Produits inclus par défaut' : 'Produits' }}
</h4> </h4>
<button type="button" class="btn btn-outline btn-xs" @click="addProduct"> <button v-if="!restrictedMode" type="button" class="btn btn-outline btn-xs" @click="addProduct">
<IconLucidePlus class="w-3 h-3 mr-2" aria-hidden="true" /> <IconLucidePlus class="w-3 h-3 mr-2" aria-hidden="true" />
Ajouter Ajouter
</button> </button>
@@ -179,6 +198,7 @@
<select <select
v-model="product.typeProductId" v-model="product.typeProductId"
class="select select-bordered select-xs" class="select select-bordered select-xs"
:disabled="isProductLocked(index)"
@change="handleProductTypeSelect(product)" @change="handleProductTypeSelect(product)"
> >
<option value=""> <option value="">
@@ -194,9 +214,18 @@
</select> </select>
</div> </div>
</div> </div>
<button type="button" class="btn btn-error btn-xs btn-square" @click="removeProduct(index)"> <button v-if="!isProductLocked(index)" type="button" class="btn btn-error btn-xs btn-square" @click="removeProduct(index)">
<IconLucideTrash class="w-4 h-4" aria-hidden="true" /> <IconLucideTrash class="w-4 h-4" aria-hidden="true" />
</button> </button>
<div v-else class="tooltip tooltip-left" data-tip="Ce produit ne peut pas être supprimé car des éléments utilisent cette catégorie">
<button
type="button"
class="btn btn-ghost btn-xs btn-square opacity-30 cursor-not-allowed"
disabled
>
<IconLucideTrash class="w-4 h-4" aria-hidden="true" />
</button>
</div>
</div> </div>
</div> </div>
</div> </div>
@@ -207,7 +236,7 @@
<h4 :class="headingClass"> <h4 :class="headingClass">
{{ isRoot ? 'Pièces incluses par défaut' : 'Pièces' }} {{ isRoot ? 'Pièces incluses par défaut' : 'Pièces' }}
</h4> </h4>
<button type="button" class="btn btn-outline btn-xs" @click="addPiece"> <button v-if="!restrictedMode" type="button" class="btn btn-outline btn-xs" @click="addPiece">
<IconLucidePlus class="w-3 h-3 mr-2" aria-hidden="true" /> <IconLucidePlus class="w-3 h-3 mr-2" aria-hidden="true" />
Ajouter Ajouter
</button> </button>
@@ -243,6 +272,7 @@
<select <select
v-model="piece.typePieceId" v-model="piece.typePieceId"
class="select select-bordered select-xs" class="select select-bordered select-xs"
:disabled="isPieceLocked(index)"
@change="handlePieceTypeSelect(piece)" @change="handlePieceTypeSelect(piece)"
> >
<option value=""> <option value="">
@@ -262,9 +292,14 @@
</p> </p>
</div> </div>
</div> </div>
<button type="button" class="btn btn-error btn-xs btn-square" @click="removePiece(index)"> <button v-if="!isPieceLocked(index)" type="button" class="btn btn-error btn-xs btn-square" @click="removePiece(index)">
<IconLucideTrash class="w-4 h-4" aria-hidden="true" /> <IconLucideTrash class="w-4 h-4" aria-hidden="true" />
</button> </button>
<div v-else class="tooltip tooltip-left" data-tip="Cette pièce ne peut pas être supprimée">
<button type="button" class="btn btn-ghost btn-xs btn-square opacity-30 cursor-not-allowed" disabled>
<IconLucideTrash class="w-4 h-4" aria-hidden="true" />
</button>
</div>
</div> </div>
</div> </div>
</div> </div>
@@ -274,7 +309,7 @@
<div class="flex items-center justify-between gap-2"> <div class="flex items-center justify-between gap-2">
<h4 :class="headingClass">Sous-composants</h4> <h4 :class="headingClass">Sous-composants</h4>
<button <button
v-if="canManageSubcomponents" v-if="canManageSubcomponents && !restrictedMode"
type="button" type="button"
class="btn btn-outline btn-xs" class="btn btn-outline btn-xs"
@click="addSubComponent" @click="addSubComponent"
@@ -317,6 +352,8 @@
:product-types="productTypes" :product-types="productTypes"
:allow-subcomponents="childAllowSubcomponents" :allow-subcomponents="childAllowSubcomponents"
:max-subcomponent-depth="maxSubcomponentDepth" :max-subcomponent-depth="maxSubcomponentDepth"
:restricted-mode="restrictedMode"
:is-locked="isSubcomponentLocked(index)"
@remove="removeSubComponent(index)" @remove="removeSubComponent(index)"
/> />
</div> </div>
@@ -359,6 +396,8 @@ const props = withDefaults(defineProps<{
lockedTypeLabel?: string lockedTypeLabel?: string
allowSubcomponents?: boolean allowSubcomponents?: boolean
maxSubcomponentDepth?: number maxSubcomponentDepth?: number
restrictedMode?: boolean
isLocked?: boolean
}>(), { }>(), {
depth: 0, depth: 0,
componentTypes: () => [], componentTypes: () => [],
@@ -369,10 +408,52 @@ const props = withDefaults(defineProps<{
lockedTypeLabel: '', lockedTypeLabel: '',
allowSubcomponents: true, allowSubcomponents: true,
maxSubcomponentDepth: Infinity, maxSubcomponentDepth: Infinity,
restrictedMode: false,
isLocked: false,
}) })
const emit = defineEmits(['remove']) const emit = defineEmits(['remove'])
const initialCustomFieldIndices = ref<Set<number>>(new Set())
const initialPieceIndices = ref<Set<number>>(new Set())
const initialProductIndices = ref<Set<number>>(new Set())
const initialSubcomponentIndices = ref<Set<number>>(new Set())
const initializeLockedIndices = () => {
if (props.restrictedMode) {
const customFieldsLength = Array.isArray(props.node.customFields) ? props.node.customFields.length : 0
const piecesLength = Array.isArray(props.node.pieces) ? props.node.pieces.length : 0
const productsLength = Array.isArray(props.node.products) ? props.node.products.length : 0
const subcomponentsLength = Array.isArray(props.node.subcomponents) ? props.node.subcomponents.length : 0
initialCustomFieldIndices.value = new Set(Array.from({ length: customFieldsLength }, (_, i) => i))
initialPieceIndices.value = new Set(Array.from({ length: piecesLength }, (_, i) => i))
initialProductIndices.value = new Set(Array.from({ length: productsLength }, (_, i) => i))
initialSubcomponentIndices.value = new Set(Array.from({ length: subcomponentsLength }, (_, i) => i))
}
}
initializeLockedIndices()
const isCustomFieldLocked = (index: number): boolean => {
return props.restrictedMode === true && initialCustomFieldIndices.value.has(index)
}
const isPieceLocked = (index: number): boolean => {
return props.restrictedMode === true && initialPieceIndices.value.has(index)
}
const isProductLocked = (index: number): boolean => {
return props.restrictedMode === true && initialProductIndices.value.has(index)
}
const isSubcomponentLocked = (index: number): boolean => {
return props.restrictedMode === true && initialSubcomponentIndices.value.has(index)
}
const isLocked = computed(() => props.isLocked === true)
const restrictedMode = computed(() => props.restrictedMode === true)
const componentTypes = computed(() => props.componentTypes ?? []) const componentTypes = computed(() => props.componentTypes ?? [])
const pieceTypes = computed(() => props.pieceTypes ?? []) const pieceTypes = computed(() => props.pieceTypes ?? [])
const productTypes = computed(() => props.productTypes ?? []) const productTypes = computed(() => props.productTypes ?? [])

View File

@@ -15,6 +15,7 @@
minlength="2" minlength="2"
maxlength="120" maxlength="120"
required required
:disabled="restrictedMode"
/> />
<p v-if="errors.name" class="mt-1 text-sm text-error">{{ errors.name }}</p> <p v-if="errors.name" class="mt-1 text-sm text-error">{{ errors.name }}</p>
</div> </div>
@@ -47,6 +48,7 @@
rows="4" rows="4"
name="notes" name="notes"
maxlength="2000" maxlength="2000"
:disabled="restrictedMode"
></textarea> ></textarea>
<p class="mt-1 text-xs text-base-content/70">Saisissez des informations complémentaires (facultatif).</p> <p class="mt-1 text-xs text-base-content/70">Saisissez des informations complémentaires (facultatif).</p>
</div> </div>
@@ -81,6 +83,7 @@
v-model="componentStructure" v-model="componentStructure"
:allow-subcomponents="allowComponentSubcomponents" :allow-subcomponents="allowComponentSubcomponents"
:max-subcomponent-depth="componentSubcomponentMaxDepth" :max-subcomponent-depth="componentSubcomponentMaxDepth"
:restricted-mode="restrictedMode"
/> />
</div> </div>
@@ -92,7 +95,7 @@
Aperçu : Aperçu :
<span class="font-medium text-base-content">{{ pieceStructurePreview }}</span> <span class="font-medium text-base-content">{{ pieceStructurePreview }}</span>
</p> </p>
<PieceModelStructureEditor v-model="pieceStructure" /> <PieceModelStructureEditor v-model="pieceStructure" :restricted-mode="restrictedMode" />
</div> </div>
<div <div
@@ -103,11 +106,21 @@
Aperçu : Aperçu :
<span class="font-medium text-base-content">{{ productStructurePreview }}</span> <span class="font-medium text-base-content">{{ productStructurePreview }}</span>
</p> </p>
<PieceModelStructureEditor v-model="productStructure" /> <PieceModelStructureEditor v-model="productStructure" :restricted-mode="restrictedMode" />
</div> </div>
</template> </template>
</section> </section>
<div
v-if="restrictedMode && restrictedModeMessage"
class="alert alert-info"
role="status"
aria-live="polite"
>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="stroke-current shrink-0 w-6 h-6"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>
<span>{{ restrictedModeMessage }}</span>
</div>
<div <div
v-if="disableSubmit" v-if="disableSubmit"
class="alert alert-warning" class="alert alert-warning"
@@ -161,6 +174,8 @@ const props = withDefaults(defineProps<{
componentSubcomponentMaxDepth?: number componentSubcomponentMaxDepth?: number
disableSubmit?: boolean disableSubmit?: boolean
disableSubmitMessage?: string disableSubmitMessage?: string
restrictedMode?: boolean
restrictedModeMessage?: string
}>(), { }>(), {
initialData: null, initialData: null,
saving: false, saving: false,
@@ -170,6 +185,8 @@ const props = withDefaults(defineProps<{
componentSubcomponentMaxDepth: 1, componentSubcomponentMaxDepth: 1,
disableSubmit: false, disableSubmit: false,
disableSubmitMessage: '', disableSubmitMessage: '',
restrictedMode: false,
restrictedModeMessage: '',
}) })
const emit = defineEmits<{ const emit = defineEmits<{
@@ -192,6 +209,12 @@ const disableSubmitMessage = computed(() =>
? props.disableSubmitMessage ? props.disableSubmitMessage
: 'Cette catégorie ne peut pas être modifiée car des éléments y sont déjà liés.', : 'Cette catégorie ne peut pas être modifiée car des éléments y sont déjà liés.',
) )
const restrictedMode = computed(() => props.restrictedMode === true)
const restrictedModeMessage = computed(() =>
(props.restrictedModeMessage && props.restrictedModeMessage.trim())
? props.restrictedModeMessage
: '',
)
const form = reactive<ModelTypePayload>({ const form = reactive<ModelTypePayload>({
name: '', name: '',

View File

@@ -32,7 +32,7 @@ const extractTotal = (payload: any, fallbackLength: number) => {
export function useCategoryEditGuard (config: GuardConfig) { export function useCategoryEditGuard (config: GuardConfig) {
const { get } = useApi() const { get } = useApi()
const { showError } = useToast() const { showInfo } = useToast()
const linkedCount = ref(0) const linkedCount = ref(0)
const linkedLoading = ref(false) const linkedLoading = ref(false)
@@ -64,11 +64,15 @@ export function useCategoryEditGuard (config: GuardConfig) {
} }
} }
const isSubmitBlocked = computed( const isRestrictedMode = computed(
() => linkedLoading.value || linkedCount.value > 0, () => !linkedLoading.value && linkedCount.value > 0,
) )
const submitBlockMessage = computed(() => { const isSubmitBlocked = computed(
() => linkedLoading.value,
)
const restrictedModeMessage = computed(() => {
if (linkedLoading.value) { if (linkedLoading.value) {
return config.labels.verifying return config.labels.verifying
} }
@@ -76,23 +80,32 @@ export function useCategoryEditGuard (config: GuardConfig) {
return '' return ''
} }
if (linkedCount.value === 1) { if (linkedCount.value === 1) {
return `Modification bloquée : 1 ${config.labels.singular} est déjà lié à cette catégorie.` return `Mode restreint : 1 ${config.labels.singular} est déjà lié à cette catégorie. Vous pouvez ajouter de nouveaux champs personnalisés, mais pas modifier ou supprimer les existants.`
} }
return `Modification bloquée : ${linkedCount.value} ${config.labels.plural} sont déjà liés à cette catégorie.` return `Mode restreint : ${linkedCount.value} ${config.labels.plural} sont déjà liés à cette catégorie. Vous pouvez ajouter de nouveaux champs personnalisés, mais pas modifier ou supprimer les existants.`
})
const submitBlockMessage = computed(() => {
if (linkedLoading.value) {
return config.labels.verifying
}
return ''
}) })
const guardSubmitOrNotify = () => { const guardSubmitOrNotify = () => {
if (!isSubmitBlocked.value) { if (!isSubmitBlocked.value) {
return false return false
} }
showError(submitBlockMessage.value || 'Modification bloquée pour cette catégorie.') showInfo(submitBlockMessage.value || 'Veuillez patienter...')
return true return true
} }
return { return {
linkedCount, linkedCount,
linkedLoading, linkedLoading,
isRestrictedMode,
isSubmitBlocked, isSubmitBlocked,
restrictedModeMessage,
submitBlockMessage, submitBlockMessage,
loadLinkedCount, loadLinkedCount,
guardSubmitOrNotify, guardSubmitOrNotify,

View File

@@ -28,6 +28,8 @@
:saving="saving" :saving="saving"
:disable-submit="isSubmitBlocked" :disable-submit="isSubmitBlocked"
:disable-submit-message="submitBlockMessage" :disable-submit-message="submitBlockMessage"
:restricted-mode="isRestrictedMode"
:restricted-mode-message="restrictedModeMessage"
@submit="handleSubmit" @submit="handleSubmit"
@cancel="handleCancel" @cancel="handleCancel"
/> />
@@ -52,7 +54,9 @@ const saving = ref(false)
const initialData = ref<Partial<ModelTypePayload> | null>(null) const initialData = ref<Partial<ModelTypePayload> | null>(null)
const { const {
isRestrictedMode,
isSubmitBlocked, isSubmitBlocked,
restrictedModeMessage,
submitBlockMessage, submitBlockMessage,
loadLinkedCount, loadLinkedCount,
guardSubmitOrNotify, guardSubmitOrNotify,

View File

@@ -28,6 +28,8 @@
:saving="saving" :saving="saving"
:disable-submit="isSubmitBlocked" :disable-submit="isSubmitBlocked"
:disable-submit-message="submitBlockMessage" :disable-submit-message="submitBlockMessage"
:restricted-mode="isRestrictedMode"
:restricted-mode-message="restrictedModeMessage"
@submit="handleSubmit" @submit="handleSubmit"
@cancel="handleCancel" @cancel="handleCancel"
/> />
@@ -52,7 +54,9 @@ const saving = ref(false)
const initialData = ref<Partial<ModelTypePayload> | null>(null) const initialData = ref<Partial<ModelTypePayload> | null>(null)
const { const {
isRestrictedMode,
isSubmitBlocked, isSubmitBlocked,
restrictedModeMessage,
submitBlockMessage, submitBlockMessage,
loadLinkedCount, loadLinkedCount,
guardSubmitOrNotify, guardSubmitOrNotify,

View File

@@ -28,6 +28,8 @@
:saving="saving" :saving="saving"
:disable-submit="isSubmitBlocked" :disable-submit="isSubmitBlocked"
:disable-submit-message="submitBlockMessage" :disable-submit-message="submitBlockMessage"
:restricted-mode="isRestrictedMode"
:restricted-mode-message="restrictedModeMessage"
@submit="handleSubmit" @submit="handleSubmit"
@cancel="handleCancel" @cancel="handleCancel"
/> />
@@ -52,7 +54,9 @@ const saving = ref(false)
const initialData = ref<Partial<ModelTypePayload> | null>(null) const initialData = ref<Partial<ModelTypePayload> | null>(null)
const { const {
isRestrictedMode,
isSubmitBlocked, isSubmitBlocked,
restrictedModeMessage,
submitBlockMessage, submitBlockMessage,
loadLinkedCount, loadLinkedCount,
guardSubmitOrNotify, guardSubmitOrNotify,