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
101 changes: 63 additions & 38 deletions components/ProjectGroupPicker.vue
Original file line number Diff line number Diff line change
Expand Up @@ -45,11 +45,11 @@
</li>
<li
v-for="(pg, index) in projectGroups"
:key="pg.id"
:key="pg.tdei_project_group_id"
:id="'pg-item-' + index"
class="list-group-item list-group-item-action cursor-pointer"
:class="{ highlighted: activeIndex === index, 'fw-bold': model === pg.id }"
@click="selectGroup(pg.id)"
:class="{ highlighted: activeIndex === index, 'fw-bold': model === pg.tdei_project_group_id }"
@click="selectGroup(pg.tdei_project_group_id)"
@mouseenter="activeIndex = index"
>
{{ pg.name }}
Expand Down Expand Up @@ -86,29 +86,34 @@ function persistCachedName(id: string, name: string) {
<script setup lang="ts">
import { ref, watch, onMounted, onUnmounted, nextTick } from 'vue'
import { tdeiUserClient } from '~/services/index'
import type { TdeiProjectGroupItem } from '~/types/tdei'

const props = withDefaults(defineProps<{ disabled?: boolean }>(), {
const props = withDefaults(defineProps<{ disabled?: boolean; options?: TdeiProjectGroupItem[] }>(), {
disabled: false,
})

const model = defineModel({ required: true })
const searchText = ref('')
const isOpen = ref(false)
const projectGroups = ref<{ id: string; name: string }[]>([])
const fetchedGroups = ref<TdeiProjectGroupItem[]>([])
const selectedGroupName = ref('')
const loading = ref(false)
const totalCount = ref<number | undefined>(undefined)
const pickerRef = ref<HTMLElement | null>(null)
const listRef = ref<HTMLElement | null>(null)
const activeIndex = ref(-1)

const projectGroups = computed(() => props.options ?? fetchedGroups.value)

let pageNo = 1
const hasMore = ref(true)
let pendingReset = false
const pageSize = 10
let hasUnfilteredResults = false

const loadGroups = async (reset = false) => {
if (props.options) return

if (loading.value) {
pendingReset = pendingReset || reset
return
Expand All @@ -117,7 +122,7 @@ const loadGroups = async (reset = false) => {
if (reset) {
pageNo = 1
hasMore.value = true
projectGroups.value = []
fetchedGroups.value = []
activeIndex.value = -1
totalCount.value = undefined
}
Expand All @@ -136,9 +141,9 @@ const loadGroups = async (reset = false) => {

const { items: newGroups, total } = await tdeiUserClient.getMyProjectGroups(pageNo, query, pageSize)
if (total !== undefined) totalCount.value = total
projectGroups.value.push(...newGroups)
const selected = newGroups.find(g => g.id === model.value)
if (selected) persistCachedName(selected.id, selected.name)
fetchedGroups.value.push(...newGroups)
const selected = newGroups.find(g => g.tdei_project_group_id === model.value)
if (selected) persistCachedName(selected.tdei_project_group_id, selected.name)

if (newGroups.length < pageSize) {
hasMore.value = false
Expand Down Expand Up @@ -178,7 +183,7 @@ const onInput = () => {
}

watch(model, (newId) => {
const pg = projectGroups.value.find(p => p.id === newId)
const pg = projectGroups.value.find(p => p.tdei_project_group_id === newId)
if (pg && !isOpen.value) {
searchText.value = pg.name
selectedGroupName.value = pg.name
Expand All @@ -195,11 +200,11 @@ const onScroll = (e: Event) => {
const selectGroup = (id: string) => {
model.value = id
isOpen.value = false
const pg = projectGroups.value.find(p => p.id === id)
const pg = projectGroups.value.find(p => p.tdei_project_group_id === id)
if (pg) {
searchText.value = pg.name
selectedGroupName.value = pg.name
persistCachedName(pg.id, pg.name)
persistCachedName(pg.tdei_project_group_id, pg.name)
}
}

Expand Down Expand Up @@ -257,7 +262,7 @@ const onKeydown = (e: KeyboardEvent) => {
e.preventDefault()
if (activeIndex.value >= 0 && activeIndex.value < projectGroups.value.length) {
const pg = projectGroups.value[activeIndex.value]
if (pg) selectGroup(pg.id)
if (pg) selectGroup(pg.tdei_project_group_id)
}
} else if (e.key === 'Escape') {
e.preventDefault()
Expand All @@ -273,7 +278,7 @@ const applyCachedName = () => {

const closeDropdown = () => {
isOpen.value = false
const pg = projectGroups.value.find(p => p.id === model.value)
const pg = projectGroups.value.find(p => p.tdei_project_group_id === model.value)
const name = pg?.name ?? selectedGroupName.value
searchText.value = name
if (pg) selectedGroupName.value = name
Expand All @@ -285,38 +290,58 @@ const onFocusOut = (e: FocusEvent) => {
}
}

watch(
projectGroups,
(groups) => {
if (groups.length > 0) {
const pgId = model.value as string | undefined
if (!pgId || !groups.some(pg => pg.tdei_project_group_id === pgId)) {
model.value = groups[0]?.tdei_project_group_id
}
const selected = groups.find(pg => pg.tdei_project_group_id === model.value)
if (selected && !isOpen.value) {
searchText.value = selected.name
selectedGroupName.value = selected.name
}
}
},
{ immediate: true },
)

onMounted(async () => {
// Show cached name immediately before the API call completes
if (model.value && loadCachedName(model.value as string)) {
applyCachedName()
}

await loadGroups(true)

if (projectGroups.value.length > 0) {
const selected = projectGroups.value.find(pg => pg.id === model.value)
if (selected) {
searchText.value = selected.name
selectedGroupName.value = selected.name
} else if (model.value && loadCachedName(model.value as string)) {
// Group is beyond page 1 — use the cached name for display
applyCachedName()
} else if (model.value) {
// model is set but name is unknown — paginate until the group is found
while (hasMore.value) {
await loadGroups()
const found = projectGroups.value.find(pg => pg.id === model.value)
if (found) {
searchText.value = found.name
selectedGroupName.value = found.name
break
if (!props.options) {
await loadGroups(true)

if (fetchedGroups.value.length > 0) {
const selected = fetchedGroups.value.find(pg => pg.tdei_project_group_id === model.value)
if (selected) {
searchText.value = selected.name
selectedGroupName.value = selected.name
} else if (model.value && loadCachedName(model.value as string)) {
// Group is beyond page 1 — use the cached name for display
applyCachedName()
} else if (model.value) {
// model is set but name is unknown — paginate until the group is found
while (hasMore.value) {
await loadGroups()
const found = fetchedGroups.value.find(pg => pg.tdei_project_group_id === model.value)
if (found) {
searchText.value = found.name
selectedGroupName.value = found.name
break
}
}
} else if (!model.value) {
const first = fetchedGroups.value[0]!
model.value = first.tdei_project_group_id
searchText.value = first.name
selectedGroupName.value = first.name
}
} else if (!model.value) {
const first = projectGroups.value[0]!
model.value = first.id
searchText.value = first.name
selectedGroupName.value = first.name
}
}
})
Expand Down
58 changes: 50 additions & 8 deletions components/dashboard/DetailsTable.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<template>
<div class="table-responsive border-top">
<table class="table table-striped mb-0">
<div class="table-responsive border-top mb-0">
<table class="table table-striped">
<tbody>
<tr>
<th><app-icon variant="schedule" />Created At</th>
Expand All @@ -10,6 +10,41 @@
<th><app-icon variant="person_outline" />Created By</th>
<td>{{ workspace.createdByName }}</td>
</tr>
<tr>
<th><app-icon variant="badge" />My Role</th>
<td>
<span
v-if="workspace.role === 'lead'"
class="badge bg-dark text-uppercase"
>
<app-icon variant="star" /> Owner
</span>
<span
v-else-if="workspace.role === 'validator'"
class="badge bg-dark text-uppercase"
>
<app-icon variant="task_alt" /> Validator
</span>
<span
v-else
class="badge bg-secondary text-uppercase"
>
<app-icon variant="person" /> Member
</span>
Comment thread
cyrossignol marked this conversation as resolved.
<span
v-if="isPoc"
class="badge bg-warning text-dark text-uppercase ms-1"
>
<app-icon variant="local_police" /> POC
</span>
<span
v-else-if="isDataGenerator"
class="badge bg-warning text-dark text-uppercase ms-1"
>
<app-icon variant="offline_bolt" /> Data Generator
</span>
</td>
</tr>
<tr>
<th><app-icon variant="phonelink_setup" />App Access</th>
<td>
Expand Down Expand Up @@ -42,12 +77,19 @@
</template>

<script setup lang="ts">
const props = defineProps({
workspace: {
type: Object,
required: true
}
});
import type { Workspace } from '~/types/workspaces';

interface Props {
workspace: Workspace;
myTdeiRoles: string[];
Comment thread
cyrossignol marked this conversation as resolved.
}

const props = defineProps<Props>();

const isPoc = computed(() => props.myTdeiRoles.includes('poc'));
const isDataGenerator = computed(() =>
props.myTdeiRoles.includes(`${props.workspace.type}_data_generator`),
);
</script>

<style lang="scss">
Expand Down
4 changes: 2 additions & 2 deletions components/dashboard/Toolbar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,8 @@
<app-icon variant="settings" size="24" no-margin />
<span class="d-none d-sm-inline ms-2">Settings</span>
</nuxt-link>
</div>
</div>
</div><!-- .btn-group -->
</div><!-- .btn-toolbar -->
</template>

<script setup lang="ts">
Expand Down
15 changes: 15 additions & 0 deletions components/dashboard/WorkspaceItem.vue
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,25 @@
<app-icon v-else variant="lock" />
App
</span>

<span
v-if="workspace.role === 'lead'"
class="badge bg-dark ms-2"
>
<app-icon variant="star" /> {{ ROLE_LABELS.lead }}
</span>
<span
v-else-if="workspace.role === 'validator'"
class="badge bg-dark ms-2"
>
<app-icon variant="task_alt" /> {{ ROLE_LABELS.validator }}
</span>
</button>
</template>

<script setup lang="ts">
import { ROLE_LABELS } from '~/util/roles';

const props = defineProps({
workspace: {
type: Object,
Expand Down
30 changes: 21 additions & 9 deletions components/review/Toolbar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -45,16 +45,27 @@
{{ props.item.commentCount }}
</span>
</button>
<button
v-show="props.item.isFeedback"
class="btn btn-sm btn-success ms-2"
<BPopover
content="Only validators and owners can resolve feedback"
placement="bottom"
:manual="isValidator"
>
<app-icon
variant="check"
no-margin
/>
<span class="d-none d-sm-inline ms-2">Mark as Resolved</span>
</button>
<template #target>
<div class="d-inline-block ms-2">
<button
v-show="props.item.isFeedback && !props.item.isResolved"
class="btn btn-sm btn-success"
:disabled="!isValidator"
>
<app-icon
variant="check"
no-margin
/>
<span class="d-none d-sm-inline ms-2">Mark as Resolved</span>
</button>
Comment thread
coderabbitai[bot] marked this conversation as resolved.
</div>
</template>
</BPopover>
</nav>
</template>

Expand All @@ -66,6 +77,7 @@ interface Props {
}

const props = defineProps<Props>();
const { isValidator } = useWorkspaceRole();

const emit = defineEmits(['edit']);
const showDetails = defineModel<boolean>('showDetails');
Expand Down
8 changes: 7 additions & 1 deletion components/settings/Nav.vue
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,15 @@
>
General
</settings-nav-link>
<settings-nav-link
to="/members"
icon="admin_panel_settings"
>
Members
</settings-nav-link>
Comment thread
cyrossignol marked this conversation as resolved.
<settings-nav-link
to="/teams"
icon="group"
icon="diversity_3"
>
Teams
</settings-nav-link>
Expand Down
Loading
Loading