Add user interface (#549)

This commit is contained in:
Rijk van Zanten
2020-05-11 13:23:41 -04:00
committed by GitHub
parent 62ef2bb59a
commit 648c2a4e0b
4 changed files with 441 additions and 3 deletions

View File

@@ -17,6 +17,7 @@ import InterfaceManyToOne from './many-to-one';
import InterfaceOneToMany from './one-to-many';
import InterfaceHash from './hash';
import InterfaceSlug from './slug';
import InterfaceUser from './user';
export const interfaces = [
InterfaceTextInput,
@@ -38,6 +39,7 @@ export const interfaces = [
InterfaceOneToMany,
InterfaceHash,
InterfaceSlug,
InterfaceUser,
];
export default interfaces;

View File

@@ -0,0 +1,10 @@
import InterfaceUser from './user.vue';
import { defineInterface } from '@/interfaces/define';
export default defineInterface(({ i18n }) => ({
id: 'user',
name: i18n.t('user'),
icon: 'person',
component: InterfaceUser,
options: [],
}));

View File

@@ -0,0 +1,424 @@
<template>
<div class="user">
<v-menu v-model="menuActive" attached close-on-content-click>
<template #activator="{ active }">
<v-skeleton-loader type="input" v-if="loadingCurrent" />
<v-input
:active="active"
@click="onPreviewClick"
v-else
:placeholder="$t('select_an_item')"
>
<template #input v-if="currentUser">
<div class="preview">
<render-template
collection="directus_users"
:item="currentUser"
:template="displayTemplate"
/>
</div>
</template>
<template #append>
<template v-if="currentUser">
<v-icon
name="open_in_new"
class="edit"
v-tooltip="$t('edit')"
@click.stop="editModalActive = true"
/>
<v-icon
name="close"
class="deselect"
@click.stop="$emit('input', null)"
v-tooltip="$t('deselect')"
/>
</template>
<template v-else>
<v-icon
class="add"
name="add"
v-tooltip="$t('add_new_item')"
@click.stop="editModalActive = true"
/>
<v-icon class="expand" :class="{ active }" name="expand_more" />
</template>
</template>
</v-input>
</template>
<v-list dense>
<template v-if="usersLoading">
<v-list-item v-for="n in 10" :key="`loader-${n}`">
<v-list-item-content>
<v-skeleton-loader type="text" />
</v-list-item-content>
</v-list-item>
</template>
<template v-else>
<v-list-item
v-for="item in users"
:key="item.id"
:active="value === item.id"
@click="setCurrent(item)"
>
<v-list-item-content>
<render-template
collection="directus_users"
:template="displayTemplate"
:item="item"
/>
</v-list-item-content>
</v-list-item>
</template>
</v-list>
</v-menu>
<modal-detail
:active.sync="editModalActive"
collection="directus_users"
:primary-key="currentPrimaryKey"
:edits="edits"
@input="stageEdits"
/>
<modal-browse
:active.sync="selectModalActive"
collection="directus_users"
:selection="selection"
@input="stageSelection"
/>
</div>
</template>
<script lang="ts">
import { defineComponent, computed, ref, watch, PropType } from '@vue/composition-api';
import useCollection from '@/composables/use-collection';
import api from '@/api';
import useProjectsStore from '@/stores/projects';
import ModalDetail from '@/views/private/components/modal-detail';
import ModalBrowse from '@/views/private/components/modal-browse';
export default defineComponent({
components: { ModalDetail, ModalBrowse },
props: {
value: {
type: [Number, Object],
default: null,
},
template: {
type: String,
default: null,
},
selectMode: {
type: String as PropType<'auto' | 'dropdown' | 'modal'>,
default: 'auto',
},
},
setup(props, { emit }) {
const projectsStore = useProjectsStore();
const { usesMenu, menuActive } = useMenu();
const { info: collectionInfo } = useCollection(ref('directus_users'));
const { selection, stageSelection, selectModalActive } = useSelection();
const { displayTemplate, onPreviewClick, requiredFields } = usePreview();
const { totalCount, loading: usersLoading, fetchUsers, users } = useUsers();
const {
setCurrent,
currentUser,
loading: loadingCurrent,
currentPrimaryKey,
} = useCurrent();
const { edits, stageEdits } = useEdits();
const editModalActive = ref(false);
return {
collectionInfo,
currentUser,
displayTemplate,
users,
usersLoading,
loadingCurrent,
menuActive,
onPreviewClick,
selection,
selectModalActive,
setCurrent,
totalCount,
stageSelection,
useMenu,
currentPrimaryKey,
edits,
stageEdits,
editModalActive,
};
function useCurrent() {
const currentUser = ref<Record<string, any>>(null);
const loading = ref(false);
const error = ref(null);
watch(
() => props.value,
(newValue) => {
// When the newly configured value is a primitive, assume it's the primary key
// of the item and fetch it from the API to render the preview
if (
newValue !== null &&
newValue !== currentUser.value?.id &&
typeof newValue === 'number'
) {
fetchCurrent();
}
// If the value isn't a primary key, the current value will be set by the editing
// handlers in useEdit()
if (newValue === null) {
currentUser.value = null;
}
}
);
const currentPrimaryKey = computed<string | number>(() => {
if (!currentUser.value) return '+';
if (!props.value) return '+';
if (typeof props.value === 'number' || typeof props.value === 'string') {
return props.value;
}
if (typeof props.value === 'object' && props.value.hasOwnProperty('id')) {
return props.value.id;
}
return '+';
});
return { setCurrent, currentUser, loading, currentPrimaryKey };
function setCurrent(item: Record<string, any>) {
currentUser.value = item;
emit('input', item.id);
}
async function fetchCurrent() {
const { currentProjectKey } = projectsStore.state;
loading.value = true;
const fields = requiredFields;
if (fields.includes('id') === false) {
fields.push('id');
}
try {
const response = await api.get(`/${currentProjectKey}/users/${props.value}`, {
params: {
fields: fields,
},
});
currentUser.value = response.data.data;
} catch (err) {
error.value = err;
} finally {
loading.value = false;
}
}
}
function useUsers() {
const totalCount = ref<number>(null);
const users = ref<Record<string, any>[]>(null);
const loading = ref(false);
const error = ref(null);
fetchTotalCount();
users.value = null;
return { totalCount, fetchUsers, users, loading };
async function fetchUsers() {
if (users.value !== null) return;
const { currentProjectKey } = projectsStore.state;
loading.value = true;
const fields = requiredFields;
if (fields.includes('id') === false) {
fields.push('id');
}
try {
const response = await api.get(`/${currentProjectKey}/users`, {
params: {
fields: fields,
limit: -1,
},
});
users.value = response.data.data;
} catch (err) {
error.value = err;
} finally {
loading.value = false;
}
}
async function fetchTotalCount() {
const { currentProjectKey } = projectsStore.state;
const response = await api.get(`/${currentProjectKey}/users`, {
params: {
limit: 0,
meta: 'total_count',
},
});
totalCount.value = response.data.meta.total_count;
}
}
function useMenu() {
const menuActive = ref(false);
const usesMenu = computed(() => {
if (props.selectMode === 'modal') return false;
if (props.selectMode === 'dropdown') return true;
// auto
if (totalCount.value && totalCount.value > 100) return false;
return true;
});
return { menuActive, usesMenu };
}
function usePreview() {
const displayTemplate = '{{ first_name }} {{ last_name }}';
const requiredFields = ['first_name', 'last_name'];
return { onPreviewClick, displayTemplate, requiredFields };
function onPreviewClick() {
if (usesMenu.value === true) {
const newActive = !menuActive.value;
menuActive.value = newActive;
if (newActive === true) fetchUsers();
} else {
selectModalActive.value = true;
}
}
}
function useSelection() {
const selectModalActive = ref(false);
const selection = computed<(number | string)[]>(() => {
if (!props.value) return [];
if (typeof props.value === 'object' && props.value.hasOwnProperty('id')) {
return [props.value.id];
}
if (typeof props.value === 'string' || typeof props.value === 'number') {
return [props.value];
}
return [];
});
return { selection, stageSelection, selectModalActive };
function stageSelection(newSelection: (number | string)[]) {
if (newSelection.length === 0) {
emit('input', null);
} else {
emit('input', newSelection[0]);
}
}
}
function useEdits() {
const edits = computed(() => {
// If the current value isn't a primitive, it means we've already staged some changes
// This ensures we continue on those changes instead of starting over
if (props.value && typeof props.value === 'object') {
return props.value;
}
return {};
});
return { edits, stageEdits };
function stageEdits(newEdits: Record<string, any>) {
// Make sure we stage the primary key if it exists. This is needed to have the API
// update the existing item instead of create a new one
if (currentPrimaryKey.value && currentPrimaryKey.value !== '+') {
emit('input', {
id: currentPrimaryKey.value,
...newEdits,
});
} else {
if (newEdits.hasOwnProperty('id') && newEdits.id === '+') {
delete newEdits.id;
}
emit('input', newEdits);
}
currentUser.value = {
...currentUser.value,
...newEdits,
};
}
}
},
});
</script>
<style lang="scss" scoped>
.many-to-one {
position: relative;
}
.v-skeleton-loader {
top: 0;
left: 0;
}
.preview {
display: block;
flex-grow: 1;
}
.expand {
transition: transform var(--fast) var(--transition);
&.active {
transform: scaleY(-1);
}
}
.edit {
margin-right: 4px;
&:hover {
--v-icon-color: var(--foreground-normal);
}
}
.add:hover {
--v-icon-color: var(--primary);
}
.deselect:hover {
--v-icon-color: var(--danger);
}
</style>

View File

@@ -111,10 +111,12 @@ export default defineComponent({
loading.value = true;
const endpoint = props.collection.startsWith('directus_')
? `/${currentProjectKey}/${props.collection.substring(9)}/${props.primaryKey}`
: `/${currentProjectKey}/items/${props.collection}/${props.primaryKey}`;
try {
const response = await api.get(
`/${currentProjectKey}/items/${props.collection}/${props.primaryKey}`
);
const response = await api.get(endpoint);
item.value = response.data.data;
} catch (err) {