Skip to content

Commit 5fd4b87

Browse files
committed
feat(pages): add page selection
1 parent 90fe85a commit 5fd4b87

File tree

10 files changed

+434
-2
lines changed

10 files changed

+434
-2
lines changed

apps/client/quasar.config.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ module.exports = configure(function (ctx) {
8484
// https://github.com/quasarframework/quasar/tree/dev/extras
8585
extras: [
8686
// 'ionicons-v4',
87-
'mdi-v5',
87+
'mdi-v7',
8888
// 'fontawesome-v6',
8989
// 'eva-icons',
9090
// 'themify',

apps/client/src/boot/ui.client.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,4 +25,6 @@ export default boot(({ store }) => {
2525
internals.localStorage.getItem('currentPathExpanded') !== 'false';
2626
uiStore(store).recentPagesExpanded =
2727
internals.localStorage.getItem('recentPagesExpanded') !== 'false';
28+
uiStore(store).selectedPagesExpanded =
29+
internals.localStorage.getItem('recentPagesExpanded') === 'true';
2830
});

apps/client/src/code/stores.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@ import { useAuthStore } from 'src/stores/auth';
44
import { usePagesStore } from 'src/stores/pages';
55
import { useUIStore } from 'src/stores/ui';
66

7-
function makeStoreFunc<T extends (...args: any[]) => any>(storeDefinition: T) {
7+
export function makeStoreFunc<T extends (...args: any[]) => any>(
8+
storeDefinition: T,
9+
) {
810
let _store: ReturnType<T>;
911

1012
return (pinia?: Pinia): ReturnType<T> => {

apps/client/src/layouts/PagesLayout/LeftSidebar/LeftSidebar.vue

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,12 @@
3131
/>
3232

3333
<RecentPages />
34+
35+
<q-separator
36+
style="background-color: rgba(255, 255, 255, 0.15) !important"
37+
/>
38+
39+
<SelectedPages />
3440
</q-drawer>
3541
</template>
3642

@@ -39,6 +45,7 @@ import { listenPointerEvents } from '@stdlib/misc';
3945
4046
import CurrentPath from './CurrentPath.vue';
4147
import RecentPages from './RecentPages.vue';
48+
import SelectedPages from './SelectedPages.vue';
4249
4350
function resizeLeftSidebar(event: PointerEvent) {
4451
listenPointerEvents(event, {
Lines changed: 303 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,303 @@
1+
<template>
2+
<q-toolbar
3+
style="
4+
padding: 0;
5+
background-color: #141414;
6+
min-height: 0;
7+
overflow: hidden;
8+
"
9+
>
10+
<DeepBtn
11+
flat
12+
style="width: 100%; height: 32px; min-height: 0; border-radius: 0"
13+
no-caps
14+
@click="negateProp(uiStore(), 'selectedPagesExpanded')"
15+
>
16+
<div style="width: 100%; height: 0; display: flex; align-items: center">
17+
<q-avatar style="margin-top: -1px; margin-left: -8px">
18+
<q-icon
19+
name="mdi-selection-multiple"
20+
size="20px"
21+
/>
22+
</q-avatar>
23+
24+
<q-toolbar-title
25+
style="
26+
margin-left: -2px;
27+
text-align: left;
28+
color: rgba(255, 255, 255, 0.85);
29+
font-size: 13.5px;
30+
"
31+
>
32+
Selected pages
33+
</q-toolbar-title>
34+
</div>
35+
</DeepBtn>
36+
37+
<q-btn
38+
icon="mdi-menu"
39+
style="
40+
position: absolute;
41+
right: 4px;
42+
width: 32px;
43+
height: 32px;
44+
min-height: 0;
45+
"
46+
>
47+
<q-menu auto-close>
48+
<q-list>
49+
<q-item
50+
clickable
51+
@click="movePages"
52+
:disable="pageSelectionStore().selectedPages.size === 0"
53+
>
54+
<q-item-section avatar>
55+
<q-icon name="mdi-file-move" />
56+
</q-item-section>
57+
58+
<q-item-section>
59+
<q-item-label>Move selection</q-item-label>
60+
</q-item-section>
61+
</q-item>
62+
63+
<q-item
64+
clickable
65+
@click="deletePages"
66+
:disable="pageSelectionStore().selectedPages.size === 0"
67+
>
68+
<q-item-section avatar>
69+
<q-icon name="mdi-trash-can" />
70+
</q-item-section>
71+
72+
<q-item-section>
73+
<q-item-label>Delete selection</q-item-label>
74+
</q-item-section>
75+
</q-item>
76+
77+
<q-item
78+
clickable
79+
@click="pageSelectionStore().selectedPages.clear()"
80+
:disable="pageSelectionStore().selectedPages.size === 0"
81+
>
82+
<q-item-section avatar>
83+
<q-icon name="mdi-selection-remove" />
84+
</q-item-section>
85+
86+
<q-item-section>
87+
<q-item-label>Clear selection</q-item-label>
88+
</q-item-section>
89+
</q-item>
90+
</q-list>
91+
</q-menu>
92+
</q-btn>
93+
</q-toolbar>
94+
95+
<q-list
96+
style="
97+
height: 0;
98+
overflow-x: hidden;
99+
overflow-y: auto;
100+
transition: all 0.2s;
101+
"
102+
:style="{ flex: uiStore().selectedPagesExpanded ? '1' : '0' }"
103+
>
104+
<q-item v-if="pageSelectionStore().selectedPages.size === 0">
105+
<q-item-section>
106+
<q-item-label
107+
style="color: rgba(255, 255, 255, 0.7); font-size: 13.5px"
108+
>
109+
No pages selected.
110+
</q-item-label>
111+
</q-item-section>
112+
</q-item>
113+
114+
<div
115+
v-for="pageId in pageSelectionStore().selectedPages"
116+
:key="pageId"
117+
class="selected-page"
118+
>
119+
<PageItem
120+
icon
121+
:page-id="pageId"
122+
:active="pageId === internals.pages.react.pageId"
123+
prefer="absolute"
124+
class="selected-pages"
125+
/>
126+
127+
<DeepBtn
128+
icon="mdi-close"
129+
size="14px"
130+
round
131+
flat
132+
class="remove-btn"
133+
title="Remove from selected pages"
134+
@click.stop="pageSelectionStore().selectedPages.delete(pageId)"
135+
/>
136+
</div>
137+
</q-list>
138+
</template>
139+
140+
<script setup lang="ts">
141+
import { negateProp } from '@stdlib/misc';
142+
import type { QNotifyUpdateOptions } from 'quasar';
143+
import { deletePage } from 'src/code/api-interface/pages/deletion/delete';
144+
import { movePage } from 'src/code/api-interface/pages/move';
145+
import { asyncDialog } from 'src/code/utils/misc';
146+
import { pageSelectionStore } from 'src/stores/page-selection';
147+
148+
import MovePageDialog from '../RightSidebar/PageProperties/MovePageDialog.vue';
149+
150+
async function movePages() {
151+
const movePageParams: Parameters<typeof movePage>[0] = await asyncDialog({
152+
component: MovePageDialog,
153+
154+
componentProps: {
155+
groupId: internals.pages.react.page.react.groupId,
156+
},
157+
});
158+
159+
const notif = $quasar().notify({
160+
group: false,
161+
timeout: 0,
162+
message: 'Moving pages...',
163+
});
164+
165+
let numSuccess = 0;
166+
let numFailed = 0;
167+
168+
for (const [index, pageId] of Array.from(
169+
pageSelectionStore().selectedPages,
170+
).entries()) {
171+
try {
172+
notif({
173+
caption: `${index + 1} of ${pageSelectionStore().selectedPages.size}`,
174+
});
175+
176+
await movePage({
177+
...movePageParams,
178+
179+
pageId,
180+
});
181+
182+
numSuccess++;
183+
} catch (error) {
184+
numFailed++;
185+
}
186+
}
187+
188+
let notifUpdateOptions: QNotifyUpdateOptions = {
189+
timeout: undefined,
190+
};
191+
192+
if (numFailed === 0) {
193+
notifUpdateOptions = {
194+
...notifUpdateOptions,
195+
message: 'Pages moved successfully.',
196+
color: 'positive',
197+
};
198+
} else {
199+
notifUpdateOptions = {
200+
...notifUpdateOptions,
201+
message: `${
202+
numSuccess > 0 ? numSuccess : 'No'
203+
} pages were moved successfully.<br/>Failed to move ${numFailed} page${
204+
numFailed === 1 ? '' : 's'
205+
}.`,
206+
caption: undefined,
207+
color: 'negative',
208+
html: true,
209+
};
210+
}
211+
212+
notif(notifUpdateOptions);
213+
}
214+
215+
async function deletePages() {
216+
await asyncDialog({
217+
title: 'Delete pages',
218+
message: 'Are you sure you want to delete these pages?',
219+
220+
focus: 'cancel',
221+
cancel: { label: 'No', flat: true, color: 'primary' },
222+
ok: { label: 'Yes', flat: true, color: 'negative' },
223+
});
224+
225+
const notif = $quasar().notify({
226+
group: false,
227+
timeout: 0,
228+
message: 'Deleting pages...',
229+
});
230+
231+
let numSuccess = 0;
232+
let numFailed = 0;
233+
234+
for (const [index, pageId] of Array.from(
235+
pageSelectionStore().selectedPages,
236+
).entries()) {
237+
try {
238+
notif({
239+
caption: `${index + 1} of ${pageSelectionStore().selectedPages.size}`,
240+
});
241+
242+
await deletePage(pageId);
243+
244+
numSuccess++;
245+
} catch (error) {
246+
numFailed++;
247+
}
248+
}
249+
250+
let notifUpdateOptions: QNotifyUpdateOptions = {
251+
timeout: undefined,
252+
};
253+
254+
if (numFailed === 0) {
255+
notifUpdateOptions = {
256+
...notifUpdateOptions,
257+
message: 'Pages deleted successfully.',
258+
color: 'positive',
259+
};
260+
} else {
261+
notifUpdateOptions = {
262+
...notifUpdateOptions,
263+
message: `${
264+
numSuccess > 0 ? numSuccess : 'No'
265+
} pages were deleted successfully.<br/>Failed to delete ${numFailed} page${
266+
numFailed === 1 ? '' : 's'
267+
}.`,
268+
caption: undefined,
269+
color: 'negative',
270+
html: true,
271+
};
272+
}
273+
274+
notif(notifUpdateOptions);
275+
}
276+
</script>
277+
278+
<style scoped lang="scss">
279+
.selected-page {
280+
position: relative;
281+
282+
> .remove-btn {
283+
position: absolute;
284+
285+
top: 50%;
286+
right: -6px;
287+
288+
transform: translate(-50%, -50%);
289+
290+
min-width: 30px;
291+
min-height: 30px;
292+
width: 30px;
293+
height: 30px;
294+
295+
opacity: 0;
296+
transition: opacity 0.2s;
297+
}
298+
}
299+
300+
.selected-page:hover > .remove-btn {
301+
opacity: 1;
302+
}
303+
</style>

0 commit comments

Comments
 (0)