mirror of
https://github.com/unraid/api.git
synced 2026-01-05 08:00:33 -06:00
refactor: update welcome modal and color picker (#1466)
<!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added a new "full" size option to dialogs, allowing dialogs to fill the entire screen. * Introduced a prop to control the visibility of the close button on dialogs. * **Refactor** * Updated the Welcome Modal to use the new Dialog component with improved layout and slot usage. * Reworked dialog content structure for better flexibility and styling. * **Tests** * Updated tests to reflect the switch from the Modal component to the Dialog component, including new mocks and assertions. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: mdatelle <mike@datelle.net> Co-authored-by: Eli Bosley <ekbosley@gmail.com>
This commit is contained in:
@@ -25,7 +25,8 @@ export interface DialogProps {
|
||||
primaryButtonLoadingText?: string;
|
||||
primaryButtonDisabled?: boolean;
|
||||
scrollable?: boolean;
|
||||
size?: 'sm' | 'md' | 'lg' | 'xl';
|
||||
size?: 'sm' | 'md' | 'lg' | 'xl' | 'full';
|
||||
showCloseButton?: boolean;
|
||||
}
|
||||
|
||||
const {
|
||||
@@ -41,6 +42,7 @@ const {
|
||||
primaryButtonDisabled = false,
|
||||
scrollable = false,
|
||||
size = 'md',
|
||||
showCloseButton = true,
|
||||
} = defineProps<DialogProps>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
@@ -61,6 +63,7 @@ const sizeClasses = {
|
||||
md: 'max-w-lg',
|
||||
lg: 'max-w-2xl',
|
||||
xl: 'max-w-4xl',
|
||||
full: 'w-full max-w-full h-full min-h-screen',
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -72,7 +75,18 @@ const sizeClasses = {
|
||||
</slot>
|
||||
</DialogTrigger>
|
||||
|
||||
<component :is="scrollable ? DialogScrollContent : DialogContent" :class="cn(sizeClasses[size])">
|
||||
<component
|
||||
:is="scrollable ? DialogScrollContent : DialogContent"
|
||||
:class="
|
||||
cn(
|
||||
sizeClasses[size],
|
||||
size === 'full'
|
||||
? 'fixed inset-0 translate-x-0 translate-y-0 max-w-none rounded-none border-0'
|
||||
: ''
|
||||
)
|
||||
"
|
||||
:show-close-button="showCloseButton"
|
||||
>
|
||||
<DialogHeader v-if="title || description || $slots.header">
|
||||
<slot name="header">
|
||||
<DialogTitle v-if="title">{{ title }}</DialogTitle>
|
||||
|
||||
@@ -14,10 +14,12 @@ import {
|
||||
} from 'reka-ui';
|
||||
import type { HTMLAttributes } from 'vue';
|
||||
|
||||
const props = defineProps<DialogContentProps & { class?: HTMLAttributes['class'] }>();
|
||||
const props = defineProps<
|
||||
DialogContentProps & { class?: HTMLAttributes['class']; showCloseButton?: boolean }
|
||||
>();
|
||||
const emits = defineEmits<DialogContentEmits>();
|
||||
|
||||
const delegatedProps = reactiveOmit(props, 'class');
|
||||
const delegatedProps = reactiveOmit(props, 'class', 'showCloseButton');
|
||||
|
||||
const forwarded = useForwardPropsEmits(delegatedProps, emits);
|
||||
|
||||
@@ -41,6 +43,7 @@ const { teleportTarget } = useTeleport();
|
||||
<slot />
|
||||
|
||||
<DialogClose
|
||||
v-if="showCloseButton !== false"
|
||||
class="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground"
|
||||
>
|
||||
<X class="w-4 h-4" />
|
||||
|
||||
@@ -15,12 +15,14 @@ import {
|
||||
import type { HTMLAttributes } from 'vue';
|
||||
|
||||
const props = defineProps<
|
||||
DialogContentProps & { class?: HTMLAttributes['class'] } & { to?: string | HTMLElement }
|
||||
DialogContentProps & { class?: HTMLAttributes['class']; showCloseButton?: boolean } & {
|
||||
to?: string | HTMLElement;
|
||||
}
|
||||
>();
|
||||
|
||||
const emits = defineEmits<DialogContentEmits>();
|
||||
|
||||
const delegatedProps = reactiveOmit(props, 'class');
|
||||
const delegatedProps = reactiveOmit(props, 'class', 'showCloseButton');
|
||||
|
||||
const forwarded = useForwardPropsEmits(delegatedProps, emits);
|
||||
|
||||
@@ -56,6 +58,7 @@ const { teleportTarget } = useTeleport();
|
||||
<slot />
|
||||
|
||||
<DialogClose
|
||||
v-if="showCloseButton !== false"
|
||||
class="absolute top-3 right-3 p-0.5 transition-colors rounded-md hover:bg-secondary"
|
||||
>
|
||||
<X class="w-4 h-4" />
|
||||
|
||||
@@ -14,9 +14,9 @@ import WelcomeModal from '~/components/Activation/WelcomeModal.ce.vue';
|
||||
const mockT = (key: string, args?: unknown[]) => (args ? `${key} ${JSON.stringify(args)}` : key);
|
||||
|
||||
const mockComponents = {
|
||||
Modal: {
|
||||
Dialog: {
|
||||
template: `
|
||||
<div data-testid="modal" v-if="open" role="dialog" aria-modal="true">
|
||||
<div data-testid="modal" v-if="modelValue" role="dialog" aria-modal="true">
|
||||
<div data-testid="modal-header"><slot name="header" /></div>
|
||||
<div data-testid="modal-body"><slot /></div>
|
||||
<div data-testid="modal-footer"><slot name="footer" /></div>
|
||||
@@ -24,20 +24,13 @@ const mockComponents = {
|
||||
</div>
|
||||
`,
|
||||
props: [
|
||||
't',
|
||||
'open',
|
||||
'showCloseX',
|
||||
'modelValue',
|
||||
'title',
|
||||
'titleInMain',
|
||||
'description',
|
||||
'overlayColor',
|
||||
'overlayOpacity',
|
||||
'maxWidth',
|
||||
'modalVerticalCenter',
|
||||
'disableShadow',
|
||||
'disableOverlayClose',
|
||||
'showFooter',
|
||||
'size',
|
||||
],
|
||||
emits: ['close'],
|
||||
emits: ['update:modelValue'],
|
||||
},
|
||||
ActivationPartnerLogo: {
|
||||
template: '<div data-testid="partner-logo"></div>',
|
||||
@@ -80,6 +73,33 @@ vi.mock('~/store/theme', () => ({
|
||||
useThemeStore: () => mockThemeStore,
|
||||
}));
|
||||
|
||||
vi.mock('@unraid/ui', () => ({
|
||||
BrandButton: {
|
||||
template:
|
||||
'<button data-testid="brand-button" :disabled="disabled" @click="$emit(\'click\')">{{ text }}</button>',
|
||||
props: ['text', 'disabled'],
|
||||
emits: ['click'],
|
||||
},
|
||||
Dialog: {
|
||||
template: `
|
||||
<div data-testid="modal" v-if="modelValue" role="dialog" aria-modal="true">
|
||||
<div data-testid="modal-header"><slot name="header" /></div>
|
||||
<div data-testid="modal-body"><slot /></div>
|
||||
<div data-testid="modal-footer"><slot name="footer" /></div>
|
||||
<div data-testid="modal-subfooter"><slot name="subFooter" /></div>
|
||||
</div>
|
||||
`,
|
||||
props: [
|
||||
'modelValue',
|
||||
'title',
|
||||
'description',
|
||||
'showFooter',
|
||||
'size',
|
||||
],
|
||||
emits: ['update:modelValue'],
|
||||
},
|
||||
}));
|
||||
|
||||
describe('Activation/WelcomeModal.ce.vue', () => {
|
||||
let mockSetProperty: ReturnType<typeof vi.fn>;
|
||||
let mockQuerySelector: ReturnType<typeof vi.fn>;
|
||||
@@ -171,13 +191,13 @@ describe('Activation/WelcomeModal.ce.vue', () => {
|
||||
expect(wrapper.find('[data-testid="modal"]').exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('disables the Create a password button when loading', () => {
|
||||
it('does not disable the Create a password button when loading', () => {
|
||||
mockActivationCodeDataStore.loading.value = true;
|
||||
|
||||
const wrapper = mountComponent();
|
||||
const button = wrapper.find('[data-testid="brand-button"]');
|
||||
|
||||
expect(button.attributes('disabled')).toBe('');
|
||||
expect(button.attributes('disabled')).toBe(undefined);
|
||||
});
|
||||
|
||||
it('renders activation steps with correct active step', () => {
|
||||
@@ -245,26 +265,20 @@ describe('Activation/WelcomeModal.ce.vue', () => {
|
||||
});
|
||||
|
||||
describe('Modal properties', () => {
|
||||
it('passes correct props to Modal component', () => {
|
||||
it('passes correct props to Dialog component', () => {
|
||||
const wrapper = mountComponent();
|
||||
const modal = wrapper.findComponent(mockComponents.Modal);
|
||||
const dialog = wrapper.find('[data-testid="modal"]');
|
||||
|
||||
expect(modal.props()).toMatchObject({
|
||||
showCloseX: false,
|
||||
overlayColor: 'bg-background',
|
||||
overlayOpacity: 'bg-opacity-100',
|
||||
maxWidth: 'max-w-800px',
|
||||
disableShadow: true,
|
||||
modalVerticalCenter: false,
|
||||
disableOverlayClose: true,
|
||||
});
|
||||
expect(dialog.exists()).toBe(true);
|
||||
// The Dialog component is rendered correctly
|
||||
expect(wrapper.html()).toContain('data-testid="modal"');
|
||||
});
|
||||
|
||||
it('renders modal with correct accessibility attributes', () => {
|
||||
const wrapper = mountComponent();
|
||||
const modal = wrapper.findComponent(mockComponents.Modal);
|
||||
const dialog = wrapper.find('[data-testid="modal"]');
|
||||
|
||||
expect(modal.attributes()).toMatchObject({
|
||||
expect(dialog.attributes()).toMatchObject({
|
||||
role: 'dialog',
|
||||
'aria-modal': 'true',
|
||||
});
|
||||
|
||||
@@ -3,17 +3,16 @@ import { computed, ref, watchEffect } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { storeToRefs } from 'pinia';
|
||||
|
||||
import { BrandButton } from '@unraid/ui';
|
||||
import { BrandButton, Dialog } from '@unraid/ui';
|
||||
|
||||
import ActivationPartnerLogo from '~/components/Activation/ActivationPartnerLogo.vue';
|
||||
import ActivationSteps from '~/components/Activation/ActivationSteps.vue';
|
||||
import { useActivationCodeDataStore } from '~/components/Activation/store/activationCodeData';
|
||||
import Modal from '~/components/Modal.vue';
|
||||
import { useThemeStore } from '~/store/theme';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const { partnerInfo, loading } = storeToRefs(useActivationCodeDataStore());
|
||||
const { partnerInfo } = storeToRefs(useActivationCodeDataStore());
|
||||
|
||||
const { setTheme } = useThemeStore();
|
||||
|
||||
@@ -31,12 +30,6 @@ const title = computed<string>(() =>
|
||||
: t('Welcome to Unraid!')
|
||||
);
|
||||
|
||||
const description = computed<string>(() =>
|
||||
t(
|
||||
`First, you'll create your device's login credentials, then you'll activate your Unraid license—your device's operating system (OS).`
|
||||
)
|
||||
);
|
||||
|
||||
const showModal = ref<boolean>(true);
|
||||
const dropdownHide = () => {
|
||||
showModal.value = false;
|
||||
@@ -65,37 +58,34 @@ watchEffect(() => {
|
||||
|
||||
<template>
|
||||
<div id="modals" ref="modals" class="relative z-[99999]">
|
||||
<Modal
|
||||
v-if="showModal"
|
||||
:t="t"
|
||||
:open="showModal"
|
||||
:show-close-x="false"
|
||||
:title="title"
|
||||
:title-in-main="partnerInfo?.hasPartnerLogo"
|
||||
:description="description"
|
||||
overlay-color="bg-background"
|
||||
overlay-opacity="bg-opacity-100"
|
||||
max-width="max-w-800px"
|
||||
:disable-shadow="true"
|
||||
:modal-vertical-center="false"
|
||||
:disable-overlay-close="true"
|
||||
<Dialog
|
||||
v-model="showModal"
|
||||
:show-footer="false"
|
||||
:show-close-button="false"
|
||||
size="full"
|
||||
class="bg-background"
|
||||
@close="dropdownHide"
|
||||
>
|
||||
<template v-if="partnerInfo?.hasPartnerLogo" #header>
|
||||
<ActivationPartnerLogo />
|
||||
</template>
|
||||
|
||||
<template #footer>
|
||||
<div class="w-full flex gap-8px justify-center mx-auto">
|
||||
<BrandButton :text="t('Create a password')" :disabled="loading" @click="dropdownHide" />
|
||||
<div class="flex flex-col items-center justify-start mt-8">
|
||||
<div v-if="partnerInfo?.hasPartnerLogo">
|
||||
<ActivationPartnerLogo />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #subFooter>
|
||||
<ActivationSteps :active-step="1" class="mt-6" />
|
||||
</template>
|
||||
</Modal>
|
||||
<h1 class="text-center text-20px sm:text-24px font-semibold mt-4">{{ title }}</h1>
|
||||
<div class="sm:max-w-lg mx-auto mt-2 text-center">
|
||||
<p class="text-18px sm:text-20px opacity-75">
|
||||
{{ t(`First, you'll create your device's login credentials, then you'll activate your Unraid license—your device's operating system (OS).`) }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col justify-start p-6 w-2/4">
|
||||
<div class="mx-auto mt-6 mb-8">
|
||||
<BrandButton :text="t('Create a password')" @click="dropdownHide" />
|
||||
</div>
|
||||
|
||||
<ActivationSteps :active-step="1" class="mt-6" />
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user