Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bookmark improvements #12031

Merged
merged 9 commits into from Mar 28, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
@@ -0,0 +1,15 @@
import { Knex } from 'knex';

export async function up(knex: Knex): Promise<void> {
await knex.schema.alterTable('directus_presets', (table) => {
table.string('icon', 30).notNullable().defaultTo('bookmark_outline');
table.string('color').nullable();
});
}

export async function down(knex: Knex): Promise<void> {
await knex.schema.alterTable('directus_presets', (table) => {
table.dropColumn('icon');
table.dropColumn('color');
});
}
6 changes: 6 additions & 0 deletions api/src/database/system-data/fields/presets.yaml
Expand Up @@ -33,6 +33,12 @@ fields:
- field: bookmark
width: half

- field: icon
width: half

- field: color
width: half

- field: search
width: half

Expand Down
5 changes: 5 additions & 0 deletions app/src/composables/use-preset/use-preset.ts
Expand Up @@ -90,6 +90,11 @@ export function usePreset(
initLocalPreset();
});

// update current bookmark title when it is edited in navigation-bookmark
presetsStore.$subscribe(() => {
initLocalPreset();
});

const layoutOptions = computed<Record<string, any>>({
get() {
return localPreset.value.layout_options?.[layout.value] || null;
Expand Down
9 changes: 8 additions & 1 deletion app/src/lang/translations/en-US.yaml
Expand Up @@ -92,11 +92,13 @@ delete_folder: Delete Folder
prefix: Prefix
suffix: Suffix
reset_bookmark: Reset Bookmark
rename_bookmark: Rename Bookmark
update_bookmark: Update Bookmark
delete_bookmark: Delete Bookmark
delete_bookmark_copy: >-
Are you sure you want to delete the "{bookmark}" bookmark? This action cannot be undone.
delete_personal_bookmark: Delete Personal Bookmark
delete_role_bookmark: Delete Role Bookmark
delete_global_bookmark: Delete Global Bookmark
logoutReason:
SIGN_OUT: Signed out
SESSION_EXPIRED: Session expired
Expand Down Expand Up @@ -569,6 +571,11 @@ value_hashed: Value Securely Hashed
bookmark_name: Bookmark name...
create_bookmark: Create Bookmark
edit_bookmark: Edit Bookmark
edit_personal_bookmark: Edit Personal Bookmark
edit_role_bookmark: Edit Role Bookmark
edit_global_bookmark: Edit Global Bookmark
cannot_edit_role_bookmarks: Can't Edit Role Bookmarks
cannot_edit_global_bookmarks: Can't Edit Global Bookmarks
bookmarks: Bookmarks
presets: Presets
unexpected_error: Unexpected Error
Expand Down
137 changes: 102 additions & 35 deletions app/src/modules/content/components/navigation-bookmark.vue
@@ -1,46 +1,64 @@
<template>
<v-list-item
v-context-menu="'contextMenu'"
:to="`/content/${bookmark.collection}?bookmark=${bookmark.id}`"
query
class="bookmark"
clickable
@contextmenu.stop=""
>
<v-list-item-icon><v-icon name="bookmark_outline" /></v-list-item-icon>
<v-list-item-icon><v-icon :name="bookmark.icon" :color="bookmark.color" /></v-list-item-icon>
<v-list-item-content>
<v-text-overflow :text="bookmark.bookmark" />
</v-list-item-content>

<v-menu ref="contextMenu" show-arrow placement="bottom-start">
<v-menu placement="bottom-start" show-arrow>
<template #activator="{ toggle }">
<v-icon
v-tooltip.bottom="!hasPermission && t(`cannot_edit_${scope}_bookmarks`)"
:name="hasPermission ? 'more_vert' : 'lock'"
:clickable="hasPermission"
small
class="ctx-toggle"
@click.prevent="hasPermission ? toggle() : null"
/>
</template>
<v-list>
<v-list-item clickable :disabled="isMine === false" @click="renameActive = true">
<v-list-item
clickable
:to="scope !== 'personal' ? `/settings/presets/${bookmark.id}` : undefined"
@click="scope === 'personal' ? (editActive = true) : undefined"
>
<v-list-item-icon>
<v-icon name="edit" outline />
</v-list-item-icon>
<v-list-item-content>
<v-text-overflow :text="t('rename_bookmark')" />
<v-text-overflow :text="t(`edit_${scope}_bookmark`)" />
</v-list-item-content>
</v-list-item>
<v-list-item clickable class="danger" :disabled="isMine === false" @click="deleteActive = true">
<v-list-item clickable class="danger" @click="deleteActive = true">
<v-list-item-icon>
<v-icon name="delete" outline />
</v-list-item-icon>
<v-list-item-content>
<v-text-overflow :text="t('delete_bookmark')" />
<v-text-overflow :text="t(`delete_${scope}_bookmark`)" />
</v-list-item-content>
</v-list-item>
</v-list>
</v-menu>

<v-dialog v-model="renameActive" persistent @esc="renameActive = false">
<v-dialog v-model="editActive" persistent @esc="editCancel">
<v-card>
<v-card-title>{{ t('rename_bookmark') }}</v-card-title>
<v-card-title>{{ t('edit_personal_bookmark') }}</v-card-title>
<v-card-text>
<v-input v-model="renameValue" autofocus @keyup.enter="renameSave" />
<div class="fields">
<v-input v-model="editValue.name" class="full" autofocus @keyup.enter="editSave" />
<interface-select-icon width="half" :value="editValue.icon" @input="editValue.icon = $event" />
<interface-select-color width="half" :value="editValue.color" @input="editValue.color = $event" />
</div>
</v-card-text>
<v-card-actions>
<v-button secondary @click="renameActive = false">{{ t('cancel') }}</v-button>
<v-button :disabled="renameValue === null" :loading="renameSaving" @click="renameSave">
<v-button secondary @click="editCancel">{{ t('cancel') }}</v-button>
<v-button :disabled="editValue.name === null" :loading="editSaving" @click="editSave">
{{ t('save') }}
</v-button>
</v-card-actions>
Expand All @@ -63,7 +81,7 @@

<script lang="ts">
import { useI18n } from 'vue-i18n';
import { defineComponent, PropType, ref, computed } from 'vue';
import { defineComponent, PropType, ref, computed, reactive } from 'vue';
import { Preset } from '@directus/shared/types';
import { useUserStore, usePresetsStore } from '@/stores';
import { unexpectedError } from '@/utils/unexpected-error';
Expand All @@ -82,66 +100,88 @@ export default defineComponent({
const router = useRouter();
const route = useRoute();

const userStore = useUserStore();
const { currentUser, isAdmin } = useUserStore();
const presetsStore = usePresetsStore();

const isMine = computed(() => props.bookmark.user === userStore.currentUser!.id);
const isMine = computed(() => props.bookmark.user === currentUser!.id);

const { renameActive, renameValue, renameSave, renameSaving } = useRenameBookmark();
const { deleteActive, deleteValue, deleteSave, deleteSaving } = useDeleteBookmark();
const hasPermission = computed(() => isMine.value || isAdmin);

const scope = computed(() => {
if (props.bookmark.user && !props.bookmark.role) return 'personal';
if (!props.bookmark.user && props.bookmark.role) return 'role';
return 'global';
});

const { editActive, editValue, editSave, editSaving, editCancel } = useEditBookmark();
const { deleteActive, deleteSave, deleteSaving } = useDeleteBookmark();

return {
t,
isMine,
renameActive,
renameValue,
renameSave,
renameSaving,
hasPermission,
scope,
editActive,
editValue,
editSave,
editSaving,
editCancel,
deleteActive,
deleteValue,
deleteSave,
deleteSaving,
};

function useRenameBookmark() {
const renameActive = ref(false);
const renameValue = ref(props.bookmark.bookmark);
const renameSaving = ref(false);
function useEditBookmark() {
const editActive = ref(false);
const editValue = reactive({
name: props.bookmark.bookmark,
icon: props.bookmark?.icon ?? 'bookmark_outline',
color: props.bookmark?.color ?? null,
});
const editSaving = ref(false);

return { renameActive, renameValue, renameSave, renameSaving };
return { editActive, editValue, editSave, editSaving, editCancel };

async function renameSave() {
renameSaving.value = true;
async function editSave() {
editSaving.value = true;

try {
await presetsStore.savePreset({
...props.bookmark,
bookmark: renameValue.value,
bookmark: editValue.name,
icon: editValue.icon,
color: editValue.color,
});

renameActive.value = false;
editActive.value = false;
} catch (err: any) {
unexpectedError(err);
} finally {
renameSaving.value = false;
editSaving.value = false;
}
}

function editCancel() {
editActive.value = false;
editValue.name = props.bookmark.bookmark;
editValue.icon = props.bookmark?.icon ?? 'bookmark_outline';
editValue.color = props.bookmark?.color ?? null;
}
}

function useDeleteBookmark() {
const deleteActive = ref(false);
const deleteValue = ref(props.bookmark.bookmark);
const deleteSaving = ref(false);

return { deleteActive, deleteValue, deleteSave, deleteSaving };
return { deleteActive, deleteSave, deleteSaving };

async function deleteSave() {
deleteSaving.value = true;

try {
let navigateTo: string | null = null;

if (+route.query?.bookmark === props.bookmark.id) {
if (route.query?.bookmark && +route.query.bookmark === props.bookmark.id) {
navigateTo = `/content/${props.bookmark.collection}`;
}

Expand All @@ -167,4 +207,31 @@ export default defineComponent({
--v-list-item-color: var(--danger);
--v-list-item-icon-color: var(--danger);
}

.v-list-item {
.ctx-toggle {
--v-icon-color: var(--foreground-subdued);

opacity: 0;
user-select: none;
transition: opacity var(--fast) var(--transition);
}

&:hover {
.ctx-toggle {
opacity: 1;
user-select: auto;
}
}
}

.fields {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;

.full {
grid-column: 1 / span 2;
}
}
</style>
22 changes: 7 additions & 15 deletions app/src/modules/content/routes/collection.vue
Expand Up @@ -282,7 +282,6 @@ import RefreshSidebarDetail from '@/views/private/components/refresh-sidebar-det
import ExportSidebarDetail from '@/views/private/components/export-sidebar-detail.vue';
import SearchInput from '@/views/private/components/search-input';
import BookmarkAdd from '@/views/private/components/bookmark-add';
import BookmarkEdit from '@/views/private/components/bookmark-edit';
import { useRouter } from 'vue-router';
import { usePermissionsStore, useUserStore } from '@/stores';
import DrawerBatch from '@/views/private/components/drawer-batch';
Expand All @@ -303,7 +302,6 @@ export default defineComponent({
LayoutSidebarDetail,
SearchInput,
BookmarkAdd,
BookmarkEdit,
DrawerBatch,
ArchiveSidebarDetail,
RefreshSidebarDetail,
Expand Down Expand Up @@ -372,7 +370,7 @@ export default defineComponent({
batchEditActive,
} = useBatch();

const { bookmarkDialogActive, creatingBookmark, createBookmark, editingBookmark, editBookmark } = useBookmarks();
const { bookmarkDialogActive, creatingBookmark, createBookmark } = useBookmarks();

const currentLayout = computed(() => layouts.value.find((l) => l.id === layout.value));

Expand Down Expand Up @@ -447,8 +445,6 @@ export default defineComponent({
creatingBookmark,
createBookmark,
bookmarkTitle,
editingBookmark,
editBookmark,
breadcrumb,
clearFilters,
confirmArchive,
Expand Down Expand Up @@ -583,21 +579,22 @@ export default defineComponent({
function useBookmarks() {
const bookmarkDialogActive = ref(false);
const creatingBookmark = ref(false);
const editingBookmark = ref(false);

return {
bookmarkDialogActive,
creatingBookmark,
createBookmark,
editingBookmark,
editBookmark,
};

async function createBookmark(name: string) {
async function createBookmark(bookmark: any) {
creatingBookmark.value = true;

try {
const newBookmark = await saveCurrentAsBookmark({ bookmark: name });
const newBookmark = await saveCurrentAsBookmark({
bookmark: bookmark.name,
icon: bookmark.icon,
color: bookmark.color,
});
router.push(`/content/${newBookmark.collection}?bookmark=${newBookmark.id}`);

bookmarkDialogActive.value = false;
Expand All @@ -607,11 +604,6 @@ export default defineComponent({
creatingBookmark.value = false;
}
}

async function editBookmark(name: string) {
bookmarkTitle.value = name;
bookmarkDialogActive.value = false;
}
}

function clearFilters() {
Expand Down