Skip to content

Commit 3ea6bd6

Browse files
committed
Cache project group name; use undefined total
Add sessionStorage caching for selected project group (stores {id,name}) and wire ProjectGroupPicker to read/write the cached name so the UI can show a name immediately even when the group isn't on page 1. Remove the separate nameModel/v-model:name usage in dashboard and consolidate storage to the single JSON key. Change getMyProjectGroups to use an optional total (undefined instead of null) and update callers accordingly. Other updates: persist cached name on selection, simplify input open logic, switch ProjectGroupPicker styles to SCSS and theme variables, clear the removed storage key from auth sign-out, and add dataset validation/error display in workspace/create/tdei.vue when dataset metadata is incomplete.
1 parent 6c29fef commit 3ea6bd6

4 files changed

Lines changed: 77 additions & 67 deletions

File tree

components/ProjectGroupPicker.vue

Lines changed: 43 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
>
1919
<div class="pg-header">
2020
<span v-if="projectGroups.length > 0" class="pg-count">
21-
<template v-if="totalCount !== null">
21+
<template v-if="totalCount !== undefined">
2222
Showing first {{ projectGroups.length }} of {{ totalCount }} project groups
2323
<span v-if="hasMore && !loading" class="pg-scroll-hint">&#183; Scroll to continue loading</span>
2424
</template>
@@ -60,6 +60,29 @@
6060
</div>
6161
</template>
6262

63+
<script lang="ts">
64+
const STORAGE_KEY_PROJECT_GROUP = 'tdei-selected-project-group'
65+
66+
function loadCachedName(id: string): string | undefined {
67+
if (typeof window === 'undefined') return undefined
68+
try {
69+
const raw = sessionStorage.getItem(STORAGE_KEY_PROJECT_GROUP)
70+
if (!raw) return undefined
71+
const stored = JSON.parse(raw) as { id: string; name: string }
72+
return stored.id === id ? stored.name : undefined
73+
} catch {
74+
return undefined
75+
}
76+
}
77+
78+
function persistCachedName(id: string, name: string) {
79+
if (typeof window === 'undefined') return
80+
try {
81+
sessionStorage.setItem(STORAGE_KEY_PROJECT_GROUP, JSON.stringify({ id, name }))
82+
} catch { /* silently fail */ }
83+
}
84+
</script>
85+
6386
<script setup lang="ts">
6487
import { ref, watch, onMounted, onUnmounted, nextTick } from 'vue'
6588
import { tdeiUserClient } from '~/services/index'
@@ -69,13 +92,12 @@ const props = withDefaults(defineProps<{ disabled?: boolean }>(), {
6992
})
7093
7194
const model = defineModel({ required: true })
72-
const nameModel = defineModel('name', { default: '' })
7395
const searchText = ref('')
7496
const isOpen = ref(false)
7597
const projectGroups = ref<{ id: string; name: string }[]>([])
7698
const selectedGroupName = ref('')
7799
const loading = ref(false)
78-
const totalCount = ref<number | null>(null)
100+
const totalCount = ref<number | undefined>(undefined)
79101
const pickerRef = ref<HTMLElement | null>(null)
80102
const listRef = ref<HTMLElement | null>(null)
81103
const activeIndex = ref(-1)
@@ -97,7 +119,7 @@ const loadGroups = async (reset = false) => {
97119
hasMore.value = true
98120
projectGroups.value = []
99121
activeIndex.value = -1
100-
totalCount.value = null
122+
totalCount.value = undefined
101123
}
102124
if (!hasMore.value) return
103125
@@ -113,8 +135,10 @@ const loadGroups = async (reset = false) => {
113135
}
114136
115137
const { items: newGroups, total } = await tdeiUserClient.getMyProjectGroups(pageNo, query, pageSize)
116-
if (total !== null) totalCount.value = total
138+
if (total !== undefined) totalCount.value = total
117139
projectGroups.value.push(...newGroups)
140+
const selected = newGroups.find(g => g.id === model.value)
141+
if (selected) persistCachedName(selected.id, selected.name)
118142
119143
if (newGroups.length < pageSize) {
120144
hasMore.value = false
@@ -146,9 +170,7 @@ const onInputClick = () => {
146170
}
147171
148172
const onInput = () => {
149-
if (!isOpen.value) {
150-
isOpen.value = true
151-
}
173+
isOpen.value = true
152174
clearTimeout(timeoutId)
153175
timeoutId = setTimeout(() => {
154176
loadGroups(true)
@@ -160,7 +182,6 @@ watch(model, (newId) => {
160182
if (pg && !isOpen.value) {
161183
searchText.value = pg.name
162184
selectedGroupName.value = pg.name
163-
nameModel.value = pg.name
164185
}
165186
})
166187
@@ -178,7 +199,7 @@ const selectGroup = (id: string) => {
178199
if (pg) {
179200
searchText.value = pg.name
180201
selectedGroupName.value = pg.name
181-
nameModel.value = pg.name
202+
persistCachedName(pg.id, pg.name)
182203
}
183204
}
184205
@@ -249,8 +270,9 @@ const onKeydown = (e: KeyboardEvent) => {
249270
}
250271
251272
const applyCachedName = () => {
252-
searchText.value = nameModel.value
253-
selectedGroupName.value = nameModel.value
273+
const cached = loadCachedName(model.value as string) ?? ''
274+
searchText.value = cached
275+
selectedGroupName.value = cached
254276
}
255277
256278
const handleClickOutside = (event: MouseEvent) => {
@@ -269,7 +291,7 @@ onMounted(async () => {
269291
document.addEventListener('mousedown', handleClickOutside)
270292
271293
// Show cached name immediately before the API call completes
272-
if (model.value && nameModel.value) {
294+
if (model.value && loadCachedName(model.value as string)) {
273295
applyCachedName()
274296
}
275297
@@ -280,7 +302,7 @@ onMounted(async () => {
280302
if (selected) {
281303
searchText.value = selected.name
282304
selectedGroupName.value = selected.name
283-
} else if (model.value && nameModel.value) {
305+
} else if (model.value && loadCachedName(model.value as string)) {
284306
// Group is beyond page 1 — use the cached name for display
285307
applyCachedName()
286308
} else if (model.value) {
@@ -291,7 +313,6 @@ onMounted(async () => {
291313
if (found) {
292314
searchText.value = found.name
293315
selectedGroupName.value = found.name
294-
nameModel.value = found.name
295316
break
296317
}
297318
}
@@ -300,7 +321,6 @@ onMounted(async () => {
300321
model.value = first.id
301322
searchText.value = first.name
302323
selectedGroupName.value = first.name
303-
nameModel.value = first.name
304324
}
305325
}
306326
})
@@ -311,7 +331,9 @@ onUnmounted(() => {
311331
})
312332
</script>
313333

314-
<style scoped>
334+
<style lang="scss" scoped>
335+
@import "assets/scss/theme.scss";
336+
315337
.cursor-pointer {
316338
cursor: pointer;
317339
}
@@ -328,17 +350,17 @@ onUnmounted(() => {
328350
align-items: center;
329351
gap: 8px;
330352
padding: 5px 12px;
331-
border-bottom: 1px solid #e9ecef;
332-
background: #f8f9fa;
353+
border-bottom: 1px solid $gray-200;
354+
background: $gray-100;
333355
min-height: 30px;
334356
}
335357
.pg-count {
336358
font-size: 0.74rem;
337-
color: #6c757d;
359+
color: $gray-600;
338360
flex: 1;
339361
}
340362
.pg-scroll-hint {
341-
color: #0d6efd;
363+
color: $primary;
342364
}
343365
.pg-list-wrap {
344366
position: relative;

pages/dashboard.vue

Lines changed: 16 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
<h2 class="visually-hidden">My Workspaces</h2>
55

66
<label for="ws_project_group_picker">Project Group</label>
7-
<project-group-picker v-model="currentProjectGroup" v-model:name="currentProjectGroupName" id="ws_project_group_picker" />
7+
<project-group-picker v-model="currentProjectGroup" id="ws_project_group_picker" />
88

99
<nuxt-link class="btn btn-primary flex-shrink-0" to="/workspace/create">
1010
<app-icon variant="add" size="24" />
@@ -45,37 +45,19 @@
4545

4646
<script lang="ts">
4747
const STORAGE_KEY_PROJECT_GROUP = 'tdei-selected-project-group';
48-
const STORAGE_KEY_PROJECT_GROUP_NAME = 'tdei-selected-project-group-name';
4948
const STORAGE_KEY_WORKSPACE = 'tdei-selected-workspace';
5049
5150
function getLastProjectGroupId(): string | null {
5251
if (typeof window === 'undefined') return null;
5352
try {
54-
return sessionStorage.getItem(STORAGE_KEY_PROJECT_GROUP);
53+
const raw = sessionStorage.getItem(STORAGE_KEY_PROJECT_GROUP);
54+
if (!raw) return null;
55+
const stored = JSON.parse(raw) as { id: string; name: string };
56+
return stored.id ?? null;
5557
} catch {
5658
return null;
5759
}
5860
}
59-
function setLastProjectGroupId(id: string) {
60-
if (typeof window === 'undefined') return;
61-
try {
62-
sessionStorage.setItem(STORAGE_KEY_PROJECT_GROUP, id);
63-
} catch { /* silently fail */ }
64-
}
65-
function getLastProjectGroupName(): string | null {
66-
if (typeof window === 'undefined') return null;
67-
try {
68-
return sessionStorage.getItem(STORAGE_KEY_PROJECT_GROUP_NAME);
69-
} catch {
70-
return null;
71-
}
72-
}
73-
function setLastProjectGroupName(name: string) {
74-
if (typeof window === 'undefined') return;
75-
try {
76-
sessionStorage.setItem(STORAGE_KEY_PROJECT_GROUP_NAME, name);
77-
} catch { /* silently fail */ }
78-
}
7961
function getLastWorkspaceId(): number | null {
8062
if (typeof window === 'undefined') return null;
8163
try {
@@ -103,7 +85,6 @@ const workspaces = (await workspacesClient.getMyWorkspaces()).sort(compareWorksp
10385
const workspacesByProjectGroup = Map.groupBy(workspaces, w => w.tdeiProjectGroupId);
10486
10587
const currentProjectGroup = ref(getLastProjectGroupId());
106-
const currentProjectGroupName = ref(getLastProjectGroupName());
10788
const currentWorkspace = ref({});
10889
const currentWorkspaces = computed(() => workspacesByProjectGroup.get(currentProjectGroup.value));
10990
@@ -115,8 +96,6 @@ for (const w of workspaces) {
11596
11697
onMounted(() => {
11798
watch(currentWorkspace, (val) => { if (val?.id) setLastWorkspaceId(val.id) });
118-
watch(currentProjectGroup, (val) => { if (val) setLastProjectGroupId(val) });
119-
watch(currentProjectGroupName, (val) => { if (val) setLastProjectGroupName(val) });
12099
watch(currentWorkspaces, onCurrentWorkspacesChange);
121100
122101
autoSelectPreferredView();
@@ -186,21 +165,19 @@ async function selectWorkspace(workspace) {
186165
}
187166
188167
.project-group-picker {
189-
width: auto;
190-
min-width: 300px;
191-
border-color: transparent;
192-
border-left-color: $border-color;
193-
margin-right: auto;
194-
195-
&:hover {
196-
border-color: $border-color;
197-
}
168+
width: 100%;
169+
border-color: $border-color;
170+
margin-right: 1rem;
198171
199-
@include media-breakpoint-down(md) {
200-
& {
201-
width: 100%;
172+
@include media-breakpoint-up(md) {
173+
width: auto;
174+
min-width: 300px;
175+
border-color: transparent;
176+
border-left-color: $border-color;
177+
margin-right: auto;
178+
179+
&:hover {
202180
border-color: $border-color;
203-
margin-right: 1rem;
204181
}
205182
}
206183
}

pages/workspace/create/tdei.vue

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,9 @@
4646
required
4747
/>
4848
</label>
49+
<div v-if="datasetError" class="alert alert-danger py-2" role="alert">
50+
{{ datasetError }}
51+
</div>
4952
</div><!-- .card-body -->
5053

5154
<div class="card-footer">
@@ -162,16 +165,20 @@ const record = reactive<Record<string, any>>({})
162165
const map = ref<any>({})
163166
const workspaceTitle = ref('')
164167
const projectGroupId = ref<string | null>(null)
168+
const datasetError = ref<string | null>(null)
165169
166170
watch(tdeiRecordId, val => getDatasetInfo(val))
167171
168172
const complete = computed(() =>
169173
workspaceTitle.value.trim().length > 0
170174
&& projectGroupId.value !== null
171-
&& tdeiRecordId.value !== null,
175+
&& tdeiRecordId.value !== null
176+
&& datasetError.value === null,
172177
)
173178
174179
async function getDatasetInfo(id: string | null) {
180+
datasetError.value = null
181+
175182
if (id === null) {
176183
for (const prop in record) {
177184
record[prop] = ''
@@ -197,9 +204,14 @@ async function getDatasetInfo(id: string | null) {
197204
198205
await nextTick()
199206
207+
if (!record.project_group?.tdei_project_group_id || !record.tdei_dataset_id) {
208+
datasetError.value = 'The selected dataset returned incomplete data. Please try a different dataset or contact support.'
209+
return
210+
}
211+
200212
workspaceTitle.value = record.metadata?.dataset_detail?.name ?? ''
201-
projectGroupId.value = record.project_group?.tdei_project_group_id ?? null
202-
tdeiRecordId.value = record.tdei_dataset_id ?? null
213+
projectGroupId.value = record.project_group.tdei_project_group_id
214+
tdeiRecordId.value = record.tdei_dataset_id
203215
204216
initMap()
205217
}

services/tdei.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,6 @@ export class TdeiAuthStore {
124124

125125
localStorage.removeItem(this._storageKey);
126126
sessionStorage.removeItem('tdei-selected-project-group');
127-
sessionStorage.removeItem('tdei-selected-project-group-name');
128127
sessionStorage.removeItem('tdei-selected-workspace');
129128
}
130129
}
@@ -460,7 +459,7 @@ export class TdeiUserClient extends BaseHttpClient implements ICancelableClient
460459
return new TdeiClient(this._baseUrl, this.#auth, signal ?? this._abortSignal);
461460
}
462461

463-
async getMyProjectGroups(pageNo: number = 1, searchText: string = '', pageSize: number = 10, sortBy: 'name' | 'created_at' = 'name'): Promise<{ items: TdeiProjectGroup[], total: number | null }> {
462+
async getMyProjectGroups(pageNo: number = 1, searchText: string = '', pageSize: number = 10, sortBy: 'name' | 'created_at' = 'name'): Promise<{ items: TdeiProjectGroup[], total?: number }> {
464463
let url = `project-group-roles/${this.#auth.subject}?page_size=${pageSize}&page_no=${pageNo}&sort_by=${sortBy}`;
465464
if (searchText) {
466465
url += `&searchText=${encodeURIComponent(searchText)}`;
@@ -471,12 +470,12 @@ export class TdeiUserClient extends BaseHttpClient implements ICancelableClient
471470
try {
472471
const totalHeader = response.headers.get('X-Total-Count');
473472
const totalParsed = totalHeader !== null ? parseInt(totalHeader, 10) : NaN;
474-
const total = Number.isNaN(totalParsed) ? null : totalParsed;
473+
const total = Number.isNaN(totalParsed) ? undefined : totalParsed;
475474
const items = (await response.json() as TdeiProjectGroupApiResponse[]) ?? [];
476475
return { items: items.map(p => ({ id: p.tdei_project_group_id, name: p.project_group_name })), total };
477476
} catch (e) {
478477
console.warn('getMyProjectGroups: failed to parse API response', e);
479-
return { items: [], total: null };
478+
return { items: [] };
480479
}
481480
}
482481

0 commit comments

Comments
 (0)