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 { authOrAutomationKeyMiddleware } from '@api/middlewares/auth-or-automation-key';
|
||||||
import { handleError } from '@api/utils/errors';
|
import { handleError } from '@api/utils/errors';
|
||||||
import { orgAdminMiddleware } from '@api/middlewares/org-admin';
|
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 { orgAdminOrAutomationKeyMiddleware } from '@api/middlewares/org-admin-or-automation-key';
|
||||||
import { assertMcpAutomationUsageAllowed, recordMcpAutomationUsage } from '@api/services/organization/automation-usage';
|
import { assertMcpAutomationUsageAllowed, recordMcpAutomationUsage } from '@api/services/organization/automation-usage';
|
||||||
import { zValidator } from '@hono/zod-validator';
|
import { zValidator } from '@hono/zod-validator';
|
||||||
@@ -44,7 +45,7 @@ export const tagsRouter = new Hono()
|
|||||||
return handleError(c, error, 'Failed to fetch public tags');
|
return handleError(c, error, 'Failed to fetch public tags');
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.get('/', authMiddleware, orgAdminMiddleware, async (c) => {
|
.get('/', authMiddleware, orgMemberMiddleware, async (c) => {
|
||||||
try {
|
try {
|
||||||
const orgId = c.req.header('cio-org-id')!;
|
const orgId = c.req.header('cio-org-id')!;
|
||||||
const groups = await getOrganizationTagGroups(orgId);
|
const groups = await getOrganizationTagGroups(orgId);
|
||||||
|
|||||||
+30
-11
@@ -1,8 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import * as DropdownMenu from '@cio/ui/base/dropdown-menu';
|
import * as DropdownMenu from '@cio/ui/base/dropdown-menu';
|
||||||
import ExternalLinkIcon from '@lucide/svelte/icons/external-link';
|
import { page } from '$app/state';
|
||||||
import CopyIcon from '@lucide/svelte/icons/copy';
|
|
||||||
import EyeIcon from '@lucide/svelte/icons/eye';
|
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
import { resolve } from '$app/paths';
|
import { resolve } from '$app/paths';
|
||||||
import { copyCourseModal, deleteCourseModal } from '$features/course/utils/store';
|
import { copyCourseModal, deleteCourseModal } from '$features/course/utils/store';
|
||||||
@@ -43,12 +41,22 @@
|
|||||||
}: Props = $props();
|
}: Props = $props();
|
||||||
|
|
||||||
const showPublicCourseLinks = $derived(isPublished && courseType === 'PUBLIC' && slug.trim().length > 0);
|
const showPublicCourseLinks = $derived(isPublished && courseType === 'PUBLIC' && slug.trim().length > 0);
|
||||||
|
const courseSettingsPath = $derived(resolve(`/courses/${id}/settings`, {}));
|
||||||
|
|
||||||
function redirect(url: string) {
|
function redirect(url: string) {
|
||||||
goto(resolve(url, {}));
|
goto(resolve(url, {}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function scrollToSettingsSection(sectionId: string) {
|
||||||
|
document.getElementById(sectionId)?.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||||
|
}
|
||||||
|
|
||||||
function handleShareCourse() {
|
function handleShareCourse() {
|
||||||
|
if (page.url.pathname === courseSettingsPath) {
|
||||||
|
scrollToSettingsSection('share');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
redirect(`/courses/${id}/settings#share`);
|
redirect(`/courses/${id}/settings#share`);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,13 +65,29 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handlePublishCourse() {
|
function handlePublishCourse() {
|
||||||
|
if (page.url.pathname === courseSettingsPath) {
|
||||||
|
scrollToSettingsSection('publish');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
redirect(`/courses/${id}/settings#publish`);
|
redirect(`/courses/${id}/settings#publish`);
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleDeleteCourse() {
|
function handleDeleteCourse() {
|
||||||
$deleteCourseModal.open = true;
|
if (page.url.pathname === courseSettingsPath) {
|
||||||
$deleteCourseModal.id = id;
|
scrollToSettingsSection('delete');
|
||||||
$deleteCourseModal.title = title;
|
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() {
|
function handleCloneCourse() {
|
||||||
@@ -92,17 +116,14 @@
|
|||||||
|
|
||||||
{#if lmsPublicQuickOnly}
|
{#if lmsPublicQuickOnly}
|
||||||
<DropdownMenu.Item onclick={handleViewCourseSite}>
|
<DropdownMenu.Item onclick={handleViewCourseSite}>
|
||||||
<ExternalLinkIcon size={16} class="mr-2" />
|
|
||||||
{$t('courses.course_card.context_menu.view_course_site')}
|
{$t('courses.course_card.context_menu.view_course_site')}
|
||||||
</DropdownMenu.Item>
|
</DropdownMenu.Item>
|
||||||
<DropdownMenu.Item onclick={() => void handleCopyCourseUrl()}>
|
<DropdownMenu.Item onclick={() => void handleCopyCourseUrl()}>
|
||||||
<CopyIcon size={16} class="mr-2" />
|
|
||||||
{$t('courses.course_card.context_menu.copy_course_url')}
|
{$t('courses.course_card.context_menu.copy_course_url')}
|
||||||
</DropdownMenu.Item>
|
</DropdownMenu.Item>
|
||||||
{:else}
|
{:else}
|
||||||
{#if includeViewAsStudent}
|
{#if includeViewAsStudent}
|
||||||
<DropdownMenu.Item onclick={() => onViewAsStudent?.()}>
|
<DropdownMenu.Item onclick={() => onViewAsStudent?.()}>
|
||||||
<EyeIcon size={16} class="mr-2" />
|
|
||||||
{$t('course.header.view_as_student')}
|
{$t('course.header.view_as_student')}
|
||||||
</DropdownMenu.Item>
|
</DropdownMenu.Item>
|
||||||
<DropdownMenu.Separator />
|
<DropdownMenu.Separator />
|
||||||
@@ -110,12 +131,10 @@
|
|||||||
|
|
||||||
{#if isPublished}
|
{#if isPublished}
|
||||||
<DropdownMenu.Item onclick={handleViewCourseSite}>
|
<DropdownMenu.Item onclick={handleViewCourseSite}>
|
||||||
<ExternalLinkIcon size={16} class="mr-2" />
|
|
||||||
{$t('courses.course_card.context_menu.view_course_site')}
|
{$t('courses.course_card.context_menu.view_course_site')}
|
||||||
</DropdownMenu.Item>
|
</DropdownMenu.Item>
|
||||||
{#if showPublicCourseLinks}
|
{#if showPublicCourseLinks}
|
||||||
<DropdownMenu.Item onclick={() => void handleCopyCourseUrl()}>
|
<DropdownMenu.Item onclick={() => void handleCopyCourseUrl()}>
|
||||||
<CopyIcon size={16} class="mr-2" />
|
|
||||||
{$t('courses.course_card.context_menu.copy_course_url')}
|
{$t('courses.course_card.context_menu.copy_course_url')}
|
||||||
</DropdownMenu.Item>
|
</DropdownMenu.Item>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -307,6 +307,17 @@
|
|||||||
loadCourseTags(courseId);
|
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 selectedTagChips = $derived.by(() => {
|
||||||
const allTags = tagApi.tagGroups.flatMap((group) =>
|
const allTags = tagApi.tagGroups.flatMap((group) =>
|
||||||
group.tags.map((tag) => ({
|
group.tags.map((tag) => ({
|
||||||
|
|||||||
Reference in New Issue
Block a user