mirror of
https://gitlab.com/walljm/dynamicbible.git
synced 2025-07-23 23:39:50 -04:00
drag and drop to reorder cards on the saved pages admin, some bug fixes and unit tests also
This commit is contained in:
parent
f83a4f5d48
commit
f723efbf66
@ -51,6 +51,7 @@ To get more help on the Angular CLI use `ng help` or go check out the [Angular C
|
|||||||
- Test note search
|
- Test note search
|
||||||
- remove old ionic project
|
- remove old ionic project
|
||||||
- setup CI/CD
|
- setup CI/CD
|
||||||
|
- ignore reserved search words (the ones that are too big to fit in the index)
|
||||||
|
|
||||||
## Optionally for Future
|
## Optionally for Future
|
||||||
|
|
||||||
|
@ -11,6 +11,8 @@ import { FirebaseConfig } from './constants';
|
|||||||
import { AngularFireAuthModule } from '@angular/fire/auth';
|
import { AngularFireAuthModule } from '@angular/fire/auth';
|
||||||
import { AngularFireDatabaseModule } from '@angular/fire/database';
|
import { AngularFireDatabaseModule } from '@angular/fire/database';
|
||||||
|
|
||||||
|
import { DragDropModule } from '@angular/cdk/drag-drop';
|
||||||
|
|
||||||
import { MatCheckboxModule } from '@angular/material/checkbox';
|
import { MatCheckboxModule } from '@angular/material/checkbox';
|
||||||
import { MatButtonModule } from '@angular/material/button';
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
import { MatInputModule } from '@angular/material/input';
|
import { MatInputModule } from '@angular/material/input';
|
||||||
@ -104,6 +106,7 @@ import { AddToPageModalComponent } from './components/add-to-page-modal/add-to-p
|
|||||||
AngularFireAuthModule,
|
AngularFireAuthModule,
|
||||||
AngularFireDatabaseModule,
|
AngularFireDatabaseModule,
|
||||||
|
|
||||||
|
DragDropModule,
|
||||||
MatSidenavModule,
|
MatSidenavModule,
|
||||||
MatToolbarModule,
|
MatToolbarModule,
|
||||||
MatIconModule,
|
MatIconModule,
|
||||||
|
39
app/db/src/app/common/array-operations.spec.ts
Normal file
39
app/db/src/app/common/array-operations.spec.ts
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import { moveItemUpOrDown, moveItem } from './array-operations';
|
||||||
|
import { MoveDirection } from './move-direction';
|
||||||
|
|
||||||
|
describe('Array Movement', () => {
|
||||||
|
it('Should move an item up', () => {
|
||||||
|
const a1: number[] = [1, 2, 3, 4];
|
||||||
|
const a2 = moveItemUpOrDown(a1, 2, MoveDirection.Up);
|
||||||
|
|
||||||
|
expect(a2[0]).toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should not move an item up if at start of array', () => {
|
||||||
|
const a1: number[] = [1, 2, 3, 4];
|
||||||
|
const a2 = moveItemUpOrDown(a1, 1, MoveDirection.Up);
|
||||||
|
|
||||||
|
expect(a2).toBe(a1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should move an item down', () => {
|
||||||
|
const a1: number[] = [1, 2, 3, 4];
|
||||||
|
const a2 = moveItemUpOrDown(a1, 2, MoveDirection.Down);
|
||||||
|
|
||||||
|
expect(a2[2]).toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should not move an item down if at the end of a list', () => {
|
||||||
|
const a1: number[] = [1, 2, 3, 4];
|
||||||
|
const a2 = moveItemUpOrDown(a1, 4, MoveDirection.Down);
|
||||||
|
|
||||||
|
expect(a2).toBe(a1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should move an item from one index to another', () => {
|
||||||
|
const a1: number[] = [1, 2, 3, 4];
|
||||||
|
const a2 = moveItem(a1, 1, 3);
|
||||||
|
|
||||||
|
expect(a2[3]).toBe(2);
|
||||||
|
});
|
||||||
|
});
|
34
app/db/src/app/common/array-operations.ts
Normal file
34
app/db/src/app/common/array-operations.ts
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
import { MoveDirection } from './move-direction';
|
||||||
|
import { moveItemInArray } from '@angular/cdk/drag-drop';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Moves an item up (1 index towards 0) or down (1 index away from 0) immutably, returning a new array as the value.
|
||||||
|
* @param items Array in which to move the item.
|
||||||
|
* @param item Item to move up or down
|
||||||
|
* @param direction MoveDirection to go.
|
||||||
|
*/
|
||||||
|
export function moveItemUpOrDown<T = any>(items: readonly T[], item: T, direction: MoveDirection): readonly T[] {
|
||||||
|
const idx = items.indexOf(item);
|
||||||
|
|
||||||
|
if (
|
||||||
|
(idx === 0 && direction === MoveDirection.Up) || // can't go up if you're at the top
|
||||||
|
(idx === items.length - 1 && direction === MoveDirection.Down) // can't go down if you're at the bottom
|
||||||
|
) {
|
||||||
|
// you can't go up or down.
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
|
||||||
|
return moveItem(items, idx, direction === MoveDirection.Up ? idx - 1 : idx + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Moves an item one index in an array to another immutably, returning a new array as the value.
|
||||||
|
* @param items Array in which to move the item.
|
||||||
|
* @param fromIndex Starting index of the item.
|
||||||
|
* @param toIndex Index to which the item should be moved.
|
||||||
|
*/
|
||||||
|
export function moveItem<T = any>(items: readonly T[], fromIndex: number, toIndex: number): readonly T[] {
|
||||||
|
const array = [...items];
|
||||||
|
moveItemInArray(array, fromIndex, toIndex); // copy the array.
|
||||||
|
return array;
|
||||||
|
}
|
@ -5,14 +5,18 @@
|
|||||||
<span *ngIf="savedPage">Page: {{ savedPage.title }}</span>
|
<span *ngIf="savedPage">Page: {{ savedPage.title }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-content" *ngIf="savedPage">
|
<div class="card-content" *ngIf="savedPage">
|
||||||
<mat-nav-list>
|
<div
|
||||||
<mat-list-item [disableRipple]="true" *ngFor="let q of savedPage.queries">
|
class="card-list"
|
||||||
|
cdkDropList
|
||||||
|
(cdkDropListDropped)="moveSavedPageCard($event)"
|
||||||
|
>
|
||||||
|
<div class="card-item" cdkDrag *ngFor="let q of savedPage.queries">
|
||||||
<span matLine>{{ format(q) }}</span>
|
<span matLine>{{ format(q) }}</span>
|
||||||
<button mat-icon-button (click)="onRemoveCard(q)">
|
<button mat-icon-button (click)="onRemoveCard(q)">
|
||||||
<mat-icon>delete</mat-icon>
|
<mat-icon>delete</mat-icon>
|
||||||
</button>
|
</button>
|
||||||
</mat-list-item>
|
</div>
|
||||||
</mat-nav-list>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-actions">
|
<div class="card-actions">
|
||||||
<span class="card-actions-left"> </span>
|
<span class="card-actions-left"> </span>
|
||||||
|
@ -13,3 +13,50 @@
|
|||||||
font-size: var(--card-font-size);
|
font-size: var(--card-font-size);
|
||||||
padding: 0.5rem;
|
padding: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.card-list {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
min-height: 1rem;
|
||||||
|
display: block;
|
||||||
|
background: white;
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-item {
|
||||||
|
font-family: var(--card-font-family);
|
||||||
|
font-size: var(--card-font-size);
|
||||||
|
border-bottom: 1px solid whitesmoke;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
box-sizing: border-box;
|
||||||
|
cursor: move;
|
||||||
|
background: white;
|
||||||
|
padding-bottom: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-item:last-child {
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-list.cdk-drop-list-dragging .card-item:not(.cdk-drag-placeholder) {
|
||||||
|
transition: transform 250ms cubic-bezier(0, 0, 0.2, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cdk-drag-preview {
|
||||||
|
box-sizing: border-box;
|
||||||
|
border-radius: 4px;
|
||||||
|
box-shadow: 0 5px 5px -3px rgba(0, 0, 0, 0.2),
|
||||||
|
0 8px 10px 1px rgba(0, 0, 0, 0.14), 0 3px 14px 2px rgba(0, 0, 0, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cdk-drag-placeholder {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cdk-drag-animating {
|
||||||
|
transition: transform 250ms cubic-bezier(0, 0, 0.2, 1);
|
||||||
|
}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { Component, ElementRef, ChangeDetectionStrategy, Input, OnInit } from '@angular/core';
|
import { Component, Input, OnInit } from '@angular/core';
|
||||||
|
import { CdkDragDrop } from '@angular/cdk/drag-drop';
|
||||||
import { MatDialog } from '@angular/material/dialog';
|
import { MatDialog } from '@angular/material/dialog';
|
||||||
import { AppService } from '../../services/app.service';
|
import { AppService } from '../../services/app.service';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
@ -79,7 +80,12 @@ export class SavedPageCardComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
moveSavedPageCard(event: CdkDragDrop<string[]>) {
|
||||||
|
this.appService.moveSavedPageCard(this.savedPage, event.previousIndex, event.currentIndex);
|
||||||
|
}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
console.log(this.savedPage);
|
// console.log(this.savedPage);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -41,6 +41,15 @@ export class AppActionFactory {
|
|||||||
} as AppAction;
|
} as AppAction;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static newMoveSavedPageCard(savedPage: SavedPage, fromIndex: number, toIndex: number): AppAction {
|
||||||
|
return {
|
||||||
|
type: 'MOVE_SAVED_PAGE_CARD',
|
||||||
|
savedPage,
|
||||||
|
fromIndex,
|
||||||
|
toIndex,
|
||||||
|
} as AppAction;
|
||||||
|
}
|
||||||
|
|
||||||
static newRemoveSavedPage(savedPage: SavedPage): AppAction {
|
static newRemoveSavedPage(savedPage: SavedPage): AppAction {
|
||||||
return {
|
return {
|
||||||
type: 'REMOVE_SAVED_PAGE',
|
type: 'REMOVE_SAVED_PAGE',
|
||||||
@ -197,6 +206,12 @@ export type AppAction =
|
|||||||
type: 'REMOVE_SAVED_PAGE';
|
type: 'REMOVE_SAVED_PAGE';
|
||||||
savedPage: SavedPage;
|
savedPage: SavedPage;
|
||||||
}
|
}
|
||||||
|
| {
|
||||||
|
type: 'MOVE_SAVED_PAGE_CARD';
|
||||||
|
fromIndex: number;
|
||||||
|
toIndex: number;
|
||||||
|
savedPage: SavedPage;
|
||||||
|
}
|
||||||
| {
|
| {
|
||||||
type: 'ADD_CARD_TO_SAVED_PAGE';
|
type: 'ADD_CARD_TO_SAVED_PAGE';
|
||||||
card: CardItem;
|
card: CardItem;
|
||||||
|
@ -242,6 +242,14 @@ describe('AppService Reducer', () => {
|
|||||||
|
|
||||||
// 'SAVE_PAGE';
|
// 'SAVE_PAGE';
|
||||||
// 'GET_SAVED_PAGE';
|
// 'GET_SAVED_PAGE';
|
||||||
|
it('MOVE_SAVED_PAGE_CARD', () => {
|
||||||
|
const page = preState.savedPages.value[1];
|
||||||
|
|
||||||
|
const action1 = AppActionFactory.newMoveSavedPageCard(page, 1, 0);
|
||||||
|
const testState = reducer(preState, action1);
|
||||||
|
expect(testState.savedPages.value[1].queries[0].qry).toBe('G1', 'Failed to move card in saved page');
|
||||||
|
});
|
||||||
|
|
||||||
// 'ADD_CARD_TO_SAVED_PAGE';
|
// 'ADD_CARD_TO_SAVED_PAGE';
|
||||||
|
|
||||||
it('ADD_CARD', () => {
|
it('ADD_CARD', () => {
|
||||||
|
@ -4,13 +4,13 @@ import { AppState, DisplaySettings, PageSettings } from '../models/app-state';
|
|||||||
import { IStorable, Storable } from '../common/storable';
|
import { IStorable, Storable } from '../common/storable';
|
||||||
import { NoteItem } from '../models/note-state';
|
import { NoteItem } from '../models/note-state';
|
||||||
|
|
||||||
import { MoveDirection } from '../common/move-direction';
|
|
||||||
import { mergeCardList } from '../common/card-operations';
|
import { mergeCardList } from '../common/card-operations';
|
||||||
|
|
||||||
import { AppAction } from './app-state-actions';
|
import { AppAction, AppActionFactory } from './app-state-actions';
|
||||||
import { initialState } from './app-state-initial-state';
|
import { initialState } from './app-state-initial-state';
|
||||||
import { SavedPage } from '../models/page-state';
|
import { SavedPage } from '../models/page-state';
|
||||||
import { CardType, CardItem } from '../models/card-state';
|
import { CardType, CardItem } from '../models/card-state';
|
||||||
|
import { moveItem, moveItemUpOrDown } from '../common/array-operations';
|
||||||
|
|
||||||
export function getNewestStorable<T>(candidate: IStorable<T>, incumbant: IStorable<T>): IStorable<T> {
|
export function getNewestStorable<T>(candidate: IStorable<T>, incumbant: IStorable<T>): IStorable<T> {
|
||||||
// if the candidate is null, then return the state.
|
// if the candidate is null, then return the state.
|
||||||
@ -105,17 +105,35 @@ export function reducer(state: AppState, action: AppAction): AppState {
|
|||||||
autocomplete: [...action.words],
|
autocomplete: [...action.words],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//#region Saved Pages
|
||||||
|
|
||||||
case 'UPDATE_SAVED_PAGES': {
|
case 'UPDATE_SAVED_PAGES': {
|
||||||
const item = getNewestStorable(action.savedPages, state.savedPages);
|
const savedPages = getNewestStorable(action.savedPages, state.savedPages);
|
||||||
|
|
||||||
|
// only true if a currentSavedPage was set, indicating that the user
|
||||||
|
// is currently looking at a saved page.
|
||||||
|
const hasCurrentSavedPage =
|
||||||
|
state.currentSavedPage !== null &&
|
||||||
|
state.currentSavedPage !== undefined &&
|
||||||
|
action.savedPages.value.some((o) => o.id === state.currentSavedPage.id);
|
||||||
|
|
||||||
|
const currentSavedPage = hasCurrentSavedPage
|
||||||
|
? action.savedPages.value.find((o) => o.id === state.currentSavedPage.id)
|
||||||
|
: null;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
|
// if the currentSavedPage was loaded, replace it with the info from the
|
||||||
|
// new savedPages array, as it might have changed.
|
||||||
|
cards: hasCurrentSavedPage ? currentSavedPage.queries : state.cards,
|
||||||
|
currentSavedPage: hasCurrentSavedPage ? currentSavedPage : state.currentSavedPage,
|
||||||
savedPagesLoaded: true,
|
savedPagesLoaded: true,
|
||||||
savedPages: item,
|
savedPages, // update the savedPages
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
case 'UPDATE_SAVED_PAGE': {
|
case 'UPDATE_SAVED_PAGE': {
|
||||||
const savedPages = new Storable<SavedPage[]>(
|
const newSavedPages = new Storable<SavedPage[]>(
|
||||||
state.savedPages.value.map((o) => {
|
state.savedPages.value.map((o) => {
|
||||||
if (o.id === action.savedPage.id) {
|
if (o.id === action.savedPage.id) {
|
||||||
return action.savedPage;
|
return action.savedPage;
|
||||||
@ -124,12 +142,8 @@ export function reducer(state: AppState, action: AppAction): AppState {
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
const item = getNewestStorable(savedPages, state.savedPages);
|
const savedPages = getNewestStorable(newSavedPages, state.savedPages);
|
||||||
return {
|
return reducer(state, AppActionFactory.newUpdateSavedPages(savedPages));
|
||||||
...state,
|
|
||||||
savedPagesLoaded: true,
|
|
||||||
savedPages: item,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
case 'REMOVE_SAVED_PAGE': {
|
case 'REMOVE_SAVED_PAGE': {
|
||||||
const savedPages = new Storable<SavedPage[]>(state.savedPages.value.filter((o) => o.id !== action.savedPage.id));
|
const savedPages = new Storable<SavedPage[]>(state.savedPages.value.filter((o) => o.id !== action.savedPage.id));
|
||||||
@ -190,6 +204,15 @@ export function reducer(state: AppState, action: AppAction): AppState {
|
|||||||
cards: [...page.queries],
|
cards: [...page.queries],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
case 'MOVE_SAVED_PAGE_CARD': {
|
||||||
|
const queries = moveItem(action.savedPage.queries, action.fromIndex, action.toIndex);
|
||||||
|
const savedPage = {
|
||||||
|
...action.savedPage,
|
||||||
|
queries, // update the queries.
|
||||||
|
};
|
||||||
|
|
||||||
|
return reducer(state, AppActionFactory.newUpdateSavedPage(savedPage));
|
||||||
|
}
|
||||||
case 'ADD_CARD_TO_SAVED_PAGE': {
|
case 'ADD_CARD_TO_SAVED_PAGE': {
|
||||||
const savedPages = new Storable([
|
const savedPages = new Storable([
|
||||||
...state.savedPages.value.map((o) => {
|
...state.savedPages.value.map((o) => {
|
||||||
@ -214,6 +237,9 @@ export function reducer(state: AppState, action: AppAction): AppState {
|
|||||||
savedPages,
|
savedPages,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//#endregion
|
||||||
|
|
||||||
case 'ADD_CARD': {
|
case 'ADD_CARD': {
|
||||||
let cards = [];
|
let cards = [];
|
||||||
|
|
||||||
@ -259,25 +285,7 @@ export function reducer(state: AppState, action: AppAction): AppState {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
case 'MOVE_CARD': {
|
case 'MOVE_CARD': {
|
||||||
let cards = [];
|
const cards = moveItemUpOrDown(state.cards, action.card, action.direction);
|
||||||
const idx = state.cards.indexOf(action.card);
|
|
||||||
|
|
||||||
if (
|
|
||||||
(idx === 0 && action.direction === MoveDirection.Up) || // can't go up if you're at the top
|
|
||||||
(idx === state.cards.length - 1 && action.direction === MoveDirection.Down) // can't go down if you're at the bottom
|
|
||||||
) {
|
|
||||||
// you can't go up.
|
|
||||||
return state;
|
|
||||||
}
|
|
||||||
|
|
||||||
const before = state.cards.slice(0, idx);
|
|
||||||
const after = state.cards.slice(idx + 1);
|
|
||||||
|
|
||||||
if (action.direction === MoveDirection.Down) {
|
|
||||||
cards = [...before, after[0], action.card, ...after.slice(1)];
|
|
||||||
} else {
|
|
||||||
cards = [...before.slice(0, before.length - 1), action.card, before[before.length - 1], ...after];
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
|
@ -30,6 +30,7 @@ import { reducer } from './app-state-reducer';
|
|||||||
import { initialState } from './app-state-initial-state';
|
import { initialState } from './app-state-initial-state';
|
||||||
import { CardItem, CardType } from '../models/card-state';
|
import { CardItem, CardType } from '../models/card-state';
|
||||||
import { SavedPage } from '../models/page-state';
|
import { SavedPage } from '../models/page-state';
|
||||||
|
import { AppActionFactory } from './app-state-actions';
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root',
|
providedIn: 'root',
|
||||||
@ -118,18 +119,15 @@ export class AppService extends createStateService(reducer, initialState) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
removeSavedPage(savedPage: SavedPage) {
|
removeSavedPage(savedPage: SavedPage) {
|
||||||
this.dispatch({
|
this.dispatch(AppActionFactory.newRemoveSavedPage(savedPage));
|
||||||
type: 'REMOVE_SAVED_PAGE',
|
}
|
||||||
savedPage,
|
|
||||||
});
|
moveSavedPageCard(savedPage: SavedPage, fromIndex: number, toIndex: number) {
|
||||||
|
this.dispatch(AppActionFactory.newMoveSavedPageCard(savedPage, fromIndex, toIndex));
|
||||||
}
|
}
|
||||||
|
|
||||||
addCardToSavedPage(pageId: string, card: CardItem) {
|
addCardToSavedPage(pageId: string, card: CardItem) {
|
||||||
this.dispatch({
|
this.dispatch(AppActionFactory.newAddCardToSavedPage(card, pageId));
|
||||||
type: 'ADD_CARD_TO_SAVED_PAGE',
|
|
||||||
card,
|
|
||||||
pageId,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//#endregion
|
//#endregion
|
||||||
|
Loading…
x
Reference in New Issue
Block a user