Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
3f51e46
feat: add toggle button for flexible field in foldable list
timosville Nov 13, 2025
0a50de1
refactor: update mutation condition for flexible fields in foldable list
timosville Nov 13, 2025
fc5f1f5
feat: add SVG icons for plus and minus in icon component
timosville Nov 13, 2025
e79b408
feat: add isFlexible property to FieldSpec interface
timosville Nov 13, 2025
744296d
feat: add Flexible List variant to TranslationIndex story and mock da…
timosville Nov 14, 2025
d420501
feat: unify icon behavior for flexible and standard fields in list co…
timosville Nov 14, 2025
28f02e3
refactor: enhance class binding for flexible fields and update icon b…
timosville Nov 17, 2025
c2d4cf4
feat: add dynamic synchronization for flexible list items and enhance…
timosville Nov 17, 2025
2923872
refactor: update layout classes for object-field and flat-list compon…
timosville Nov 17, 2025
39e0f63
feat: enhance mock data descriptions and add new fields for flexible …
timosville Nov 17, 2025
5af1af4
refactor: initialize foldable list item toggles and correct flexible…
timosville Nov 19, 2025
fab983d
feat: conditionally render foldable list items based on an `isEmpty` …
timosville Nov 19, 2025
9b44194
feat: conditionally render `flat-list` items and styling based on fl…
timosville Nov 19, 2025
60435cd
ops: format file
timosville Dec 2, 2025
40e1d7a
refactor: remove unused SVG for 'plus' icon in icon component
timosville Dec 2, 2025
fbb6781
refactor: implement item removal functionality in foldable list with …
timosville Dec 2, 2025
9555fe2
refactor: update toggle behavior to ensure correct state management f…
timosville Dec 2, 2025
9bc44a8
refactor: simplify class binding in foldable list item for improved r…
timosville Dec 2, 2025
32a9f01
ops: sort tailwind classes
timosville Dec 3, 2025
f9cc26e
refactor: update class binding in foldable list
timosville Dec 3, 2025
3dc77b3
refactor: enhance class bindings for read-only and flexible states in…
timosville Dec 3, 2025
9c2b5d0
refactor: adjust z-index for button in foldable list to improve layering
timosville Dec 3, 2025
bb5ecb4
feat: add state management for translation-index page with responsive…
timosville Dec 3, 2025
0dfe380
refactor: streamline foldable list component by removing unused remov…
timosville Dec 3, 2025
fcffdeb
refactor: simplify list item structure in foldable list component
timosville Dec 3, 2025
eeee24d
feat: add isFlexible prop to list-field and foldable-list components …
timosville Dec 3, 2025
277605d
refactor: improve class bindings and button structure in foldable lis…
timosville Dec 4, 2025
0b8636a
feat: add remove functionality
timosville Dec 4, 2025
ed38ff8
refactor: restructure button rendering logic in foldable list
timosville Dec 4, 2025
6530011
refactor: remove redundant mutation logic in foldable list and enhanc…
timosville Dec 4, 2025
8ca4db3
Merge branch 'main' into sco-547-flexible-list-widget
timosville Jan 27, 2026
0376ecf
fix: adjust grid row span for foldable list items
timosville Jan 28, 2026
9595b01
Merge branch 'main' into sco-547-flexible-list-widget
timosville Jan 28, 2026
182dfad
refactor: update grid layout classes and styles across various compon…
timosville Feb 5, 2026
ea57584
refactor: simplify object-field component layout and comment out unus…
timosville Feb 5, 2026
fe7a297
refactor: update grid row span calculations in foldable list
timosville Feb 5, 2026
beed299
refactor: enhance layout and grid span calculations in flat and folda…
timosville Feb 5, 2026
710f90b
feat: add Alpha Course variant to translation index story and define …
timosville Feb 5, 2026
dcb7c48
refactor: correct grid layout classes and enhance field span calculat…
timosville Feb 5, 2026
def91ae
refactor: streamline object-field component layout by consolidating l…
timosville Feb 5, 2026
c058569
refactor: improve object-field component structure and enhance mock d…
timosville Feb 6, 2026
88aefb5
refactor: enhance object-field and panel-field components with improv…
timosville Feb 6, 2026
791ef04
refactor: update flat-list component to improve grid layout and span …
timosville Feb 6, 2026
8e22bef
Merge branch 'main' into sco-547-flexible-list-widget
timosville Feb 6, 2026
05e0f4d
Merge branch 'main' into sco-547-flexible-list-widget
timosville Feb 6, 2026
bc121a8
refactor: update list-field component to use field-specific flexibili…
timosville Feb 10, 2026
c1490f1
refactor: update list-field component to use props for flexibility co…
timosville Feb 10, 2026
c558104
refactor: add is-flexible prop to components in translation index
timosville Feb 10, 2026
cb9ccf7
refactor: remove is-flexible prop from list-field and foldable-list c…
timosville Feb 10, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
105 changes: 97 additions & 8 deletions src/frontend/fields/list-field.vue
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,12 @@
</template>

<script setup lang="ts">
import { ref, computed } from 'vue';
import { ref, computed, watch } from 'vue';
import { commonProps } from '../shared/helpers';
import type { FieldSpec } from '../../types';
import FlatList from './list/flat-list.vue';
import FoldableList from './list/foldable-list.vue';
import { useModelStore } from '../store';
import { useModelStore, useSharedStore } from '../store';

const props = defineProps({
...commonProps,
Expand All @@ -38,23 +38,112 @@ const fieldPath = computed((): string => {
});

const model = useModelStore();
const shared = useSharedStore();

let isAddingFlexibleItem = false;
const addSet = () => {
if (!props.isReadOnly && shared.isTranslation && field.value.isFlexible) {
isAddingFlexibleItem = true;
}
model.addListItem(fieldPath.value);
};

let isRemovingFlexibleItem = false;

const removeSet = (index: number) => {
if (!props.isReadOnly && shared.isTranslation && field.value.isFlexible) {
isRemovingFlexibleItem = true;
}
model.removeListItem(fieldPath.value, index);
};

const sourceItems = computed(() => {
return model.getSourceField(fieldPath.value, []) as Record<string, unknown>[];
});

const translationItems = computed(() => {
return model.getField(fieldPath.value, []) as Record<string, unknown>[];
});

const listItems = props.isReadOnly
? ref(model.getSourceField(fieldPath.value, []) as unknown[])
: ref(model.getField(fieldPath.value, []) as unknown[]);
? ref(buildReadOnlyListItems())
: ref([...translationItems.value]);

function buildReadOnlyListItems(): Record<string, unknown>[] {
const baseItems = [...sourceItems.value];

model.$subscribe(() => {
if (!props.isReadOnly) return baseItems;
if (!shared.isTranslation) return baseItems;
if (!field.value.isFlexible) return baseItems;

const translationLength = translationItems.value.length;
const sourceLength = baseItems.length;

if (translationLength > sourceLength) {
const placeholdersToAdd = translationLength - sourceLength;
const placeholders = Array.from({ length: placeholdersToAdd }, () => ({}));
return [...baseItems, ...placeholders];
}
if (translationLength < sourceLength) {
return baseItems.slice(0, translationLength);
}

return baseItems;
}

const syncFlexibleListLength = () => {
if (props.isReadOnly) return;
if (!shared.isTranslation) return;
if (field.value.isFlexible) return;
if (isRemovingFlexibleItem) {
isRemovingFlexibleItem = false;
return;
}
if (isAddingFlexibleItem) {
isAddingFlexibleItem = false;
return;
}

const fresh = model.getField(fieldPath.value, []) as unknown[];
listItems.value = fresh;
});
const desiredLength = sourceItems.value.length;
const currentItems = model.getField(fieldPath.value, []) as Record<string, unknown>[];

if (desiredLength === currentItems.length) return;

const changedItems = [...currentItems];

if (desiredLength > changedItems.length) {
for (let index = changedItems.length; index < desiredLength; index += 1) {
changedItems.push({});
}
} else {
changedItems.length = desiredLength;
}

model.setField(fieldPath.value, changedItems);
};

const refreshListItems = () => {
if (props.isReadOnly) {
listItems.value = buildReadOnlyListItems();
return;
}
listItems.value = model.getField(fieldPath.value, []) as Record<string, unknown>[];
};

if (!props.isReadOnly) {
syncFlexibleListLength();
}

refreshListItems();

watch(
[sourceItems, translationItems],
() => {
if (!props.isReadOnly) {
syncFlexibleListLength();
}
refreshListItems();
},
{ deep: true },
);
</script>
88 changes: 69 additions & 19 deletions src/frontend/fields/list/flat-list.vue
Original file line number Diff line number Diff line change
@@ -1,28 +1,51 @@
<template>
<div class="ml-8">
<div
:class="[
'ml-8',
{
subgrid: !isNested,
'grid grid-cols-1': isNested,
},
]"
:style="!isNested ? { gridRow: `span ${containerSpan}` } : undefined"
>
<ul
v-for="(_listItem, index) in listItems"
:key="index"
role="listitem"
class="relative my-2 grid gap-y-8 bg-gray-100 p-8"
:class="[
'subgrid relative my-2 gap-y-8',
isEmpty(index) ? '' : 'bg-gray-100 p-8',
{ 'mr-2': shared.showSourceColumn && field.isFlexible },
]"
:style="{ gridRow: `span ${totalSpan}` }"
>
<div
v-if="canMutate"
class="absolute right-0 mr-3 cursor-pointer text-gray-500"
@click="emit('removeSet', index)"
>
<Icon name="trash" class="h-10 w-10" />
</div>
<template v-if="!isEmpty(index)">
<div
v-if="canMutate"
class="absolute right-0 mr-3 cursor-pointer text-gray-500"
@click="emit('removeSet', index)"
>
<Icon
:name="field.isFlexible && canMutate ? 'minus' : 'trash'"
:class="field.isFlexible && canMutate ? 'size-5' : 'h-auto w-auto'"
/>
</div>

<li v-for="(item, i) in fields" :key="item.name + `${i.toString()}`">
<component
:is="store.picker(item.widget)"
:field="item"
:is-read-only="props.isReadOnly"
:root-path="`${fieldPath}.${index.toString()}`"
:is-nested="true"
/>
</li>
<li
v-for="(item, i) in fields"
:key="item.name + `${i.toString()}`"
:style="{ gridRow: `span ${getFieldSpan(item)}` }"
>
<component
:is="store.picker(item.widget)"
:field="item"
:is-read-only="props.isReadOnly"
:root-path="`${fieldPath}.${index.toString()}`"
:is-nested="true"
/>
</li>
</template>
</ul>
<div v-if="canMutate" class="mt-8 flex flex-row items-center gap-4">
<AddItemButton :label="field.label" @add="emit('addSet')" />
Expand Down Expand Up @@ -64,6 +87,11 @@ const props = defineProps({
required: false,
default: false,
},
isNested: {
type: Boolean,
required: false,
default: false,
},
});

const emit = defineEmits(['addSet', 'removeSet']);
Expand All @@ -75,7 +103,8 @@ const shared = useSharedStore();
const canMutate = computed(() => {
if (props.isReadOnly) return false;

return !shared.isTranslation;
// Allow mutations if not in translation mode OR if field is flexible
return !shared.isTranslation || field.value.isFlexible === true;
});

const showEmptyListWarning = (): boolean => {
Expand All @@ -88,4 +117,25 @@ const showEmptyListWarning = (): boolean => {
}
return false;
};

const isEmpty = (index: number): boolean => {
if (!props.isReadOnly) return false;
const item = props.listItems[index] as Record<string, unknown>;
return Object.keys(item).length === 0;
};

const getFieldSpan = (field: FieldSpec): number => {
if (field.widget === 'object' && field.fields) {
return Object.keys(field.fields).length;
}
return 1;
};

const totalSpan = computed(() => {
return fields.reduce((acc, field) => acc + getFieldSpan(field), 0);
});

const containerSpan = computed(() => {
return props.listItems.length * totalSpan.value + 1;
});
</script>
Loading