mirror of
https://gitlab.com/walljm/dynamicbible.git
synced 2025-07-23 07:19: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
|
||||
- remove old ionic project
|
||||
- setup CI/CD
|
||||
- ignore reserved search words (the ones that are too big to fit in the index)
|
||||
|
||||
## Optionally for Future
|
||||
|
||||
|
@ -11,6 +11,8 @@ import { FirebaseConfig } from './constants';
|
||||
import { AngularFireAuthModule } from '@angular/fire/auth';
|
||||
import { AngularFireDatabaseModule } from '@angular/fire/database';
|
||||
|
||||
import { DragDropModule } from '@angular/cdk/drag-drop';
|
||||
|
||||
import { MatCheckboxModule } from '@angular/material/checkbox';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
@ -104,6 +106,7 @@ import { AddToPageModalComponent } from './components/add-to-page-modal/add-to-p
|
||||
AngularFireAuthModule,
|
||||
AngularFireDatabaseModule,
|
||||
|
||||
DragDropModule,
|
||||
MatSidenavModule,
|
||||
MatToolbarModule,
|
||||
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>
|
||||
</div>
|
||||
<div class="card-content" *ngIf="savedPage">
|
||||
<mat-nav-list>
|
||||
<mat-list-item [disableRipple]="true" *ngFor="let q of savedPage.queries">
|
||||
<div
|
||||
class="card-list"
|
||||
cdkDropList
|
||||
(cdkDropListDropped)="moveSavedPageCard($event)"
|
||||
>
|
||||
<div class="card-item" cdkDrag *ngFor="let q of savedPage.queries">
|
||||
<span matLine>{{ format(q) }}</span>
|
||||
<button mat-icon-button (click)="onRemoveCard(q)">
|
||||
<mat-icon>delete</mat-icon>
|
||||
</button>
|
||||
</mat-list-item>
|
||||
</mat-nav-list>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<span class="card-actions-left"> </span>
|
||||
|
@ -13,3 +13,50 @@
|
||||
font-size: var(--card-font-size);
|
||||
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 { AppService } from '../../services/app.service';
|
||||
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 {
|
||||
console.log(this.savedPage);
|
||||
// console.log(this.savedPage);
|
||||
}
|
||||
}
|
||||
|
@ -41,6 +41,15 @@ export class AppActionFactory {
|
||||
} 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 {
|
||||
return {
|
||||
type: 'REMOVE_SAVED_PAGE',
|
||||
@ -197,6 +206,12 @@ export type AppAction =
|
||||
type: 'REMOVE_SAVED_PAGE';
|
||||
savedPage: SavedPage;
|
||||
}
|
||||
| {
|
||||
type: 'MOVE_SAVED_PAGE_CARD';
|
||||
fromIndex: number;
|
||||
toIndex: number;
|
||||
savedPage: SavedPage;
|
||||
}
|
||||
| {
|
||||
type: 'ADD_CARD_TO_SAVED_PAGE';
|
||||
card: CardItem;
|
||||
|
@ -242,6 +242,14 @@ describe('AppService Reducer', () => {
|
||||
|
||||
// 'SAVE_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';
|
||||
|
||||
it('ADD_CARD', () => {
|
||||
|
@ -4,13 +4,13 @@ import { AppState, DisplaySettings, PageSettings } from '../models/app-state';
|
||||
import { IStorable, Storable } from '../common/storable';
|
||||
import { NoteItem } from '../models/note-state';
|
||||
|
||||
import { MoveDirection } from '../common/move-direction';
|
||||
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 { SavedPage } from '../models/page-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> {
|
||||
// if the candidate is null, then return the state.
|
||||
@ -105,17 +105,35 @@ export function reducer(state: AppState, action: AppAction): AppState {
|
||||
autocomplete: [...action.words],
|
||||
};
|
||||
}
|
||||
|
||||
//#region 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 {
|
||||
...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,
|
||||
savedPages: item,
|
||||
savedPages, // update the savedPages
|
||||
};
|
||||
}
|
||||
case 'UPDATE_SAVED_PAGE': {
|
||||
const savedPages = new Storable<SavedPage[]>(
|
||||
const newSavedPages = new Storable<SavedPage[]>(
|
||||
state.savedPages.value.map((o) => {
|
||||
if (o.id === action.savedPage.id) {
|
||||
return action.savedPage;
|
||||
@ -124,12 +142,8 @@ export function reducer(state: AppState, action: AppAction): AppState {
|
||||
})
|
||||
);
|
||||
|
||||
const item = getNewestStorable(savedPages, state.savedPages);
|
||||
return {
|
||||
...state,
|
||||
savedPagesLoaded: true,
|
||||
savedPages: item,
|
||||
};
|
||||
const savedPages = getNewestStorable(newSavedPages, state.savedPages);
|
||||
return reducer(state, AppActionFactory.newUpdateSavedPages(savedPages));
|
||||
}
|
||||
case 'REMOVE_SAVED_PAGE': {
|
||||
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],
|
||||
};
|
||||
}
|
||||
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': {
|
||||
const savedPages = new Storable([
|
||||
...state.savedPages.value.map((o) => {
|
||||
@ -214,6 +237,9 @@ export function reducer(state: AppState, action: AppAction): AppState {
|
||||
savedPages,
|
||||
});
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
case 'ADD_CARD': {
|
||||
let cards = [];
|
||||
|
||||
@ -259,25 +285,7 @@ export function reducer(state: AppState, action: AppAction): AppState {
|
||||
};
|
||||
}
|
||||
case 'MOVE_CARD': {
|
||||
let cards = [];
|
||||
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];
|
||||
}
|
||||
const cards = moveItemUpOrDown(state.cards, action.card, action.direction);
|
||||
|
||||
return {
|
||||
...state,
|
||||
|
@ -30,6 +30,7 @@ import { reducer } from './app-state-reducer';
|
||||
import { initialState } from './app-state-initial-state';
|
||||
import { CardItem, CardType } from '../models/card-state';
|
||||
import { SavedPage } from '../models/page-state';
|
||||
import { AppActionFactory } from './app-state-actions';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
@ -118,18 +119,15 @@ export class AppService extends createStateService(reducer, initialState) {
|
||||
});
|
||||
}
|
||||
removeSavedPage(savedPage: SavedPage) {
|
||||
this.dispatch({
|
||||
type: 'REMOVE_SAVED_PAGE',
|
||||
savedPage,
|
||||
});
|
||||
this.dispatch(AppActionFactory.newRemoveSavedPage(savedPage));
|
||||
}
|
||||
|
||||
moveSavedPageCard(savedPage: SavedPage, fromIndex: number, toIndex: number) {
|
||||
this.dispatch(AppActionFactory.newMoveSavedPageCard(savedPage, fromIndex, toIndex));
|
||||
}
|
||||
|
||||
addCardToSavedPage(pageId: string, card: CardItem) {
|
||||
this.dispatch({
|
||||
type: 'ADD_CARD_TO_SAVED_PAGE',
|
||||
card,
|
||||
pageId,
|
||||
});
|
||||
this.dispatch(AppActionFactory.newAddCardToSavedPage(card, pageId));
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
Loading…
x
Reference in New Issue
Block a user