Interface many to one (#524)

* Start on m2o

* Render preview in m2o

* Add icons

* Style inline icons

* Add editing modal

* Disable any linter

* Add add-new button

* Pass existing selection on to layout

* Update v-table to allow for keys-only-selection

* Fix batch on tabular
This commit is contained in:
Rijk van Zanten
2020-05-05 14:22:01 -04:00
committed by GitHub
parent f6d040a687
commit 70c3f05050
57 changed files with 686 additions and 116 deletions

View File

@@ -16,6 +16,7 @@ module.exports = {
'@typescript-eslint/camelcase': 0,
'@typescript-eslint/no-use-before-define': 0,
'@typescript-eslint/ban-ts-ignore': 0,
'@typescript-eslint/no-explicit-any': 0,
'comma-dangle': [
'error',
{
@@ -37,7 +38,6 @@ module.exports = {
jest: true,
},
rules: {
'@typescript-eslint/no-explicit-any': 0,
'@typescript-eslint/no-empty-function': 0,
'@typescript-eslint/no-non-null-assertion': 0,
},

View File

@@ -90,6 +90,8 @@
"
:width="field.width"
:type="field.type"
:collection="field.collection"
:field="field.field"
@input="setValue(field, $event)"
/>
</div>
@@ -112,7 +114,6 @@ import marked from 'marked';
import getDefaultInterfaceForType from '@/utils/get-default-interface-for-type';
type FieldValues = {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
[field: string]: any;
};
@@ -273,7 +274,6 @@ export default defineComponent({
return { formFields, gridClass };
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function setValue(field: Field, value: any) {
const edits = props.edits ? clone(props.edits) : {};
edits[field.field] = value;

View File

@@ -12,18 +12,20 @@
<slot name="prepend" :value="value" :disabled="disabled" />
</div>
<span v-if="prefix" class="prefix">{{ prefix }}</span>
<input
v-bind="$attrs"
v-focus="autofocus"
v-on="_listeners"
:type="type"
:min="min"
:max="max"
:step="step"
:disabled="disabled"
:value="value"
ref="input"
/>
<slot name="input">
<input
v-bind="$attrs"
v-focus="autofocus"
v-on="_listeners"
:type="type"
:min="min"
:max="max"
:step="step"
:disabled="disabled"
:value="value"
ref="input"
/>
</slot>
<span v-if="suffix" class="suffix">{{ suffix }}</span>
<span v-if="(type === 'number')">
<v-icon

View File

@@ -50,7 +50,6 @@ export function usePopper(
}
function getModifiers(callback: () => void = () => undefined) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const modifiers: Partial<Modifier<string, any>>[] = [
popperOffsets,
{

View File

@@ -43,7 +43,7 @@
</template>
<script lang="ts">
import { defineComponent, ref, PropType, computed } from '@vue/composition-api';
import { defineComponent, ref, PropType, computed, watch } from '@vue/composition-api';
import { usePopper } from './use-popper';
import { Placement } from '@popperjs/core';
@@ -118,6 +118,7 @@ export default defineComponent({
function useActiveState() {
const localIsActive = ref(false);
const isActive = computed<boolean>({
get() {
if (props.value !== undefined) {
@@ -127,17 +128,19 @@ export default defineComponent({
return localIsActive.value;
},
async set(newActive) {
if (newActive === true) {
await start();
} else {
stop();
}
localIsActive.value = newActive;
emit('input', newActive);
},
});
watch(isActive, async (newActive) => {
if (newActive !== null && newActive === true) {
await start();
} else {
stop();
}
});
return { isActive, activate, deactivate, toggle };
function activate() {

View File

@@ -70,7 +70,6 @@ export default defineComponent({
},
setup(props, { emit }) {
const sidebarActive = ref(false);
const localActive = ref(false);
const _active = computed({

View File

@@ -119,7 +119,6 @@ type Item = {
value: string;
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type ItemsRaw = (string | any)[];
type InputValue = string[] | string;

View File

@@ -13,7 +13,6 @@ export type HeaderRaw = {
export type Header = Required<HeaderRaw>;
export type Item = {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
[key: string]: any;
};

View File

@@ -136,7 +136,7 @@ export default defineComponent({
default: null,
},
selection: {
type: Array as PropType<Item[]>,
type: Array as PropType<any>,
default: () => [],
},
fixedHeader: {
@@ -159,6 +159,10 @@ export default defineComponent({
type: Number,
default: 48,
},
selectionUseKeys: {
type: Boolean,
default: false,
},
},
setup(props, { emit, listeners, slots }) {
const _headers = computed({
@@ -287,23 +291,31 @@ export default defineComponent({
function onItemSelected(event: ItemSelectEvent) {
emit('item-selected', event);
const selection: Item[] = clone(props.selection);
let selection = clone(props.selection) as any[];
if (event.value === true) {
selection.push(event.item);
if (props.selectionUseKeys) {
selection.push(event.item[props.itemKey]);
} else {
selection.push(event.item);
}
} else {
const itemIndex: number = selection.findIndex(
(item: Item) => item[props.itemKey] === event.item[props.itemKey]
);
selection = selection.filter((item) => {
if (props.selectionUseKeys) {
return item !== event.item[props.itemKey];
}
selection.splice(itemIndex, 1);
return item[props.itemKey] !== event.item[props.itemKey];
});
}
emit('select', selection);
}
function getSelectedState(item: Item) {
const selectedKeys = props.selection.map((item: Item) => item[props.itemKey]);
const selectedKeys = props.selectionUseKeys
? props.selection
: props.selection.map((item: any) => item[props.itemKey]);
return selectedKeys.includes(item[props.itemKey]);
}
@@ -319,7 +331,7 @@ export default defineComponent({
moved?: {
oldIndex: number;
newIndex: number;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
element: Record<string, any>;
};
}

View File

@@ -13,9 +13,9 @@ export type CollectionPreset = {
search_query: string | null;
title: string | null;
user: number | null;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
view_options: Record<string, any>;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
view_query: Record<string, any>;
view_type: string | null;
};

View File

@@ -35,7 +35,6 @@ export function useCollectionPreset(
watch(collection, initLocalPreset);
watch(bookmark, initLocalPreset);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const viewOptions = computed<Record<string, any>>({
get() {
if (!localPreset.value.view_type) return null;
@@ -56,7 +55,6 @@ export function useCollectionPreset(
},
});
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const viewQuery = computed<Record<string, any>>({
get() {
if (!localPreset.value.view_type) return null;

View File

@@ -18,9 +18,11 @@ export function useCollection(collection: Ref<string>) {
});
const primaryKeyField = computed(() => {
// Every collection has a primary key; rules of the land
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
return fields.value?.find(
(field) => field.collection === collection.value && field.primary_key === true
);
)!;
});
const ownerField = computed(() => {

View File

@@ -1,7 +1,6 @@
import { Ref, ref, computed, watch } from '@vue/composition-api';
import { nanoid } from 'nanoid';
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type EmitFunction = (event: string, ...args: any[]) => void;
type Items = Readonly<

View File

@@ -4,7 +4,6 @@ import { ResizeObserver as ResizeObserverPolyfill } from 'resize-observer';
declare global {
interface Window {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
ResizeObserver: any;
}
}

View File

@@ -132,7 +132,6 @@ export function useItem(collection: Ref<string>, primaryKey: Ref<string | number
async function saveAsCopy() {
saving.value = true;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const newItem: { [field: string]: any } = {
...(item.value || {}),
...edits.value,
@@ -249,7 +248,6 @@ export function useItem(collection: Ref<string>, primaryKey: Ref<string | number
} else {
const valuesThatAreEqual = { ...response.data.data[0] };
// eslint-disable-next-line @typescript-eslint/no-explicit-any
response.data.data.forEach((existingItem: any) => {
for (const [key, value] of Object.entries(existingItem)) {
if (valuesThatAreEqual[key] !== value) {

View File

@@ -31,7 +31,6 @@ export function useItems(collection: Ref<string>, query: Query) {
: `/${currentProjectKey}/items/${collection.value}`;
});
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const items = ref<any>([]);
const loading = ref(false);
const error = ref(null);
@@ -176,7 +175,6 @@ export function useItems(collection: Ref<string>, query: Query) {
* pretend there is a file m2o, so we can use the regular layout logic for files as well
*/
if (collection.value === 'directus_files') {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
fetchedItems = fetchedItems.map((file: any) => ({
...file,
$file: file,
@@ -231,7 +229,6 @@ export function useItems(collection: Ref<string>, query: Query) {
}
type ManualSortData = {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
item: Record<string, any>;
oldIndex: number;
newIndex: number;
@@ -245,7 +242,6 @@ export function useItems(collection: Ref<string>, query: Query) {
const selectionRange =
move === 'down' ? [oldIndex + 1, newIndex + 1] : [newIndex, oldIndex];
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const updates = items.value.slice(...selectionRange).map((toBeUpdatedItem: any) => {
const sortValue = getPositionForItem(toBeUpdatedItem);
@@ -271,12 +267,11 @@ export function useItems(collection: Ref<string>, query: Query) {
// Used as default value for the sort position. This is the index of the given item in the array
// of items, offset by the page count and current page
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function getPositionForItem(item: any) {
const pk = primaryKeyField.value?.field;
if (!pk) return;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const index = items.value.findIndex((existingItem: any) => existingItem[pk] === item[pk]);
return index + 1 + limit.value * (page.value - 1);

View File

@@ -4,7 +4,7 @@
function useSync<T, K extends keyof T>(
props: T,
key: K,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
emit: (event: string, ...args: any[]) => void
): Ref<Readonly<T[K]>>
```

View File

@@ -3,7 +3,7 @@ import { computed, Ref } from '@vue/composition-api';
export default function useSync<T, K extends keyof T>(
props: T,
key: K,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
emit: (event: string, ...args: any[]) => void
): Ref<Readonly<T[K]>> {
return computed<T[K]>({

View File

@@ -2,14 +2,13 @@ import VueI18n from 'vue-i18n';
import { Component } from 'vue';
import { Field } from '@/stores/fields/types';
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type DisplayHandlerFunction = (value: any, options: any) => string | null;
export type DisplayConfig = {
id: string;
icon: string;
name: string | VueI18n.TranslateResult;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
handler: DisplayHandlerFunction | Component;
options: null | Partial<Field>[] | Component;
types: string[];

View File

@@ -15,7 +15,7 @@ type GenericStore = {
id: string;
hydrate?: () => Promise<void>;
dehydrate?: () => Promise<void>;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
[key: string]: any;
};

View File

@@ -13,6 +13,7 @@ import InterfaceStatus from './status';
import InterfaceDateTime from './datetime';
import InterfaceImage from './image';
import InterfaceIcon from './icon';
import InterfaceManyToOne from './many-to-one';
export const interfaces = [
InterfaceTextInput,
@@ -30,6 +31,7 @@ export const interfaces = [
InterfaceDateTime,
InterfaceImage,
InterfaceIcon,
InterfaceManyToOne,
];
export default interfaces;

View File

@@ -0,0 +1,17 @@
import { defineInterface } from '../define';
import InterfaceManyToOne from './many-to-one.vue';
export default defineInterface(({ i18n }) => ({
id: 'many-to-one',
name: i18n.t('many-to-one'),
icon: 'arrow_right_alt',
component: InterfaceManyToOne,
options: [
{
field: 'template',
name: i18n.t('display_template'),
width: 'half',
interface: 'text-input',
},
],
}));

View File

@@ -0,0 +1,579 @@
<template>
<v-notice warning v-if="!relation">
{{ $t('relationship_not_setup') }}
</v-notice>
<v-notice warning v-else-if="!displayTemplate">
{{ $t('display_template_not_setup') }}
</v-notice>
<div class="many-to-one" v-else>
<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="currentItem">
<div class="preview">
<render-template
:collection="collection"
:item="currentItem"
:template="displayTemplate"
/>
</div>
</template>
<template #append>
<template v-if="currentItem">
<v-icon
name="open_in_new"
class="edit"
v-tooltip="$t('edit')"
@click.stop="startEditing"
/>
<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="startEditing"
/>
<v-icon class="expand" :class="{ active }" name="expand_more" />
</template>
</template>
</v-input>
</template>
<v-list dense>
<template v-if="itemsLoading">
<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 items"
:key="item[primaryKeyField.field]"
:active="value === item[primaryKeyField.field]"
@click="setCurrent(item)"
>
<v-list-item-content>
<render-template
:collection="collection"
:template="displayTemplate"
:item="item"
/>
</v-list-item-content>
</v-list-item>
</template>
</v-list>
</v-menu>
<v-modal
v-model="editModalActive"
:title="$t('editing_in', { collection: relatedCollection.name })"
persistent
>
<v-form
:loading="editLoading"
:initial-values="existingItem"
:collection="relatedCollection.collection"
v-model="edits"
/>
<template #footer>
<v-button @click="cancelEditing" secondary>{{ $t('cancel') }}</v-button>
<v-button @click="stopEditing">{{ $t('save') }}</v-button>
</template>
</v-modal>
<v-modal v-model="selectModalActive" :title="$t('select_item')" no-padding>
<layout-tabular
class="layout"
:collection="relatedCollection.collection"
:selection="selection"
@update:selection="onSelect"
select-mode
/>
<template #footer>
<v-button @click="cancelSelecting" secondary>{{ $t('cancel') }}</v-button>
<v-button @click="stopSelecting">{{ $t('save') }}</v-button>
</template>
</v-modal>
</div>
</template>
<script lang="ts">
import { defineComponent, computed, ref, toRefs, watch, PropType } from '@vue/composition-api';
import { useRelationsStore } from '@/stores/relations';
import useCollection from '@/composables/use-collection';
import getFieldsFromTemplate from '@/utils/get-fields-from-template';
import api from '@/api';
import useProjectsStore from '@/stores/projects';
import useCollectionsStore from '@/stores/collections';
/**
* @NOTE
*
* The value of a many to one can be one of three things: A primary key (number/string), a nested
* object of edits (including primary key = editing existing) or an object with new values (no
* primary key)
*/
export default defineComponent({
props: {
value: {
type: [Number, String, Object],
default: null,
},
collection: {
type: String,
required: true,
},
field: {
type: String,
required: true,
},
template: {
type: String,
default: null,
},
selectMode: {
type: String as PropType<'auto' | 'dropdown' | 'modal'>,
default: 'auto',
},
},
setup(props, { emit }) {
const { collection } = toRefs(props);
const projectsStore = useProjectsStore();
const relationsStore = useRelationsStore();
const collectionsStore = useCollectionsStore();
const { relation, relatedCollection } = useRelation();
const { usesMenu, menuActive } = useMenu();
const { info: collectionInfo, primaryKeyField } = useCollection(collection);
const { displayTemplate, onPreviewClick, requiredFields } = usePreview();
const { totalCount, loading: itemsLoading, fetchItems, items } = useItems();
const { setCurrent, currentItem, loading: loadingCurrent } = useCurrent();
const {
edits,
editModalActive,
startEditing,
stopEditing,
loading: editLoading,
error: editError,
existingItem,
cancelEditing,
} = useEdit();
const {
startSelecting,
stopSelecting,
cancelSelecting,
active: selectModalActive,
selection,
onSelect,
} = useSelectionModal();
return {
cancelEditing,
cancelSelecting,
collectionInfo,
currentItem,
displayTemplate,
editError,
editLoading,
editModalActive,
edits,
existingItem,
items,
itemsLoading,
loadingCurrent,
menuActive,
onPreviewClick,
primaryKeyField,
relatedCollection,
relation,
selection,
selectModalActive,
setCurrent,
startEditing,
startSelecting,
stopEditing,
stopSelecting,
totalCount,
onSelect,
useMenu,
};
function useCurrent() {
const currentItem = 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 &&
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
newValue !== currentItem.value?.[primaryKeyField.value!.field] &&
(typeof newValue === 'string' || 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) {
currentItem.value = null;
}
}
);
return { setCurrent, currentItem, loading };
function setCurrent(item: Record<string, any>) {
currentItem.value = item;
emit('input', item[primaryKeyField.value.field]);
}
async function fetchCurrent() {
const { currentProjectKey } = projectsStore.state;
loading.value = true;
const fields = requiredFields.value || [];
if (fields.includes(primaryKeyField.value.field) === false) {
fields.push(primaryKeyField.value.field);
}
try {
const response = await api.get(
`/${currentProjectKey}/items/${relatedCollection.value.collection}/${props.value}`,
{
params: {
fields: fields,
},
}
);
currentItem.value = response.data.data;
} catch (err) {
error.value = err;
} finally {
loading.value = false;
}
}
}
function useItems() {
const totalCount = ref<number>(null);
const items = ref<Record<string, any>[]>(null);
const loading = ref(false);
const error = ref(null);
watch(relatedCollection, () => {
fetchTotalCount();
items.value = null;
});
return { totalCount, fetchItems, items, loading };
async function fetchItems() {
if (items.value !== null) return;
const { currentProjectKey } = projectsStore.state;
loading.value = true;
const fields = requiredFields.value || [];
if (fields.includes(primaryKeyField.value.field) === false) {
fields.push(primaryKeyField.value.field);
}
try {
const response = await api.get(
`/${currentProjectKey}/items/${relatedCollection.value.collection}`,
{
params: {
fields: fields,
limit: -1,
},
}
);
items.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}/items/${relatedCollection.value.collection}`,
{
params: {
limit: 0,
meta: 'total_count',
},
}
);
totalCount.value = response.data.meta.total_count;
}
}
function useEdit() {
const loading = ref(false);
const error = ref(null);
const existingItem = ref<any>(null);
const edits = ref<any>(null);
const editModalActive = ref(false);
return {
edits,
editModalActive,
startEditing,
stopEditing,
loading,
error,
existingItem,
cancelEditing,
};
async function startEditing() {
editModalActive.value = true;
loading.value = true;
// If the current value is an object, it's the previously created changes to the item
if (props.value && typeof props.value === 'object') {
edits.value = props.value;
}
// if not, it's the primary key of the existing item (fresh load). It's important for
// us to stage the existing ID back up in the object of edits, otherwise the API will
// treat the edits as a creation of a new item instead of editing the values of an
// existing one
else if (
props.value &&
(typeof props.value === 'number' || typeof props.value === 'string')
) {
edits.value = {
[primaryKeyField.value.field]: props.value,
};
}
// If the current item has a primary key, it means that it's an existing item we're
// about to edit. In that case, we want to fetch the whole existing item, so we can
// render the full form inline
if (currentItem.value?.hasOwnProperty(primaryKeyField.value.field)) {
loading.value = true;
const { currentProjectKey } = projectsStore.state;
try {
const response = await api.get(
`/${currentProjectKey}/items/${relatedCollection.value.collection}/${
currentItem.value[primaryKeyField.value.field]
}`
);
existingItem.value = response.data.data;
} catch (err) {
error.value = err;
} finally {
loading.value = false;
}
}
// When the current item doesn't have a primary key, it means it's new. In that case
// we don't have to bother fetching anything, as all the edits are already stored
// in current item
else {
loading.value = false;
}
}
function cancelEditing() {
editModalActive.value = false;
error.value = null;
loading.value = false;
existingItem.value = null;
edits.value = null;
}
function stopEditing() {
emit('input', edits.value);
// Merging the previously fetched existing current item makes sure we don't remove
// any fields in the preview that wasn't edited, but still used in the preview
currentItem.value = {
...currentItem.value,
...edits.value,
};
cancelEditing();
}
}
function useRelation() {
const relation = computed(() => {
return relationsStore.getRelationsForField(props.collection, props.field)?.[0];
});
const relatedCollection = computed(() => {
if (!relation.value) return null;
return collectionsStore.getCollection(relation.value.collection_one);
});
return { relation, relatedCollection };
}
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 = computed(() => {
if (props.template !== null) return props.template;
return collectionInfo.value?.display_template;
});
const requiredFields = computed(() => {
if (!displayTemplate.value) return null;
return getFieldsFromTemplate(displayTemplate.value);
});
return { onPreviewClick, displayTemplate, requiredFields };
function onPreviewClick() {
if (usesMenu.value === true) {
const newActive = !menuActive.value;
menuActive.value = newActive;
if (newActive === true) fetchItems();
} else {
startSelecting();
}
}
}
function useSelectionModal() {
const active = ref(false);
const selection = ref<any[]>([]);
return { active, selection, onSelect, startSelecting, stopSelecting, cancelSelecting };
function onSelect(newSelection: any[]) {
if (newSelection.length > 0) {
selection.value = [newSelection[newSelection.length - 1]];
} else {
selection.value = [];
}
}
function startSelecting() {
active.value = true;
if (props.value) {
if (
typeof props.value === 'object' &&
props.value.hasOwnProperty(primaryKeyField.value.field)
) {
selection.value = [props.value[primaryKeyField.value.field]];
} else if (typeof props.value === 'string' || typeof props.value === 'number') {
selection.value = [props.value];
}
}
}
function stopSelecting() {
if (!selection.value[0]) {
emit('input', null);
} else {
emit('input', selection.value[0]);
}
cancelSelecting();
}
function cancelSelecting() {
active.value = false;
selection.value = [];
}
}
},
});
</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);
}
.layout {
--layout-offset-top: 0px;
}
</style>

View File

View File

@@ -81,7 +81,6 @@ export default defineComponent({
},
},
setup(props, { emit }) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const editorElement = ref<any>(null);
const _value = computed({

View File

@@ -140,6 +140,8 @@
"comments": "Comments",
"select_item": "Select Item",
"item_count": "No Items | One Item | {count} Items",
"no_items_copy": "It looks like you dont have any items in this collection. You can click the button below to add an item.",
"all_items": "All Items",
@@ -152,6 +154,9 @@
"radio_buttons": "Radio Buttons",
"checkboxes": "Checkboxes",
"relationship_not_setup": "The relationship hasn't been configured correctly.",
"display_template_not_setup": "The display template hasn't been configured correctly.",
"choose_status": "Choose Status...",
"users": "Users",
@@ -174,6 +179,9 @@
"bookmark_doesnt_exist_copy": "The bookmark you're trying to open couldn't be found.",
"bookmark_doesnt_exist_cta": "Return to collection",
"select_an_item": "Select an item...",
"edit": "Edit",
"errors": {
"3": "Only super admins have access to this",
"4": "Super Admin Token not provided",
@@ -325,6 +333,9 @@
"add_new_item": "Add New Item",
"many-to-one": "Many to One",
"display_template": "Display Template",
"n_items_selected": "No Items Selected | 1 Item Selected | {n} Items Selected",
"per_page": "Per Page",
"all_files": "All Files",
@@ -796,7 +807,6 @@
"related_entries": "Has related entries",
"relational": "Relational",
"relationship": "Relationship",
"relationship_not_setup": "The relationship hasn't been configured correctly.",
"remove": "Remove",
"remove_related": "Remove Related Item",
"report_issue": "Report Issue",

View File

@@ -150,7 +150,6 @@ import CardsHeader from './components/header.vue';
import i18n from '@/lang';
import adjustFieldsForDisplays from '@/utils/adjust-fields-for-displays';
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type Item = Record<string, any>;
type ViewOptions = {
@@ -317,7 +316,6 @@ export default defineComponent({
return { size, icon, imageSource, title, subtitle, imageFit };
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function createViewOption<T>(key: keyof ViewOptions, defaultValue: any) {
return computed<T>({
get() {
@@ -375,7 +373,6 @@ export default defineComponent({
return { sort, limit, page, fields };
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function createViewQueryOption<T>(key: keyof ViewQuery, defaultValue: any) {
return computed<T>({
get() {
@@ -392,7 +389,6 @@ export default defineComponent({
}
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function getLinkForItem(item: Record<string, any>) {
const currentProjectKey = projectsStore.state.currentProjectKey;

View File

@@ -40,7 +40,6 @@ import { defineComponent, PropType, computed } from '@vue/composition-api';
import router from '@/router';
type File = {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
[key: string]: any;
type: string;
data: {
@@ -71,12 +70,10 @@ export default defineComponent({
default: false,
},
item: {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type: Object as PropType<Record<string, any>>,
default: null,
},
value: {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type: Array as PropType<Record<string, any>[]>,
default: () => [],
},

View File

@@ -65,7 +65,6 @@ export default defineComponent({
required: true,
},
selection: {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type: Array as PropType<Record<string, any>>,
default: () => [],
},

View File

@@ -76,7 +76,8 @@
:item-key="primaryKeyField.field"
:show-manual-sort="_filters && _filters.length === 0 && sortField !== null"
:manual-sort-key="sortField && sortField.field"
@click:row="readonly ? null : onRowClick"
selection-use-keys
@click:row="onRowClick"
@update:sort="onSortChange"
@manual-sort="changeManualSort"
>
@@ -471,11 +472,13 @@ export default defineComponent({
};
function onRowClick(item: Item) {
if (props.readonly === true) return;
if (props.selectMode || _selection.value?.length > 0) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(table.value as any).onItemSelected({
item,
value: _selection.value?.includes(item) === false,
value:
_selection.value?.includes(item[primaryKeyField.value.field]) === false,
});
} else {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion

View File

@@ -32,7 +32,6 @@ import { LayoutComponent } from '@/layouts/types';
import useCollectionPreset from '@/composables/use-collection-preset';
type Item = {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
[field: string]: any;
};

View File

@@ -27,7 +27,6 @@ import useItem from '@/composables/use-item';
import SaveOptions from '@/views/private/components/save-options';
type Values = {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
[field: string]: any;
};

View File

@@ -21,7 +21,7 @@
</template>
<template #drawer>
<layout-drawer-detail v-model="viewType" />
<layout-drawer-detail @input="viewType = $event" :value="viewType || 'tabular'" />
<portal-target name="drawer" />
</template>
@@ -85,7 +85,7 @@
v-else
class="layout"
ref="layout"
:is="`layout-${viewType}`"
:is="`layout-${viewType || 'tabular'}`"
:collection="collection"
:selection.sync="selection"
:view-options.sync="viewOptions"
@@ -140,7 +140,6 @@ const redirectIfNeeded: NavigationGuard = async (to, from, next) => {
};
type Item = {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
[field: string]: any;
};
@@ -190,10 +189,6 @@ export default defineComponent({
const { addBookmarkActive, creatingBookmark, createBookmark } = useBookmarks();
if (viewType.value === null) {
viewType.value = 'tabular';
}
return {
addNewLink,
batchDelete,
@@ -267,10 +262,7 @@ export default defineComponent({
const batchLink = computed<string>(() => {
const currentProjectKey = projectsStore.state.currentProjectKey;
const batchPrimaryKeys = selection.value
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
.map((item) => item[primaryKeyField.value!.field])
.join();
const batchPrimaryKeys = selection.value.join();
return `/${currentProjectKey}/collections/${props.collection}/${batchPrimaryKeys}`;
});

View File

@@ -163,7 +163,6 @@ import useItem from '@/composables/use-item';
import SaveOptions from '@/views/private/components/save-options';
type Values = {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
[field: string]: any;
};
@@ -250,7 +249,6 @@ export default defineComponent({
}
async function saveAndStay() {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const savedItem: Record<string, any> = await save();
if (props.primaryKey === '+') {

View File

@@ -16,7 +16,7 @@ export type Folder = {
let loading: Ref<boolean> | null = null;
let folders: Ref<Folder[] | null> | null = null;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let error: Ref<any> | null = null;
export default function useFolders() {

View File

@@ -84,7 +84,6 @@ import AddFolder from '../../components/add-folder';
import SearchInput from '@/views/private/components/search-input';
type Item = {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
[field: string]: any;
};

View File

@@ -126,7 +126,6 @@ import { nanoid } from 'nanoid';
import FileLightbox from '@/views/private/components/file-lightbox';
type Values = {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
[field: string]: any;
};
@@ -220,7 +219,6 @@ export default defineComponent({
}
async function saveAndStay() {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const savedItem: Record<string, any> = await save();
if (props.primaryKey === '+') {

View File

@@ -29,7 +29,6 @@ export default defineComponent({
default: null,
},
options: {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type: Object as PropType<any>,
default: null,
},

View File

@@ -29,7 +29,6 @@ export default defineComponent({
default: null,
},
options: {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type: Object as PropType<any>,
default: null,
},

View File

@@ -120,7 +120,6 @@ export default defineComponent({
const isNew = computed(() => props.existingField === null);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const edits = ref<Partial<Field>>({});
watch(
@@ -302,7 +301,6 @@ export default defineComponent({
() => localType.value && ['relational', 'file', 'files'].includes(localType.value)
);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const relationships: any[] = [];
return { needsRelationalSetup, relationships };
@@ -325,7 +323,7 @@ export default defineComponent({
get() {
return edits.value.options || props.existingField?.options;
},
// eslint-disable-next-line @typescript-eslint/no-explicit-any
set(newOptions: { [key: string]: any } | null) {
edits.value = {
...edits.value,
@@ -354,7 +352,7 @@ export default defineComponent({
get() {
return edits.value.display_options || props.existingField?.display_options;
},
// eslint-disable-next-line @typescript-eslint/no-explicit-any
set(newOptions: { [key: string]: any } | null) {
edits.value = {
...edits.value,

View File

@@ -36,7 +36,6 @@ export default defineComponent({
const initialValues = settingsStore.formatted;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const edits = ref<{ [key: string]: any }>(null);
const noEdits = computed<boolean>(

View File

@@ -101,9 +101,9 @@ type FormattedPreset = {
collection: string;
layout: string | null;
name: string | null;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
view_query: Record<string, any> | null;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
view_options: Record<string, any> | null;
filters: readonly Filter[] | null;
};
@@ -228,7 +228,6 @@ export default defineComponent({
}
function useValues() {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const edits = ref<any>({});
const hasEdits = computed(() => Object.keys(edits.value).length > 0);
@@ -363,7 +362,6 @@ export default defineComponent({
},
});
// eslint-disable-next-line @typescript-eslint/no-explicit-any
users.value = response.data.data.map((user: any) => ({
name: user.first_name + ' ' + user.last_name,
id: user.id,

View File

@@ -71,7 +71,6 @@ import { LayoutComponent } from '@/layouts/types';
import useCollectionPreset from '@/composables/use-collection-preset';
type Item = {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
[field: string]: any;
};

View File

@@ -72,7 +72,6 @@ export default defineComponent({
const allowedStatuses = ref<string[]>([]);
const indeterminate = ref<string[]>([]);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const statusKeys = computed(() => props.statuses.map((status: any) => status.value));
const allAllowed = computed(() => {

View File

@@ -101,7 +101,6 @@ import SaveOptions from '@/views/private/components/save-options';
import PermissionsManagement from './components/permissions-management';
type Values = {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
[field: string]: any;
};

View File

@@ -71,7 +71,6 @@ import { LayoutComponent } from '@/layouts/types';
import useCollectionPreset from '@/composables/use-collection-preset';
type Item = {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
[field: string]: any;
};

View File

@@ -91,7 +91,6 @@ import useItem from '@/composables/use-item';
import SaveOptions from '@/views/private/components/save-options';
type Values = {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
[field: string]: any;
};

View File

@@ -81,7 +81,6 @@ import LayoutDrawerDetail from '@/views/private/components/layout-drawer-detail'
import SearchInput from '@/views/private/components/search-input';
type Item = {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
[field: string]: any;
};

View File

@@ -91,7 +91,6 @@ import useItem from '@/composables/use-item';
import SaveOptions from '@/views/private/components/save-options';
type Values = {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
[field: string]: any;
};
@@ -164,7 +163,6 @@ export default defineComponent({
}
async function saveAndStay() {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const savedItem: Record<string, any> = await save();
if (props.primaryKey === '+') {

View File

@@ -37,8 +37,8 @@ export type CollectionPreset = {
search_query: string | null;
filters: readonly Filter[] | null;
view_type: string | null;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
view_query: { [view_type: string]: any } | null;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
view_options: { [view_type: string]: any } | null;
};

View File

@@ -35,7 +35,7 @@ export const useSettingsStore = createStore({
async dehydrate() {
this.reset();
},
// eslint-disable-next-line @typescript-eslint/no-explicit-any
async updateSettings(updates: { [key: string]: any }) {
const projectsStore = useProjectsStore();
const currentProjectKey = projectsStore.state.currentProjectKey;
@@ -79,7 +79,6 @@ export const useSettingsStore = createStore({
this.state.settings = this.state.settings.map((setting) => {
const updated = response.data.data.find(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(update: any) => update.id === setting.id
);

View File

@@ -1,6 +1,6 @@
export type Setting = {
id: number;
key: string;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
value: any;
};

View File

@@ -1,7 +1,6 @@
import { Filter } from '@/stores/collection-presets/types';
export default function filtersToQuery(filters: readonly Filter[]) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const query: Record<string, any> = {};
filters.forEach((filter) => {

View File

@@ -1,4 +1,3 @@
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export default function moveInArray(array: readonly any[], fromIndex: number, toIndex: number) {
const item = array[fromIndex];
const length = array.length;

View File

@@ -2,7 +2,7 @@ import Vue, { Component } from 'vue';
function registerComponent(id: string, component: Component): void;
function registerComponent(id: string, component: Parameters<typeof Vue.component>[1]): void;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function registerComponent(id: string, component: any) {
Vue.component(id, component);
}

View File

@@ -162,7 +162,6 @@ export default defineComponent({
});
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
activity.value = records;
} catch (error) {
error.value = error;

View File

@@ -32,7 +32,6 @@ export default defineComponent({
required: true,
},
item: {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type: Object as PropType<Record<string, any>>,
required: true,
},