Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 3 additions & 0 deletions frontend/components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ declare module 'vue' {
BasePropertyValueEditor: typeof import('./src/features/wikibase-schema/components/BasePropertyValueEditor.vue')['default']
Button: typeof import('primevue/button')['default']
Card: typeof import('primevue/card')['default']
Checkbox: typeof import('primevue/checkbox')['default']
Chip: typeof import('primevue/chip')['default']
ClaimEditor: typeof import('./src/features/wikibase-schema/components/ClaimEditor.vue')['default']
Column: typeof import('primevue/column')['default']
Expand All @@ -23,6 +24,7 @@ declare module 'vue' {
DataTable: typeof import('primevue/datatable')['default']
DataTabPanel: typeof import('./src/features/data-processing/components/DataTabPanel.vue')['default']
DefaultLayout: typeof import('./src/core/layouts/DefaultLayout.vue')['default']
Dialog: typeof import('primevue/dialog')['default']
DropZone: typeof import('./src/features/wikibase-schema/components/DropZone.vue')['default']
FileUpload: typeof import('primevue/fileupload')['default']
Header: typeof import('./src/shared/components/Header.vue')['default']
Expand All @@ -39,6 +41,7 @@ declare module 'vue' {
QualifiersEditor: typeof import('./src/features/wikibase-schema/components/QualifiersEditor.vue')['default']
ReferenceContainer: typeof import('./src/features/wikibase-schema/components/ReferenceContainer.vue')['default']
ReferencesEditor: typeof import('./src/features/wikibase-schema/components/ReferencesEditor.vue')['default']
ReplaceDialog: typeof import('./src/features/data-processing/components/ReplaceDialog.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
SchemaSelector: typeof import('./src/features/wikibase-schema/components/SchemaSelector.vue')['default']
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,15 @@ const props = defineProps<{
isPrimaryKey: boolean
}>()

const { showSuccess } = useErrorHandling()

const menu = ref()
const isOpen = ref(false)
const showReplaceDialog = ref(false)

const handleReplaceCompleted = (affectedRows: number) => {
showSuccess(`Replace completed: ${affectedRows} rows affected`)
}

const menuItems = ref<MenuItem[]>([
{
Expand Down Expand Up @@ -77,7 +84,9 @@ const menuItems = ref<MenuItem[]>([
{
label: 'Replace',
icon: 'pi pi-code',
command: () => console.log(`Replace in ${props.columnHeader}`),
command: () => {
showReplaceDialog.value = true
},
},
],
},
Expand All @@ -104,5 +113,12 @@ const menuItems = ref<MenuItem[]>([
@show="() => (isOpen = true)"
@hide="() => (isOpen = false)"
/>
<ReplaceDialog
:visible="showReplaceDialog"
:column-field="columnField"
:column-header="columnHeader"
@update:visible="showReplaceDialog = $event"
@replace-completed="handleReplaceCompleted"
/>
</div>
</template>
151 changes: 151 additions & 0 deletions frontend/src/features/data-processing/components/ReplaceDialog.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
<script setup lang="ts">
const props = defineProps<{
visible: boolean
columnField: string
columnHeader: string
}>()

const emit = defineEmits<{
'update:visible': [value: boolean]
'replace-completed': [affectedRows: number]
}>()

const api = useApi()
const { showError } = useErrorHandling()
const projectId = useRouteParams('id') as Ref<UUID>

const findText = ref('')
const replaceText = ref('')
const caseSensitive = ref(false)
const wholeWord = ref(false)
const isLoading = ref(false)

const handleReplace = async () => {
if (!findText.value) {
return
}

isLoading.value = true

try {
const { data, error } = await api.project({ projectId: projectId.value }).replace.post({
column: props.columnField,
find: findText.value,
replace: replaceText.value,
caseSensitive: caseSensitive.value,
wholeWord: wholeWord.value,
})

if (error) {
showError(error.value as ExtendedError[])
return
}

if (!data?.affectedRows) {
showError([{ code: 'NOT_FOUND', message: 'Replace operation failed' }])
return
}

if (data?.affectedRows !== undefined && data?.affectedRows !== null) {
emit('replace-completed', data.affectedRows)
}
Comment on lines +44 to +51
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Handle 0-row results without flagging failure.

Line 44 treats data.affectedRows === 0 as a failure, which is a valid outcome when nothing matched the search. That branch shows an error and returns before we emit replace-completed, so callers never learn that the operation succeeded (with zero matches) and users see a spurious failure message. Please adjust the guard to only bail out when affectedRows is null/undefined, and continue to emit the event (and close the dialog) when it’s 0.

-    if (!data?.affectedRows) {
-      showError([{ code: 'NOT_FOUND', message: 'Replace operation failed' }])
+    if (data?.affectedRows == null) {
+      showError([{ code: 'NOT_FOUND', message: 'Replace operation failed' }])
       return
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (!data?.affectedRows) {
showError([{ code: 'NOT_FOUND', message: 'Replace operation failed' }])
return
}
if (data?.affectedRows !== undefined && data?.affectedRows !== null) {
emit('replace-completed', data.affectedRows)
}
if (data?.affectedRows == null) {
showError([{ code: 'NOT_FOUND', message: 'Replace operation failed' }])
return
}
if (data?.affectedRows !== undefined && data?.affectedRows !== null) {
emit('replace-completed', data.affectedRows)
}
🤖 Prompt for AI Agents
In frontend/src/features/data-processing/components/ReplaceDialog.vue around
lines 44 to 51, the current guard treats data.affectedRows === 0 as a failure
and returns early; change it to only treat affectedRows as missing (null or
undefined) as an error. Specifically, remove the check that interprets falsy
affectedRows as failure, instead check for affectedRows === null || affectedRows
=== undefined before calling showError and returning, and ensure
emit('replace-completed', data.affectedRows) runs for 0 as well so callers get
the zero-match result and the dialog can close.

} catch (error) {
console.error('Replace operation failed:', error)
} finally {
isLoading.value = false
closeDialog()
}
}

const closeDialog = () => {
emit('update:visible', false)
// Reset form
findText.value = ''
replaceText.value = ''
caseSensitive.value = false
wholeWord.value = false
}

const handleVisibleChange = (visible: boolean) => {
emit('update:visible', visible)
if (!visible) {
// Reset form when dialog is closed
findText.value = ''
replaceText.value = ''
caseSensitive.value = false
wholeWord.value = false
}
}
</script>

<template>
<Dialog
:visible="visible"
modal
:header="`Replace in ${columnHeader}`"
:style="{ width: '30vw' }"
@update:visible="handleVisibleChange"
>
<div class="flex flex-col gap-4">
<div class="flex flex-col gap-2">
<label for="find-text" class="font-semibold">Find</label>
<InputText
id="find-text"
v-model="findText"
placeholder="Enter text to find"
:disabled="isLoading"
/>
</div>

<div class="flex flex-col gap-2">
<label for="replace-text" class="font-semibold">Replace with</label>
<InputText
id="replace-text"
v-model="replaceText"
placeholder="Enter replacement text"
:disabled="isLoading"
/>
</div>

<div class="flex flex-col gap-3">
<div class="flex items-center gap-2">
<Checkbox
id="case-sensitive"
v-model="caseSensitive"
binary
:disabled="isLoading"
/>
<label for="case-sensitive" class="cursor-pointer">Case sensitive</label>
</div>

<div class="flex items-center gap-2">
<Checkbox
id="whole-word"
v-model="wholeWord"
binary
:disabled="isLoading"
/>
<label for="whole-word" class="cursor-pointer">Whole word only</label>
</div>
</div>
</div>

<template #footer>
<Button
label="Cancel"
icon="pi pi-times"
text
severity="secondary"
:disabled="isLoading"
@click="closeDialog"
/>
<Button
label="Replace"
icon="pi pi-check"
:loading="isLoading"
:disabled="!findText"
@click="handleReplace"
/>
</template>
</Dialog>
</template>