fix(dashboard): course menu icons, delete scroll, and settings tags auth (#691)

* fix(dashboard): course menu icons, delete scroll, and settings tags auth

Remove inconsistent dropdown icons, scroll to settings sections for delete
and related actions when already on the settings page, and skip org tag
group fetches for non-admin users to avoid ORG_TEAM_NOT_AUTHORIZED errors.

* fix(api): allow org members to fetch organization tags

Use orgMemberMiddleware for GET /organization/tags instead of restricting
reads to admins. Revert the dashboard settings workaround that skipped tag
group fetches for non-admin users.
---------
This commit is contained in:
Rotimi Best
2026-06-25 23:53:16 +01:00
committed by GitHub
parent bd13d0724d
commit 4fe45ac642
3 changed files with 43 additions and 12 deletions
+2 -1
View File
@@ -23,6 +23,7 @@ import { authMiddleware } from '@api/middlewares/auth';
import { authOrAutomationKeyMiddleware } from '@api/middlewares/auth-or-automation-key';
import { handleError } from '@api/utils/errors';
import { orgAdminMiddleware } from '@api/middlewares/org-admin';
import { orgMemberMiddleware } from '@api/middlewares/org-member';
import { orgAdminOrAutomationKeyMiddleware } from '@api/middlewares/org-admin-or-automation-key';
import { assertMcpAutomationUsageAllowed, recordMcpAutomationUsage } from '@api/services/organization/automation-usage';
import { zValidator } from '@hono/zod-validator';
@@ -44,7 +45,7 @@ export const tagsRouter = new Hono()
return handleError(c, error, 'Failed to fetch public tags');
}
})
.get('/', authMiddleware, orgAdminMiddleware, async (c) => {
.get('/', authMiddleware, orgMemberMiddleware, async (c) => {
try {
const orgId = c.req.header('cio-org-id')!;
const groups = await getOrganizationTagGroups(orgId);
@@ -1,8 +1,6 @@
<script lang="ts">
import * as DropdownMenu from '@cio/ui/base/dropdown-menu';
import ExternalLinkIcon from '@lucide/svelte/icons/external-link';
import CopyIcon from '@lucide/svelte/icons/copy';
import EyeIcon from '@lucide/svelte/icons/eye';
import { page } from '$app/state';
import { goto } from '$app/navigation';
import { resolve } from '$app/paths';
import { copyCourseModal, deleteCourseModal } from '$features/course/utils/store';
@@ -43,12 +41,22 @@
}: Props = $props();
const showPublicCourseLinks = $derived(isPublished && courseType === 'PUBLIC' && slug.trim().length > 0);
const courseSettingsPath = $derived(resolve(`/courses/${id}/settings`, {}));
function redirect(url: string) {
goto(resolve(url, {}));
}
function scrollToSettingsSection(sectionId: string) {
document.getElementById(sectionId)?.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
function handleShareCourse() {
if (page.url.pathname === courseSettingsPath) {
scrollToSettingsSection('share');
return;
}
redirect(`/courses/${id}/settings#share`);
}
@@ -57,13 +65,29 @@
}
function handlePublishCourse() {
if (page.url.pathname === courseSettingsPath) {
scrollToSettingsSection('publish');
return;
}
redirect(`/courses/${id}/settings#publish`);
}
function handleDeleteCourse() {
if (page.url.pathname === courseSettingsPath) {
scrollToSettingsSection('delete');
return;
}
const isOnOrgCoursesList = /\/org\/[^/]+\/courses\/?$/.test(page.url.pathname);
if (isOnOrgCoursesList) {
$deleteCourseModal.open = true;
$deleteCourseModal.id = id;
$deleteCourseModal.title = title;
return;
}
redirect(`/courses/${id}/settings#delete`);
}
function handleCloneCourse() {
@@ -92,17 +116,14 @@
{#if lmsPublicQuickOnly}
<DropdownMenu.Item onclick={handleViewCourseSite}>
<ExternalLinkIcon size={16} class="mr-2" />
{$t('courses.course_card.context_menu.view_course_site')}
</DropdownMenu.Item>
<DropdownMenu.Item onclick={() => void handleCopyCourseUrl()}>
<CopyIcon size={16} class="mr-2" />
{$t('courses.course_card.context_menu.copy_course_url')}
</DropdownMenu.Item>
{:else}
{#if includeViewAsStudent}
<DropdownMenu.Item onclick={() => onViewAsStudent?.()}>
<EyeIcon size={16} class="mr-2" />
{$t('course.header.view_as_student')}
</DropdownMenu.Item>
<DropdownMenu.Separator />
@@ -110,12 +131,10 @@
{#if isPublished}
<DropdownMenu.Item onclick={handleViewCourseSite}>
<ExternalLinkIcon size={16} class="mr-2" />
{$t('courses.course_card.context_menu.view_course_site')}
</DropdownMenu.Item>
{#if showPublicCourseLinks}
<DropdownMenu.Item onclick={() => void handleCopyCourseUrl()}>
<CopyIcon size={16} class="mr-2" />
{$t('courses.course_card.context_menu.copy_course_url')}
</DropdownMenu.Item>
{/if}
@@ -307,6 +307,17 @@
loadCourseTags(courseId);
});
$effect(() => {
const sectionId = $page.url.hash.replace('#', '').trim();
if (!sectionId) {
return;
}
requestAnimationFrame(() => {
document.getElementById(sectionId)?.scrollIntoView({ behavior: 'smooth', block: 'start' });
});
});
const selectedTagChips = $derived.by(() => {
const allTags = tagApi.tagGroups.flatMap((group) =>
group.tags.map((tag) => ({