feat(ui): Page.SettingsActions for settings save bar (#819)
* feat(ui): add Page.SettingsActions for settings save bar Introduce a sticky bottom save/discard bar on Page and migrate settings pages to use it instead of header or inline save buttons. Includes profile, org, customize-lms, notifications, auth, AI tutor, course settings, certificates, and cohort settings. Co-authored-by: Rotimi Best <[email protected]> * fix(ui): always show Page.SettingsActions, disable when clean Keep the settings save bar visible at all times and disable Save and Discard when hasChanges is false instead of hiding the bar entirely. Co-authored-by: Rotimi Best <[email protected]> * fix(ui): add bottom spacing and shadow to Page.SettingsActions Wrap the actions card in a sticky container with bottom padding so it does not sit flush against the viewport edge, and use shadow-md on the card for clearer elevation. Co-authored-by: Rotimi Best <[email protected]> * fix(ui): stop viewport-sticking Page.SettingsActions on scroll Remove sticky bottom positioning so the save bar stays at the end of the page content and scrolls away when viewing upper sections. Co-authored-by: Rotimi Best <[email protected]> * fix(ui): use sticky bottom for Page.SettingsActions dock behavior Replace the IntersectionObserver fixed/dock toggle that caused layout thrashing and flipping with native position: sticky; bottom: 0. Move overflow-x-hidden from Page.Root to Page.Body so sticky is not broken. Co-authored-by: Rotimi Best <[email protected]> * fix(ui): keep Page.SettingsActions sticky and above editor content Page.Root carried md:overflow-x-auto, which makes overflow-y compute to auto, turning the root into the sticky bar's nearest scrolling ancestor. Since the root sizes to its content it never scrolls internally, so the bar rendered at its natural flow position instead of pinning to the viewport. Drop the overflow so sticky resolves against the viewport; horizontal containment stays on Page.Body. Raise the bar to z-50 so it is not painted over by the editor root, which is relative z-50. Matching rather than exceeding 50 keeps body-portaled dialogs and sheets above the bar. Show the shadow only while the bar is pinned away from its flow position, tracked with an IntersectionObserver on a 1px sentinel sibling. Unlike the previous observer this toggles a box-shadow only, so it cannot feed back into layout. * fix(ui): tighten settings actions spacing and tab visibility - Remove mt-6 and lower z-index so popovers sit above the bar - Wrap sentinel in a single root to avoid extra Page.Root flex gap - Remove Page.Body mb-4 redundant with settings actions spacing - Show certificate save bar only on the settings tab - Raise theme color picker z-index via --picker-z-index Co-authored-by: Rotimi Best <[email protected]> * fix(dashboard): address PR review feedback for settings actions - Capture profile/org saved baselines only on initial load, not on edits - Reset course settings errors with explicit field shape in handleDiscard - Restore VisitOrgSiteButton on org profile settings header - Derive AI tutor dirty state from stores directly instead of mirrored state - Use discard-specific French label and document SettingsActions props Co-authored-by: Rotimi Best <[email protected]> * docs(ui): clarify Page.Root ownership for Page.SettingsActions "Last child of Page.Root" is a runtime relationship. Several settings pages get their Page.Root from a +layout.svelte, so the page file holds only Page.Header, Page.Body, and Page.SettingsActions. Document that this is correct and list the pages, since adding a nested Page.Root stacks two min-height containers and shortens the sticky bar's containing block. Also document that Page.SettingsActions must never be rendered from inside Page.Body: overflow-x-hidden makes Page.Body a scroll container that never scrolls, so a sticky bar in it has no travel. ---------
This commit is contained in:
@@ -1,11 +1,11 @@
|
||||
<script lang="ts">
|
||||
import * as Page from '@cio/ui/base/page';
|
||||
import { Button } from '@cio/ui/base/button';
|
||||
import { Input } from '@cio/ui/base/input';
|
||||
import { Textarea } from '@cio/ui/base/textarea';
|
||||
import { Label } from '@cio/ui/base/label';
|
||||
import * as Select from '@cio/ui/base/select';
|
||||
import { Separator } from '@cio/ui/base/separator';
|
||||
import { Button } from '@cio/ui/base/button';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import { cohortApi } from '$features/cohort/api';
|
||||
import { goto } from '$app/navigation';
|
||||
@@ -70,6 +70,15 @@
|
||||
}
|
||||
}
|
||||
|
||||
function handleDiscard() {
|
||||
const currentCohort = cohortApi.cohort;
|
||||
if (!currentCohort) return;
|
||||
|
||||
name = currentCohort.name;
|
||||
description = currentCohort.description ?? '';
|
||||
status = (currentCohort.status as 'ACTIVE' | 'INACTIVE' | 'ARCHIVED') ?? 'ACTIVE';
|
||||
}
|
||||
|
||||
async function handleDelete() {
|
||||
if (
|
||||
!confirm(
|
||||
@@ -84,15 +93,10 @@
|
||||
</script>
|
||||
|
||||
<Page.Root class="mx-auto flex w-[90%] px-4 md:max-w-2xl lg:max-w-3xl">
|
||||
<Page.Header class="sticky top-13 z-10 bg-white">
|
||||
<Page.Header>
|
||||
<Page.HeaderContent>
|
||||
<Page.Title>{$t('cohorts.sidebar.settings') || 'Settings'}</Page.Title>
|
||||
</Page.HeaderContent>
|
||||
<Page.Action>
|
||||
<Button variant="secondary" loading={isSaving} disabled={isSaving || !hasUnsavedChanges} onclick={handleSave}>
|
||||
{$t('cohorts.settings.save') || 'Save Changes'}
|
||||
</Button>
|
||||
</Page.Action>
|
||||
</Page.Header>
|
||||
<Page.Body>
|
||||
{#snippet child()}
|
||||
@@ -149,4 +153,14 @@
|
||||
</div>
|
||||
{/snippet}
|
||||
</Page.Body>
|
||||
<Page.SettingsActions
|
||||
hasChanges={hasUnsavedChanges}
|
||||
loading={isSaving}
|
||||
disabled={isSaving}
|
||||
statusLabel={$t('common.unsaved_changes.label')}
|
||||
discardLabel={$t('common.discard')}
|
||||
saveLabel={$t('common.save_changes')}
|
||||
onSave={handleSave}
|
||||
onDiscard={handleDiscard}
|
||||
/>
|
||||
</Page.Root>
|
||||
|
||||
@@ -7,8 +7,6 @@
|
||||
import { parseCertificateThemeId } from '$features/course/utils/certificate-utils';
|
||||
import { isFreePlan, isOrgAdmin } from '$lib/utils/store/org';
|
||||
import { profile } from '$lib/utils/store/user';
|
||||
import { Button } from '@cio/ui/base/button';
|
||||
import ZapIcon from '@lucide/svelte/icons/zap';
|
||||
import { RefreshPageData, UnsavedChanges } from '$features/ui';
|
||||
import { ZCourseUpdate } from '@cio/utils/validation/course/course';
|
||||
import { validateWithTranslation } from '$lib/utils/validation';
|
||||
@@ -16,6 +14,7 @@
|
||||
import { openUpgradeModal } from '$lib/utils/functions/org';
|
||||
|
||||
let errors = $state<Record<string, string>>({});
|
||||
let certificateActiveTab = $state('design');
|
||||
let hasUnsavedChanges = $state(false);
|
||||
let savedCertificateState = $state<string | null>(null);
|
||||
let savedCertificateStateCourseId = $state<string | null>(null);
|
||||
@@ -122,6 +121,19 @@
|
||||
snackbar.success('snackbar.course_settings.success.saved');
|
||||
}
|
||||
}
|
||||
|
||||
function handleDiscard() {
|
||||
if (!savedCertificateState || !courseApi.course) return;
|
||||
|
||||
const snapshot = JSON.parse(savedCertificateState) as ReturnType<typeof getCertificateFormState>;
|
||||
|
||||
if (!snapshot) return;
|
||||
|
||||
courseApi.course.description = snapshot.description;
|
||||
courseApi.course.certificate = { ...snapshot.certificate };
|
||||
hasUnsavedChanges = false;
|
||||
errors = {};
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
@@ -143,26 +155,24 @@
|
||||
</Page.Title>
|
||||
</Page.HeaderContent>
|
||||
<Page.Action>
|
||||
<div class="flex w-full justify-end gap-2">
|
||||
{#if canEditCertificates}
|
||||
<Button
|
||||
onclick={saveCertificate}
|
||||
loading={courseApi.isLoading}
|
||||
disabled={courseApi.isLoading || !hasUnsavedChanges}
|
||||
>
|
||||
{#if $isFreePlan}
|
||||
<ZapIcon size={16} class="filled" />
|
||||
{/if}
|
||||
{$t('course.navItem.certificates.save')}
|
||||
</Button>
|
||||
{/if}
|
||||
<RefreshPageData />
|
||||
</div>
|
||||
<RefreshPageData />
|
||||
</Page.Action>
|
||||
</Page.Header>
|
||||
<Page.Body>
|
||||
{#snippet child()}
|
||||
<CertificatesPage {errors} />
|
||||
<CertificatesPage {errors} bind:activeTab={certificateActiveTab} />
|
||||
{/snippet}
|
||||
</Page.Body>
|
||||
{#if canEditCertificates && certificateActiveTab === 'settings'}
|
||||
<Page.SettingsActions
|
||||
hasChanges={hasUnsavedChanges}
|
||||
loading={courseApi.isLoading}
|
||||
disabled={courseApi.isLoading}
|
||||
statusLabel={$t('common.unsaved_changes.label')}
|
||||
discardLabel={$t('common.discard')}
|
||||
saveLabel={$t('common.save_changes')}
|
||||
onSave={saveCertificate}
|
||||
onDiscard={handleDiscard}
|
||||
/>
|
||||
{/if}
|
||||
</Page.Root>
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
import { CourseSettingsPage } from '$features/course/pages';
|
||||
import * as Page from '@cio/ui/base/page';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import { Button } from '@cio/ui/base/button';
|
||||
import { courseApi } from '$features/course/api';
|
||||
import { profile } from '$lib/utils/store/user';
|
||||
import { RefreshPageData } from '$features/ui';
|
||||
@@ -20,22 +19,21 @@
|
||||
isSaving = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handleDiscard() {
|
||||
settingsComponent?.handleDiscard();
|
||||
}
|
||||
</script>
|
||||
|
||||
<Page.Root class="mx-auto flex w-[90%] px-4 md:max-w-2xl lg:max-w-3xl">
|
||||
<Page.Header isSticky class="ui:bg-background z-10">
|
||||
<Page.Header>
|
||||
<Page.HeaderContent>
|
||||
<Page.Title>
|
||||
{$t('course.navItem.settings.heading')}
|
||||
</Page.Title>
|
||||
</Page.HeaderContent>
|
||||
<Page.Action>
|
||||
<div class="flex items-center gap-2">
|
||||
<Button variant="secondary" loading={isSaving} disabled={isSaving || !hasUnsavedChanges} onclick={handleSave}>
|
||||
{$t('course.navItem.settings.save')}
|
||||
</Button>
|
||||
<RefreshPageData disabled={isSaving} onRefresh={() => courseApi.refreshCourse(data.courseId, $profile.id)} />
|
||||
</div>
|
||||
<RefreshPageData disabled={isSaving} onRefresh={() => courseApi.refreshCourse(data.courseId, $profile.id)} />
|
||||
</Page.Action>
|
||||
</Page.Header>
|
||||
<Page.Body>
|
||||
@@ -43,4 +41,14 @@
|
||||
<CourseSettingsPage bind:this={settingsComponent} bind:hasUnsavedChanges />
|
||||
{/snippet}
|
||||
</Page.Body>
|
||||
<Page.SettingsActions
|
||||
hasChanges={hasUnsavedChanges}
|
||||
loading={isSaving}
|
||||
disabled={isSaving}
|
||||
statusLabel={$t('common.unsaved_changes.label')}
|
||||
discardLabel={$t('common.discard')}
|
||||
saveLabel={$t('common.save_changes')}
|
||||
onSave={handleSave}
|
||||
onDiscard={handleDiscard}
|
||||
/>
|
||||
</Page.Root>
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<script lang="ts">
|
||||
import { ProfilePage } from '$features/settings/pages';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import { Button } from '@cio/ui/base/button';
|
||||
import * as Page from '@cio/ui/base/page';
|
||||
|
||||
let profileComponent: ProfilePage | null = $state(null);
|
||||
let isLoading = $state(false);
|
||||
let hasUnsavedChanges = $state(false);
|
||||
|
||||
async function handleUpdate() {
|
||||
isLoading = true;
|
||||
@@ -15,24 +15,32 @@
|
||||
isLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handleDiscard() {
|
||||
profileComponent?.handleDiscard();
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Settings - ClassroomIO</title>
|
||||
</svelte:head>
|
||||
|
||||
<Page.Header isSticky>
|
||||
<Page.Header>
|
||||
<Page.HeaderContent>
|
||||
<Page.Title>{$t('settings.profile.heading')}</Page.Title>
|
||||
</Page.HeaderContent>
|
||||
<Page.Action>
|
||||
<Button variant="secondary" loading={isLoading} onclick={handleUpdate}>
|
||||
{$t('settings.profile.update_profile')}
|
||||
</Button>
|
||||
</Page.Action>
|
||||
</Page.Header>
|
||||
<Page.Body>
|
||||
{#snippet child()}
|
||||
<ProfilePage bind:this={profileComponent} />
|
||||
<ProfilePage bind:this={profileComponent} bind:hasUnsavedChanges />
|
||||
{/snippet}
|
||||
</Page.Body>
|
||||
<Page.SettingsActions
|
||||
hasChanges={hasUnsavedChanges}
|
||||
loading={isLoading}
|
||||
statusLabel={$t('common.unsaved_changes.label')}
|
||||
discardLabel={$t('common.discard')}
|
||||
saveLabel={$t('common.save_changes')}
|
||||
onSave={handleUpdate}
|
||||
onDiscard={handleDiscard}
|
||||
/>
|
||||
|
||||
@@ -2,6 +2,23 @@
|
||||
import { NotificationsPage } from '$features/settings/pages';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import * as Page from '@cio/ui/base/page';
|
||||
|
||||
let notificationsComponent: NotificationsPage | null = $state(null);
|
||||
let hasUnsavedChanges = $state(false);
|
||||
let isSaving = $state(false);
|
||||
|
||||
async function handleSave() {
|
||||
isSaving = true;
|
||||
try {
|
||||
await notificationsComponent?.handleSaveAll();
|
||||
} finally {
|
||||
isSaving = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handleDiscard() {
|
||||
notificationsComponent?.handleDiscard();
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
@@ -11,11 +28,21 @@
|
||||
<Page.Header>
|
||||
<Page.HeaderContent>
|
||||
<Page.Title>{$t('settings.notifications.heading')}</Page.Title>
|
||||
<Page.Subtitle>{$t('settings.notifications.personal_page_subtitle')}</Page.Subtitle>
|
||||
<Page.Subtitle>{$t('settings.notifications.page_subtitle')}</Page.Subtitle>
|
||||
</Page.HeaderContent>
|
||||
</Page.Header>
|
||||
<Page.Body>
|
||||
{#snippet child()}
|
||||
<NotificationsPage />
|
||||
<NotificationsPage bind:this={notificationsComponent} bind:hasUnsavedChanges />
|
||||
{/snippet}
|
||||
</Page.Body>
|
||||
<Page.SettingsActions
|
||||
hasChanges={hasUnsavedChanges}
|
||||
loading={isSaving}
|
||||
disabled={isSaving}
|
||||
statusLabel={$t('common.unsaved_changes.label')}
|
||||
discardLabel={$t('common.discard')}
|
||||
saveLabel={$t('common.save_changes')}
|
||||
onSave={handleSave}
|
||||
onDiscard={handleDiscard}
|
||||
/>
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/state';
|
||||
import { Button } from '@cio/ui/base/button';
|
||||
interface Props {
|
||||
children?: import('svelte').Snippet;
|
||||
}
|
||||
|
||||
import * as Page from '@cio/ui/base/page';
|
||||
import { OrgSettingsInlineTabs } from '$features/settings/components';
|
||||
import { settingsHeaderAction } from '$features/settings/utils/store';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
|
||||
let { children }: Props = $props();
|
||||
@@ -27,16 +25,6 @@
|
||||
<Page.Title>{$t('settings.organization.organization_profile.heading')}</Page.Title>
|
||||
<Page.Subtitle>{$t('settings.organization.page_subtitle')}</Page.Subtitle>
|
||||
</Page.HeaderContent>
|
||||
<Page.Action>
|
||||
<Button
|
||||
variant="secondary"
|
||||
loading={$settingsHeaderAction.loading}
|
||||
disabled={$settingsHeaderAction.disabled || !$settingsHeaderAction.onClick}
|
||||
onclick={() => $settingsHeaderAction.onClick?.()}
|
||||
>
|
||||
{$settingsHeaderAction.label}
|
||||
</Button>
|
||||
</Page.Action>
|
||||
</Page.Header>
|
||||
|
||||
<OrgSettingsInlineTabs />
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
import { ProfilePage } from '$features/settings/pages';
|
||||
import { VisitOrgSiteButton } from '$features/ui';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import { Button } from '@cio/ui/base/button';
|
||||
import * as Page from '@cio/ui/base/page';
|
||||
|
||||
let profileComponent: ProfilePage | null = $state(null);
|
||||
let isLoading = $state(false);
|
||||
let hasUnsavedChanges = $state(false);
|
||||
|
||||
async function handleUpdate() {
|
||||
isLoading = true;
|
||||
@@ -16,27 +16,36 @@
|
||||
isLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handleDiscard() {
|
||||
profileComponent?.handleDiscard();
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Settings - ClassroomIO</title>
|
||||
</svelte:head>
|
||||
|
||||
<Page.Header isSticky>
|
||||
<Page.Header>
|
||||
<Page.HeaderContent>
|
||||
<Page.Title>{$t('settings.profile.heading')}</Page.Title>
|
||||
<Page.Subtitle>{$t('settings.profile.page_subtitle')}</Page.Subtitle>
|
||||
</Page.HeaderContent>
|
||||
<Page.Action>
|
||||
<Button variant="secondary" loading={isLoading} onclick={handleUpdate}>
|
||||
{$t('settings.profile.update_profile')}
|
||||
</Button>
|
||||
|
||||
<VisitOrgSiteButton />
|
||||
</Page.Action>
|
||||
</Page.Header>
|
||||
<Page.Body>
|
||||
{#snippet child()}
|
||||
<ProfilePage bind:this={profileComponent} />
|
||||
<ProfilePage bind:this={profileComponent} bind:hasUnsavedChanges />
|
||||
{/snippet}
|
||||
</Page.Body>
|
||||
<Page.SettingsActions
|
||||
hasChanges={hasUnsavedChanges}
|
||||
loading={isLoading}
|
||||
statusLabel={$t('common.unsaved_changes.label')}
|
||||
discardLabel={$t('common.discard')}
|
||||
saveLabel={$t('common.save_changes')}
|
||||
onSave={handleUpdate}
|
||||
onDiscard={handleDiscard}
|
||||
/>
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
<script lang="ts">
|
||||
import { onDestroy } from 'svelte';
|
||||
import { CustomizeLmsPage } from '$features/settings/pages';
|
||||
import { settingsHeaderAction } from '$features/settings/utils/store';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import * as Page from '@cio/ui/base/page';
|
||||
|
||||
let customizeLmsComponent: CustomizeLmsPage | null = $state(null);
|
||||
let isSaving = $state(false);
|
||||
let hasUnsavedChanges = $state(false);
|
||||
|
||||
async function handleSave() {
|
||||
isSaving = true;
|
||||
@@ -15,27 +16,22 @@
|
||||
}
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
settingsHeaderAction.set({
|
||||
label: 'Save',
|
||||
disabled: isSaving,
|
||||
loading: isSaving,
|
||||
onClick: handleSave
|
||||
});
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
settingsHeaderAction.set({
|
||||
label: 'Save',
|
||||
disabled: true,
|
||||
loading: false,
|
||||
onClick: null
|
||||
});
|
||||
});
|
||||
function handleDiscard() {
|
||||
customizeLmsComponent?.handleDiscard();
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Customize LMS - ClassroomIO</title>
|
||||
</svelte:head>
|
||||
|
||||
<CustomizeLmsPage bind:this={customizeLmsComponent} />
|
||||
<CustomizeLmsPage bind:this={customizeLmsComponent} bind:hasUnsavedChanges />
|
||||
<Page.SettingsActions
|
||||
hasChanges={hasUnsavedChanges}
|
||||
loading={isSaving}
|
||||
statusLabel={$t('common.unsaved_changes.label')}
|
||||
discardLabel={$t('common.discard')}
|
||||
saveLabel={$t('common.save_changes')}
|
||||
onSave={handleSave}
|
||||
onDiscard={handleDiscard}
|
||||
/>
|
||||
|
||||
@@ -1,25 +1,5 @@
|
||||
<script>
|
||||
import { onDestroy } from 'svelte';
|
||||
import { DomainsPage } from '$features/settings/pages';
|
||||
import { settingsHeaderAction } from '$features/settings/utils/store';
|
||||
|
||||
$effect(() => {
|
||||
settingsHeaderAction.set({
|
||||
label: 'Save',
|
||||
disabled: true,
|
||||
loading: false,
|
||||
onClick: null
|
||||
});
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
settingsHeaderAction.set({
|
||||
label: 'Save',
|
||||
disabled: true,
|
||||
loading: false,
|
||||
onClick: null
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
|
||||
@@ -2,6 +2,23 @@
|
||||
import { NotificationsPage } from '$features/settings/pages';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import * as Page from '@cio/ui/base/page';
|
||||
|
||||
let notificationsComponent: NotificationsPage | null = $state(null);
|
||||
let hasUnsavedChanges = $state(false);
|
||||
let isSaving = $state(false);
|
||||
|
||||
async function handleSave() {
|
||||
isSaving = true;
|
||||
try {
|
||||
await notificationsComponent?.handleSaveAll();
|
||||
} finally {
|
||||
isSaving = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handleDiscard() {
|
||||
notificationsComponent?.handleDiscard();
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
@@ -16,6 +33,16 @@
|
||||
</Page.Header>
|
||||
<Page.Body>
|
||||
{#snippet child()}
|
||||
<NotificationsPage />
|
||||
<NotificationsPage bind:this={notificationsComponent} bind:hasUnsavedChanges />
|
||||
{/snippet}
|
||||
</Page.Body>
|
||||
<Page.SettingsActions
|
||||
hasChanges={hasUnsavedChanges}
|
||||
loading={isSaving}
|
||||
disabled={isSaving}
|
||||
statusLabel={$t('common.unsaved_changes.label')}
|
||||
discardLabel={$t('common.discard')}
|
||||
saveLabel={$t('common.save_changes')}
|
||||
onSave={handleSave}
|
||||
onDiscard={handleDiscard}
|
||||
/>
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
<script lang="ts">
|
||||
import { onDestroy } from 'svelte';
|
||||
import { OrgPage } from '$features/settings/pages';
|
||||
import { settingsHeaderAction } from '$features/settings/utils/store';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import * as Page from '@cio/ui/base/page';
|
||||
|
||||
let orgComponent: OrgPage | null = $state(null);
|
||||
let loading = $state(false);
|
||||
let hasUnsavedChanges = $state(false);
|
||||
|
||||
async function handleUpdate() {
|
||||
loading = true;
|
||||
@@ -15,27 +16,22 @@
|
||||
}
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
settingsHeaderAction.set({
|
||||
label: 'Save',
|
||||
disabled: loading,
|
||||
loading,
|
||||
onClick: handleUpdate
|
||||
});
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
settingsHeaderAction.set({
|
||||
label: 'Save',
|
||||
disabled: true,
|
||||
loading: false,
|
||||
onClick: null
|
||||
});
|
||||
});
|
||||
function handleDiscard() {
|
||||
orgComponent?.handleDiscard();
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Organization Settings - ClassroomIO</title>
|
||||
</svelte:head>
|
||||
|
||||
<OrgPage bind:this={orgComponent} />
|
||||
<OrgPage bind:this={orgComponent} bind:hasUnsavedChanges />
|
||||
<Page.SettingsActions
|
||||
hasChanges={hasUnsavedChanges}
|
||||
{loading}
|
||||
statusLabel={$t('common.unsaved_changes.label')}
|
||||
discardLabel={$t('common.discard')}
|
||||
saveLabel={$t('common.save_changes')}
|
||||
onSave={handleUpdate}
|
||||
onDiscard={handleDiscard}
|
||||
/>
|
||||
|
||||
@@ -1,25 +1,5 @@
|
||||
<script>
|
||||
import { onDestroy } from 'svelte';
|
||||
import { TeamsPage } from '$features/settings/pages';
|
||||
import { settingsHeaderAction } from '$features/settings/utils/store';
|
||||
|
||||
$effect(() => {
|
||||
settingsHeaderAction.set({
|
||||
label: 'Save',
|
||||
disabled: true,
|
||||
loading: false,
|
||||
onClick: null
|
||||
});
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
settingsHeaderAction.set({
|
||||
label: 'Save',
|
||||
disabled: true,
|
||||
loading: false,
|
||||
onClick: null
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
|
||||
Reference in New Issue
Block a user