mirror of
https://github.com/directus/directus.git
synced 2026-04-03 03:00:39 -04:00
Add header bar to private view (#142)
* Add header bar basic * Fix alignment of breadcrumb * Fix icon size in breadcrumb * Add slots / stories for header bar * Fix typo * Add disabled color overrides to button * Fix box icon * Add header actions section for collapsable buttons * Tweak css of drawer responsively * Cover viewport (for notched use) * Hide gray boxes on iOS taps * Only show hover effect for devices that support hover * Finish collapsable header buttons * Remove wrong reference * Tweak spacing of nav toggle * Update storybook entry * Add storybook entry for header actions * Update structure of private-view and subcomponents * Add provide support to storybook * Update storybook / readme's for private view components * Use defineComponent instead of createComponetn * Fix broken import * Fix tests, update readmes, etc * Add storybook entries for header actions and module bar * Remove unused utils * Use defineComponent instead of createComponent * Update structure of stories * Fix story of private view
This commit is contained in:
5
.storybook/decorators/with-alt-colors.ts
Normal file
5
.storybook/decorators/with-alt-colors.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export default function withBackground() {
|
||||
return {
|
||||
template: `<div class="alt-colors" style="height: 100%; background-color: var(--background-color);"><story /></div>`
|
||||
}
|
||||
}
|
||||
5
.storybook/decorators/with-background.ts
Normal file
5
.storybook/decorators/with-background.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export default function withBackground() {
|
||||
return {
|
||||
template: `<div style="background-color: #dde3e6; height: 100%;"><story /></div>`
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
export default function withPadding() {
|
||||
return {
|
||||
template: `<div style="padding: 24px;"><story /></div>`
|
||||
template: `<div style="padding: 24px; height: 100%;"><story /></div>`
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
|
||||
<link rel="shortcut icon" href="favicon.ico">
|
||||
<link rel="manifest" href="manifest.webmanifest" />
|
||||
<title>Directus</title>
|
||||
|
||||
@@ -37,6 +37,6 @@ Vue.component('v-slider', VSlider);
|
||||
Vue.component('v-switch', VSwitch);
|
||||
Vue.component('v-table', VTable);
|
||||
|
||||
import DrawerDetail from '@/views/private/drawer-detail/';
|
||||
import DrawerDetail from '@/views/private-view/drawer-detail/';
|
||||
|
||||
Vue.component('drawer-detail', DrawerDetail);
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
>
|
||||
<v-icon v-if="index > 0" name="chevron_right" small />
|
||||
<router-link v-if="!item.disabled" :to="item.to" class="section-link">
|
||||
<v-icon v-if="item.icon" :name="item.icon" />
|
||||
<v-icon v-if="item.icon" :name="item.icon" small />
|
||||
{{ item.name }}
|
||||
</router-link>
|
||||
<span v-else class="section-link">
|
||||
@@ -25,7 +25,6 @@ import { defineComponent, PropType } from '@vue/composition-api';
|
||||
interface Breadcrumb {
|
||||
to: string;
|
||||
name: string;
|
||||
icon?: string;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
@@ -49,11 +48,11 @@ export default defineComponent({
|
||||
--v-breadcrumb-color-disabled: var(--foreground-color-tertiary);
|
||||
--v-breadcrumb-divider-color: var(--foreground-color-tertiary);
|
||||
|
||||
display: inline-block;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.section {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
display: contents;
|
||||
|
||||
.v-icon {
|
||||
--v-icon-color: var(--v-breadcrumb-divider-color);
|
||||
|
||||
@@ -80,7 +80,9 @@ The loading slot is rendered _on top_ of the content that was there before. Make
|
||||
| `--v-button-color` | `var(--button-primary-foreground-color)` |
|
||||
| `--v-button-color-hover` | `var(--button-primary-foreground-color-hover)` |
|
||||
| `--v-button-color-activated` | `var(--button-primary-foreground-color-activated)` |
|
||||
| `--v-button-color-disabled` | `var(--button-primary-foreground-color-disabled)` |
|
||||
| `--v-button-background-color` | `var(--button-primary-background-color)` |
|
||||
| `--v-button-background-color-hover` | `var(--button-primary-background-color-hover)` |
|
||||
| `--v-button-background-color-activated` | `var(--button-primary-background-color-activated)` |
|
||||
| `--v-button-background-color-disabled` | `var(--button-primary-background-color-disabled)` |
|
||||
| `--v-button-font-size` | `16px` |
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
:is="component"
|
||||
active-class="activated"
|
||||
class="v-button"
|
||||
:class="[sizeClass, { block, rounded, icon, outlined, loading }]"
|
||||
:class="[sizeClass, { block, rounded, icon, outlined, loading, secondary }]"
|
||||
:type="type"
|
||||
:disabled="disabled"
|
||||
:to="to"
|
||||
@@ -57,6 +57,10 @@ export default defineComponent({
|
||||
type: [String, Object] as PropType<string | Location>,
|
||||
default: null
|
||||
},
|
||||
secondary: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
...sizeProps
|
||||
},
|
||||
setup(props, { emit }) {
|
||||
@@ -81,9 +85,11 @@ export default defineComponent({
|
||||
--v-button-color: var(--button-primary-foreground-color);
|
||||
--v-button-color-hover: var(--button-primary-foreground-color-hover);
|
||||
--v-button-color-activated: var(--button-primary-foreground-color-activated);
|
||||
--v-button-color-disabled: var(--button-primary-foreground-color-disabled);
|
||||
--v-button-background-color: var(--button-primary-background-color);
|
||||
--v-button-background-color-hover: var(--button-primary-background-color-hover);
|
||||
--v-button-background-color-activated: var(--button-primary-background-color-activated);
|
||||
--v-button-background-color-disabled: var(--button-primary-background-color-disabled);
|
||||
--v-button-font-size: 16px;
|
||||
|
||||
position: relative;
|
||||
@@ -105,6 +111,16 @@ export default defineComponent({
|
||||
transition: var(--fast) var(--transition);
|
||||
transition-property: background-color border;
|
||||
|
||||
&.secondary {
|
||||
--v-button-color: var(--button-secondary-foreground-color);
|
||||
--v-button-color-hover: var(--button-secondary-foreground-color-hover);
|
||||
--v-button-color-activated: var(--button-secondary-foreground-color-activated);
|
||||
--v-button-background-color: var(--button-secondary-background-color);
|
||||
--v-button-background-color-hover: var(--button-secondary-background-color-hover);
|
||||
--v-button-background-color-activated: var(--button-secondary-background-color-activated);
|
||||
--v-button-background-color-disabled: var(--button-secondary-background-color-disabled);
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: scale(0.96);
|
||||
}
|
||||
@@ -114,22 +130,16 @@ export default defineComponent({
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
color: var(--button-primary-text-color-disabled);
|
||||
background-color: var(--button-primary-background-color-disabled);
|
||||
border: var(--input-border-width) solid var(--button-primary-background-color-disabled);
|
||||
color: var(--v-button-color-disabled);
|
||||
background-color: var(--v-button-background-color-disabled);
|
||||
border: var(--input-border-width) solid var(--v-button-background-color-disabled);
|
||||
cursor: not-allowed;
|
||||
|
||||
&:active {
|
||||
transform: unset;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
&:not(.loading):not(:disabled):not(.activated):hover {
|
||||
color: var(--v-button-color-hover);
|
||||
background-color: var(--v-button-background-color-hover);
|
||||
border: var(--button-border-width) solid var(--v-button-background-color-hover);
|
||||
}
|
||||
|
||||
&.block {
|
||||
display: block;
|
||||
min-width: 100%;
|
||||
@@ -140,6 +150,8 @@ export default defineComponent({
|
||||
}
|
||||
|
||||
&.outlined {
|
||||
--v-button-color: var(--v-button-background-color);
|
||||
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
@@ -178,10 +190,6 @@ export default defineComponent({
|
||||
width: var(--v-button-height);
|
||||
min-width: 0;
|
||||
padding: 0;
|
||||
|
||||
&:active {
|
||||
transform: scale(0.94);
|
||||
}
|
||||
}
|
||||
|
||||
.content,
|
||||
@@ -216,5 +224,13 @@ export default defineComponent({
|
||||
--v-button-color: var(--v-button-color-activated) !important;
|
||||
--v-button-background-color: var(--v-button-background-color-activated) !important;
|
||||
}
|
||||
|
||||
@media (hover: hover) {
|
||||
&:not(.loading):not(:disabled):not(.activated):hover {
|
||||
color: var(--v-button-color-hover);
|
||||
background-color: var(--v-button-background-color-hover);
|
||||
border: var(--button-border-width) solid var(--v-button-background-color-hover);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
<template functional>
|
||||
<svg
|
||||
viewBox="0 0 96 100"
|
||||
viewBox="0 0 22 22"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
stroke-linejoin="round"
|
||||
stroke-miterlimit="1.414"
|
||||
stroke-miterlimit="2"
|
||||
>
|
||||
<path
|
||||
d="M3.153 79.825l42.917 19.73c1.287.593 2.573.593 3.86 0l42.906-19.73c1.787-.821 2.683-2.216 2.685-4.183V24.358a4.632 4.632 0 0 0-.081-.83v-.242c-.048-.2-.11-.396-.184-.587l-.069-.196a4.73 4.73 0 0 0-.369-.692l-.104-.149a4.668 4.668 0 0 0-.403-.485l-.219-.149a4.476 4.476 0 0 0-.507-.415l-.127-.092a4.558 4.558 0 0 0-.622-.346L49.919.445c-1.287-.593-2.574-.593-3.861 0L3.153 20.175a4.51 4.51 0 0 0-.623.346l-.126.092a4.476 4.476 0 0 0-.507.415l-.15.161c-.146.152-.28.313-.404.484l-.103.15c-.143.22-.266.45-.369.691l-.127.185a4.592 4.592 0 0 0-.184.587v.242a4.632 4.632 0 0 0-.081.83v51.284c0 1.964.891 3.358 2.674 4.183zm6.534-48.264l33.697 15.523v41.142L9.687 72.692V31.561zm42.917 56.654V47.084l33.697-15.523v41.142L52.604 88.215zm-4.61-78.55l31.9 14.693-31.9 14.694-31.899-14.694L47.994 9.665z"
|
||||
d="M2.036 16.961l8.578 3.944a.886.886 0 00.772 0l8.576-3.944c.357-.164.536-.443.536-.836V5.875a.924.924 0 00-.016-.166v-.048a.95.95 0 00-.037-.118l-.014-.039a.91.91 0 00-.073-.138l-.021-.03a1.067 1.067 0 00-.081-.097l-.043-.03a.901.901 0 00-.102-.083l-.025-.018a1.003 1.003 0 00-.124-.069l-8.578-3.944a.886.886 0 00-.772 0L2.036 5.039a.935.935 0 00-.124.069l-.025.018a.901.901 0 00-.102.083l-.03.032a.915.915 0 00-.08.097l-.021.03a.916.916 0 00-.074.138l-.025.037a1.006 1.006 0 00-.037.118v.048a.924.924 0 00-.016.166v10.25c0 .393.178.671.534.836zm1.306-9.646l6.735 3.102v8.223l-6.735-3.104V7.315zm8.578 11.323v-8.221l6.735-3.102v8.223l-6.735 3.1zm-.921-15.7l6.376 2.937-6.376 2.937-6.376-2.937 6.376-2.937z"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
@@ -105,11 +105,7 @@ export default defineComponent({
|
||||
}
|
||||
|
||||
svg {
|
||||
position: relative;
|
||||
top: 2px;
|
||||
left: 2px;
|
||||
width: calc(100% - 4px); // the material icons all have a slight padding
|
||||
height: calc(100% - 4px);
|
||||
display: block;
|
||||
color: inherit;
|
||||
fill: currentColor;
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@ describe('Hydration', () => {
|
||||
expect(hydrated).toBe(false);
|
||||
});
|
||||
|
||||
it('Does not hydrate when already hydrated', async () => {
|
||||
it('Does not dehydrate when already dehydrated', async () => {
|
||||
await dehydrate();
|
||||
|
||||
const collectionsStore = useCollectionsStore({});
|
||||
|
||||
@@ -3,7 +3,7 @@ import VueCompositionAPI from '@vue/composition-api';
|
||||
import { shallowMount, createLocalVue } from '@vue/test-utils';
|
||||
import useNavigation from '../compositions/use-navigation';
|
||||
import VTable from '@/components/v-table';
|
||||
import PrivateView from '@/views/private/';
|
||||
import PrivateView from '@/views/private-view/';
|
||||
import router from '@/router';
|
||||
|
||||
jest.mock('../compositions/use-navigation');
|
||||
@@ -22,12 +22,22 @@ describe('Modules / Collections / Routes / CollectionsOverview', () => {
|
||||
});
|
||||
|
||||
it('Uses useNavigation to get navigation links', () => {
|
||||
shallowMount(CollectionsOverview, { localVue });
|
||||
shallowMount(CollectionsOverview, {
|
||||
localVue,
|
||||
mocks: {
|
||||
$tc: () => 'title'
|
||||
}
|
||||
});
|
||||
expect(useNavigation).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('Calls router.push on navigation', () => {
|
||||
const component = shallowMount(CollectionsOverview, { localVue });
|
||||
const component = shallowMount(CollectionsOverview, {
|
||||
localVue,
|
||||
mocks: {
|
||||
$tc: () => 'title'
|
||||
}
|
||||
});
|
||||
(component.vm as any).navigateToCollection({
|
||||
collection: 'test',
|
||||
name: 'Test',
|
||||
|
||||
@@ -1,5 +1,21 @@
|
||||
<template>
|
||||
<private-view class="collections-overview">
|
||||
<private-view class="collections-overview" :title="$tc('collection', 2)">
|
||||
<template #title-outer:prepend>
|
||||
<v-button rounded disabled icon secondary><v-icon name="box" /></v-button>
|
||||
</template>
|
||||
|
||||
<template #actions>
|
||||
<v-button rounded icon style="--v-button-background-color: var(--success);">
|
||||
<v-icon name="add" />
|
||||
</v-button>
|
||||
<v-button rounded icon style="--v-button-background-color: var(--warning);">
|
||||
<v-icon name="delete" />
|
||||
</v-button>
|
||||
<v-button rounded icon style="--v-button-background-color: var(--danger);">
|
||||
<v-icon name="favorite" />
|
||||
</v-button>
|
||||
</template>
|
||||
|
||||
<template #navigation>
|
||||
<collections-navigation />
|
||||
</template>
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
|
||||
html {
|
||||
font-size: 15px;
|
||||
-webkit-tap-highlight-color: rgba(0,0,0,0);
|
||||
}
|
||||
|
||||
body {
|
||||
|
||||
@@ -77,10 +77,10 @@ body,
|
||||
|
||||
--button-secondary-background-color: var(--blue-grey-100);
|
||||
--button-secondary-background-color-hover: var(--blue-grey-200);
|
||||
--button-secondary-background-color-disabled: var(--blue-grey-400);
|
||||
--button-secondary-background-color-disabled: var(--blue-grey-50);
|
||||
--button-secondary-foreground-color: var(--white);
|
||||
--button-secondary-foreground-color-hover: var(--blue-grey-600);
|
||||
--button-secondary-foreground-color-disabled: var(--blue-grey-600);
|
||||
--button-secondary-foreground-color-disabled: var(--blue-grey-400);
|
||||
|
||||
/* Misc.
|
||||
Please try to use one of the variables above if applicable.
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
import parseCSSVar from './parse-css-var';
|
||||
|
||||
describe('Utils / parseCSSVar', () => {
|
||||
it('Wraps CSS variables in var()', () => {
|
||||
const result = parseCSSVar('--red');
|
||||
|
||||
expect(result).toBe('var(--red)');
|
||||
});
|
||||
|
||||
it('Passes through regular CSS', () => {
|
||||
const result = parseCSSVar('#abcabc');
|
||||
|
||||
expect(result).toBe('#abcabc');
|
||||
});
|
||||
});
|
||||
@@ -1,6 +0,0 @@
|
||||
// If the passed color string starts with --, it will be returned wrapped in `var()`, so it can be
|
||||
// used in CSS.
|
||||
export default function parseCSSVar(color: string): string {
|
||||
if (color.startsWith('--')) return `var(${color})`;
|
||||
return color;
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import { withKnobs } from '@storybook/addon-knobs';
|
||||
import markdown from './readme.md';
|
||||
import withPadding from '../../../../.storybook/decorators/with-padding';
|
||||
import withAltColors from '../../../../.storybook/decorators/with-alt-colors';
|
||||
|
||||
import { defineComponent, provide } from '@vue/composition-api';
|
||||
import DrawerDetailGroup from './drawer-detail-group.vue';
|
||||
import DrawerDetail from '../drawer-detail/';
|
||||
|
||||
export default {
|
||||
title: 'Views / Private / Drawer Detail Group',
|
||||
decorators: [withKnobs, withAltColors, withPadding],
|
||||
parameters: {
|
||||
notes: markdown
|
||||
}
|
||||
};
|
||||
|
||||
export const basic = () =>
|
||||
defineComponent({
|
||||
components: { DrawerDetailGroup, DrawerDetail },
|
||||
setup() {
|
||||
provide('drawer-open', true);
|
||||
},
|
||||
template: `
|
||||
<drawer-detail-group>
|
||||
<drawer-detail icon="person" title="People">
|
||||
Hi there!
|
||||
</drawer-detail>
|
||||
<drawer-detail icon="settings" title="Settings">
|
||||
These sections can be whatever you want them to be
|
||||
</drawer-detail>
|
||||
<drawer-detail icon="sentiment_satisfied_alt" title="Fun times">
|
||||
This is a third section in the sidebar
|
||||
</drawer-detail>
|
||||
<drawer-detail icon="forum" title="Comments">
|
||||
These sections can hold any markup:
|
||||
|
||||
<v-input full-width placeholder="I'm an input" />
|
||||
</drawer-detail>
|
||||
</drawer-detail-group>`
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
import { shallowMount, createLocalVue } from '@vue/test-utils';
|
||||
import VueCompositionAPI from '@vue/composition-api';
|
||||
import DrawerDetailGroup from './_drawer-detail-group.vue';
|
||||
import DrawerDetailGroup from './drawer-detail-group.vue';
|
||||
import VItemGroup from '@/components/v-item-group';
|
||||
|
||||
const localVue = createLocalVue();
|
||||
4
src/views/private-view/drawer-detail-group/index.ts
Normal file
4
src/views/private-view/drawer-detail-group/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import DrawerDetailGroup from './drawer-detail-group.vue';
|
||||
|
||||
export { DrawerDetailGroup };
|
||||
export default DrawerDetailGroup;
|
||||
39
src/views/private-view/drawer-detail-group/readme.md
Normal file
39
src/views/private-view/drawer-detail-group/readme.md
Normal file
@@ -0,0 +1,39 @@
|
||||
# Drawer Detail Group
|
||||
|
||||
Used in the private view to manage the active state of [the nested `drawer-detail` components](../drawer-detail/).
|
||||
|
||||
## Usage
|
||||
|
||||
```html
|
||||
<drawer-detail-group :drawer-open="drawerOpen">
|
||||
<drawer-detail icon="person" title="Users" >
|
||||
<!-- section content -->
|
||||
</drawer-detail>
|
||||
<drawer-detail icon="settings" title="Settings" >
|
||||
<!-- section content -->
|
||||
</drawer-detail>
|
||||
<drawer-detail icon="map" title="Routes" >
|
||||
<!-- section content -->
|
||||
</drawer-detail>
|
||||
</drawer-detail-group>
|
||||
```
|
||||
|
||||
## Drawer open state
|
||||
|
||||
Once the drawer closes, all open drawer details should be closed. By watching the `drawer-open` prop, we can dynamically close all details.
|
||||
|
||||
## Props
|
||||
| Prop | Description | Default |
|
||||
|---------------|----------------------------------------------------------|---------|
|
||||
| `drawer-open` | If the drawer sidebar in the private view is open or not | `false` |
|
||||
|
||||
## Slots
|
||||
| Slot | Description |
|
||||
|-----------|-------------|
|
||||
| _default_ | |
|
||||
|
||||
## Events
|
||||
n/a
|
||||
|
||||
## CSS Variables
|
||||
n/a
|
||||
48
src/views/private-view/drawer-detail/drawer-detail.story.ts
Normal file
48
src/views/private-view/drawer-detail/drawer-detail.story.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import { withKnobs, text, boolean } from '@storybook/addon-knobs';
|
||||
import markdown from './readme.md';
|
||||
import { defineComponent, provide, ref, watch } from '@vue/composition-api';
|
||||
import withPadding from '../../../../.storybook/decorators/with-padding';
|
||||
import withAltColors from '../../../../.storybook/decorators/with-alt-colors';
|
||||
|
||||
export default {
|
||||
title: 'Views / Private / Drawer Detail',
|
||||
decorators: [withKnobs, withAltColors, withPadding],
|
||||
parameters: {
|
||||
notes: markdown
|
||||
}
|
||||
};
|
||||
|
||||
export const basic = () =>
|
||||
defineComponent({
|
||||
props: {
|
||||
drawerOpen: {
|
||||
default: boolean('Drawer open', true)
|
||||
},
|
||||
icon: {
|
||||
default: text('Icon', 'person')
|
||||
},
|
||||
title: {
|
||||
default: text('Title', 'People')
|
||||
}
|
||||
},
|
||||
setup(props) {
|
||||
const open = ref(false);
|
||||
provide('drawer-open', open);
|
||||
|
||||
watch(
|
||||
() => props.drawerOpen,
|
||||
newOpen => (open.value = newOpen)
|
||||
);
|
||||
|
||||
provide('item-group', {
|
||||
register: () => {},
|
||||
unregister: () => {},
|
||||
toggle: () => {}
|
||||
});
|
||||
},
|
||||
template: `
|
||||
<drawer-detail :title="title" :icon="icon">
|
||||
Content
|
||||
</drawer-detail>
|
||||
`
|
||||
});
|
||||
@@ -0,0 +1,32 @@
|
||||
import markdown from './readme.md';
|
||||
import HeaderBarActions from './header-bar-actions.vue';
|
||||
import { defineComponent } from '@vue/composition-api';
|
||||
import withPadding from '../../../../.storybook/decorators/with-padding';
|
||||
|
||||
export default {
|
||||
title: 'Views / Private / Header Bar Actions',
|
||||
decorators: [withPadding],
|
||||
parameters: {
|
||||
notes: markdown
|
||||
}
|
||||
};
|
||||
|
||||
export const basic = () =>
|
||||
defineComponent({
|
||||
components: { HeaderBarActions },
|
||||
template: `
|
||||
<div style="display: flex; justify-content: flex-end; position: relative; height: 44px;">
|
||||
<header-bar-actions>
|
||||
<v-button rounded icon style="--v-button-background-color: var(--success);">
|
||||
<v-icon name="add" />
|
||||
</v-button>
|
||||
<v-button rounded icon style="--v-button-background-color: var(--warning);">
|
||||
<v-icon name="delete" />
|
||||
</v-button>
|
||||
<v-button rounded icon style="--v-button-background-color: var(--danger);">
|
||||
<v-icon name="favorite" />
|
||||
</v-button>
|
||||
</header-bar-actions>
|
||||
</div>
|
||||
`
|
||||
});
|
||||
@@ -0,0 +1,21 @@
|
||||
import { mount, createLocalVue } from '@vue/test-utils';
|
||||
import VueCompositionAPI from '@vue/composition-api';
|
||||
import HeaderBarActions from './header-bar-actions.vue';
|
||||
|
||||
import VButton from '@/components/v-button';
|
||||
import VIcon from '@/components/v-icon';
|
||||
|
||||
const localVue = createLocalVue();
|
||||
localVue.use(VueCompositionAPI);
|
||||
localVue.component('v-button', VButton);
|
||||
localVue.component('v-icon', VIcon);
|
||||
|
||||
describe('Views / Private / Header Bar Actions', () => {
|
||||
it('Renders', () => {
|
||||
const component = mount(HeaderBarActions, {
|
||||
localVue
|
||||
});
|
||||
|
||||
expect(component.isVueInstance()).toBeTruthy();
|
||||
});
|
||||
});
|
||||
106
src/views/private-view/header-bar-actions/header-bar-actions.vue
Normal file
106
src/views/private-view/header-bar-actions/header-bar-actions.vue
Normal file
@@ -0,0 +1,106 @@
|
||||
<template>
|
||||
<div class="actions" :class="{ active }">
|
||||
<v-button class="expand" icon rounded secondary outlined @click="active = !active">
|
||||
<v-icon name="arrow_left" />
|
||||
</v-button>
|
||||
|
||||
<div class="action-buttons">
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
<v-button
|
||||
class="drawer-toggle"
|
||||
icon
|
||||
rounded
|
||||
secondary
|
||||
outlined
|
||||
@click="$emit('toggle:drawer')"
|
||||
>
|
||||
<v-icon name="info" />
|
||||
</v-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref } from '@vue/composition-api';
|
||||
|
||||
export default defineComponent({
|
||||
props: {},
|
||||
setup() {
|
||||
const active = ref(false);
|
||||
return { active };
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '@/styles/mixins/breakpoint';
|
||||
|
||||
.actions {
|
||||
position: relative;
|
||||
display: flex;
|
||||
background-color: transparent;
|
||||
|
||||
.gradient-wrapper {
|
||||
display: contents;
|
||||
}
|
||||
|
||||
.expand {
|
||||
flex-shrink: 0;
|
||||
margin-right: 8px;
|
||||
|
||||
@include breakpoint(medium) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
|
||||
> *:not(:last-child) {
|
||||
display: none;
|
||||
margin-right: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.drawer-toggle {
|
||||
flex-shrink: 0;
|
||||
margin-left: 8px;
|
||||
|
||||
@include breakpoint(medium) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
&.active {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
height: 100%;
|
||||
padding: inherit;
|
||||
padding-left: 8px;
|
||||
background-color: var(--background-color);
|
||||
|
||||
.expand {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
> * {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@include breakpoint(medium) {
|
||||
.action-buttons {
|
||||
> * {
|
||||
display: block !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
4
src/views/private-view/header-bar-actions/index.ts
Normal file
4
src/views/private-view/header-bar-actions/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import HeaderBarActions from './header-bar-actions.vue';
|
||||
|
||||
export { HeaderBarActions };
|
||||
export default HeaderBarActions;
|
||||
39
src/views/private-view/header-bar-actions/readme.md
Normal file
39
src/views/private-view/header-bar-actions/readme.md
Normal file
@@ -0,0 +1,39 @@
|
||||
# Header Bar Actions
|
||||
|
||||
Wrapper component that will hold all the actions in the right of the header bar. This component is
|
||||
made to work well with nested `v-button`s, but also works with any other markup.
|
||||
|
||||
On mobile, it will only render the last element in the default slot and render a toggle button to
|
||||
expand / collapse the actions.
|
||||
|
||||
## Usage
|
||||
|
||||
```html
|
||||
<header-bar-actions>
|
||||
<v-button rounded icon style="--v-button-background-color: var(--success);">
|
||||
<v-icon name="add" />
|
||||
</v-button>
|
||||
<v-button rounded icon style="--v-button-background-color: var(--warning);">
|
||||
<v-icon name="delete" />
|
||||
</v-button>
|
||||
<v-button rounded icon style="--v-button-background-color: var(--danger);">
|
||||
<v-icon name="favorite" />
|
||||
</v-button>
|
||||
</header-bar-actions>
|
||||
```
|
||||
|
||||
## Props
|
||||
n/a
|
||||
|
||||
## Slots
|
||||
| Slot | Description |
|
||||
|-----------|-------------|
|
||||
| _default_ | |
|
||||
|
||||
## Events
|
||||
| Event | Description | Value |
|
||||
|-----------------|-------------------------------------------------------|-------|
|
||||
| `toggle:drawer` | Emitted when the user clicks the toggle drawer button | -- |
|
||||
|
||||
## CSS Variables
|
||||
n/a
|
||||
244
src/views/private-view/header-bar/header-bar.story.ts
Normal file
244
src/views/private-view/header-bar/header-bar.story.ts
Normal file
@@ -0,0 +1,244 @@
|
||||
import { withKnobs, text, boolean } from '@storybook/addon-knobs';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import markdown from './readme.md';
|
||||
import withBackground from '../../../../.storybook/decorators/with-background';
|
||||
|
||||
import { defineComponent } from '@vue/composition-api';
|
||||
import HeaderBar from './header-bar.vue';
|
||||
import VueRouter from 'vue-router';
|
||||
|
||||
export default {
|
||||
title: 'Views / Private / Header Bar',
|
||||
decorators: [withKnobs, withBackground],
|
||||
parameters: {
|
||||
notes: markdown
|
||||
}
|
||||
};
|
||||
|
||||
export const basic = () =>
|
||||
defineComponent({
|
||||
components: { HeaderBar },
|
||||
props: {
|
||||
title: {
|
||||
default: text('Title', 'Hello World')
|
||||
},
|
||||
showDrawerToggle: {
|
||||
default: boolean('Show Drawer Toggle', false)
|
||||
},
|
||||
dense: {
|
||||
default: boolean('Dense', false)
|
||||
}
|
||||
},
|
||||
setup() {
|
||||
const navToggle = action('update:nav-open');
|
||||
const drawerToggle = action('update:drawer-open');
|
||||
|
||||
return { navToggle, drawerToggle };
|
||||
},
|
||||
template: `
|
||||
<header-bar
|
||||
:title="title"
|
||||
:nav-open="false"
|
||||
:drawer-open="false"
|
||||
:show-drawer-toggle="showDrawerToggle"
|
||||
:dense="dense"
|
||||
@update:nav-open="navToggle"
|
||||
@update:drawer-open="drawerToggle"
|
||||
/>
|
||||
`
|
||||
});
|
||||
|
||||
export const withBreadcrumb = () =>
|
||||
defineComponent({
|
||||
router: new VueRouter(),
|
||||
components: { HeaderBar },
|
||||
props: {
|
||||
title: {
|
||||
default: text('Title', 'Hello World')
|
||||
},
|
||||
showDrawerToggle: {
|
||||
default: boolean('Show Drawer Toggle', false)
|
||||
},
|
||||
dense: {
|
||||
default: boolean('Dense', false)
|
||||
}
|
||||
},
|
||||
setup() {
|
||||
const navToggle = action('update:nav-open');
|
||||
const drawerToggle = action('update:drawer-open');
|
||||
|
||||
return { navToggle, drawerToggle };
|
||||
},
|
||||
template: `
|
||||
<header-bar
|
||||
:title="title"
|
||||
:nav-open="false"
|
||||
:drawer-open="false"
|
||||
:show-drawer-toggle="showDrawerToggle"
|
||||
:dense="dense"
|
||||
@update:nav-open="navToggle"
|
||||
@update:drawer-open="drawerToggle"
|
||||
>
|
||||
<template #headline>
|
||||
<v-breadcrumb :items="[
|
||||
{
|
||||
name: 'Settings',
|
||||
to: '/settings'
|
||||
},
|
||||
{
|
||||
name: 'Collection & Fields',
|
||||
to: '/settings/fields'
|
||||
}
|
||||
]" />
|
||||
</template>
|
||||
</header-bar>
|
||||
`
|
||||
});
|
||||
|
||||
export const withBackButton = () =>
|
||||
defineComponent({
|
||||
router: new VueRouter(),
|
||||
components: { HeaderBar },
|
||||
props: {
|
||||
title: {
|
||||
default: text('Title', 'Hello World')
|
||||
},
|
||||
showDrawerToggle: {
|
||||
default: boolean('Show Drawer Toggle', false)
|
||||
},
|
||||
dense: {
|
||||
default: boolean('Dense', false)
|
||||
}
|
||||
},
|
||||
setup() {
|
||||
const navToggle = action('update:nav-open');
|
||||
const drawerToggle = action('update:drawer-open');
|
||||
|
||||
return { navToggle, drawerToggle };
|
||||
},
|
||||
template: `
|
||||
<header-bar
|
||||
:title="title"
|
||||
:nav-open="false"
|
||||
:drawer-open="false"
|
||||
:show-drawer-toggle="showDrawerToggle"
|
||||
:dense="dense"
|
||||
@update:nav-open="navToggle"
|
||||
@update:drawer-open="drawerToggle"
|
||||
>
|
||||
<template #title-outer:prepend>
|
||||
<v-button icon rounded secondary>
|
||||
<v-icon name="arrow_back" />
|
||||
</v-button>
|
||||
</template>
|
||||
</header-bar>
|
||||
`
|
||||
});
|
||||
|
||||
export const slots = () => {
|
||||
const SlotLabel = defineComponent({
|
||||
template: `<span style="display: block; font-size: 10px; padding: 2px; border-radius: 2px; font-family: monospace; color: red; background-color: rgba(255, 0, 0, 0.2)"><slot /></span>`
|
||||
});
|
||||
|
||||
return defineComponent({
|
||||
components: { HeaderBar, SlotLabel },
|
||||
props: {
|
||||
title: {
|
||||
default: text('Title', 'Hello World')
|
||||
},
|
||||
showDrawerToggle: {
|
||||
default: boolean('Show Drawer Toggle', false)
|
||||
},
|
||||
dense: {
|
||||
default: boolean('Dense', false)
|
||||
}
|
||||
},
|
||||
setup() {
|
||||
const navToggle = action('update:nav-open');
|
||||
const drawerToggle = action('update:drawer-open');
|
||||
|
||||
return { navToggle, drawerToggle };
|
||||
},
|
||||
template: `
|
||||
<header-bar
|
||||
:title="title"
|
||||
:nav-open="false"
|
||||
:drawer-open="false"
|
||||
:show-drawer-toggle="showDrawerToggle"
|
||||
:dense="dense"
|
||||
@update:nav-open="navToggle"
|
||||
@update:drawer-open="drawerToggle"
|
||||
>
|
||||
<template #title-outer:prepend>
|
||||
<slot-label>title-outer:prepend</slot-label>
|
||||
</template>
|
||||
<template #headline>
|
||||
<slot-label>breadcrumb</slot-label>
|
||||
</template>
|
||||
<template #title:prepend>
|
||||
<slot-label>title:prepend</slot-label>
|
||||
</template>
|
||||
<template #title:append>
|
||||
<slot-label>title:append</slot-label>
|
||||
</template>
|
||||
<template #title-outer:append>
|
||||
<slot-label>title-outer:append</slot-label>
|
||||
</template>
|
||||
<template #actions:prepend>
|
||||
<slot-label>actions:prepend</slot-label>
|
||||
</template>
|
||||
<template #actions>
|
||||
<slot-label>actions</slot-label>
|
||||
</template>
|
||||
<template #actions:append>
|
||||
<slot-label>actions:append</slot-label>
|
||||
</template>
|
||||
</header-bar>
|
||||
`
|
||||
});
|
||||
};
|
||||
|
||||
export const withActions = () =>
|
||||
defineComponent({
|
||||
components: { HeaderBar },
|
||||
props: {
|
||||
title: {
|
||||
default: text('Title', 'Hello World')
|
||||
},
|
||||
showDrawerToggle: {
|
||||
default: boolean('Show Drawer Toggle', false)
|
||||
},
|
||||
dense: {
|
||||
default: boolean('Dense', false)
|
||||
}
|
||||
},
|
||||
setup() {
|
||||
const navToggle = action('update:nav-open');
|
||||
const drawerToggle = action('update:drawer-open');
|
||||
|
||||
return { navToggle, drawerToggle };
|
||||
},
|
||||
template: `
|
||||
<header-bar
|
||||
:title="title"
|
||||
:nav-open="false"
|
||||
:drawer-open="false"
|
||||
:show-drawer-toggle="showDrawerToggle"
|
||||
:dense="dense"
|
||||
@update:nav-open="navToggle"
|
||||
@update:drawer-open="drawerToggle"
|
||||
>
|
||||
<template #actions>
|
||||
<v-button rounded icon style="--v-button-background-color: var(--success);">
|
||||
<v-icon name="add" />
|
||||
</v-button>
|
||||
<v-button rounded icon style="--v-button-background-color: var(--warning);">
|
||||
<v-icon name="delete" />
|
||||
</v-button>
|
||||
<v-button rounded icon style="--v-button-background-color: var(--danger);">
|
||||
<v-icon name="favorite" />
|
||||
</v-button>
|
||||
</template>
|
||||
</header-bar>
|
||||
`
|
||||
});
|
||||
32
src/views/private-view/header-bar/header-bar.test.ts
Normal file
32
src/views/private-view/header-bar/header-bar.test.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { mount, createLocalVue } from '@vue/test-utils';
|
||||
import VueCompositionAPI from '@vue/composition-api';
|
||||
import HeaderBar from './header-bar.vue';
|
||||
|
||||
import VButton from '@/components/v-button';
|
||||
import VIcon from '@/components/v-icon';
|
||||
|
||||
const localVue = createLocalVue();
|
||||
localVue.use(VueCompositionAPI);
|
||||
localVue.component('v-button', VButton);
|
||||
localVue.component('v-icon', VIcon);
|
||||
|
||||
describe('Views / Private / Header Bar', () => {
|
||||
it('Emits toggle event when toggle buttons are clicked', () => {
|
||||
const component = mount(HeaderBar, {
|
||||
localVue,
|
||||
propsData: {
|
||||
title: 'Title'
|
||||
}
|
||||
});
|
||||
|
||||
const navToggle = component.find('.nav-toggle');
|
||||
navToggle.trigger('click');
|
||||
|
||||
expect(component.emitted('toggle:nav')[0]).toBeTruthy();
|
||||
|
||||
const drawerToggle = component.find('.drawer-toggle');
|
||||
drawerToggle.trigger('click');
|
||||
|
||||
expect(component.emitted('toggle:drawer')[0]).toBeTruthy();
|
||||
});
|
||||
});
|
||||
120
src/views/private-view/header-bar/header-bar.vue
Normal file
120
src/views/private-view/header-bar/header-bar.vue
Normal file
@@ -0,0 +1,120 @@
|
||||
<template>
|
||||
<header class="header-bar" :class="{ dense }">
|
||||
<v-button secondary class="nav-toggle" icon rounded @click="$emit('toggle:nav')">
|
||||
<v-icon name="menu" />
|
||||
</v-button>
|
||||
|
||||
<div class="title-outer-prepend" v-if="$scopedSlots['title-outer:prepend']">
|
||||
<slot name="title-outer:prepend" />
|
||||
</div>
|
||||
|
||||
<div class="title-container">
|
||||
<slot name="headline" />
|
||||
<div class="title">
|
||||
<slot name="title:prepend" />
|
||||
<h1>{{ title }}</h1>
|
||||
<slot name="title:append" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<slot name="title-outer:append" />
|
||||
|
||||
<div class="spacer" />
|
||||
|
||||
<slot name="actions:prepend" />
|
||||
<header-bar-actions @toggle:drawer="$emit('toggle:drawer')">
|
||||
<slot name="actions" />
|
||||
</header-bar-actions>
|
||||
<slot name="actions:append" />
|
||||
</header>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from '@vue/composition-api';
|
||||
import HeaderBarActions from '../header-bar-actions';
|
||||
|
||||
export default defineComponent({
|
||||
components: { HeaderBarActions },
|
||||
props: {
|
||||
title: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
dense: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
setup() {
|
||||
return {};
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '@/styles/mixins/breakpoint';
|
||||
@import '@/styles/mixins/type-styles';
|
||||
|
||||
.header-bar {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
width: 100%;
|
||||
height: 64px;
|
||||
padding: 0 12px;
|
||||
background-color: var(--background-color);
|
||||
transition: height var(--medium) var(--transition);
|
||||
|
||||
&.dense {
|
||||
height: 64px;
|
||||
box-shadow: 0 1px 4px 0 rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.nav-toggle {
|
||||
@include breakpoint(medium) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.title-outer-prepend {
|
||||
display: none;
|
||||
|
||||
@include breakpoint(medium) {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.title-container {
|
||||
margin-left: 12px;
|
||||
|
||||
.title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
h1 {
|
||||
flex-grow: 1;
|
||||
@include type-title;
|
||||
}
|
||||
}
|
||||
|
||||
.spacer {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.drawer-toggle {
|
||||
flex-shrink: 0;
|
||||
margin-left: 8px;
|
||||
|
||||
@include breakpoint(medium) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@include breakpoint(small) {
|
||||
height: 112px;
|
||||
padding: 0 32px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
4
src/views/private-view/header-bar/index.ts
Normal file
4
src/views/private-view/header-bar/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import HeaderBar from './header-bar.vue';
|
||||
|
||||
export { HeaderBar };
|
||||
export default HeaderBar;
|
||||
49
src/views/private-view/header-bar/readme.md
Normal file
49
src/views/private-view/header-bar/readme.md
Normal file
@@ -0,0 +1,49 @@
|
||||
# Header Bar
|
||||
|
||||
The header bar is the header bar displayed above the content of every component that relies on
|
||||
`private-view`. It needs to have a lot of slots in order to allow the different routes and modules
|
||||
to control what things are shown, while also providing enough consistency between views.
|
||||
|
||||
## Usage
|
||||
|
||||
```html
|
||||
<header-bar title="Global Settings">
|
||||
<template #actions>
|
||||
<v-button to="/collections/settings/+">
|
||||
<v-icon name="add" />
|
||||
</v-button>
|
||||
</template>
|
||||
</header-bar>
|
||||
```
|
||||
|
||||
## Navigation toggle / `title-outer:prepend` slot
|
||||
|
||||
On mobile views, the `title-outer:prepend` slot is replaced with the navigation toggle button. Never
|
||||
put any view critical actions in this slot.
|
||||
|
||||
## Props
|
||||
| Prop | Description | Default |
|
||||
|---------------|-------------------------------------------------------------------------------------|---------|
|
||||
| `title`* | The title of the current page | -- |
|
||||
| `dense` | Render the header in a smaller total height, leaving more room for the view content | -- |
|
||||
|
||||
## Slots
|
||||
| Slot | Description |
|
||||
|-----------------------|----------------------------------------------------------------------------------------------------------------------|
|
||||
| `title-outer:prepend` | Before the title box. This is hidden on mobile. |
|
||||
| `headline` | Line above the title. Used for breadcrumbs and other secondary information |
|
||||
| `title:prepend` | Before the actual title |
|
||||
| `title:append` | After the actual title. Used for status indicator / bookmark toggle |
|
||||
| `title-outer:append` | After the title box. |
|
||||
| `actions:prepend` | Before the action buttons |
|
||||
| `actions` | The actual action buttons. This slot is rendered inside [the `header-bar-actions` component](../header-bar-actions/) |
|
||||
| `actions:append` | After the actions section. |
|
||||
|
||||
## Events
|
||||
| Event | Description | Value |
|
||||
|-----------------|------------------------------------------|-------|
|
||||
| `toggle:nav` | When the nav toggle button is clicked | -- |
|
||||
| `toggle:drawer` | When the drawer toggle button is clicked | -- |
|
||||
|
||||
## CSS Variables
|
||||
n/a
|
||||
4
src/views/private-view/module-bar-logo/index.ts
Normal file
4
src/views/private-view/module-bar-logo/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import ModuleBarLogo from './module-bar-logo.vue';
|
||||
|
||||
export { ModuleBarLogo };
|
||||
export default ModuleBarLogo;
|
||||
@@ -0,0 +1,94 @@
|
||||
import markdown from './readme.md';
|
||||
import ModuleBarLogo from './module-bar-logo.vue';
|
||||
import withPadding from '../../../../.storybook/decorators/with-padding';
|
||||
import useRequestsStore from '@/stores/requests';
|
||||
import useProjectsStore from '@/stores/projects';
|
||||
import { defineComponent } from '@vue/composition-api';
|
||||
|
||||
export default {
|
||||
title: 'Views / Private / Module Bar Logo',
|
||||
decorators: [withPadding],
|
||||
parameters: {
|
||||
notes: markdown
|
||||
}
|
||||
};
|
||||
|
||||
export const basic = () =>
|
||||
defineComponent({
|
||||
components: { ModuleBarLogo },
|
||||
setup() {
|
||||
useProjectsStore({});
|
||||
useRequestsStore({});
|
||||
},
|
||||
template: `
|
||||
<module-bar-logo />
|
||||
`
|
||||
});
|
||||
|
||||
export const withQueue = () =>
|
||||
defineComponent({
|
||||
components: { ModuleBarLogo },
|
||||
setup() {
|
||||
useProjectsStore({});
|
||||
const requestsStore = useRequestsStore({});
|
||||
requestsStore.state.queue = ['abc'];
|
||||
},
|
||||
template: `
|
||||
<module-bar-logo />
|
||||
`
|
||||
});
|
||||
|
||||
export const withCustomLogo = () =>
|
||||
defineComponent({
|
||||
components: { ModuleBarLogo },
|
||||
setup() {
|
||||
useRequestsStore({});
|
||||
const projectsStore = useProjectsStore({});
|
||||
|
||||
projectsStore.state.projects = [
|
||||
{
|
||||
key: 'my-project',
|
||||
api: {
|
||||
version: '8.5.5',
|
||||
requires2FA: false,
|
||||
database: 'mysql',
|
||||
project_name: 'Thumper',
|
||||
project_logo: {
|
||||
full_url:
|
||||
'https://demo.directus.io/uploads/thumper/originals/19acff06-4969-5c75-9cd5-dc3f27506de2.svg',
|
||||
url:
|
||||
'/uploads/thumper/originals/19acff06-4969-5c75-9cd5-dc3f27506de2.svg'
|
||||
},
|
||||
project_color: '#4CAF50',
|
||||
project_foreground: null,
|
||||
project_background: null,
|
||||
telemetry: true,
|
||||
default_locale: 'en-US',
|
||||
project_public_note: null
|
||||
},
|
||||
server: {
|
||||
max_upload_size: 104857600,
|
||||
general: {
|
||||
php_version: '7.2.22',
|
||||
php_api: 'fpm-fcgi'
|
||||
}
|
||||
}
|
||||
}
|
||||
];
|
||||
projectsStore.state.currentProjectKey = 'my-project';
|
||||
},
|
||||
template: `
|
||||
<module-bar-logo /> `
|
||||
});
|
||||
|
||||
export const withCustomColor = () =>
|
||||
defineComponent({
|
||||
components: { ModuleBarLogo },
|
||||
setup() {
|
||||
useProjectsStore({});
|
||||
useRequestsStore({});
|
||||
},
|
||||
template: `
|
||||
<module-bar-logo style="--brand: red;" />
|
||||
`
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
import { mount, createLocalVue, Wrapper } from '@vue/test-utils';
|
||||
import VueCompositionAPI from '@vue/composition-api';
|
||||
import ModuleBarLogo from './_module-bar-logo.vue';
|
||||
import ModuleBarLogo from './module-bar-logo.vue';
|
||||
import { useProjectsStore } from '@/stores/projects';
|
||||
import { useRequestsStore } from '@/stores/requests';
|
||||
|
||||
@@ -72,7 +72,7 @@ export default defineComponent({
|
||||
width: 40px;
|
||||
height: 32px;
|
||||
margin: 0 auto;
|
||||
background-image: url('../../assets/sprite.svg');
|
||||
background-image: url('../../../assets/sprite.svg');
|
||||
background-position: 0% 0%;
|
||||
background-size: 600px 32px;
|
||||
}
|
||||
24
src/views/private-view/module-bar-logo/readme.md
Normal file
24
src/views/private-view/module-bar-logo/readme.md
Normal file
@@ -0,0 +1,24 @@
|
||||
# Module Bar Logo
|
||||
|
||||
Logo at the top of the module bar. Renders either the rabbit (default), or the custom project logo
|
||||
if it exists.
|
||||
|
||||
It listens to the requestsStore, and makes the bunny run whenever a request is made.
|
||||
|
||||
## Usage
|
||||
|
||||
```html
|
||||
<module-bar-logo />
|
||||
```
|
||||
|
||||
## Props
|
||||
n/a
|
||||
|
||||
## Slots
|
||||
n/a
|
||||
|
||||
## Events
|
||||
n/a
|
||||
|
||||
## CSS Variables
|
||||
n/a
|
||||
4
src/views/private-view/module-bar/index.ts
Normal file
4
src/views/private-view/module-bar/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import ModuleBar from './module-bar.vue';
|
||||
|
||||
export { ModuleBar };
|
||||
export default ModuleBar;
|
||||
26
src/views/private-view/module-bar/module-bar.story.ts
Normal file
26
src/views/private-view/module-bar/module-bar.story.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import markdown from './readme.md';
|
||||
import ModuleBar from './module-bar.vue';
|
||||
import { defineComponent } from '@vue/composition-api';
|
||||
import VueRouter from 'vue-router';
|
||||
import useProjectsStore from '@/stores/projects';
|
||||
import useRequestsStore from '@/stores/requests';
|
||||
|
||||
export default {
|
||||
title: 'Views / Private / Module Bar',
|
||||
parameters: {
|
||||
notes: markdown
|
||||
}
|
||||
};
|
||||
|
||||
export const basic = () =>
|
||||
defineComponent({
|
||||
router: new VueRouter(),
|
||||
components: { ModuleBar },
|
||||
setup() {
|
||||
useProjectsStore({});
|
||||
useRequestsStore({});
|
||||
},
|
||||
template: `
|
||||
<module-bar />
|
||||
`
|
||||
});
|
||||
@@ -9,7 +9,7 @@
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from '@vue/composition-api';
|
||||
import ModuleBarLogo from './_module-bar-logo.vue';
|
||||
import ModuleBarLogo from '../module-bar-logo/';
|
||||
import { useProjectsStore } from '@/stores/projects';
|
||||
import { modules } from '@/modules/';
|
||||
|
||||
21
src/views/private-view/module-bar/readme.md
Normal file
21
src/views/private-view/module-bar/readme.md
Normal file
@@ -0,0 +1,21 @@
|
||||
# Module bar
|
||||
|
||||
The left most sidebar that holds the module navigation.
|
||||
|
||||
## Usage
|
||||
|
||||
```html
|
||||
<module-bar />
|
||||
```
|
||||
|
||||
## Props
|
||||
n/a
|
||||
|
||||
## Slots
|
||||
n/a
|
||||
|
||||
## Events
|
||||
n/a
|
||||
|
||||
## CSS Variables
|
||||
n/a
|
||||
@@ -1,24 +1,23 @@
|
||||
import Vue from 'vue';
|
||||
import PrivateView from './private-view.vue';
|
||||
import markdown from './private-view.readme.md';
|
||||
import markdown from './readme.md';
|
||||
import VueRouter from 'vue-router';
|
||||
import { defineComponent } from '@vue/composition-api';
|
||||
|
||||
Vue.component('private-view', PrivateView);
|
||||
Vue.use(VueRouter);
|
||||
|
||||
const router = new VueRouter();
|
||||
|
||||
export default {
|
||||
title: 'Views / Private',
|
||||
component: PrivateView,
|
||||
parameters: {
|
||||
notes: markdown
|
||||
}
|
||||
};
|
||||
|
||||
export const basic = () => ({
|
||||
router: router,
|
||||
template: `
|
||||
<private-view />
|
||||
`
|
||||
});
|
||||
export const basic = () =>
|
||||
defineComponent({
|
||||
router: new VueRouter(),
|
||||
template: `
|
||||
<private-view />
|
||||
`
|
||||
});
|
||||
44
src/views/private-view/private-view.test.ts
Normal file
44
src/views/private-view/private-view.test.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import { createLocalVue, shallowMount } from '@vue/test-utils';
|
||||
import VueCompositionAPI from '@vue/composition-api';
|
||||
import PrivateView from './private-view.vue';
|
||||
import VOverlay from '@/components/v-overlay';
|
||||
|
||||
const localVue = createLocalVue();
|
||||
localVue.use(VueCompositionAPI);
|
||||
localVue.component('v-overlay', VOverlay);
|
||||
|
||||
describe('Views / Private', () => {
|
||||
it('Adds the is-open class to the nav', async () => {
|
||||
const component = shallowMount(PrivateView, {
|
||||
localVue,
|
||||
propsData: {
|
||||
title: 'Title'
|
||||
}
|
||||
});
|
||||
|
||||
expect(component.find('.navigation').classes()).toEqual(['navigation']);
|
||||
|
||||
(component.vm as any).navOpen = true;
|
||||
|
||||
await component.vm.$nextTick();
|
||||
|
||||
expect(component.find('.navigation').classes()).toEqual(['navigation', 'is-open']);
|
||||
});
|
||||
|
||||
it('Adds the is-open class to the drawer', async () => {
|
||||
const component = shallowMount(PrivateView, {
|
||||
localVue,
|
||||
propsData: {
|
||||
title: 'Title'
|
||||
}
|
||||
});
|
||||
|
||||
expect(component.find('.drawer').classes()).toEqual(['drawer', 'alt-colors']);
|
||||
|
||||
(component.vm as any).drawerOpen = true;
|
||||
|
||||
await component.vm.$nextTick();
|
||||
|
||||
expect(component.find('.drawer').classes()).toEqual(['drawer', 'alt-colors', 'is-open']);
|
||||
});
|
||||
});
|
||||
@@ -14,18 +14,23 @@
|
||||
</div>
|
||||
</aside>
|
||||
<div class="content">
|
||||
<header>
|
||||
<button @click="navOpen = true">Toggle nav</button>
|
||||
<button v-if="drawerHasContent" @click="drawerOpen = !drawerOpen">
|
||||
Toggle drawer
|
||||
</button>
|
||||
</header>
|
||||
<header-bar
|
||||
:title="title"
|
||||
@toggle:drawer="drawerOpen = !drawerOpen"
|
||||
@toggle:nav="navOpen = !navOpen"
|
||||
>
|
||||
<template
|
||||
v-for="(_, scopedSlotName) in $scopedSlots"
|
||||
v-slot:[scopedSlotName]="slotData"
|
||||
>
|
||||
<slot :name="scopedSlotName" v-bind="slotData" />
|
||||
</template>
|
||||
</header-bar>
|
||||
<main>
|
||||
<slot />
|
||||
</main>
|
||||
</div>
|
||||
<aside
|
||||
v-if="drawerHasContent"
|
||||
class="drawer alt-colors"
|
||||
:class="{ 'is-open': drawerOpen }"
|
||||
@click="drawerOpen = true"
|
||||
@@ -35,56 +40,38 @@
|
||||
</drawer-detail-group>
|
||||
</aside>
|
||||
|
||||
<v-overlay
|
||||
v-if="navWithOverlay"
|
||||
class="nav-overlay"
|
||||
:active="navOpen"
|
||||
@click="navOpen = false"
|
||||
/>
|
||||
<v-overlay
|
||||
v-if="drawerWithOverlay"
|
||||
class="drawer-overlay"
|
||||
:active="drawerOpen"
|
||||
@click="drawerOpen = false"
|
||||
/>
|
||||
<v-overlay class="nav-overlay" :active="navOpen" @click="navOpen = false" />
|
||||
<v-overlay class="drawer-overlay" :active="drawerOpen" @click="drawerOpen = false" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref, computed, provide } from '@vue/composition-api';
|
||||
import useWindowSize from '@/compositions/window-size';
|
||||
import ModuleBar from './_module-bar.vue';
|
||||
import DrawerDetailGroup from './_drawer-detail-group.vue';
|
||||
|
||||
// Breakpoints:
|
||||
// 600, 960, 1260, 1900
|
||||
import { defineComponent, ref, provide } from '@vue/composition-api';
|
||||
import ModuleBar from './module-bar/';
|
||||
import DrawerDetailGroup from './drawer-detail-group/';
|
||||
import HeaderBar from './header-bar';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
ModuleBar,
|
||||
DrawerDetailGroup
|
||||
DrawerDetailGroup,
|
||||
HeaderBar
|
||||
},
|
||||
props: {},
|
||||
setup(props, { slots }) {
|
||||
props: {
|
||||
title: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
setup() {
|
||||
const navOpen = ref(false);
|
||||
const drawerOpen = ref(false);
|
||||
|
||||
const { width } = useWindowSize();
|
||||
|
||||
const navWithOverlay = computed(() => width.value < 960);
|
||||
const drawerWithOverlay = computed(() => width.value < 1260);
|
||||
|
||||
const drawerHasContent = computed(() => !!slots.drawer);
|
||||
|
||||
provide('drawer-open', drawerOpen);
|
||||
|
||||
return {
|
||||
navOpen,
|
||||
drawerOpen,
|
||||
navWithOverlay,
|
||||
drawerWithOverlay,
|
||||
width,
|
||||
drawerHasContent
|
||||
drawerOpen
|
||||
};
|
||||
}
|
||||
});
|
||||
@@ -94,7 +81,7 @@ export default defineComponent({
|
||||
@import '@/styles/mixins/breakpoint';
|
||||
|
||||
.private-view {
|
||||
--private-view-content-padding: 32px;
|
||||
--private-view-content-padding: 12px;
|
||||
|
||||
display: flex;
|
||||
width: 100%;
|
||||
@@ -102,10 +89,18 @@ export default defineComponent({
|
||||
|
||||
.nav-overlay {
|
||||
--v-overlay-z-index: 49;
|
||||
|
||||
@include breakpoint(medium) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.drawer-overlay {
|
||||
--v-overlay-z-index: 29;
|
||||
|
||||
@include breakpoint(large) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.navigation {
|
||||
@@ -149,6 +144,15 @@ export default defineComponent({
|
||||
main {
|
||||
padding: var(--private-view-content-padding);
|
||||
}
|
||||
|
||||
// Offset for partially visible drawer
|
||||
@include breakpoint(medium) {
|
||||
padding-right: 64px;
|
||||
}
|
||||
|
||||
@include breakpoint(large) {
|
||||
padding-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.drawer {
|
||||
@@ -173,6 +177,7 @@ export default defineComponent({
|
||||
@include breakpoint(large) {
|
||||
position: relative;
|
||||
flex-basis: 64px;
|
||||
flex-shrink: 0;
|
||||
transform: none;
|
||||
transition: flex-basis var(--slow) var(--transition);
|
||||
|
||||
@@ -182,5 +187,9 @@ export default defineComponent({
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@include breakpoint(small) {
|
||||
--private-view-content-padding: 32px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,46 +0,0 @@
|
||||
import Vue from 'vue';
|
||||
import markdown from './drawer-detail.readme.md';
|
||||
import DrawerDetail from './drawer-detail.vue';
|
||||
import PrivateView from '@/views/private';
|
||||
import VButton from '@/components/v-button';
|
||||
import VueRouter from 'vue-router';
|
||||
|
||||
Vue.component('v-button', VButton);
|
||||
Vue.component('drawer-detail', DrawerDetail);
|
||||
Vue.component('private-view', PrivateView);
|
||||
|
||||
Vue.use(VueRouter);
|
||||
|
||||
const router = new VueRouter();
|
||||
|
||||
export default {
|
||||
title: 'Views / Private / Drawer Detail',
|
||||
component: DrawerDetail,
|
||||
parameters: {
|
||||
notes: markdown
|
||||
}
|
||||
};
|
||||
|
||||
export const basic = () => ({
|
||||
router: router,
|
||||
template: `
|
||||
<private-view>
|
||||
<template #drawer>
|
||||
<drawer-detail icon="person" title="Users">
|
||||
<p>Users:</p>
|
||||
<ul>
|
||||
<li>Admin</li>
|
||||
</ul>
|
||||
</drawer-detail>
|
||||
<drawer-detail icon="settings" title="Settings">
|
||||
Hello!
|
||||
</drawer-detail>
|
||||
<drawer-detail icon="shopping_cart" title="Extra">
|
||||
These details hold any other markup:
|
||||
|
||||
<v-button style="margin-top: 12px;">I'm a button!</v-button>
|
||||
</drawer-detail>
|
||||
</template>
|
||||
</private-view>
|
||||
`
|
||||
});
|
||||
@@ -1,67 +0,0 @@
|
||||
import { shallowMount, createLocalVue } from '@vue/test-utils';
|
||||
import VueCompositionAPI from '@vue/composition-api';
|
||||
import PrivateView from './private-view.vue';
|
||||
import VOverlay from '@/components/v-overlay';
|
||||
import * as windowSize from '@/compositions/window-size';
|
||||
|
||||
const localVue = createLocalVue();
|
||||
localVue.use(VueCompositionAPI);
|
||||
localVue.component('v-overlay', VOverlay);
|
||||
|
||||
describe('Views / Private', () => {
|
||||
it('Shows nav with overlay if screen is < 960px', async () => {
|
||||
jest.spyOn(windowSize, 'default').mockImplementation(() => ({
|
||||
width: { value: 600 },
|
||||
height: { value: 600 }
|
||||
}));
|
||||
|
||||
const component = shallowMount(PrivateView, { localVue });
|
||||
expect((component.vm as any).navWithOverlay).toBe(true);
|
||||
});
|
||||
|
||||
it('Does not render overlay for nav if screen is >= 960px', async () => {
|
||||
jest.spyOn(windowSize, 'default').mockImplementation(() => ({
|
||||
width: { value: 960 },
|
||||
height: { value: 960 }
|
||||
}));
|
||||
|
||||
let component = shallowMount(PrivateView, { localVue });
|
||||
expect((component.vm as any).navWithOverlay).toBe(false);
|
||||
|
||||
(windowSize.default as jest.Mock).mockImplementation(() => ({
|
||||
width: { value: 1000 },
|
||||
height: { value: 1000 }
|
||||
}));
|
||||
|
||||
component = shallowMount(PrivateView, { localVue });
|
||||
expect((component.vm as any).navWithOverlay).toBe(false);
|
||||
});
|
||||
|
||||
it('Shows drawer with overlay if screen is < 1260px', async () => {
|
||||
jest.spyOn(windowSize, 'default').mockImplementation(() => ({
|
||||
width: { value: 600 },
|
||||
height: { value: 600 }
|
||||
}));
|
||||
|
||||
const component = shallowMount(PrivateView, { localVue });
|
||||
expect((component.vm as any).drawerWithOverlay).toBe(true);
|
||||
});
|
||||
|
||||
it('Does not render overlay for drawer if screen is >= 1260px', async () => {
|
||||
jest.spyOn(windowSize, 'default').mockImplementation(() => ({
|
||||
width: { value: 1260 },
|
||||
height: { value: 1260 }
|
||||
}));
|
||||
|
||||
let component = shallowMount(PrivateView, { localVue });
|
||||
expect((component.vm as any).drawerWithOverlay).toBe(false);
|
||||
|
||||
(windowSize.default as jest.Mock).mockImplementation(() => ({
|
||||
width: { value: 1300 },
|
||||
height: { value: 1300 }
|
||||
}));
|
||||
|
||||
component = shallowMount(PrivateView, { localVue });
|
||||
expect((component.vm as any).drawerWithOverlay).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -1,7 +1,7 @@
|
||||
import Vue from 'vue';
|
||||
|
||||
import PublicView from './public/';
|
||||
const PrivateView = () => import(/* webpackChunkName: "private-view" */ './private/');
|
||||
const PrivateView = () => import(/* webpackChunkName: "private-view" */ './private-view/');
|
||||
|
||||
Vue.component('public-view', PublicView);
|
||||
Vue.component('private-view', PrivateView);
|
||||
|
||||
Reference in New Issue
Block a user