mirror of
https://github.com/directus/directus.git
synced 2026-02-19 10:14:33 -05:00
Collections module additions (#201)
* Render add new link, only render delete on isnew is false * Add header actions buttons based on state * Add header buttons and breadcrumbs * Style tweaks * Add navigation guard for single collections * Add delete button logic * Add ability to delete items on browse * Add select mode to tabular layout * Add saving / deleting logic to detail view * remove tests (temporarily) * Remove empty tests temporarily * Add pagination to tabular layout if collection is large * Add server sort * wip table tweaks * show shadow only on scroll, fix padding on top of private view. * Update table * fix header hiding the scrollbar * Fix rAF leak * Make pagination sticky * fix double scroll bug * add selfScroll prop to private view * Last try * Lower the default limit * Fix tests for table / private / public view * finish header * remove unnessesary code * Fix debug overflow + icon alignment * Fix breadcrumbs * Fix item fetching * browse view now collapses on scroll * Add drawer-button component * Fix styling of drawer-button drawer-detail * Revert "browse view now collapses on scroll" This reverts commit a8534484d496deef01e399574126f7ba877e098c. * Final commit for the night * Add scroll solution for header overflow * Render table header over shadow * Add useScrollDistance compositoin * Add readme for scroll distance * Restructure header bar using sticky + margin / add shadow * Tweak box shadow to not show up at top on scroll up * Fix tests Co-authored-by: Nitwel <nitwel@arcor.de>
This commit is contained in:
@@ -0,0 +1,31 @@
|
||||
import markdown from './readme.md';
|
||||
import { defineComponent, provide, toRefs } from '@vue/composition-api';
|
||||
import { withKnobs, boolean } from '@storybook/addon-knobs';
|
||||
import withPadding from '../../../../../.storybook/decorators/with-padding';
|
||||
import withAltColors from '../../../../../.storybook/decorators/with-alt-colors';
|
||||
|
||||
import DrawerButton from './drawer-button.vue';
|
||||
|
||||
export default {
|
||||
title: 'Views / Private / Components / Drawer Button',
|
||||
parameters: {
|
||||
notes: markdown
|
||||
},
|
||||
decorators: [withKnobs, withAltColors, withPadding]
|
||||
};
|
||||
|
||||
export const basic = () =>
|
||||
defineComponent({
|
||||
components: { DrawerButton },
|
||||
props: {
|
||||
drawerOpen: {
|
||||
default: boolean('Drawer Open', true)
|
||||
}
|
||||
},
|
||||
setup(props) {
|
||||
provide('drawer-open', toRefs(props).drawerOpen);
|
||||
},
|
||||
template: `
|
||||
<drawer-button icon="info">Close Drawer</drawer-button>
|
||||
`
|
||||
});
|
||||
@@ -0,0 +1,21 @@
|
||||
import { shallowMount, createLocalVue } from '@vue/test-utils';
|
||||
import VueCompositionAPI from '@vue/composition-api';
|
||||
import VIcon from '@/components/v-icon/';
|
||||
import DrawerButton from './drawer-button.vue';
|
||||
|
||||
const localVue = createLocalVue();
|
||||
localVue.use(VueCompositionAPI);
|
||||
localVue.component('v-icon', VIcon);
|
||||
|
||||
describe('Views / Private / Components / Drawer Button', () => {
|
||||
it('Does not render the title when the drawer is closed', () => {
|
||||
const component = shallowMount(DrawerButton, {
|
||||
localVue,
|
||||
provide: {
|
||||
'drawer-open': false
|
||||
}
|
||||
});
|
||||
|
||||
expect(component.find('.title').exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
75
src/views/private/components/drawer-button/drawer-button.vue
Normal file
75
src/views/private/components/drawer-button/drawer-button.vue
Normal file
@@ -0,0 +1,75 @@
|
||||
<template>
|
||||
<component
|
||||
class="drawer-button"
|
||||
:is="to ? 'router-link' : 'button'"
|
||||
@click="$emit('click', $event)"
|
||||
>
|
||||
<div class="icon">
|
||||
<v-icon :name="icon" />
|
||||
</div>
|
||||
<div class="title" v-if="drawerOpen">
|
||||
<slot />
|
||||
</div>
|
||||
</component>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, inject, ref } from '@vue/composition-api';
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
to: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
icon: {
|
||||
type: String,
|
||||
default: 'box'
|
||||
}
|
||||
},
|
||||
setup() {
|
||||
const drawerOpen = inject('drawer-open', ref(false));
|
||||
|
||||
return { drawerOpen };
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.drawer-button {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 64px;
|
||||
color: var(--foreground-color);
|
||||
transition: background-color var(--fast) var(--transition);
|
||||
|
||||
&:hover {
|
||||
background-color: var(--background-color-hover);
|
||||
}
|
||||
|
||||
.icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 64px;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.title {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 52px;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: opacity var(--medium) var(--transition);
|
||||
}
|
||||
.fade-enter, .fade-leave-to /* .fade-leave-active below version 2.1.8 */ {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
4
src/views/private/components/drawer-button/index.ts
Normal file
4
src/views/private/components/drawer-button/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import DrawerButton from './drawer-button.vue';
|
||||
|
||||
export { DrawerButton };
|
||||
export default DrawerButton;
|
||||
28
src/views/private/components/drawer-button/readme.md
Normal file
28
src/views/private/components/drawer-button/readme.md
Normal file
@@ -0,0 +1,28 @@
|
||||
# Drawer Button
|
||||
|
||||
Looks the same as a drawer detail, but can be used as a button / router link.
|
||||
|
||||
## Usage
|
||||
|
||||
```html
|
||||
<drawer-button icon="info">Close Sidebar</drawer-button>
|
||||
```
|
||||
|
||||
## Props
|
||||
| Prop | Description | Default |
|
||||
|---------|----------------------------------------------------|---------|
|
||||
| `icon`* | What icon to render on the left of the button | `box` |
|
||||
| `to` | router-link to prop. Turns button into router-link | `null` |
|
||||
|
||||
## Events
|
||||
| Event | Description | Value |
|
||||
|---------|----------------------------------|--------------|
|
||||
| `click` | When the button has been clicked | `MouseEvent` |
|
||||
|
||||
## Slots
|
||||
| Slot | Description | Data |
|
||||
|-----------|---------------------|------|
|
||||
| _default_ | Title of the button | -- |
|
||||
|
||||
## CSS Variables
|
||||
n/a
|
||||
@@ -44,9 +44,7 @@ export default defineComponent({
|
||||
<style lang="scss" scoped>
|
||||
.drawer-detail {
|
||||
.toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 64px;
|
||||
color: var(--foreground-color);
|
||||
@@ -65,10 +63,17 @@ export default defineComponent({
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 64px;
|
||||
height: 100%;
|
||||
margin-right: 12px;
|
||||
padding: 20px;
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
.title {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 52px;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.content {
|
||||
|
||||
@@ -90,7 +90,7 @@ export default defineComponent({
|
||||
|
||||
.action-buttons {
|
||||
> * {
|
||||
display: block;
|
||||
display: inherit;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -98,7 +98,7 @@ export default defineComponent({
|
||||
@include breakpoint(medium) {
|
||||
.action-buttons {
|
||||
> * {
|
||||
display: block !important;
|
||||
display: inherit !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,15 @@ localVue.component('v-button', VButton);
|
||||
localVue.component('v-icon', VIcon);
|
||||
|
||||
describe('Views / Private / Header Bar', () => {
|
||||
const observeMock = {
|
||||
observe: () => null,
|
||||
disconnect: () => null // maybe not needed
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
(window as any).IntersectionObserver = jest.fn(() => observeMock);
|
||||
});
|
||||
|
||||
it('Emits toggle event when toggle buttons are clicked', () => {
|
||||
const component = mount(HeaderBar, {
|
||||
localVue,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<header class="header-bar" :class="{ dense }">
|
||||
<header class="header-bar" ref="headerEl" :class="{ shadow: showShadow }">
|
||||
<v-button secondary class="nav-toggle" icon rounded @click="$emit('toggle:nav')">
|
||||
<v-icon name="menu" />
|
||||
</v-button>
|
||||
@@ -30,7 +30,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from '@vue/composition-api';
|
||||
import { defineComponent, ref, onMounted, onUnmounted } from '@vue/composition-api';
|
||||
import HeaderBarActions from '../header-bar-actions';
|
||||
|
||||
export default defineComponent({
|
||||
@@ -39,14 +39,29 @@ export default defineComponent({
|
||||
title: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
dense: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
setup() {
|
||||
return {};
|
||||
const headerEl = ref<Element>();
|
||||
|
||||
const showShadow = ref(false);
|
||||
|
||||
const observer = new IntersectionObserver(
|
||||
([e]) => {
|
||||
showShadow.value = e.intersectionRatio < 1;
|
||||
},
|
||||
{ threshold: [1] }
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
observer.observe(headerEl.value as HTMLElement);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
observer.disconnect();
|
||||
});
|
||||
|
||||
return { headerEl, showShadow };
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@@ -56,19 +71,23 @@ export default defineComponent({
|
||||
@import '@/styles/mixins/type-styles';
|
||||
|
||||
.header-bar {
|
||||
position: relative;
|
||||
position: sticky;
|
||||
top: -1px;
|
||||
left: 0;
|
||||
z-index: 5;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
width: 100%;
|
||||
height: 64px;
|
||||
height: 65px;
|
||||
margin: 24px 0;
|
||||
padding: 0 12px;
|
||||
background-color: var(--background-color);
|
||||
transition: height var(--medium) var(--transition);
|
||||
box-shadow: 0;
|
||||
transition: box-shadow var(--medium) var(--transition);
|
||||
|
||||
&.dense {
|
||||
height: 64px;
|
||||
box-shadow: 0 1px 4px 0 rgba(0, 0, 0, 0.2);
|
||||
&.shadow {
|
||||
box-shadow: 0 4px 7px -4px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.nav-toggle {
|
||||
@@ -113,7 +132,6 @@ export default defineComponent({
|
||||
}
|
||||
|
||||
@include breakpoint(small) {
|
||||
height: 112px;
|
||||
padding: 0 32px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,10 +15,9 @@
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
<div class="content">
|
||||
<div class="content" ref="contentEl">
|
||||
<header-bar
|
||||
:title="title"
|
||||
:dense="_headerDense"
|
||||
@toggle:drawer="drawerOpen = !drawerOpen"
|
||||
@toggle:nav="navOpen = !navOpen"
|
||||
>
|
||||
@@ -29,7 +28,8 @@
|
||||
<slot :name="scopedSlotName" v-bind="slotData" />
|
||||
</template>
|
||||
</header-bar>
|
||||
<main ref="mainContent" @scroll="onMainScroll" :class="{ 'header-dense': headerDense }">
|
||||
|
||||
<main>
|
||||
<slot />
|
||||
</main>
|
||||
</div>
|
||||
@@ -40,6 +40,10 @@
|
||||
:class="{ 'is-open': drawerOpen }"
|
||||
@click="drawerOpen = true"
|
||||
>
|
||||
<drawer-button class="drawer-toggle" @click.stop="drawerOpen = !drawerOpen" icon="info">
|
||||
Close Drawer
|
||||
</drawer-button>
|
||||
|
||||
<drawer-detail-group :drawer-open="drawerOpen">
|
||||
<slot name="drawer" />
|
||||
</drawer-detail-group>
|
||||
@@ -51,68 +55,36 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref, provide, computed } from '@vue/composition-api';
|
||||
import { defineComponent, ref, provide } from '@vue/composition-api';
|
||||
import ModuleBar from './components/module-bar/';
|
||||
import DrawerDetailGroup from './components/drawer-detail-group/';
|
||||
import HeaderBar from './components/header-bar';
|
||||
import ProjectChooser from './components/project-chooser';
|
||||
import { throttle } from 'lodash';
|
||||
import DrawerButton from './components/drawer-button/';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
ModuleBar,
|
||||
DrawerDetailGroup,
|
||||
HeaderBar,
|
||||
ProjectChooser
|
||||
ProjectChooser,
|
||||
DrawerButton
|
||||
},
|
||||
props: {
|
||||
title: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
headerDense: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
headerDenseAuto: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
},
|
||||
setup(props) {
|
||||
const mainElement = ref<Element>(null);
|
||||
|
||||
setup() {
|
||||
const navOpen = ref(false);
|
||||
const drawerOpen = ref(false);
|
||||
|
||||
provide('drawer-open', drawerOpen);
|
||||
|
||||
const headerDenseAutoState = ref(false);
|
||||
|
||||
const _headerDense = computed<boolean>(() => {
|
||||
if (props.headerDenseAuto === true) {
|
||||
return headerDenseAutoState.value;
|
||||
} else {
|
||||
return props.headerDense;
|
||||
}
|
||||
});
|
||||
|
||||
const onMainScroll = throttle(event => {
|
||||
const { scrollTop } = event.target;
|
||||
|
||||
if (scrollTop <= 5 && headerDenseAutoState.value === true) {
|
||||
headerDenseAutoState.value = false;
|
||||
} else if (scrollTop > 5 && headerDenseAutoState.value === false) {
|
||||
headerDenseAutoState.value = true;
|
||||
}
|
||||
}, 50);
|
||||
|
||||
return {
|
||||
navOpen,
|
||||
drawerOpen,
|
||||
_headerDense,
|
||||
mainElement,
|
||||
onMainScroll
|
||||
drawerOpen
|
||||
};
|
||||
}
|
||||
});
|
||||
@@ -122,8 +94,6 @@ export default defineComponent({
|
||||
@import '@/styles/mixins/breakpoint';
|
||||
|
||||
.private-view {
|
||||
--private-view-content-padding: 12px;
|
||||
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
@@ -182,30 +152,21 @@ export default defineComponent({
|
||||
.content {
|
||||
position: relative;
|
||||
flex-grow: 1;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
|
||||
.header-bar {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
}
|
||||
overflow: auto;
|
||||
|
||||
main {
|
||||
height: 100%;
|
||||
padding: var(--private-view-content-padding);
|
||||
padding-top: 114px;
|
||||
overflow: auto;
|
||||
display: contents;
|
||||
}
|
||||
|
||||
// Offset for partially visible drawer
|
||||
@include breakpoint(medium) {
|
||||
padding-right: 64px;
|
||||
margin-right: 64px;
|
||||
}
|
||||
|
||||
@include breakpoint(large) {
|
||||
padding-right: 0;
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -62,7 +62,9 @@ describe('Views / Public', () => {
|
||||
describe('Background', () => {
|
||||
it('Defaults background art to color when current project key is unknown', () => {
|
||||
expect((component.vm as any).artStyles).toEqual({
|
||||
background: '#263238'
|
||||
background: '#263238',
|
||||
backgroundPosition: 'center center',
|
||||
backgroundSize: 'cover'
|
||||
});
|
||||
});
|
||||
|
||||
@@ -81,7 +83,9 @@ describe('Views / Public', () => {
|
||||
await component.vm.$nextTick();
|
||||
|
||||
expect((component.vm as any).artStyles).toEqual({
|
||||
background: '#4CAF50'
|
||||
background: '#4CAF50',
|
||||
backgroundPosition: 'center center',
|
||||
backgroundSize: 'cover'
|
||||
});
|
||||
});
|
||||
|
||||
@@ -99,7 +103,9 @@ describe('Views / Public', () => {
|
||||
store.state.currentProjectKey = 'my-project';
|
||||
|
||||
expect((component.vm as any).artStyles).toEqual({
|
||||
background: '#263238'
|
||||
background: '#263238',
|
||||
backgroundPosition: 'center center',
|
||||
backgroundSize: 'cover'
|
||||
});
|
||||
});
|
||||
|
||||
@@ -107,7 +113,9 @@ describe('Views / Public', () => {
|
||||
store.state.projects = [mockProject];
|
||||
store.state.currentProjectKey = 'my-project';
|
||||
expect((component.vm as any).artStyles).toEqual({
|
||||
background: `url(${mockProject.api.project_background?.full_url})`
|
||||
background: `url(${mockProject.api.project_background?.full_url})`,
|
||||
backgroundPosition: 'center center',
|
||||
backgroundSize: 'cover'
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -56,7 +56,9 @@ export default defineComponent({
|
||||
});
|
||||
|
||||
const artStyles = computed(() => ({
|
||||
background: backgroundStyles.value
|
||||
background: backgroundStyles.value,
|
||||
backgroundSize: 'cover',
|
||||
backgroundPosition: 'center center'
|
||||
}));
|
||||
|
||||
return { version, artStyles };
|
||||
@@ -98,6 +100,8 @@ export default defineComponent({
|
||||
display: none;
|
||||
flex-grow: 1;
|
||||
height: 100%;
|
||||
background-position: center center;
|
||||
background-size: cover;
|
||||
|
||||
@include breakpoint(small) {
|
||||
display: block;
|
||||
|
||||
Reference in New Issue
Block a user