Skip to content

Commit

Permalink
Add ModalManager - refactor modal hook to use modal store
Browse files Browse the repository at this point in the history
  • Loading branch information
jordojordo committed Dec 18, 2024
1 parent ae6e131 commit 23f8be8
Show file tree
Hide file tree
Showing 8 changed files with 177 additions and 38 deletions.
61 changes: 61 additions & 0 deletions shell/components/ModalManager.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
<script lang="ts" setup>
import { computed, ref } from 'vue';
import { useStore } from 'vuex';
import AppModal from '@shell/components/AppModal.vue';
const store = useStore();
const isOpen = computed(() => store.getters['modal/isOpen']);
const component = computed(() => store.getters['modal/component']);
const componentProps = computed(() => store.getters['modal/componentProps']);
const resources = computed(() => store.getters['modal/resources']);
const closeOnClickOutside = computed(() => store.getters['modal/closeOnClickOutside']);
const modalWidth = computed(() => store.getters['modal/modalWidth']);
// const modalSticky = computed(() => store.getters['modal/modalSticky']); // TODO: Implement sticky modals
const backgroundClosing = ref<Function | null>(null);
function close() {
if (!isOpen.value) return;
if (backgroundClosing.value) {
backgroundClosing.value();
}
store.commit('modal/closeModal');
}
function registerBackgroundClosing(fn: Function) {
backgroundClosing.value = fn;
}
</script>

<template>
<app-modal
v-if="isOpen && component"
:click-to-close="closeOnClickOutside"
:width="modalWidth"
:style="{ '--prompt-modal-width': modalWidth }"
@close="close"
>
<component
:is="component"
v-bind="componentProps || {}"
:resources="resources"
:register-background-closing="registerBackgroundClosing"
@close="close"
/>
</app-modal>
</template>

<style lang='scss'>
.promptModal-modal {
border-radius: var(--border-radius);
overflow: scroll;
max-height: 100vh;
& ::-webkit-scrollbar-corner {
background: rgba(0,0,0,0);
}
}
</style>
3 changes: 3 additions & 0 deletions shell/components/templates/default.vue
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
} from '@shell/store/prefs';
import ActionMenu from '@shell/components/ActionMenu';
import GrowlManager from '@shell/components/GrowlManager';
import ModalManager from '@shell/components/ModalManager';
import SlideInPanelManager from '@shell/components/SlideInPanelManager';
import WindowManager from '@shell/components/nav/WindowManager';
import PromptRemove from '@shell/components/PromptRemove';
Expand Down Expand Up @@ -42,6 +43,7 @@ export default {
Header,
ActionMenu,
GrowlManager,
ModalManager,
SlideInPanelManager,
WindowManager,
FixedBanner,
Expand Down Expand Up @@ -255,6 +257,7 @@ export default {
<PromptRestore />
<AssignTo />
<PromptModal />
<ModalManager />
<button
v-if="noLocaleShortcut"
v-shortkey.once="['shift','l']"
Expand Down
3 changes: 3 additions & 0 deletions shell/components/templates/home.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import Header from '@shell/components/nav/Header';
import Brand from '@shell/mixins/brand';
import FixedBanner from '@shell/components/FixedBanner';
import GrowlManager from '@shell/components/GrowlManager';
import ModalManager from '@shell/components/ModalManager';
import SlideInPanelManager from '@shell/components/SlideInPanelManager';
import { mapPref, THEME_SHORTCUT } from '@shell/store/prefs';
import AwsComplianceBanner from '@shell/components/AwsComplianceBanner';
Expand All @@ -18,6 +19,7 @@ export default {
Header,
FixedBanner,
GrowlManager,
ModalManager,
SlideInPanelManager,
AzureWarning,
AwsComplianceBanner,
Expand Down Expand Up @@ -60,6 +62,7 @@ export default {
<AwsComplianceBanner />
<AzureWarning />
<PromptModal />
<ModalManager />
<div
class="dashboard-content"
:class="{'dashboard-padding-left': showTopLevelMenu}"
Expand Down
3 changes: 3 additions & 0 deletions shell/components/templates/plain.vue
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import IndentedPanel from '@shell/components/IndentedPanel';
import Brand from '@shell/mixins/brand';
import FixedBanner from '@shell/components/FixedBanner';
import GrowlManager from '@shell/components/GrowlManager';
import ModalManager from '@shell/components/ModalManager';
import SlideInPanelManager from '@shell/components/SlideInPanelManager';
import AwsComplianceBanner from '@shell/components/AwsComplianceBanner';
import AzureWarning from '@shell/components/auth/AzureWarning';
Expand All @@ -27,6 +28,7 @@ export default {
PromptModal,
FixedBanner,
GrowlManager,
ModalManager,
SlideInPanelManager,
AwsComplianceBanner,
AzureWarning,
Expand Down Expand Up @@ -80,6 +82,7 @@ export default {
<ActionMenu />
<PromptRemove />
<PromptModal />
<ModalManager />
<AssignTo />
<button
v-if="themeShortcut"
Expand Down
2 changes: 2 additions & 0 deletions shell/config/store.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ let store = {};
resolveStoreModules(require('../store/growl.js'), 'growl.js');
resolveStoreModules(require('../store/i18n.js'), 'i18n.js');
resolveStoreModules(require('../store/linode.js'), 'linode.js');
resolveStoreModules(require('../store/modal.ts'), 'modal.ts');
resolveStoreModules(require('../store/plugins.js'), 'plugins.js');
resolveStoreModules(require('../store/pnap.js'), 'pnap.js');
resolveStoreModules(require('../store/prefs.js'), 'prefs.js');
Expand Down Expand Up @@ -55,6 +56,7 @@ let store = {};
'../store/i18n.js',
'../store/index.js',
'../store/linode.js',
'../store/modal.ts',
'../store/plugins.js',
'../store/pnap.js',
'../store/prefs.js',
Expand Down
9 changes: 5 additions & 4 deletions shell/plugins/rancher-api/shell-api-class.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,16 +55,16 @@ export default class ShellApi {
/**
* Opens a modal by committing to the Vuex store.
*
* This method updates the store's `action-menu` module to show a modal with the
* specified configuration. The modal is rendered using the `PromptModal` component,
* This method updates the store's `modal` module to show a modal with the
* specified configuration. The modal is rendered using the `ModalManager` component,
* and its content is dynamically loaded based on the `component` field in the configuration.
*
* @param config A `ModalConfig` object defining the modal’s content and behavior.
*
* Example:
* ```ts
* this.$shell.modal({
* component: 'MyCustomDialog',
* component: MyCustomModal,
* componentProps: { title: 'Hello Modal' },
* resources: [someResource],
* modalWidth: '800px',
Expand All @@ -73,12 +73,13 @@ export default class ShellApi {
* ```
*/
modal(config: ModalConfig): void {
this.$store.commit('action-menu/togglePromptModal', {
this.$store.commit('modal/openModal', {
component: config.component,
componentProps: config.componentProps || {},
resources: config.resources || [],
modalWidth: config.modalWidth || '600px',
closeOnClickOutside: config.closeOnClickOutside ?? true,
// modalSticky: config.modalSticky ?? false // Not implemented yet
});
}

Expand Down
71 changes: 71 additions & 0 deletions shell/store/modal.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { markRaw, Component } from 'vue';
import { MutationTree, GetterTree, ActionTree } from 'vuex';

export interface ModalState {
isOpen: boolean;
component: Component | null;
componentProps: Record<string, any>;
resources: any[];
closeOnClickOutside: boolean;
modalWidth: string;
modalSticky: boolean;
}

const state = (): ModalState => ({
isOpen: false,
component: null,
componentProps: {},
resources: [],
closeOnClickOutside: false,
modalWidth: '600px',
modalSticky: false
});

const getters: GetterTree<ModalState, any> = {
isOpen: (state) => state.isOpen,
component: (state) => state.component,
componentProps: (state) => state.componentProps,
resources: (state) => state.resources,
closeOnClickOutside: (state) => state.closeOnClickOutside,
modalWidth: (state) => state.modalWidth,
modalSticky: (state) => state.modalSticky,
};

const mutations: MutationTree<ModalState> = {
openModal(state, payload: {
component: Component;
componentProps?: Record<string, any>;
resources?: any[];
closeOnClickOutside?: boolean;
modalWidth?: string;
modalSticky?: boolean;
}) {
state.isOpen = true;
state.component = markRaw(payload.component);
state.componentProps = payload.componentProps || {};
state.resources = Array.isArray(payload.resources) ? payload.resources : (payload.resources ? [payload.resources] : []);
state.closeOnClickOutside = payload.closeOnClickOutside ?? false;
state.modalWidth = payload.modalWidth || '600px';
state.modalSticky = payload.modalSticky ?? false;
},

closeModal(state) {
state.isOpen = false;
state.component = null;
state.componentProps = {};
state.resources = [];
state.closeOnClickOutside = false;
state.modalWidth = '600px';
state.modalSticky = false;
}
};

const actions: ActionTree<ModalState, any> = {};

export default {
namespaced: true,
state,
getters,
mutations,
actions
};
63 changes: 29 additions & 34 deletions shell/types/rancher-api/modal.d.ts
Original file line number Diff line number Diff line change
@@ -1,41 +1,27 @@
import { Component } from 'vue';

/**
* Configuration object for opening a modal.
*/
export interface ModalConfig {
/**
* TODO: Understand how this works with extensions
*
* The name of the component to be displayed inside the modal.
*
* The component must reside in the `dialog` directory, depending on the environment:
*
* 1. **When Using the Shell as Part of the Core Project**:
* - Components must live in the `@shell/dialog` directory.
* - Example:
* ```
* shell/dialog/MyCustomDialog.vue
* ```
* The Vue component to be displayed inside the modal.
* This can be any SFC (Single-File Component) imported and passed in as a `Component`.
*
* 2. **When Using the Shell as a Library (Extensions)**:
* - Components must live in the `pkg/<extension-pkg>/dialog` directory within the extension.
* - Example, in an extension named `my-extension`:
* ```
* <extension-root>/pkg/my-extension/dialog/MyCustomDialog.vue
* ```
*
* - The `component` value should still match the file name without the `.vue` extension.
* - Example:
* ```ts
* component: 'MyCustomDialog' // Dynamically imports MyCustomDialog.vue
* ```
* Example:
* ```ts
* import MyCustomModal from '@/components/MyCustomModal.vue';
*
* this.$shell.modal({
* component: MyCustomModal,
* componentProps: { title: 'Hello Modal' }
* });
* ```
*/
component: string;
component: Component;

/**
* Optional props to pass directly to the component rendered inside the modal.
* This can be a record of key-value pairs where keys are the prop names, and
* values are the corresponding prop values for the component.
*
* Example:
* ```ts
Expand All @@ -46,7 +32,7 @@ export interface ModalConfig {

/**
* Optional array of resources that the modal component might need.
* These resources are passed directly to the modal's `resources` prop.
* These are passed directly into the modal's `resources` prop.
*
* Example:
* ```ts
Expand All @@ -57,10 +43,9 @@ export interface ModalConfig {

/**
* Custom width for the modal. Defaults to `600px`.
* The width can be specified as a number (pixels) or as a string
* with a valid unit, such as `px` or `%`.
* The width can be specified as a string with a valid unit (`px`, `%`, `rem`, etc.).
*
* Example:
* Examples:
* ```ts
* modalWidth: '800px' // Width in pixels
* modalWidth: '75%' // Width as a percentage
Expand All @@ -69,14 +54,24 @@ export interface ModalConfig {
modalWidth?: string;

/**
* If true, clicking outside the modal will close it. Defaults to `true`.
* Set this to `false` if you want the modal to remain open until the user
* explicitly closes it.
* Determines if clicking outside the modal will close it. Defaults to `true`.
* Set this to `false` to prevent closing via outside clicks.
*
* Example:
* ```ts
* closeOnClickOutside: false
* ```
*/
closeOnClickOutside?: boolean;

/**
* If true, the modal is considered "sticky" and may not close automatically
* on certain user interactions. Defaults to `false`.
*
* Example:
* ```ts
* modalSticky: true
* ```
*/
// modalSticky?: boolean; // Not implemented yet
}

0 comments on commit 23f8be8

Please sign in to comment.