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:
@@ -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);
|
||||
|
||||
+30
-11
@@ -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() {
|
||||
$deleteCourseModal.open = true;
|
||||
$deleteCourseModal.id = id;
|
||||
$deleteCourseModal.title = title;
|
||||
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) => ({
|
||||
|
||||
Reference in New Issue
Block a user