drag and drop to reorder cards on the saved pages admin, some bug fixes and unit tests also

This commit is contained in:
Jason Wall 2020-08-17 11:46:41 -04:00
parent f83a4f5d48
commit f723efbf66
11 changed files with 208 additions and 45 deletions

View File

@ -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

View File

@ -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,

View 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);
});
});

View 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;
}

View File

@ -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>

View File

@ -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);
}

View File

@ -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);
}
}

View File

@ -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;

View File

@ -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', () => {

View File

@ -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,

View File

@ -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