mirror of
https://github.com/directus/directus.git
synced 2026-04-25 03:00:53 -04:00
Update batch style to drawer
This commit is contained in:
@@ -2,10 +2,15 @@ import express from 'express';
|
||||
import asyncHandler from 'express-async-handler';
|
||||
import collectionExists from '../middleware/collection-exists';
|
||||
import { ItemsService, MetaService } from '../services';
|
||||
import { RouteNotFoundException, ForbiddenException } from '../exceptions';
|
||||
import {
|
||||
RouteNotFoundException,
|
||||
ForbiddenException,
|
||||
FailedValidationException,
|
||||
} from '../exceptions';
|
||||
import { respond } from '../middleware/respond';
|
||||
import { InvalidPayloadException } from '../exceptions';
|
||||
import { PrimaryKey } from '../types';
|
||||
import Joi from 'joi';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
@@ -92,7 +97,35 @@ router.patch(
|
||||
return next();
|
||||
}
|
||||
|
||||
const primaryKeys = await service.update(req.body);
|
||||
if (Array.isArray(req.body)) {
|
||||
const primaryKeys = await service.update(req.body);
|
||||
|
||||
try {
|
||||
const result = await service.readByKey(primaryKeys, req.sanitizedQuery);
|
||||
res.locals.payload = { data: result || null };
|
||||
} catch (error) {
|
||||
if (error instanceof ForbiddenException) {
|
||||
return next();
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
|
||||
return next();
|
||||
}
|
||||
|
||||
const updateSchema = Joi.object({
|
||||
keys: Joi.array().items(Joi.alternatives(Joi.string(), Joi.number())).required(),
|
||||
data: Joi.object().required().unknown(),
|
||||
});
|
||||
|
||||
const { error } = updateSchema.validate(req.body);
|
||||
|
||||
if (error) {
|
||||
throw new FailedValidationException(error.details[0]);
|
||||
}
|
||||
|
||||
const primaryKeys = await service.update(req.body.data, req.body.keys);
|
||||
|
||||
try {
|
||||
const result = await service.readByKey(primaryKeys, req.sanitizedQuery);
|
||||
|
||||
@@ -64,9 +64,10 @@ export default defineComponent({
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
// Note: can be null when the form is used in batch mode
|
||||
primaryKey: {
|
||||
type: [String, Number],
|
||||
required: true,
|
||||
default: null,
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
|
||||
@@ -96,7 +96,7 @@
|
||||
|
||||
<v-dialog
|
||||
v-model="confirmArchive"
|
||||
@dsc="confirmArchive = false"
|
||||
@esc="confirmArchive = false"
|
||||
v-if="selection.length > 0 && currentCollection.meta && currentCollection.meta.archive_field"
|
||||
>
|
||||
<template #activator="{ on }">
|
||||
@@ -130,10 +130,10 @@
|
||||
rounded
|
||||
icon
|
||||
class="action-batch"
|
||||
v-if="selection.length > 1"
|
||||
:to="batchLink"
|
||||
v-tooltip.bottom="batchEditAllowed ? $t('edit') : $t('not_allowed')"
|
||||
:disabled="batchEditAllowed === false"
|
||||
@click="batchEditActive = true"
|
||||
v-if="selection.length > 1"
|
||||
v-tooltip.bottom="batchEditAllowed ? $t('edit') : $t('not_allowed')"
|
||||
>
|
||||
<v-icon name="edit" outline />
|
||||
</v-button>
|
||||
@@ -203,6 +203,13 @@
|
||||
</template>
|
||||
</component>
|
||||
|
||||
<drawer-batch
|
||||
:primary-keys="selection"
|
||||
:active.sync="batchEditActive"
|
||||
:collection="collection"
|
||||
@refresh="refresh"
|
||||
/>
|
||||
|
||||
<template #sidebar>
|
||||
<sidebar-detail icon="info_outline" :title="$t('information')" close>
|
||||
<div
|
||||
@@ -255,6 +262,7 @@ import BookmarkEdit from '@/views/private/components/bookmark-edit';
|
||||
import router from '@/router';
|
||||
import marked from 'marked';
|
||||
import { usePermissionsStore, useUserStore } from '@/stores';
|
||||
import DrawerBatch from '@/views/private/components/drawer-batch';
|
||||
|
||||
type Item = {
|
||||
[field: string]: any;
|
||||
@@ -270,6 +278,7 @@ export default defineComponent({
|
||||
SearchInput,
|
||||
BookmarkAdd,
|
||||
BookmarkEdit,
|
||||
DrawerBatch,
|
||||
},
|
||||
props: {
|
||||
collection: {
|
||||
@@ -291,7 +300,7 @@ export default defineComponent({
|
||||
|
||||
const { selection } = useSelection();
|
||||
const { info: currentCollection } = useCollection(collection);
|
||||
const { addNewLink, batchLink, currentCollectionLink } = useLinks();
|
||||
const { addNewLink, currentCollectionLink } = useLinks();
|
||||
const { breadcrumb } = useBreadcrumb();
|
||||
|
||||
const {
|
||||
@@ -319,7 +328,8 @@ export default defineComponent({
|
||||
archive,
|
||||
archiving,
|
||||
error: deleteError,
|
||||
} = useBatchDelete();
|
||||
batchEditActive,
|
||||
} = useBatch();
|
||||
|
||||
const {
|
||||
bookmarkDialogActive,
|
||||
@@ -344,7 +354,7 @@ export default defineComponent({
|
||||
return {
|
||||
addNewLink,
|
||||
batchDelete,
|
||||
batchLink,
|
||||
batchEditActive,
|
||||
|
||||
confirmDelete,
|
||||
currentCollection,
|
||||
@@ -381,8 +391,13 @@ export default defineComponent({
|
||||
bookmarkIsMine,
|
||||
bookmarkSaving,
|
||||
clearLocalSave,
|
||||
refresh,
|
||||
};
|
||||
|
||||
function refresh() {
|
||||
layoutRef.value?.refresh?.();
|
||||
}
|
||||
|
||||
function useBreadcrumb() {
|
||||
const breadcrumb = computed(() => [
|
||||
{
|
||||
@@ -406,16 +421,18 @@ export default defineComponent({
|
||||
return { selection };
|
||||
}
|
||||
|
||||
function useBatchDelete() {
|
||||
function useBatch() {
|
||||
const confirmDelete = ref(false);
|
||||
const deleting = ref(false);
|
||||
|
||||
const batchEditActive = ref(false);
|
||||
|
||||
const confirmArchive = ref(false);
|
||||
const archiving = ref(false);
|
||||
|
||||
const error = ref<any>();
|
||||
|
||||
return { confirmDelete, deleting, batchDelete, confirmArchive, archiving, archive, error };
|
||||
return { batchEditActive, confirmDelete, deleting, batchDelete, confirmArchive, archiving, archive, error };
|
||||
|
||||
async function batchDelete() {
|
||||
deleting.value = true;
|
||||
@@ -464,16 +481,11 @@ export default defineComponent({
|
||||
return `/collections/${props.collection}/+`;
|
||||
});
|
||||
|
||||
const batchLink = computed<string>(() => {
|
||||
const batchPrimaryKeys = selection.value.join();
|
||||
return `/collections/${props.collection}/${batchPrimaryKeys}`;
|
||||
});
|
||||
|
||||
const currentCollectionLink = computed<string>(() => {
|
||||
return `/collections/${props.collection}`;
|
||||
});
|
||||
|
||||
return { addNewLink, batchLink, currentCollectionLink };
|
||||
return { addNewLink, currentCollectionLink };
|
||||
}
|
||||
|
||||
function useBookmarks() {
|
||||
|
||||
@@ -10,12 +10,7 @@
|
||||
</h1>
|
||||
</template>
|
||||
|
||||
<template
|
||||
#title
|
||||
v-else-if="
|
||||
isNew === false && isBatch === false && collectionInfo.meta && collectionInfo.meta.display_template
|
||||
"
|
||||
>
|
||||
<template #title v-else-if="isNew === false && collectionInfo.meta && collectionInfo.meta.display_template">
|
||||
<v-skeleton-loader class="title-loader" type="text" v-if="loading" />
|
||||
|
||||
<h1 class="type-title" v-else>
|
||||
@@ -162,7 +157,6 @@
|
||||
:loading="loading"
|
||||
:initial-values="item"
|
||||
:fields="fields"
|
||||
:batch-mode="isBatch"
|
||||
:primary-key="primaryKey || '+'"
|
||||
:validation-errors="validationErrors"
|
||||
v-model="edits"
|
||||
@@ -186,24 +180,14 @@
|
||||
<div class="page-description" v-html="marked($t('page_help_collections_item'))" />
|
||||
</sidebar-detail>
|
||||
<revisions-drawer-detail
|
||||
v-if="
|
||||
collectionInfo.meta &&
|
||||
collectionInfo.meta.singleton === false &&
|
||||
isBatch === false &&
|
||||
isNew === false
|
||||
"
|
||||
v-if="collectionInfo.meta && collectionInfo.meta.singleton === false && isNew === false"
|
||||
:collection="collection"
|
||||
:primary-key="primaryKey"
|
||||
ref="revisionsDrawerDetail"
|
||||
@revert="refresh"
|
||||
/>
|
||||
<comments-sidebar-detail
|
||||
v-if="
|
||||
collectionInfo.meta &&
|
||||
collectionInfo.meta.singleton === false &&
|
||||
isBatch === false &&
|
||||
isNew === false
|
||||
"
|
||||
v-if="collectionInfo.meta && collectionInfo.meta.singleton === false && isNew === false"
|
||||
:collection="collection"
|
||||
:primary-key="primaryKey"
|
||||
/>
|
||||
@@ -287,7 +271,6 @@ export default defineComponent({
|
||||
archiving,
|
||||
isArchived,
|
||||
saveAsCopy,
|
||||
isBatch,
|
||||
refresh,
|
||||
validationErrors,
|
||||
} = useItem(collection, primaryKey);
|
||||
@@ -326,11 +309,6 @@ export default defineComponent({
|
||||
});
|
||||
|
||||
const title = computed(() => {
|
||||
if (isBatch.value) {
|
||||
const itemCount = props.primaryKey.split(',').length;
|
||||
return i18n.t('editing_in_batch', { count: itemCount });
|
||||
}
|
||||
|
||||
return isNew.value
|
||||
? i18n.t('creating_in', { collection: collectionInfo.value?.name })
|
||||
: i18n.t('editing_in', { collection: collectionInfo.value?.name });
|
||||
@@ -380,7 +358,6 @@ export default defineComponent({
|
||||
saveAndStay,
|
||||
saveAndAddNew,
|
||||
saveAsCopyAndNavigate,
|
||||
isBatch,
|
||||
templateValues,
|
||||
archiveTooltip,
|
||||
breadcrumb,
|
||||
|
||||
153
app/src/views/private/components/drawer-batch/drawer-batch.vue
Normal file
153
app/src/views/private/components/drawer-batch/drawer-batch.vue
Normal file
@@ -0,0 +1,153 @@
|
||||
<template>
|
||||
<v-drawer
|
||||
v-model="_active"
|
||||
:title="$t('editing_in_batch', { count: primaryKeys.length })"
|
||||
persistent
|
||||
@cancel="cancel"
|
||||
>
|
||||
<template #actions>
|
||||
<v-button @click="save" icon rounded :loading="saving" v-tooltip.bottom="$t('save')">
|
||||
<v-icon name="check" />
|
||||
</v-button>
|
||||
</template>
|
||||
|
||||
<div class="drawer-batch-content">
|
||||
<v-form :collection="collection" v-model="_edits" batch-mode primary-key="+" />
|
||||
</div>
|
||||
</v-drawer>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref, computed, PropType, watch, toRefs } from '@vue/composition-api';
|
||||
import api from '@/api';
|
||||
|
||||
import useCollection from '@/composables/use-collection';
|
||||
import { useFieldsStore, useRelationsStore } from '@/stores';
|
||||
import i18n from '@/lang';
|
||||
import { Relation, Field } from '@/types';
|
||||
|
||||
export default defineComponent({
|
||||
model: {
|
||||
prop: 'edits',
|
||||
},
|
||||
props: {
|
||||
active: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
collection: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
edits: {
|
||||
type: Object as PropType<Record<string, any>>,
|
||||
default: undefined,
|
||||
},
|
||||
primaryKeys: {
|
||||
type: Array as PropType<(number | string)[]>,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
setup(props, { emit }) {
|
||||
const fieldsStore = useFieldsStore();
|
||||
const relationsStore = useRelationsStore();
|
||||
|
||||
const { _edits } = useEdits();
|
||||
const { _active } = useActiveState();
|
||||
const { save, cancel, saving } = useActions();
|
||||
|
||||
const { collection } = toRefs(props);
|
||||
|
||||
const { info: collectionInfo } = useCollection(collection);
|
||||
|
||||
return {
|
||||
_active,
|
||||
_edits,
|
||||
save,
|
||||
saving,
|
||||
cancel,
|
||||
};
|
||||
|
||||
function useEdits() {
|
||||
const localEdits = ref<Record<string, any>>({});
|
||||
|
||||
const _edits = computed<Record<string, any>>({
|
||||
get() {
|
||||
if (props.edits !== undefined) {
|
||||
return {
|
||||
...props.edits,
|
||||
...localEdits.value,
|
||||
};
|
||||
}
|
||||
|
||||
return localEdits.value;
|
||||
},
|
||||
set(newEdits) {
|
||||
localEdits.value = newEdits;
|
||||
},
|
||||
});
|
||||
|
||||
return { _edits };
|
||||
}
|
||||
|
||||
function useActiveState() {
|
||||
const localActive = ref(false);
|
||||
|
||||
const _active = computed({
|
||||
get() {
|
||||
return props.active === undefined ? localActive.value : props.active;
|
||||
},
|
||||
set(newActive: boolean) {
|
||||
localActive.value = newActive;
|
||||
emit('update:active', newActive);
|
||||
},
|
||||
});
|
||||
|
||||
return { _active };
|
||||
}
|
||||
|
||||
function useActions() {
|
||||
const saving = ref(false);
|
||||
const error = ref<any>(null);
|
||||
|
||||
return { save, cancel, saving };
|
||||
|
||||
async function save() {
|
||||
saving.value = true;
|
||||
|
||||
try {
|
||||
await api.patch(`/items/${props.collection}`, {
|
||||
keys: props.primaryKeys,
|
||||
data: _edits.value,
|
||||
});
|
||||
|
||||
emit('refresh');
|
||||
|
||||
_active.value = false;
|
||||
_edits.value = {};
|
||||
} catch (err) {
|
||||
error.value = err;
|
||||
} finally {
|
||||
saving.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function cancel() {
|
||||
_active.value = false;
|
||||
_edits.value = {};
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.v-divider {
|
||||
margin: 52px 0;
|
||||
}
|
||||
|
||||
.drawer-batch-content {
|
||||
padding: var(--content-padding);
|
||||
padding-bottom: var(--content-padding-bottom);
|
||||
}
|
||||
</style>
|
||||
4
app/src/views/private/components/drawer-batch/index.ts
Normal file
4
app/src/views/private/components/drawer-batch/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import DrawerBatch from './drawer-batch.vue';
|
||||
|
||||
export { DrawerBatch };
|
||||
export default DrawerBatch;
|
||||
Reference in New Issue
Block a user