mirror of
https://github.com/directus/directus.git
synced 2026-04-25 03:00:53 -04:00
Add presets settings (#517)
* Add shared component creation modal * Add bookmark strings * Expose save-as-bookmark method * Fix typing of filter * Add save bookmark button * Add presets browse page * Add select / delete functionality * Render null value in layout as value-null * Start on presets detail view * Render presets detail view * Save view options correctly * Add readonly mode to cards layout * Add layout drawer to presets detail * Add delete on detail * Add empty state * Fix linter warnings
This commit is contained in:
@@ -165,9 +165,9 @@ body {
|
||||
align-items: center;
|
||||
|
||||
&.secondary {
|
||||
--v-button-color: var(--foreground-color);
|
||||
--v-button-color-hover: var(--foreground-color);
|
||||
--v-button-color-activated: var(--foreground-color);
|
||||
--v-button-color: var(--foreground-normal);
|
||||
--v-button-color-hover: var(--foreground-normal);
|
||||
--v-button-color-activated: var(--foreground-normal);
|
||||
--v-button-background-color: var(--background-normal-alt);
|
||||
--v-button-background-color-hover: var(--background-normal-alt);
|
||||
--v-button-background-color-activated: var(--background-normal-alt);
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
import useCollectionPresetStore from '@/stores/collection-presets';
|
||||
import { ref, Ref, computed, watch } from '@vue/composition-api';
|
||||
import { debounce } from 'lodash';
|
||||
import useUserStore from '@/stores/user';
|
||||
|
||||
import { Filter, CollectionPreset } from './types';
|
||||
import { Filter, CollectionPreset } from '@/stores/collection-presets/types';
|
||||
|
||||
export function useCollectionPreset(
|
||||
collection: Ref<string>,
|
||||
bookmark: Ref<number | null> = ref(null)
|
||||
) {
|
||||
const collectionPresetsStore = useCollectionPresetStore();
|
||||
const userStore = useUserStore();
|
||||
|
||||
const bookmarkExists = computed(() => {
|
||||
if (!bookmark.value) return false;
|
||||
@@ -17,10 +19,12 @@ export function useCollectionPreset(
|
||||
(preset) => preset.id === bookmark.value
|
||||
);
|
||||
});
|
||||
|
||||
const localPreset = ref<CollectionPreset>({});
|
||||
initLocalPreset();
|
||||
|
||||
const savePreset = async () => await collectionPresetsStore.savePreset(localPreset.value);
|
||||
const savePreset = async (preset?: Partial<CollectionPreset>) =>
|
||||
await collectionPresetsStore.savePreset(preset ? preset : localPreset.value);
|
||||
|
||||
const autoSave = debounce(async () => {
|
||||
if (!bookmark || bookmark.value === null) {
|
||||
@@ -86,11 +90,11 @@ export function useCollectionPreset(
|
||||
},
|
||||
});
|
||||
|
||||
const filters = computed<Filter[]>({
|
||||
const filters = computed({
|
||||
get() {
|
||||
return localPreset.value.filters || [];
|
||||
},
|
||||
set(val) {
|
||||
set(val: readonly Filter[]) {
|
||||
localPreset.value = {
|
||||
...localPreset.value,
|
||||
filters: val,
|
||||
@@ -114,7 +118,21 @@ export function useCollectionPreset(
|
||||
},
|
||||
});
|
||||
|
||||
return { bookmarkExists, viewType, viewOptions, viewQuery, filters, searchQuery, savePreset };
|
||||
const title = computed(() => {
|
||||
return localPreset.value?.title;
|
||||
});
|
||||
|
||||
return {
|
||||
bookmarkExists,
|
||||
viewType,
|
||||
viewOptions,
|
||||
viewQuery,
|
||||
filters,
|
||||
searchQuery,
|
||||
savePreset,
|
||||
saveCurrentAsBookmark,
|
||||
title,
|
||||
};
|
||||
|
||||
function initLocalPreset() {
|
||||
if (bookmark.value === null) {
|
||||
@@ -129,4 +147,27 @@ export function useCollectionPreset(
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves the current state of localPreset as a bookmark. The parameter allows you to override
|
||||
* any of the values of the collection preset on save.
|
||||
*
|
||||
* This will set the user of the bookmark to the current user, and is therefore only meant to be
|
||||
* used to create bookmarks for yourself.
|
||||
*
|
||||
* @param overrides Individual overrides for the collection preset
|
||||
*/
|
||||
async function saveCurrentAsBookmark(overrides: Partial<CollectionPreset>) {
|
||||
const data = {
|
||||
...localPreset.value,
|
||||
...overrides,
|
||||
};
|
||||
|
||||
if (data.id) delete data.id;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
data.user = userStore.state.currentUser!.id;
|
||||
|
||||
await savePreset(data);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ export default defineComponent({
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
setup() {
|
||||
return { formatTitle };
|
||||
},
|
||||
});
|
||||
|
||||
@@ -194,6 +194,14 @@
|
||||
"-1": "Couldn't Reach API"
|
||||
},
|
||||
|
||||
"bookmarks": {
|
||||
"name": "Bookmark name...",
|
||||
"create": "Create Bookmark",
|
||||
"personal": "Personal (Current User Only)",
|
||||
"global": "Global (Every User)",
|
||||
"role": "For role {role}"
|
||||
},
|
||||
|
||||
"unexpected_error": "An unexpected error occured",
|
||||
|
||||
"password_reset_sent": "We've sent you a secure link to reset your password",
|
||||
@@ -332,6 +340,13 @@
|
||||
"show_system_collections": "Show System Collections",
|
||||
"hide_system_collections": "Hide System Collections",
|
||||
|
||||
"role": "Role",
|
||||
"user": "User",
|
||||
|
||||
"no_presets": "No Presets",
|
||||
"no_presets_copy": "No presets or bookmarks have been saved yet.",
|
||||
"no_presets_cta": "Add Preset",
|
||||
|
||||
"always": "Always",
|
||||
"create": "Create",
|
||||
"full": "All",
|
||||
@@ -339,7 +354,7 @@
|
||||
"on_create": "On Creation",
|
||||
"on_update": "On Update",
|
||||
"read": "Read",
|
||||
"role": "Role Only",
|
||||
"role_only": "Role Only",
|
||||
"update": "Update",
|
||||
|
||||
"select_fields": "Select Fields",
|
||||
@@ -430,9 +445,24 @@
|
||||
"adding_in": "Adding New Item in {collection}",
|
||||
"editing_in": "Editing Item in {collection}",
|
||||
|
||||
"settings_data_model": "Data Model",
|
||||
"settings_collections_fields": "Collections & Fields",
|
||||
"settings_extensions": "Extensions",
|
||||
"settings_global": "Global Settings",
|
||||
"settings_permissions": "Roles & Permissions",
|
||||
"settings_project": "Project Settings",
|
||||
"settings_saved": "Settings Saved",
|
||||
"settings_webhooks": "Webhooks",
|
||||
"settings_presets": "Presets & Bookmarks",
|
||||
|
||||
"scope": "Scope",
|
||||
"layout": "Layout",
|
||||
"editing_file": "Editing File: {title}",
|
||||
"changes_are_immediate_and_permanent": "Changes are immediate and permanent",
|
||||
|
||||
"editing_preset": "Editing Preset",
|
||||
"layout_preview": "Layout Preview",
|
||||
|
||||
"about_directus": "About Directus",
|
||||
"activity_log": "Activity Log",
|
||||
"add_field_filter": "Add a field filter",
|
||||
@@ -454,10 +484,6 @@
|
||||
"batch_edit": "Batch Editing Items: {collection}",
|
||||
"batch_edit_field": "Batch Edit Field",
|
||||
"between": "Between",
|
||||
"bookmark_global": "Global: Save for all users",
|
||||
"bookmark_personal": "Personal: Save for me",
|
||||
"bookmark_role": "Role: Save for '{role}'",
|
||||
"bookmarks": "Bookmarks",
|
||||
"both": "Both",
|
||||
"cancel": "Cancel",
|
||||
"cant_disable_primary": "You can't disable primary key on an existing field. Remove this field instead.",
|
||||
@@ -467,7 +493,8 @@
|
||||
"choose_project": "Choose Project",
|
||||
"clear": "Clear",
|
||||
"click_to_toggle_all_none": "Click to toggle entire column on/off",
|
||||
"collection": "Collection | Collections",
|
||||
"collection": "Collection",
|
||||
"collections": "Collections",
|
||||
"collection_count": "No Collections | One Collection | {count} Collections",
|
||||
"collection_contains_items": "{collection} contains {count} items",
|
||||
"collection_invalid_name": "Invalid collection name",
|
||||
@@ -718,7 +745,6 @@
|
||||
"more_options": "More options",
|
||||
"my_activity": "My Activity",
|
||||
"my_profile": "My Profile: {name}",
|
||||
"name_bookmark": "What would you like to name this bookmark?",
|
||||
"navigate_changes": "Are you sure you want to leave this page? The changes you made will be lost if you navigate away from this page.",
|
||||
"new": "New",
|
||||
"new_field": "New Field",
|
||||
@@ -802,14 +828,6 @@
|
||||
"server_trouble": "Server Trouble",
|
||||
"server_trouble_copy": "Try again later or contact your system administrator help.",
|
||||
"settings": "Settings",
|
||||
"settings_data_model": "Data Model",
|
||||
"settings_collections_fields": "Collections & Fields",
|
||||
"settings_extensions": "Extensions",
|
||||
"settings_global": "Global Settings",
|
||||
"settings_permissions": "Roles & Permissions",
|
||||
"settings_project": "Project Settings",
|
||||
"settings_saved": "Settings Saved",
|
||||
"settings_webhooks": "Webhooks",
|
||||
"setup_2fa": "Setup 2FA",
|
||||
"show_directus_collections": "Show Directus System Collections",
|
||||
"sign_in": "Sign In",
|
||||
|
||||
@@ -77,8 +77,9 @@
|
||||
:icon="icon"
|
||||
:file="imageSource ? item[imageSource] : null"
|
||||
:item="item"
|
||||
:select-mode="selectMode || _selection.length > 0"
|
||||
:select-mode="selectMode || (_selection && _selection.length > 0)"
|
||||
:to="getLinkForItem(item)"
|
||||
:readonly="readonly"
|
||||
v-model="_selection"
|
||||
>
|
||||
<template #title v-if="title">
|
||||
@@ -206,6 +207,10 @@ export default defineComponent({
|
||||
type: String as PropType<string | null>,
|
||||
default: null,
|
||||
},
|
||||
readonly: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
setup(props, { emit }) {
|
||||
const mainElement = inject('main-element', ref<Element>(null));
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="card" :class="{ loading }" @click="handleClick">
|
||||
<div class="card" :class="{ loading, readonly }" @click="handleClick">
|
||||
<div class="header" :class="{ selected: value.includes(item) }">
|
||||
<div class="selection-indicator" :class="{ 'select-mode': selectMode }">
|
||||
<v-icon class="selector" :name="selectionIcon" @click.stop="toggleSelection" />
|
||||
@@ -88,6 +88,10 @@ export default defineComponent({
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
readonly: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
setup(props, { emit }) {
|
||||
const type = computed(() => {
|
||||
@@ -267,6 +271,10 @@ export default defineComponent({
|
||||
}
|
||||
}
|
||||
|
||||
.readonly {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.title,
|
||||
.subtitle {
|
||||
width: 100%;
|
||||
|
||||
@@ -64,7 +64,7 @@
|
||||
v-if="loading || itemCount > 0"
|
||||
ref="table"
|
||||
fixed-header
|
||||
:show-select="selection !== undefined"
|
||||
:show-select="readonly ? false : selection !== undefined"
|
||||
show-resize
|
||||
must-sort
|
||||
:sort="tableSort"
|
||||
@@ -74,9 +74,9 @@
|
||||
:row-height="tableRowHeight"
|
||||
:server-sort="itemCount === limit || totalPages > 1"
|
||||
:item-key="primaryKeyField.field"
|
||||
:show-manual-sort="_filters.length === 0 && sortField !== null"
|
||||
:show-manual-sort="_filters && _filters.length === 0 && sortField !== null"
|
||||
:manual-sort-key="sortField && sortField.field"
|
||||
@click:row="onRowClick"
|
||||
@click:row="readonly ? null : onRowClick"
|
||||
@update:sort="onSortChange"
|
||||
@manual-sort="changeManualSort"
|
||||
>
|
||||
@@ -203,6 +203,10 @@ export default defineComponent({
|
||||
type: String,
|
||||
default: `/{{project}}/collections/{{collection}}/{{primaryKey}}`,
|
||||
},
|
||||
readonly: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
setup(props, { emit }) {
|
||||
const { currentProjectKey } = toRefs(useProjectsStore().state);
|
||||
|
||||
@@ -7,6 +7,19 @@
|
||||
</v-button>
|
||||
</template>
|
||||
|
||||
<template #title-outer:append>
|
||||
<add-bookmark
|
||||
class="add-bookmark"
|
||||
v-model="addBookmarkActive"
|
||||
@save="createBookmark"
|
||||
:saving="creatingBookmark"
|
||||
>
|
||||
<template #activator="{ on }">
|
||||
<v-icon class="toggle" name="bookmark_outline" @click="on" />
|
||||
</template>
|
||||
</add-bookmark>
|
||||
</template>
|
||||
|
||||
<template #drawer>
|
||||
<layout-drawer-detail v-model="viewType" />
|
||||
<portal-target name="drawer" />
|
||||
@@ -97,6 +110,7 @@ import useCollection from '@/composables/use-collection';
|
||||
import useCollectionPreset from '@/composables/use-collection-preset';
|
||||
import LayoutDrawerDetail from '@/views/private/components/layout-drawer-detail';
|
||||
import SearchInput from '@/views/private/components/search-input';
|
||||
import AddBookmark from '@/views/private/components/add-bookmark';
|
||||
|
||||
const redirectIfNeeded: NavigationGuard = async (to, from, next) => {
|
||||
const collectionsStore = useCollectionsStore();
|
||||
@@ -139,6 +153,7 @@ export default defineComponent({
|
||||
CollectionsNotFound,
|
||||
LayoutDrawerDetail,
|
||||
SearchInput,
|
||||
AddBookmark,
|
||||
},
|
||||
props: {
|
||||
collection: {
|
||||
@@ -169,9 +184,12 @@ export default defineComponent({
|
||||
searchQuery,
|
||||
savePreset,
|
||||
bookmarkExists,
|
||||
saveCurrentAsBookmark,
|
||||
} = useCollectionPreset(collection, bookmarkID);
|
||||
const { confirmDelete, deleting, batchDelete } = useBatchDelete();
|
||||
|
||||
const { addBookmarkActive, creatingBookmark, createBookmark } = useBookmarks();
|
||||
|
||||
if (viewType.value === null) {
|
||||
viewType.value = 'tabular';
|
||||
}
|
||||
@@ -194,6 +212,9 @@ export default defineComponent({
|
||||
savePreset,
|
||||
bookmarkExists,
|
||||
currentCollectionLink,
|
||||
addBookmarkActive,
|
||||
creatingBookmark,
|
||||
createBookmark,
|
||||
};
|
||||
|
||||
function useSelection() {
|
||||
@@ -267,6 +288,27 @@ export default defineComponent({
|
||||
|
||||
return { addNewLink, batchLink, collectionsLink, currentCollectionLink };
|
||||
}
|
||||
|
||||
function useBookmarks() {
|
||||
const addBookmarkActive = ref(false);
|
||||
const creatingBookmark = ref(false);
|
||||
|
||||
return { addBookmarkActive, creatingBookmark, createBookmark };
|
||||
|
||||
async function createBookmark(name: string) {
|
||||
creatingBookmark.value = true;
|
||||
|
||||
try {
|
||||
await saveCurrentAsBookmark({ title: name });
|
||||
|
||||
addBookmarkActive.value = false;
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
} finally {
|
||||
creatingBookmark.value = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
@@ -299,4 +341,8 @@ export default defineComponent({
|
||||
.v-info {
|
||||
margin: 20vh 0;
|
||||
}
|
||||
|
||||
.add-bookmark .toggle {
|
||||
margin-left: 8px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -40,6 +40,11 @@ export default defineComponent({
|
||||
name: i18n.t('settings_webhooks'),
|
||||
to: `/${currentProjectKey.value}/settings/webhooks`,
|
||||
},
|
||||
{
|
||||
icon: 'bookmark',
|
||||
name: i18n.t('settings_presets'),
|
||||
to: `/${currentProjectKey.value}/settings/presets`,
|
||||
},
|
||||
];
|
||||
|
||||
return { navItems };
|
||||
|
||||
@@ -3,6 +3,7 @@ import SettingsGlobal from './routes/global';
|
||||
import { SettingsCollections, SettingsFields } from './routes/data-model/';
|
||||
import { SettingsRolesBrowse, SettingsRolesDetail } from './routes/roles';
|
||||
import { SettingsWebhooksBrowse, SettingsWebhooksDetail } from './routes/webhooks';
|
||||
import { SettingsPresetsBrowse, SettingsPresetsDetail } from './routes/presets';
|
||||
import SettingsNotFound from './routes/not-found';
|
||||
|
||||
export default defineModule(({ i18n }) => ({
|
||||
@@ -42,6 +43,17 @@ export default defineModule(({ i18n }) => ({
|
||||
component: SettingsRolesDetail,
|
||||
props: true,
|
||||
},
|
||||
{
|
||||
name: 'settings-presets-browse',
|
||||
path: '/presets',
|
||||
component: SettingsPresetsBrowse,
|
||||
},
|
||||
{
|
||||
name: 'settings-presets-detail',
|
||||
path: '/presets/:id',
|
||||
component: SettingsPresetsDetail,
|
||||
props: true,
|
||||
},
|
||||
{
|
||||
name: 'settings-webhooks-browse',
|
||||
path: '/webhooks',
|
||||
|
||||
327
src/modules/settings/routes/presets/browse/browse.vue
Normal file
327
src/modules/settings/routes/presets/browse/browse.vue
Normal file
@@ -0,0 +1,327 @@
|
||||
<template>
|
||||
<private-view :title="$t('settings_presets')">
|
||||
<template #title-outer:prepend>
|
||||
<v-button class="header-icon" rounded disabled icon secondary>
|
||||
<v-icon name="bookmark" />
|
||||
</v-button>
|
||||
</template>
|
||||
|
||||
<template #actions>
|
||||
<v-dialog v-model="confirmDelete">
|
||||
<template #activator="{ on }">
|
||||
<v-button
|
||||
rounded
|
||||
icon
|
||||
class="action-delete"
|
||||
@click="on"
|
||||
:disabled="selection.length === 0"
|
||||
>
|
||||
<v-icon name="delete" />
|
||||
</v-button>
|
||||
</template>
|
||||
|
||||
<v-card>
|
||||
<v-card-title>{{ $tc('batch_delete_confirm', selection.length) }}</v-card-title>
|
||||
|
||||
<v-card-actions>
|
||||
<v-button @click="confirmDelete = false" secondary>
|
||||
{{ $t('cancel') }}
|
||||
</v-button>
|
||||
<v-button
|
||||
@click="deleteSelection"
|
||||
class="action-delete"
|
||||
:loading="deleting"
|
||||
>
|
||||
{{ $t('delete') }}
|
||||
</v-button>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
<v-button rounded icon :to="addNewLink">
|
||||
<v-icon name="add" />
|
||||
</v-button>
|
||||
</template>
|
||||
|
||||
<template #navigation>
|
||||
<settings-navigation />
|
||||
</template>
|
||||
|
||||
<div class="presets-browse">
|
||||
<v-info
|
||||
type="warning"
|
||||
v-if="presets.length === 0"
|
||||
:title="$t('no_presets')"
|
||||
icon="bookmark"
|
||||
>
|
||||
{{ $t('no_presets_copy') }}
|
||||
|
||||
<template #append>
|
||||
<v-button :to="addNewLink">
|
||||
{{ $t('no_presets_cta') }}
|
||||
</v-button>
|
||||
</template>
|
||||
</v-info>
|
||||
<v-table
|
||||
:headers="headers"
|
||||
fixed-header
|
||||
:items="presets"
|
||||
:loading="loading"
|
||||
@click:row="onRowClick"
|
||||
v-model="selection"
|
||||
show-select
|
||||
v-else
|
||||
>
|
||||
<template #item.scope="{ item }">
|
||||
<span :class="{ all: item.scope === 'all' }">
|
||||
{{ item.scope === 'all' ? $t('all') : item.scope }}
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<template #item.layout="{ item }">
|
||||
<value-null v-if="!item.layout" />
|
||||
<span v-else>{{ item.layout }}</span>
|
||||
</template>
|
||||
|
||||
<template #item.name="{ item }">
|
||||
<span :class="{ default: item.name === null }">
|
||||
{{ item.name === null ? $t('default') : item.name }}
|
||||
</span>
|
||||
</template>
|
||||
</v-table>
|
||||
</div>
|
||||
</private-view>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, computed, ref } from '@vue/composition-api';
|
||||
import SettingsNavigation from '../../../components/navigation';
|
||||
import useProjectsStore from '@/stores/projects';
|
||||
import api from '@/api';
|
||||
import { Header } from '@/components/v-table/types';
|
||||
import i18n from '@/lang';
|
||||
import useCollectionsStore from '@/stores/collections';
|
||||
import layouts from '@/layouts';
|
||||
import { TranslateResult } from 'vue-i18n';
|
||||
import router from '@/router';
|
||||
import ValueNull from '@/views/private/components/value-null';
|
||||
|
||||
type PresetRaw = {
|
||||
id: number;
|
||||
title: null | string;
|
||||
user: null | { first_name: string; last_name: string };
|
||||
role: null | { name: string };
|
||||
collection: string;
|
||||
view_type: string;
|
||||
};
|
||||
|
||||
type Preset = {
|
||||
id: number;
|
||||
name: null | string | TranslateResult;
|
||||
scope: string;
|
||||
collection: string | TranslateResult;
|
||||
layout: string | TranslateResult;
|
||||
};
|
||||
|
||||
export default defineComponent({
|
||||
components: { SettingsNavigation, ValueNull },
|
||||
setup() {
|
||||
const projectsStore = useProjectsStore();
|
||||
const collectionsStore = useCollectionsStore();
|
||||
|
||||
const selection = ref<Preset[]>([]);
|
||||
|
||||
const { addNewLink } = useLinks();
|
||||
const { loading, presets, error, getPresets } = usePresets();
|
||||
const { headers } = useTable();
|
||||
const { confirmDelete, deleting, deleteSelection } = useDelete();
|
||||
|
||||
getPresets();
|
||||
|
||||
return {
|
||||
addNewLink,
|
||||
usePresets,
|
||||
loading,
|
||||
presets,
|
||||
error,
|
||||
getPresets,
|
||||
headers,
|
||||
selection,
|
||||
onRowClick,
|
||||
confirmDelete,
|
||||
deleting,
|
||||
deleteSelection,
|
||||
};
|
||||
|
||||
function useLinks() {
|
||||
const addNewLink = computed(() => {
|
||||
const { currentProjectKey } = projectsStore.state;
|
||||
return `/${currentProjectKey}/settings/presets/+`;
|
||||
});
|
||||
|
||||
return { addNewLink };
|
||||
}
|
||||
|
||||
function usePresets() {
|
||||
const loading = ref(false);
|
||||
const presetsRaw = ref<PresetRaw[]>(null);
|
||||
const error = ref(null);
|
||||
|
||||
const presets = computed<Preset[]>(() => {
|
||||
return (presetsRaw.value || []).map((preset) => {
|
||||
let scope = 'all';
|
||||
|
||||
if (preset.role) {
|
||||
scope = preset.role.name;
|
||||
}
|
||||
|
||||
if (preset.user) {
|
||||
scope = `${preset.user.first_name} ${preset.user.last_name}`;
|
||||
}
|
||||
|
||||
const collection = collectionsStore.getCollection(preset.collection)?.name;
|
||||
const layout = layouts.find((l) => l.id === preset.view_type)?.name;
|
||||
|
||||
return {
|
||||
id: preset.id,
|
||||
scope: scope,
|
||||
collection: collection,
|
||||
layout: layout,
|
||||
name: preset.title,
|
||||
} as Preset;
|
||||
});
|
||||
});
|
||||
|
||||
return { loading, presetsRaw, error, getPresets, presets };
|
||||
|
||||
async function getPresets() {
|
||||
const { currentProjectKey } = projectsStore.state;
|
||||
|
||||
loading.value = true;
|
||||
|
||||
try {
|
||||
const response = await api.get(`/${currentProjectKey}/collection_presets`, {
|
||||
params: {
|
||||
fields: [
|
||||
'id',
|
||||
'title',
|
||||
'user.first_name',
|
||||
'user.last_name',
|
||||
'role.name',
|
||||
'collection',
|
||||
'view_type',
|
||||
],
|
||||
},
|
||||
});
|
||||
presetsRaw.value = response.data.data;
|
||||
} catch (err) {
|
||||
error.value = err;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function useTable() {
|
||||
const headers: Header[] = [
|
||||
{
|
||||
text: i18n.t('collection'),
|
||||
value: 'collection',
|
||||
align: 'left',
|
||||
sortable: true,
|
||||
width: 200,
|
||||
},
|
||||
{
|
||||
text: i18n.t('scope'),
|
||||
value: 'scope',
|
||||
align: 'left',
|
||||
sortable: true,
|
||||
width: 200,
|
||||
},
|
||||
{
|
||||
text: i18n.t('layout'),
|
||||
value: 'layout',
|
||||
align: 'left',
|
||||
sortable: true,
|
||||
width: 200,
|
||||
},
|
||||
{
|
||||
text: i18n.t('name'),
|
||||
value: 'name',
|
||||
align: 'left',
|
||||
sortable: true,
|
||||
width: 200,
|
||||
},
|
||||
];
|
||||
|
||||
return { headers };
|
||||
}
|
||||
|
||||
function onRowClick(item: Preset) {
|
||||
const { currentProjectKey } = projectsStore.state;
|
||||
if (selection.value.length === 0) {
|
||||
router.push(`/${currentProjectKey}/settings/presets/${item.id}`);
|
||||
} else {
|
||||
if (selection.value.includes(item)) {
|
||||
selection.value = selection.value.filter((i) => i !== item);
|
||||
} else {
|
||||
selection.value = [...selection.value, item];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function useDelete() {
|
||||
const confirmDelete = ref(false);
|
||||
const deleting = ref(false);
|
||||
|
||||
return { confirmDelete, deleting, deleteSelection };
|
||||
|
||||
async function deleteSelection() {
|
||||
const { currentProjectKey } = projectsStore.state;
|
||||
deleting.value = true;
|
||||
|
||||
try {
|
||||
const IDs = selection.value.map((item) => item.id).join(',');
|
||||
await api.delete(`/${currentProjectKey}/collection_presets/${IDs}`);
|
||||
selection.value = [];
|
||||
await getPresets();
|
||||
confirmDelete.value = false;
|
||||
} finally {
|
||||
deleting.value = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.header-icon {
|
||||
--v-button-color-disabled: var(--warning);
|
||||
--v-button-background-color-disabled: var(--warning-25);
|
||||
}
|
||||
|
||||
.action-delete {
|
||||
--v-button-background-color: var(--danger-25);
|
||||
--v-button-color: var(--danger);
|
||||
--v-button-background-color-hover: var(--danger-50);
|
||||
--v-button-color-hover: var(--danger);
|
||||
}
|
||||
|
||||
.presets-browse {
|
||||
padding: var(--content-padding);
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
.all {
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.default {
|
||||
color: var(--foreground-subdued);
|
||||
}
|
||||
|
||||
.v-info {
|
||||
margin: 20vh 0;
|
||||
}
|
||||
</style>
|
||||
4
src/modules/settings/routes/presets/browse/index.ts
Normal file
4
src/modules/settings/routes/presets/browse/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import PresetsBrowse from './browse.vue';
|
||||
|
||||
export { PresetsBrowse };
|
||||
export default PresetsBrowse;
|
||||
511
src/modules/settings/routes/presets/detail/detail.vue
Normal file
511
src/modules/settings/routes/presets/detail/detail.vue
Normal file
@@ -0,0 +1,511 @@
|
||||
<template>
|
||||
<private-view :title="$t('editing_preset')">
|
||||
<template #title-outer:prepend>
|
||||
<v-button class="header-icon" rounded icon exact :to="backLink">
|
||||
<v-icon name="arrow_back" />
|
||||
</v-button>
|
||||
</template>
|
||||
|
||||
<template #navigation>
|
||||
<settings-navigation />
|
||||
</template>
|
||||
|
||||
<template #actions>
|
||||
<v-dialog v-model="confirmDelete">
|
||||
<template #activator="{ on }">
|
||||
<v-button
|
||||
rounded
|
||||
icon
|
||||
class="action-delete"
|
||||
:disabled="preset === null || id === '+'"
|
||||
@click="on"
|
||||
>
|
||||
<v-icon name="delete" />
|
||||
</v-button>
|
||||
</template>
|
||||
|
||||
<v-card>
|
||||
<v-card-title>{{ $t('delete_are_you_sure') }}</v-card-title>
|
||||
|
||||
<v-card-actions>
|
||||
<v-button @click="confirmDelete = false" secondary>
|
||||
{{ $t('cancel') }}
|
||||
</v-button>
|
||||
<v-button @click="deleteAndQuit" class="action-delete" :loading="deleting">
|
||||
{{ $t('delete') }}
|
||||
</v-button>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
<v-button icon rounded :disabled="hasEdits === false" :loading="saving" @click="save">
|
||||
<v-icon name="check" />
|
||||
</v-button>
|
||||
</template>
|
||||
|
||||
<template #drawer>
|
||||
<div class="layout-drawer">
|
||||
<portal-target name="drawer" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="preset-detail">
|
||||
<v-form
|
||||
:fields="fields"
|
||||
:loading="loading"
|
||||
:initial-values="initialValues"
|
||||
v-model="edits"
|
||||
/>
|
||||
|
||||
<div class="layout">
|
||||
<component
|
||||
v-if="values.layout && values.collection"
|
||||
:is="`layout-${values.layout}`"
|
||||
:collection="values.collection"
|
||||
:view-options.sync="viewOptions"
|
||||
:view-query.sync="viewQuery"
|
||||
:filters="values.filters || []"
|
||||
@update:filters="edits.filters = $event"
|
||||
readonly
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</private-view>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, computed, ref } from '@vue/composition-api';
|
||||
import useProjectsStore from '@/stores/projects';
|
||||
import SettingsNavigation from '../../../components/navigation';
|
||||
import { CollectionPreset, Filter } from '@/stores/collection-presets/types';
|
||||
import api from '@/api';
|
||||
import i18n from '@/lang';
|
||||
import useCollectionsStore from '@/stores/collections';
|
||||
import layouts from '@/layouts';
|
||||
import router from '@/router';
|
||||
import useCollectionPresetsStore from '@/stores/collection-presets';
|
||||
|
||||
type User = {
|
||||
id: number;
|
||||
name: string;
|
||||
};
|
||||
|
||||
type Role = {
|
||||
id: number;
|
||||
name: string;
|
||||
};
|
||||
|
||||
type FormattedPreset = {
|
||||
id: number;
|
||||
scope: string;
|
||||
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;
|
||||
};
|
||||
|
||||
export default defineComponent({
|
||||
components: { SettingsNavigation },
|
||||
props: {
|
||||
id: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
const projectsStore = useProjectsStore();
|
||||
const collectionsStore = useCollectionsStore();
|
||||
const collectionPresetsStore = useCollectionPresetsStore();
|
||||
const { backLink } = useLinks();
|
||||
|
||||
const isNew = computed(() => props.id === '+');
|
||||
|
||||
const { loading: usersLoading, users } = useUsers();
|
||||
const { loading: rolesLoading, roles } = useRoles();
|
||||
const { loading: presetLoading, error, preset } = usePreset();
|
||||
const { fields } = useForm();
|
||||
const { edits, hasEdits, initialValues, values, viewQuery, viewOptions } = useValues();
|
||||
const { save, saving } = useSave();
|
||||
const { deleting, deleteAndQuit, confirmDelete } = useDelete();
|
||||
|
||||
const loading = computed(
|
||||
() => usersLoading.value || presetLoading.value || rolesLoading.value
|
||||
);
|
||||
|
||||
return {
|
||||
backLink,
|
||||
loading,
|
||||
error,
|
||||
preset,
|
||||
edits,
|
||||
fields,
|
||||
values,
|
||||
initialValues,
|
||||
saving,
|
||||
save,
|
||||
viewQuery,
|
||||
viewOptions,
|
||||
hasEdits,
|
||||
deleting,
|
||||
deleteAndQuit,
|
||||
confirmDelete,
|
||||
};
|
||||
|
||||
function useSave() {
|
||||
const saving = ref(false);
|
||||
|
||||
return { saving, save };
|
||||
|
||||
async function save() {
|
||||
const { currentProjectKey } = projectsStore.state;
|
||||
|
||||
saving.value = true;
|
||||
|
||||
const editsParsed: Partial<CollectionPreset> = {};
|
||||
|
||||
if (edits.value.name) editsParsed.title = edits.value.name;
|
||||
if (edits.value.name?.length === 0) editsParsed.title = null;
|
||||
if (edits.value.collection) editsParsed.collection = edits.value.collection;
|
||||
if (edits.value.layout) editsParsed.view_type = edits.value.layout;
|
||||
if (edits.value.view_query) editsParsed.view_query = edits.value.view_query;
|
||||
if (edits.value.view_options) editsParsed.view_options = edits.value.view_options;
|
||||
if (edits.value.filters) editsParsed.filters = edits.value.filters;
|
||||
|
||||
if (edits.value.scope) {
|
||||
if (edits.value.scope.startsWith('role_')) {
|
||||
editsParsed.role = +edits.value.scope.substring(5);
|
||||
} else if (edits.value.scope.startsWith('user_')) {
|
||||
editsParsed.user = +edits.value.scope.substring(5);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
if (isNew.value === true) {
|
||||
await api.post(`/${currentProjectKey}/collection_presets`, editsParsed);
|
||||
} else {
|
||||
await api.patch(
|
||||
`/${currentProjectKey}/collection_presets/${props.id}`,
|
||||
editsParsed
|
||||
);
|
||||
}
|
||||
|
||||
await collectionPresetsStore.hydrate();
|
||||
|
||||
edits.value = {};
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
} finally {
|
||||
saving.value = false;
|
||||
router.push(`/${currentProjectKey}/settings/presets`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function useDelete() {
|
||||
const deleting = ref(false);
|
||||
const confirmDelete = ref(false);
|
||||
|
||||
return { deleting, confirmDelete, deleteAndQuit };
|
||||
|
||||
async function deleteAndQuit() {
|
||||
const { currentProjectKey } = projectsStore.state;
|
||||
|
||||
deleting.value = true;
|
||||
|
||||
try {
|
||||
await api.delete(`/${currentProjectKey}/collection_presets/${props.id}`);
|
||||
router.push(`/${currentProjectKey}/settings/presets`);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
deleting.value = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function useValues() {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const edits = ref<any>({});
|
||||
|
||||
const hasEdits = computed(() => Object.keys(edits.value).length > 0);
|
||||
|
||||
const initialValues = computed(() => {
|
||||
if (isNew.value === true) return {};
|
||||
if (preset.value === null) return {};
|
||||
|
||||
let scope = 'all';
|
||||
|
||||
if (preset.value.user !== null) {
|
||||
scope = `user_${preset.value.user}`;
|
||||
} else if (preset.value.role !== null) {
|
||||
scope = `role_${preset.value.role}`;
|
||||
}
|
||||
|
||||
const value: FormattedPreset = {
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
id: preset.value.id!,
|
||||
collection: preset.value.collection,
|
||||
layout: preset.value.view_type,
|
||||
name: preset.value.title,
|
||||
scope: scope,
|
||||
view_query: preset.value.view_query,
|
||||
view_options: preset.value.view_options,
|
||||
filters: preset.value.filters,
|
||||
};
|
||||
|
||||
return value;
|
||||
});
|
||||
|
||||
const values = computed(() => {
|
||||
return {
|
||||
...initialValues.value,
|
||||
...edits.value,
|
||||
};
|
||||
});
|
||||
|
||||
const viewQuery = computed({
|
||||
get() {
|
||||
if (!values.value.view_query) return null;
|
||||
if (!values.value.layout) return null;
|
||||
|
||||
return values.value.view_query[values.value.layout];
|
||||
},
|
||||
set(newQuery) {
|
||||
edits.value = {
|
||||
...edits.value,
|
||||
view_query: {
|
||||
...edits.value.view_query,
|
||||
[values.value.layout]: newQuery,
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
const viewOptions = computed({
|
||||
get() {
|
||||
if (!values.value.view_options) return null;
|
||||
if (!values.value.layout) return null;
|
||||
|
||||
return values.value.view_options[values.value.layout];
|
||||
},
|
||||
set(newOptions) {
|
||||
edits.value = {
|
||||
...edits.value,
|
||||
view_options: {
|
||||
...edits.value.view_options,
|
||||
[values.value.layout]: newOptions,
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
return { edits, initialValues, values, viewQuery, viewOptions, hasEdits };
|
||||
}
|
||||
|
||||
function usePreset() {
|
||||
const loading = ref(false);
|
||||
const error = ref(null);
|
||||
const preset = ref<CollectionPreset>(null);
|
||||
|
||||
fetchPreset();
|
||||
|
||||
return { loading, error, preset, fetchPreset };
|
||||
|
||||
async function fetchPreset() {
|
||||
const { currentProjectKey } = projectsStore.state;
|
||||
loading.value = true;
|
||||
|
||||
try {
|
||||
const response = await api.get(
|
||||
`/${currentProjectKey}/collection_presets/${props.id}`
|
||||
);
|
||||
|
||||
preset.value = response.data.data;
|
||||
} catch (err) {
|
||||
error.value = err;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function useLinks() {
|
||||
const backLink = computed(() => {
|
||||
const { currentProjectKey } = projectsStore.state;
|
||||
|
||||
return `/${currentProjectKey}/settings/presets`;
|
||||
});
|
||||
|
||||
return { backLink };
|
||||
}
|
||||
|
||||
function useUsers() {
|
||||
const loading = ref(false);
|
||||
const error = ref(null);
|
||||
const users = ref<User[]>(null);
|
||||
|
||||
fetchUsers();
|
||||
|
||||
return { loading, error, users };
|
||||
|
||||
async function fetchUsers() {
|
||||
const { currentProjectKey } = projectsStore.state;
|
||||
loading.value = true;
|
||||
|
||||
try {
|
||||
const response = await api.get(`/${currentProjectKey}/users`, {
|
||||
params: {
|
||||
fields: ['first_name', 'last_name', 'id'],
|
||||
},
|
||||
});
|
||||
|
||||
// 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,
|
||||
}));
|
||||
} catch (err) {
|
||||
error.value = err;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function useRoles() {
|
||||
const loading = ref(false);
|
||||
const error = ref(null);
|
||||
const roles = ref<Role[]>(null);
|
||||
|
||||
fetchRoles();
|
||||
|
||||
return { loading, error, roles };
|
||||
|
||||
async function fetchRoles() {
|
||||
const { currentProjectKey } = projectsStore.state;
|
||||
loading.value = true;
|
||||
|
||||
try {
|
||||
const response = await api.get(`/${currentProjectKey}/roles`, {
|
||||
params: {
|
||||
fields: ['name', 'id'],
|
||||
},
|
||||
});
|
||||
|
||||
roles.value = response.data.data;
|
||||
} catch (err) {
|
||||
error.value = err;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function useForm() {
|
||||
const scopeChoices = computed<string>(() => {
|
||||
if (usersLoading.value || rolesLoading.value) return '';
|
||||
|
||||
let options = `all :: ${i18n.t('all')}\n`;
|
||||
|
||||
roles.value?.forEach((role) => {
|
||||
options += `role_${role.id} :: ${i18n.t('role')}: ${role.name}\n`;
|
||||
});
|
||||
|
||||
users.value?.forEach((user) => {
|
||||
options += `user_${user.id} :: ${i18n.t('user')}: ${user.name}\n`;
|
||||
});
|
||||
|
||||
return options;
|
||||
});
|
||||
|
||||
const fields = computed(() => [
|
||||
{
|
||||
field: 'collection',
|
||||
name: i18n.t('collection'),
|
||||
interface: 'dropdown',
|
||||
options: {
|
||||
choices: collectionsStore.state.collections.reduce(
|
||||
(string, collection) =>
|
||||
(string += `${collection.collection} :: ${collection.name}\n`),
|
||||
''
|
||||
),
|
||||
},
|
||||
width: 'half',
|
||||
},
|
||||
{
|
||||
field: 'scope',
|
||||
name: i18n.t('scope'),
|
||||
interface: 'dropdown',
|
||||
options: {
|
||||
choices: scopeChoices.value,
|
||||
},
|
||||
width: 'half',
|
||||
},
|
||||
{
|
||||
field: 'layout',
|
||||
name: i18n.t('layout'),
|
||||
interface: 'dropdown',
|
||||
options: {
|
||||
choices: layouts.reduce(
|
||||
(string, layout) => (string += `${layout.id}::${layout.name}\n`),
|
||||
''
|
||||
),
|
||||
},
|
||||
width: 'half',
|
||||
},
|
||||
{
|
||||
field: 'name',
|
||||
name: i18n.t('name'),
|
||||
interface: 'text-input',
|
||||
width: 'half',
|
||||
},
|
||||
{
|
||||
field: 'divider',
|
||||
name: i18n.t('divider'),
|
||||
interface: 'divider',
|
||||
width: 'full',
|
||||
options: {
|
||||
title: i18n.t('layout_preview'),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
return { fields };
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.header-icon {
|
||||
--v-button-background-color: var(--warning-25);
|
||||
--v-button-color: var(--warning);
|
||||
--v-button-background-color-hover: var(--warning-50);
|
||||
--v-button-color-hover: var(--warning);
|
||||
}
|
||||
|
||||
.action-delete {
|
||||
--v-button-background-color: var(--danger-25);
|
||||
--v-button-color: var(--danger);
|
||||
--v-button-background-color-hover: var(--danger-50);
|
||||
--v-button-color-hover: var(--danger);
|
||||
}
|
||||
|
||||
.preset-detail {
|
||||
padding: var(--content-padding);
|
||||
}
|
||||
|
||||
.layout {
|
||||
--content-padding: 0;
|
||||
|
||||
margin-top: 48px;
|
||||
}
|
||||
|
||||
.layout-drawer {
|
||||
--drawer-detail-icon-color: var(--warning);
|
||||
}
|
||||
</style>
|
||||
4
src/modules/settings/routes/presets/detail/index.ts
Normal file
4
src/modules/settings/routes/presets/detail/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import PresetsDetail from './detail.vue';
|
||||
|
||||
export { PresetsDetail };
|
||||
export default PresetsDetail;
|
||||
5
src/modules/settings/routes/presets/index.ts
Normal file
5
src/modules/settings/routes/presets/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import SettingsPresetsBrowse from './browse/';
|
||||
import SettingsPresetsDetail from './detail/';
|
||||
|
||||
export { SettingsPresetsBrowse, SettingsPresetsDetail };
|
||||
export default { SettingsPresetsBrowse, SettingsPresetsDetail };
|
||||
@@ -34,8 +34,8 @@ export type CollectionPreset = {
|
||||
user: number | null;
|
||||
role: number | null;
|
||||
collection: string;
|
||||
search_query: null;
|
||||
filters: Filter[] | null;
|
||||
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;
|
||||
|
||||
59
src/views/private/components/add-bookmark/add-bookmark.vue
Normal file
59
src/views/private/components/add-bookmark/add-bookmark.vue
Normal file
@@ -0,0 +1,59 @@
|
||||
<template>
|
||||
<v-dialog :active="active" @toggle="$listeners.toggle" persistent>
|
||||
<template #activator="slotBinding">
|
||||
<slot name="activator" v-bind="slotBinding" />
|
||||
</template>
|
||||
|
||||
<v-card>
|
||||
<v-card-title>{{ $t('bookmarks.create') }}</v-card-title>
|
||||
|
||||
<v-card-text>
|
||||
<v-input v-model="bookmarkName" :placeholder="$t('bookmarks.name')" />
|
||||
</v-card-text>
|
||||
|
||||
<v-card-actions>
|
||||
<v-button @click="cancel" secondary>
|
||||
{{ $t('cancel') }}
|
||||
</v-button>
|
||||
<v-button
|
||||
:disabled="bookmarkName === null || bookmarkName.length === 0"
|
||||
@click="$emit('save', bookmarkName)"
|
||||
:loading="saving"
|
||||
>
|
||||
{{ $t('save') }}
|
||||
</v-button>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref } from '@vue/composition-api';
|
||||
|
||||
export default defineComponent({
|
||||
model: {
|
||||
prop: 'active',
|
||||
event: 'toggle',
|
||||
},
|
||||
props: {
|
||||
active: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
saving: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
setup(props, { emit }) {
|
||||
const bookmarkName = ref(null);
|
||||
|
||||
return { bookmarkName, cancel };
|
||||
|
||||
function cancel() {
|
||||
bookmarkName.value = null;
|
||||
emit('toggle', false);
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
4
src/views/private/components/add-bookmark/index.ts
Normal file
4
src/views/private/components/add-bookmark/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import AddBookmark from './add-bookmark.vue';
|
||||
|
||||
export { AddBookmark };
|
||||
export default AddBookmark;
|
||||
@@ -47,6 +47,12 @@ export default defineComponent({
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
body {
|
||||
--drawer-detail-icon-color: var(--foreground-normal);
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.drawer-detail {
|
||||
--v-badge-offset-x: 2px;
|
||||
@@ -62,17 +68,11 @@ export default defineComponent({
|
||||
height: 64px;
|
||||
color: var(--foreground-normal);
|
||||
background-color: var(--background-normal-alt);
|
||||
|
||||
&:not(.open):hover {
|
||||
// Show arrow
|
||||
}
|
||||
|
||||
&.open {
|
||||
// Invert arrow
|
||||
}
|
||||
}
|
||||
|
||||
.icon {
|
||||
--v-icon-color: var(--drawer-detail-icon-color);
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
Reference in New Issue
Block a user