Skip to content

Commit 4af8141

Browse files
authored
Merge pull request #40 from smartlabsAT/feature/issue-39-custom-language-field
feat: add configurable language code field support (Issue #39)
2 parents e193e94 + 0ec93f1 commit 4af8141

10 files changed

Lines changed: 154 additions & 22 deletions

File tree

CHANGELOG.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,22 @@ All notable changes to the Super Layout Table Extension will be documented in th
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [0.2.16] - 2025-11-20
9+
10+
### Added
11+
- Added configurable language code field support for translation collections (Issue #39)
12+
- New `languageCodeField` layout option to specify custom field names instead of hardcoded 'languages_code'
13+
- UI field in options panel to configure the language code field name
14+
15+
### Fixed
16+
- Fixed layout crash when translation collection uses a custom language code field name
17+
- Extension now supports any field name for language identification in translations
18+
19+
### Changed
20+
- Refactored all components to use configurable language code field via `useTranslationConfig` composable
21+
- Updated `super-table.vue`, `api.ts`, `useTableEdits.ts`, `EditableCellRelational.vue`, and `InlineEditPopover.vue`
22+
- Maintains backward compatibility with default 'languages_code' field name
23+
824
## [0.2.15] - 2025-10-27
925

1026
### Fixed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "directus-extension-super-table",
3-
"version": "0.2.15",
3+
"version": "0.2.16",
44
"description": "A powerful and feature-rich table layout extension for Directus 11+ with inline editing, quick filters, and manual sorting",
55
"keywords": [
66
"directus",

src/components/EditableCellRelational.vue

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
:is-editable="isFieldEditableComputed"
2424
:is-relational="false"
2525
:auto-save="false"
26+
:language-code-field="props.languageCodeField"
2627
:saving="saving"
2728
:collection="item?.collection || field?.collection"
2829
:primary-key-value="(item?.[primaryKeyField] || item?.id) ?? undefined"
@@ -141,6 +142,7 @@ const props = defineProps<{
141142
align?: 'left' | 'center' | 'right';
142143
directBooleanToggle?: boolean;
143144
primaryKeyFieldName?: string;
145+
languageCodeField?: string;
144146
}>();
145147
146148
const emit = defineEmits<{
@@ -218,8 +220,9 @@ const displayValue = computed(() => {
218220
const targetLanguage = fieldLanguage.value;
219221
220222
if (targetLanguage) {
223+
const languageField = props.languageCodeField || 'languages_code';
221224
const translation = props.item.translations.find(
222-
(t: any) => t.languages_code === targetLanguage
225+
(t: any) => t[languageField] === targetLanguage
223226
);
224227
225228
// Return the specific field value if translation exists

src/components/InlineEditPopover.vue

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -420,6 +420,7 @@ interface Props {
420420
fieldSupportLevel?: 'full' | 'partial' | 'none' | 'readonly';
421421
editModeActive?: boolean;
422422
fieldEditWarning?: string;
423+
languageCodeField?: string;
423424
}
424425
425426
const props = withDefaults(defineProps<Props>(), {
@@ -696,8 +697,9 @@ function formatDisplayValue(value: any): string {
696697
if (Array.isArray(value)) {
697698
if (value.length === 0) return '[]';
698699
// For translations array, show language codes
699-
if (value[0]?.languages_code) {
700-
const langs = value.map((t) => t.languages_code).join(', ');
700+
const languageField = props.languageCodeField || 'languages_code';
701+
if (value[0]?.[languageField]) {
702+
const langs = value.map((t) => t[languageField]).join(', ');
701703
return `[${value.length}] ${langs}`;
702704
}
703705
return `[${value.length} items]`;

src/composables/api.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -101,12 +101,13 @@ export function useTableApi() {
101101
collection: string,
102102
primaryKey: string | number,
103103
field: string,
104-
value: any
104+
value: any,
105+
languageCodeField = 'languages_code'
105106
): Promise<void> {
106107
try {
107108
// Handle translation updates specially
108109
if (typeof value === 'object' && value?.isTranslation) {
109-
await updateTranslation(collection, primaryKey, value);
110+
await updateTranslation(collection, primaryKey, value, languageCodeField);
110111
} else if (typeof value === 'object' && value?.isFullTranslations) {
111112
// Handle full translations update from interface-translations
112113
await api.patch(`/items/${collection}/${primaryKey}`, {
@@ -131,7 +132,8 @@ export function useTableApi() {
131132
async function updateTranslation(
132133
collection: string,
133134
primaryKey: string | number,
134-
translationData: any
135+
translationData: any,
136+
languageCodeField = 'languages_code'
135137
): Promise<void> {
136138
// Get current item with translations
137139
const currentItem = await api.get(`/items/${collection}/${primaryKey}`, {
@@ -144,7 +146,7 @@ export function useTableApi() {
144146

145147
// Find or create translation for the language
146148
const translationIndex = existingTranslations.findIndex(
147-
(t: any) => t.languages_code === translationData.language
149+
(t: any) => t[languageCodeField] === translationData.language
148150
);
149151

150152
if (translationIndex >= 0) {
@@ -154,7 +156,7 @@ export function useTableApi() {
154156
} else {
155157
// Create new translation
156158
existingTranslations.push({
157-
languages_code: translationData.language,
159+
[languageCodeField]: translationData.language,
158160
[translationData.translationField]: translationData.value,
159161
});
160162
}

src/composables/useTableEdits.ts

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ export function useTableEdits(
77
collection: Ref<string>,
88
primaryKeyField: Ref<Field | undefined>,
99
items: Ref<any[]>,
10-
getItems: () => Promise<void>
10+
getItems: () => Promise<void>,
11+
languageCodeField = 'languages_code'
1112
) {
1213
const tableApi = useTableApi();
1314
const edits = ref<Edits>({});
@@ -55,13 +56,13 @@ export function useTableEdits(
5556
// Build translation update structure
5657
const existingTranslations = item.translations || [];
5758
const translationForLang = existingTranslations.find(
58-
(t: any) => t.languages_code === value.language
59+
(t: any) => t[languageCodeField] === value.language
5960
);
6061

6162
if (translationForLang) {
6263
// Update existing translation
6364
updatePayload.translations = existingTranslations.map((t: any) => {
64-
if (t.languages_code === value.language) {
65+
if (t[languageCodeField] === value.language) {
6566
return {
6667
...t,
6768
[value.translationField]: value.value,
@@ -74,7 +75,7 @@ export function useTableEdits(
7475
updatePayload.translations = [
7576
...existingTranslations,
7677
{
77-
languages_code: value.language,
78+
[languageCodeField]: value.language,
7879
[value.translationField]: value.value,
7980
},
8081
];
@@ -90,12 +91,18 @@ export function useTableEdits(
9091
for (const [field, value] of Object.entries(updatePayload)) {
9192
if (field === 'translations') {
9293
// For full translations update
93-
await tableApi.updateItem(collection.value, itemId, 'translations', {
94-
isFullTranslations: true,
95-
translations: value,
96-
});
94+
await tableApi.updateItem(
95+
collection.value,
96+
itemId,
97+
'translations',
98+
{
99+
isFullTranslations: true,
100+
translations: value,
101+
},
102+
languageCodeField
103+
);
97104
} else {
98-
await tableApi.updateItem(collection.value, itemId, field, value);
105+
await tableApi.updateItem(collection.value, itemId, field, value, languageCodeField);
99106
}
100107
}
101108

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { computed, ComputedRef } from 'vue';
2+
3+
/**
4+
* Translation configuration interface
5+
*/
6+
export interface TranslationConfig {
7+
languageCodeField: string;
8+
}
9+
10+
/**
11+
* Default translation configuration
12+
*/
13+
const DEFAULT_CONFIG: TranslationConfig = {
14+
languageCodeField: 'languages_code',
15+
};
16+
17+
/**
18+
* Composable for managing translation configuration
19+
* This allows users to specify custom field names for language codes
20+
* instead of being forced to use 'languages_code'
21+
*
22+
* @param layoutOptions - The layout options from the parent component
23+
* @returns Translation configuration object
24+
*/
25+
export function useTranslationConfig(
26+
layoutOptions: ComputedRef<Record<string, any>> | Record<string, any>
27+
): ComputedRef<TranslationConfig> {
28+
return computed(() => {
29+
const options = 'value' in layoutOptions ? layoutOptions.value : layoutOptions;
30+
31+
return {
32+
languageCodeField: options?.languageCodeField || DEFAULT_CONFIG.languageCodeField,
33+
};
34+
});
35+
}
36+
37+
/**
38+
* Helper function to get the language code field name
39+
* This is used throughout the extension to access the correct field
40+
*
41+
* @param config - Translation configuration
42+
* @returns The field name to use for language codes
43+
*/
44+
export function getLanguageCodeField(config: TranslationConfig): string {
45+
return config.languageCodeField;
46+
}
47+
48+
/**
49+
* Helper function to build the full field path for translations
50+
*
51+
* @param config - Translation configuration
52+
* @returns The full field path (e.g., 'translations.languages_code')
53+
*/
54+
export function getTranslationLanguageFieldPath(config: TranslationConfig): string {
55+
return `translations.${config.languageCodeField}`;
56+
}

src/options.vue

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,23 @@
1616
label="Enable direct boolean field editing (single-click toggle without popover)"
1717
/>
1818
</div>
19+
20+
<div class="field">
21+
<label class="type-label">Language Code Field</label>
22+
<v-input v-model="languageCodeField" placeholder="languages_code">
23+
<template #append>
24+
<v-icon
25+
v-tooltip="
26+
'The field name used to identify languages in translation collections. Default: languages_code'
27+
"
28+
name="help"
29+
/>
30+
</template>
31+
</v-input>
32+
<div class="hint">
33+
Custom field name for language codes in translation collections (default: 'languages_code')
34+
</div>
35+
</div>
1936
</template>
2037

2138
<script lang="ts" setup>
@@ -32,6 +49,7 @@ interface LayoutOptions {
3249
customFieldNames?: Record<string, string>;
3350
widths?: Record<string, number>;
3451
align?: Record<string, 'left' | 'center' | 'right'>;
52+
languageCodeField?: string;
3553
}
3654
3755
const props = defineProps<{
@@ -72,6 +90,16 @@ const directBooleanToggle = computed({
7290
};
7391
},
7492
});
93+
94+
const languageCodeField = computed({
95+
get: () => layoutOptions.value?.languageCodeField || 'languages_code',
96+
set: (val) => {
97+
layoutOptions.value = {
98+
...layoutOptions.value,
99+
languageCodeField: val || undefined, // Store undefined if empty to use default
100+
};
101+
},
102+
});
75103
</script>
76104

77105
<style scoped>
@@ -90,4 +118,11 @@ const directBooleanToggle = computed({
90118
.v-notice {
91119
margin-top: var(--form-vertical-gap);
92120
}
121+
122+
.hint {
123+
margin-top: 4px;
124+
color: var(--foreground-subdued);
125+
font-size: 12px;
126+
line-height: 1.4;
127+
}
93128
</style>

src/super-table.vue

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,7 @@
210210
:align="header.align"
211211
:direct-boolean-toggle="(layoutOptions as any)?.directBooleanToggle"
212212
:primary-key-field-name="getPrimaryKeyFieldName()"
213+
:language-code-field="translationConfig.languageCodeField"
213214
@update="updateFieldValue"
214215
@save="autoSaveEdits"
215216
/>
@@ -286,6 +287,10 @@ import { useTableEdits } from './composables/useTableEdits';
286287
import { useTablePagination } from './composables/useTablePagination';
287288
import { useTableFields } from './composables/useTableFields';
288289
import { useFilterPresets } from './composables/useFilterPresets';
290+
import {
291+
useTranslationConfig,
292+
getTranslationLanguageFieldPath,
293+
} from './composables/useTranslationConfig';
289294
import { PER_PAGE_OPTIONS } from './constants/pagination';
290295
import { DEFAULT_LANGUAGES } from './constants/languages';
291296
import EditableCellRelational from './components/EditableCellRelational.vue';
@@ -399,6 +404,9 @@ const hasTranslationFields = computed(() => {
399404
return fields.value.some((field: string) => field.startsWith('translations.'));
400405
});
401406
407+
// Translation configuration
408+
const translationConfig = useTranslationConfig(layoutOptions);
409+
402410
// Layout Options
403411
const showToolbar = computed(() => layoutOptions.value?.showToolbar !== false);
404412
// Default to true for filters
@@ -482,9 +490,10 @@ const fieldsWithRelational = computed(() => {
482490
adjustedFields.unshift(pkField); // Add at the beginning
483491
}
484492
485-
// Ensure languages_code is included for translations
486-
if (hasTranslationFields.value && !adjustedFields.includes('translations.languages_code')) {
487-
adjustedFields.push('translations.languages_code');
493+
// Ensure language code field is included for translations
494+
const languageFieldPath = getTranslationLanguageFieldPath(translationConfig.value);
495+
if (hasTranslationFields.value && !adjustedFields.includes(languageFieldPath)) {
496+
adjustedFields.push(languageFieldPath);
488497
}
489498
490499
return adjustedFields;
@@ -1046,7 +1055,8 @@ const { edits, savingCells, updateFieldValue, autoSaveEdits } = useTableEdits(
10461055
collection,
10471056
computed(() => primaryKeyField?.value || (primaryKeyField as any) || undefined),
10481057
items,
1049-
getItems
1058+
getItems,
1059+
translationConfig.value.languageCodeField
10501060
);
10511061
10521062
// Field management

src/types/table.types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ export interface LayoutOptions {
2525
directBooleanToggle?: boolean;
2626
quickFilters?: QuickFilter[];
2727
activeQuickFilterId?: string;
28+
languageCodeField?: string; // Custom field name for language codes (default: 'languages_code')
2829
}
2930

3031
export interface LayoutQuery {

0 commit comments

Comments
 (0)