updated with better error handling for bad moves and visual indicator for when a move completes

This commit is contained in:
Matthew Rathbone
2026-03-04 16:20:38 -06:00
parent 044832008c
commit b7c68ac24c
5 changed files with 40 additions and 9 deletions

View File

@@ -197,6 +197,7 @@
:show-duplicate="true"
:pinned="pinnedConnections.includes(c)"
:privacy-mode="privacyMode"
:class="{ 'drag-pending': (pendingSaveIds || []).includes(c.id) }"
@edit="edit"
@remove="remove"
@duplicate="duplicate"
@@ -229,6 +230,7 @@
:show-duplicate="true"
:pinned="pinnedConnections.includes(c)"
:privacy-mode="privacyMode"
:class="{ 'drag-pending': (pendingSaveIds || []).includes(c.id) }"
@edit="edit"
@remove="remove"
@duplicate="duplicate"
@@ -254,6 +256,7 @@
:show-duplicate="true"
:pinned="pinnedConnections.includes(c)"
:privacy-mode="privacyMode"
:class="{ 'drag-pending': (pendingSaveIds || []).includes(c.id) }"
@edit="edit"
@remove="remove"
@duplicate="duplicate"
@@ -398,7 +401,8 @@ export default {
...mapState('data/connections', {
connectionsLoading: 'loading',
connectionsError: 'error',
connectionFilter: 'filter'
connectionFilter: 'filter',
pendingSaveIds: 'pendingSaveIds'
}),
...mapState('data/connectionFolders', {
folders: 'items',
@@ -680,7 +684,7 @@ export default {
position: { before: null }
})
} catch (ex) {
this.$noty.error(`Move error: ${ex.message}`)
this.$noty.error(`Move error: ${ex.userMessage ?? ex.message}`)
}
},
async onConnectionDrop(event, folder, currentList) {
@@ -702,7 +706,7 @@ export default {
})
}
} catch (ex) {
this.$noty.error(`Move error: ${ex.message}`)
this.$noty.error(`Move error: ${ex.userMessage ?? ex.message}`)
}
},
async submitFolderModal() {
@@ -726,4 +730,7 @@ export default {
.folder-drop-zone {
min-height: 8px;
}
.drag-pending {
opacity: 0.5;
}
</style>

View File

@@ -101,6 +101,7 @@
:item="item"
:active="isActive(item)"
:selected="selected === item"
:class="{ 'drag-pending': (pendingSaveIds || []).includes(item.id) }"
@remove="remove"
@select="select"
@open="open"
@@ -132,6 +133,7 @@
:item="item"
:active="isActive(item)"
:selected="selected === item"
:class="{ 'drag-pending': (pendingSaveIds || []).includes(item.id) }"
@remove="remove"
@select="select"
@open="open"
@@ -157,6 +159,7 @@
:item="item"
:active="isActive(item)"
:selected="selected === item"
:class="{ 'drag-pending': (pendingSaveIds || []).includes(item.id) }"
@remove="remove"
@select="select"
@open="open"
@@ -285,7 +288,7 @@ export default {
...mapGetters(['workspace', 'isCloud', 'isUltimate']),
...mapGetters('data/queries', {'filteredQueries': 'filteredQueries'}),
...mapState('tabs', {'activeTab': 'active'}),
...mapState('data/queries', {'savedQueries': 'items', 'queriesLoading': 'loading', 'queriesError': 'error', 'savedQueryFilter': 'filter'}),
...mapState('data/queries', {'savedQueries': 'items', 'queriesLoading': 'loading', 'queriesError': 'error', 'savedQueryFilter': 'filter', 'pendingSaveIds': 'pendingSaveIds'}),
...mapState('data/queryFolders', {'folders': 'items', 'foldersLoading': 'loading', 'foldersError': 'error'}),
filterQuery: {
get() {
@@ -472,7 +475,7 @@ export default {
position: { before: null }
})
} catch (ex) {
this.$noty.error(`Move error: ${ex.message}`)
this.$noty.error(`Move error: ${ex.userMessage ?? ex.message}`)
}
},
async onQueryDrop(event, folder, currentList) {
@@ -494,7 +497,7 @@ export default {
})
}
} catch (ex) {
this.$noty.error(`Move error: ${ex.message}`)
this.$noty.error(`Move error: ${ex.userMessage ?? ex.message}`)
}
},
async submitFolderModal() {
@@ -515,6 +518,9 @@ export default {
.drag-ghost {
opacity: 0.4;
}
.drag-pending {
opacity: 0.5;
}
.folder-drop-zone {
min-height: 8px;
}

View File

@@ -6,21 +6,25 @@ export interface CloudResponseBase {
code: number,
errors: [],
message: string | null,
friendlyError?: string,
}
export class CloudError extends Error {
public status: number
public errors: string[]
public userMessage: string
constructor(status: number, message?: string, errors?: any[]) {
const result = [`Cloud Error (${status}):`]
const errorStrings: string[] = errors ? errors.map((e) => {
return _.isString(e) ? e : e.message
}) : []
}).filter(Boolean) : []
if (message) result.push(message)
if (errors?.length) result.push(...errorStrings)
if (errorStrings.length) result.push(...errorStrings)
super(result.join(" "))
this.status = status
this.errors = errorStrings
this.userMessage = message || errorStrings[0] || `Server error (${status})`
}
}
@@ -32,6 +36,6 @@ export function url(...parts: (string | number)[]) {
// Accept any 2xx status as success. POST (create) returns 201, GET/PATCH return 200,
// so checking for exactly 200 would incorrectly reject successful creates.
export function res<T extends CloudResponseBase>(response: AxiosResponse < T >, key: string) {
if (response.status < 200 || response.status >= 300) throw new CloudError(response.status, response.data?.message, response.data?.errors)
if (response.status < 200 || response.status >= 300) throw new CloudError(response.status, response.data?.friendlyError || response.data?.message, response.data?.errors)
return response.data[key]
}

View File

@@ -63,6 +63,9 @@ export const CloudConnectionModule: DataStore<ICloudSavedConnection, State> = {
}
}
// Snapshot before any mutation (upsert mutates existing in-place via Object.assign)
const snapshot = { ...existing }
// Mark as pending to protect from poll overwrites
context.commit('addPendingSave', item.id)
@@ -94,6 +97,10 @@ export const CloudConnectionModule: DataStore<ICloudSavedConnection, State> = {
})
return item.id
})
} catch (e) {
// Revert optimistic update using pre-mutation snapshot
context.commit('upsert', snapshot)
throw e
} finally {
// Clear pending status
context.commit('removePendingSave', item.id)

View File

@@ -65,6 +65,9 @@ export const CloudQueryModule: DataStore<ISavedQuery, State> = {
}
}
// Snapshot before any mutation (upsert mutates existing in-place via Object.assign)
const snapshot = { ...existing }
// Mark as pending to protect from poll overwrites
context.commit('addPendingSave', item.id)
@@ -96,6 +99,10 @@ export const CloudQueryModule: DataStore<ISavedQuery, State> = {
})
return item.id
})
} catch (e) {
// Revert optimistic update using pre-mutation snapshot
context.commit('upsert', snapshot)
throw e
} finally {
// Clear pending status
context.commit('removePendingSave', item.id)