Преглед изворни кода

need to make links to existing blocks robust.

Tomi Cvetic пре 4 година
родитељ
комит
3aa467cc35

+ 3 - 0
.gitignore

@@ -30,3 +30,6 @@ yarn-error.log*
 
 # media
 media/
+
+# database backup
+dbBackup/

+ 1 - 0
docker-compose.yml

@@ -57,6 +57,7 @@ services:
       MYSQL_ROOT_PASSWORD: prisma
     volumes:
       - mysql:/var/lib/mysql
+      - "./dbBackup:/backup"
 
 volumes:
   mysql:

+ 2 - 1
frontend/jest.config.js

@@ -1,5 +1,5 @@
 module.exports = {
-  testEnvironment: 'node',
+  //testEnvironment: 'node',
   preset: 'ts-jest',
   setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],
   transform: { '^.+\\.(t|j)sx?$': 'ts-jest' },
@@ -8,3 +8,4 @@ module.exports = {
   testPathIgnorePatterns: ['<rootDir>/.next/', '<rootDir>/node_modules/'],
   globals: { 'ts-jest': { 'ts-config': '<rootDir>/tsconfig.jest.json' } }
 }
+console.log('loaded.')

+ 0 - 14
frontend/package.json

@@ -68,19 +68,5 @@
     "react-test-renderer": "16.13.1",
     "ts-jest": "^25.3.1",
     "typescript": "3.8.3"
-  },
-  "jest": {
-    "setupFilesAfterEnv": [
-      "<rootDir>/jest.setup.js"
-    ],
-    "testPathIgnorePatterns": [
-      "<rootDir>/.next/",
-      "<rootDir>/node_modules/"
-    ],
-    "transform": {
-      "\\.(gql|graphql)$": "jest-transform-graphql",
-      ".*": "babel-jest",
-      "^.+\\.js?$": "babel-jest"
-    }
   }
 }

+ 2 - 1
frontend/src/app/components/Footer.tsx

@@ -6,8 +6,9 @@ const Footer = () => (
     <style jsx>{`
       footer {
         text-align: center;
-        background-color: ${theme.colors.lightgrey};
+        background-color: ${theme.colors.black};
         color: ${theme.colors.offWhite};
+        box-shadow: ${theme.bsUp};
       }
     `}</style>
   </footer>

+ 92 - 88
frontend/src/gql/index.tsx

@@ -3355,34 +3355,15 @@ export type UserWhereUniqueInput = {
 
 export type ExerciseContentFragment = Pick<Exercise, 'id' | 'name' | 'description' | 'videos' | 'pictures' | 'targets' | 'baseExercise'>;
 
-export type BlockContentFragment = (
+export type BlockWithoutBlocksFragment = (
   Pick<Block, 'id' | 'title' | 'description' | 'videos' | 'pictures' | 'duration' | 'rest'>
-  & { format: Pick<Format, 'id' | 'name' | 'description'>, blocks: Maybe<Array<(
-    Pick<BlockInstance, 'id' | 'order' | 'rounds' | 'variation'>
-    & { block: BlockHintFragment }
-  )>>, exercises: Maybe<Array<(
+  & { format: Pick<Format, 'id' | 'name' | 'description'>, exercises: Maybe<Array<(
     Pick<ExerciseInstance, 'id' | 'order' | 'repetitions' | 'variation'>
     & { exercise: ExerciseContentFragment }
   )>> }
 );
 
-export type BlockHintFragment = (
-  Pick<Block, 'id' | 'title' | 'description' | 'videos' | 'pictures' | 'duration' | 'rest'>
-  & { format: Pick<Format, 'id' | 'name' | 'description'>, blocks: Maybe<Array<Pick<BlockInstance, 'id'>>>, exercises: Maybe<Array<(
-    Pick<ExerciseInstance, 'id' | 'order' | 'repetitions' | 'variation'>
-    & { exercise: ExerciseContentFragment }
-  )>> }
-);
-
-export type SubBlockFragment = (
-  Pick<BlockInstance, 'id' | 'order' | 'rounds' | 'variation'>
-  & { block: BlockContentFragment }
-);
-
-export type SubBlockHintFragment = (
-  Pick<BlockInstance, 'id' | 'order' | 'rounds' | 'variation'>
-  & { block: BlockHintFragment }
-);
+export type BlockInstanceWithoutBlockFragment = Pick<BlockInstance, 'id' | 'order' | 'rounds' | 'variation'>;
 
 export type TrainingQueryVariables = {
   id: Scalars['ID']
@@ -3391,7 +3372,22 @@ export type TrainingQueryVariables = {
 
 export type TrainingQuery = { training: Maybe<(
     Pick<Training, 'id' | 'title' | 'createdAt' | 'trainingDate' | 'location' | 'attendance' | 'published'>
-    & { type: Pick<TrainingType, 'id' | 'name' | 'description'>, blocks: Maybe<Array<SubBlockFragment>>, registrations: Maybe<Array<Pick<User, 'id' | 'name'>>> }
+    & { type: Pick<TrainingType, 'id' | 'name' | 'description'>, blocks: Maybe<Array<(
+      { block: (
+        { blocks: Maybe<Array<(
+          { block: (
+            { blocks: Maybe<Array<(
+              { block: BlockWithoutBlocksFragment }
+              & BlockInstanceWithoutBlockFragment
+            )>> }
+            & BlockWithoutBlocksFragment
+          ) }
+          & BlockInstanceWithoutBlockFragment
+        )>> }
+        & BlockWithoutBlocksFragment
+      ) }
+      & BlockInstanceWithoutBlockFragment
+    )>>, registrations: Maybe<Array<Pick<User, 'id' | 'name'>>> }
   )> };
 
 export type DisplayTrainingFragment = (
@@ -3444,7 +3440,22 @@ export type TrainingsQueryVariables = {};
 
 export type TrainingsQuery = { trainings: Array<(
     Pick<Training, 'id' | 'title' | 'trainingDate' | 'location' | 'attendance' | 'published'>
-    & { type: Pick<TrainingType, 'id' | 'name' | 'description'>, blocks: Maybe<Array<SubBlockHintFragment>>, registrations: Maybe<Array<Pick<User, 'id' | 'name'>>> }
+    & { type: Pick<TrainingType, 'id' | 'name' | 'description'>, blocks: Maybe<Array<(
+      { block: (
+        { blocks: Maybe<Array<(
+          { block: (
+            { blocks: Maybe<Array<(
+              { block: BlockWithoutBlocksFragment }
+              & BlockInstanceWithoutBlockFragment
+            )>> }
+            & BlockWithoutBlocksFragment
+          ) }
+          & BlockInstanceWithoutBlockFragment
+        )>> }
+        & BlockWithoutBlocksFragment
+      ) }
+      & BlockInstanceWithoutBlockFragment
+    )>>, registrations: Maybe<Array<Pick<User, 'id' | 'name'>>> }
   )> };
 
 export type TrainingTypesQueryVariables = {};
@@ -3460,7 +3471,13 @@ export type FormatsQuery = { formats: Array<Pick<Format, 'id' | 'name' | 'descri
 export type BlocksQueryVariables = {};
 
 
-export type BlocksQuery = { blocks: Array<BlockContentFragment> };
+export type BlocksQuery = { blocks: Array<(
+    { blocks: Maybe<Array<(
+      { block: BlockWithoutBlocksFragment }
+      & BlockInstanceWithoutBlockFragment
+    )>> }
+    & BlockWithoutBlocksFragment
+  )> };
 
 export type ExercisesQueryVariables = {};
 
@@ -3584,8 +3601,8 @@ export const ExerciseContentFragmentDoc = gql`
   baseExercise
 }
     `;
-export const BlockHintFragmentDoc = gql`
-    fragment blockHint on Block {
+export const BlockWithoutBlocksFragmentDoc = gql`
+    fragment blockWithoutBlocks on Block {
   id
   title
   description
@@ -3598,9 +3615,6 @@ export const BlockHintFragmentDoc = gql`
     description
   }
   rest
-  blocks {
-    id
-  }
   exercises {
     id
     exercise {
@@ -3612,63 +3626,14 @@ export const BlockHintFragmentDoc = gql`
   }
 }
     ${ExerciseContentFragmentDoc}`;
-export const BlockContentFragmentDoc = gql`
-    fragment blockContent on Block {
+export const BlockInstanceWithoutBlockFragmentDoc = gql`
+    fragment blockInstanceWithoutBlock on BlockInstance {
   id
-  title
-  description
-  videos
-  pictures
-  duration
-  format {
-    id
-    name
-    description
-  }
-  rest
-  blocks {
-    id
-    block {
-      ...blockHint
-    }
-    order
-    rounds
-    variation
-  }
-  exercises {
-    id
-    exercise {
-      ...exerciseContent
-    }
-    order
-    repetitions
-    variation
-  }
-}
-    ${BlockHintFragmentDoc}
-${ExerciseContentFragmentDoc}`;
-export const SubBlockFragmentDoc = gql`
-    fragment subBlock on BlockInstance {
-  id
-  block {
-    ...blockContent
-  }
   order
   rounds
   variation
 }
-    ${BlockContentFragmentDoc}`;
-export const SubBlockHintFragmentDoc = gql`
-    fragment subBlockHint on BlockInstance {
-  id
-  block {
-    ...blockHint
-  }
-  order
-  rounds
-  variation
-}
-    ${BlockHintFragmentDoc}`;
+    `;
 export const DisplayBlockInstanceFragmentDoc = gql`
     fragment displayBlockInstance on BlockInstance {
   id
@@ -3777,7 +3742,22 @@ export const TrainingDocument = gql`
     attendance
     published
     blocks {
-      ...subBlock
+      ...blockInstanceWithoutBlock
+      block {
+        ...blockWithoutBlocks
+        blocks {
+          ...blockInstanceWithoutBlock
+          block {
+            ...blockWithoutBlocks
+            blocks {
+              ...blockInstanceWithoutBlock
+              block {
+                ...blockWithoutBlocks
+              }
+            }
+          }
+        }
+      }
     }
     registrations {
       id
@@ -3785,7 +3765,8 @@ export const TrainingDocument = gql`
     }
   }
 }
-    ${SubBlockFragmentDoc}`;
+    ${BlockInstanceWithoutBlockFragmentDoc}
+${BlockWithoutBlocksFragmentDoc}`;
 
 /**
  * __useTrainingQuery__
@@ -3859,7 +3840,22 @@ export const TrainingsDocument = gql`
     attendance
     published
     blocks {
-      ...subBlockHint
+      ...blockInstanceWithoutBlock
+      block {
+        ...blockWithoutBlocks
+        blocks {
+          ...blockInstanceWithoutBlock
+          block {
+            ...blockWithoutBlocks
+            blocks {
+              ...blockInstanceWithoutBlock
+              block {
+                ...blockWithoutBlocks
+              }
+            }
+          }
+        }
+      }
     }
     registrations {
       id
@@ -3867,7 +3863,8 @@ export const TrainingsDocument = gql`
     }
   }
 }
-    ${SubBlockHintFragmentDoc}`;
+    ${BlockInstanceWithoutBlockFragmentDoc}
+${BlockWithoutBlocksFragmentDoc}`;
 
 /**
  * __useTrainingsQuery__
@@ -3964,10 +3961,17 @@ export type FormatsQueryResult = ApolloReactCommon.QueryResult<FormatsQuery, For
 export const BlocksDocument = gql`
     query blocks {
   blocks {
-    ...blockContent
+    ...blockWithoutBlocks
+    blocks {
+      ...blockInstanceWithoutBlock
+      block {
+        ...blockWithoutBlocks
+      }
+    }
   }
 }
-    ${BlockContentFragmentDoc}`;
+    ${BlockWithoutBlocksFragmentDoc}
+${BlockInstanceWithoutBlockFragmentDoc}`;
 
 /**
  * __useBlocksQuery__

+ 1 - 0
frontend/src/styles/theme.ts

@@ -16,6 +16,7 @@ const theme = {
   },
   maxWidth: '1000px',
   bs: '0 12px 24px 0 rgba(0,0,0,0.09)',
+  bsUp: '0 -12px 24px 0 rgba(0,0,0,0.09)',
   bsSmall: '0 5px 10px 0 rgba(0,0,0,0.19)'
 }
 

+ 35 - 35
frontend/src/timer/__tests__/hooks.test.tsx

@@ -1,44 +1,44 @@
-import { mount } from "enzyme";
-import { useTimer } from "../hooks";
+import { mount } from 'enzyme'
+import { useTimer } from '../hooks'
 
 const TestApp = () => {
-  const [time, handler] = useTimer();
+  const [time, handler] = useTimer()
   return (
     <div>
-      <p id="time">{time}</p>
-      <p id="state">{handler.running ? "running" : "stopped"}</p>
-      <button id="start" onClick={handler.start}></button>
-      <button id="stop" onClick={handler.stop}></button>
-      <button id="reset" onClick={handler.reset}></button>
-      <button id="setTime" onClick={() => handler.setTime(314)}></button>
+      <p id='time'>{time}</p>
+      <p id='state'>{handler.running ? 'running' : 'stopped'}</p>
+      <button id='start' onClick={handler.start}></button>
+      <button id='stop' onClick={handler.stop}></button>
+      <button id='reset' onClick={handler.reset}></button>
+      <button id='setTime' onClick={() => handler.setTime(314)}></button>
     </div>
-  );
-};
+  )
+}
 
-describe("timer hook", () => {
-  const container = mount(<TestApp />);
-  it("renders without error", () => {
-    expect(container.html()).toMatchSnapshot();
-    expect(container.find("p").length).toEqual(2);
-    expect(container.find("button").length).toEqual(4);
-  });
-  it("starts the timer", () => {
-    container.find("#start").simulate("click");
-    expect(container.find("#state").prop("children")).toEqual("running");
-  });
+describe('timer hook', () => {
+  const container = mount(<TestApp />)
+  it('renders without error', () => {
+    expect(container.html()).toMatchSnapshot()
+    expect(container.find('p').length).toEqual(2)
+    expect(container.find('button').length).toEqual(4)
+  })
+  it('starts the timer', () => {
+    container.find('#start').simulate('click')
+    expect(container.find('#state').prop('children')).toEqual('running')
+  })
 
-  it("stops the timer", () => {
-    container.find("#stop").simulate("click");
-    expect(container.find("#state").prop("children")).toEqual("stopped");
-  });
+  it('stops the timer', () => {
+    container.find('#stop').simulate('click')
+    expect(container.find('#state').prop('children')).toEqual('stopped')
+  })
 
-  it("sets the timer", () => {
-    container.find("#setTime").simulate("click");
-    expect(container.find("#time").prop("children")).toEqual(314);
-  });
+  it('sets the timer', () => {
+    container.find('#setTime').simulate('click')
+    expect(container.find('#time').prop('children')).toEqual(314)
+  })
 
-  it("resets the timer", () => {
-    container.find("#reset").simulate("click");
-    expect(container.find("#time").prop("children")).toEqual(0);
-  });
-});
+  it('resets the timer', () => {
+    container.find('#reset').simulate('click')
+    expect(container.find('#time').prop('children')).toEqual(0)
+  })
+})

+ 9 - 5
frontend/src/timer/components/Timer.tsx

@@ -25,6 +25,7 @@ const Timer = ({ training }: { training: TTraining }) => {
     const exerciseList = getExerciseList(training.blocks)
     const totalTime = getTrainingTime(exerciseList)
     setState({ ...state, exerciseList, totalTime })
+    console.log(training, exerciseList, totalTime)
   }, [training])
 
   const {
@@ -35,7 +36,10 @@ const Timer = ({ training }: { training: TTraining }) => {
   } = getPosition(state.exerciseList, timer.time)
 
   const videoSrc =
-    (currentExercise && currentExercise.video) || '/media/block0.mp4'
+    (currentExercise?.videos &&
+      currentExercise.videos.length > 0 &&
+      currentExercise.videos[0]) ||
+    '/media/block0.mp4'
   const [videoRef, videoPlayer] = useVideo(videoSrc)
 
   useEffect(() => {
@@ -85,7 +89,7 @@ const Timer = ({ training }: { training: TTraining }) => {
   }
 
   return (
-    <p id='timer'>
+    <section id='timer'>
       <h1>{(currentExercise && currentExercise.toplevelBlock) || 'Torture'}</h1>
       <div id='flow'>
         <Countdown
@@ -120,14 +124,14 @@ const Timer = ({ training }: { training: TTraining }) => {
           {nextExercise ? nextExercise.exercise : '😎'}
         </p>
       </div>
-      <p id='description'>
+      <div id='description'>
         <div data-vjs-player>
           <video ref={videoRef} className='video-js vjs-16-9' />
         </div>
         <p className='description'>
           {currentExercise && currentExercise.description}
         </p>
-      </p>
+      </div>
 
       <style jsx>{`
         #timer {
@@ -199,7 +203,7 @@ const Timer = ({ training }: { training: TTraining }) => {
           }
         }
       `}</style>
-    </p>
+    </section>
   )
 }
 

+ 1 - 1
frontend/src/timer/types.ts

@@ -2,7 +2,7 @@ export interface IExerciseItem {
   exercise: string
   duration: number
   offset: number
-  video?: string
+  videos?: string[]
   toplevelBlock?: string
   description?: string
 }

+ 10 - 6
frontend/src/timer/utils.ts

@@ -40,23 +40,25 @@ export function getExerciseList(
   initialOffset = 0,
   toplevelBlock: undefined | string = undefined
 ): IExerciseItem[] {
-  if (!blockInstances) return []
+  if (!blockInstances?.length) return []
   let offset = initialOffset
   return blockInstances
     .map(blockInstance => {
       const { block, rounds = 1 } = blockInstance
-      if (block.blocks) {
+      if (block.blocks?.length) {
         const blockArray = []
-        for (let i = 0; i < rounds; i++) {
+        for (let i = 0; i < (rounds ?? 1); i++) {
           const subBlocks = getExerciseList(
             block.blocks,
             offset,
             toplevelBlock || block.title
           )
           const lastItem = subBlocks[subBlocks.length - 1]
-          offset = lastItem.offset + lastItem.duration
+          if (lastItem) {
+            offset = lastItem.offset + lastItem.duration
+          }
           if (block.rest) {
-            if (lastItem.exercise === 'Rest') {
+            if (lastItem && lastItem.exercise === 'Rest') {
               lastItem.duration += block.rest
               offset += block.rest
             } else {
@@ -73,6 +75,7 @@ export function getExerciseList(
         }
         return blockArray.flat()
       } else if (block.exercises) {
+        console.log('found exercises', block.exercises)
         const blockArray: IExerciseItem[] = []
         const newItem = {
           exercise: block.exercises
@@ -90,10 +93,11 @@ export function getExerciseList(
           toplevelBlock: toplevelBlock || block.title,
           offset
         }
-        for (let i = 0; i < rounds; i++) {
+        for (let i = 0; i < (rounds ?? 1); i++) {
           blockArray.push({ ...newItem, offset })
           offset += newItem.duration
         }
+        console.log(newItem, blockArray)
         return blockArray.flat()
       }
     })

+ 91 - 18
frontend/src/training/__tests__/utils.test.ts

@@ -1,11 +1,9 @@
-import { diffDB } from '../utils'
-const testDate1 = new Date(2020, 1, 1)
-const testDate2 = new Date(2020, 2, 2)
+import { diffDB, collectMutations, transformArrayToDB } from '../utils'
+import { transform } from 'lodash'
 const typesFalsy = {
   number: 0,
   boolean: false,
   string: '',
-  //date: testDate1,
   array: [],
   object: {},
   undefined: undefined,
@@ -14,36 +12,49 @@ const typesFalsy = {
 const expectedFalsy = {
   number: 0,
   boolean: false,
-  string: ''
-  //date: testDate1
+  string: '',
+  array: [],
+  null: null
 }
 const typesTruthy = {
   number: 12,
   boolean: true,
   string: 'hello!',
-  //date: testDate2,
   array: [1, 2, 3],
   object: { a: 1, b: 2 },
   undefined: 'not undefined',
   null: 45
 }
-const dbId = 'dbid'
-const createId = '++createId'
-const updateId = '@@updateId'
-const deleteId = '--deleteId'
+const dbId = 'dbId'
+const dbChildId = 'dbChildId'
+const createId = '++fakeId'
+const deleteId = '--dbId'
+const originalContent = {
+  id: 'dbChildId',
+  some: 'original content',
+  an: ['array', 'of', 'strings']
+}
+const changedContent = {
+  id: 'dbChildId',
+  some: 'changed content',
+  an: ['array', 'of', 'more', 'strings']
+}
 
 const dbItem = {
   id: dbId,
-  ...typesTruthy,
-  child: {
-    id: dbId,
-    ...typesTruthy
-  }
+  unchangedChild: originalContent,
+  updatedChild: originalContent,
+  deletedChild: originalContent,
+  nestedChild: { ...originalContent, child: originalContent }
 }
 
-const updateItem = {
+const formItem = {
   id: dbId,
-  ...typesTruthy
+  unchangedChild: originalContent,
+  createdChild: { ...changedContent, id: createId },
+  updatedChild: changedContent,
+  deletedChild: { ...changedContent, id: deleteId },
+  nestedChild: { ...originalContent, child: changedContent }
 }
 
 describe('diffDB: Find differences of current state to database', () => {
@@ -59,4 +70,66 @@ describe('diffDB: Find differences of current state to database', () => {
     const falsyDiff = diffDB(typesFalsy, typesTruthy)
     expect(falsyDiff).toEqual(expectedFalsy)
   })
+  it('diffs nested items correctly', () => {
+    const a = diffDB(formItem, dbItem)
+    expect(a).toEqual({
+      id: '@@dbId',
+      createdChild: { ...changedContent, id: createId },
+      updatedChild: { ...changedContent, id: '@@dbChildId' },
+      deletedChild: { id: deleteId },
+      nestedChild: {
+        id: '@@dbChildId',
+        child: { ...changedContent, id: '@@dbChildId' }
+      }
+    })
+  })
+})
+
+const collection = [
+  { create: originalContent },
+  { update: changedContent },
+  { delete: changedContent },
+  { update: originalContent },
+  { connect: originalContent }
+]
+const stringArray = ['do', 'not', 'touch']
+describe('collects DB changes.', () => {
+  it('collects array of mutations', () => {
+    expect(collectMutations(collection)).toEqual({
+      create: [originalContent],
+      update: [changedContent, originalContent],
+      delete: [changedContent],
+      connect: [originalContent]
+    })
+  })
+  it('leaves other arrays untouched.', () => {
+    expect(collectMutations(stringArray)).toEqual({ set: stringArray })
+  })
+})
+
+const formData = {
+  id: '@@toUpdate',
+  value: '14',
+  child: { id: '--toDelete' },
+  children: [
+    { id: '++toCreate', value: 'nice' },
+    { id: '@@toUpdate', value: 'good' },
+    { id: 'toConnect', value: 'fine' }
+  ],
+  arrays: ['do', 'not', 'touch', 'strings']
+}
+describe('transforms form data to DB data', () => {
+  it('does it.', () => {
+    expect(transform(formData, transformArrayToDB)).toEqual({
+      id: '@@toUpdate',
+      value: '14',
+      child: { delete: { id: 'toDelete' } },
+      children: {
+        create: [{ value: 'nice' }],
+        update: [{ where: { id: 'toUpdate' }, data: { value: 'good' } }],
+        connect: [{ id: 'toConnect' }]
+      },
+      arrays: { set: ['do', 'not', 'touch', 'strings'] }
+    })
+  })
 })

+ 14 - 0
frontend/src/training/components/BlockInputs.tsx

@@ -60,6 +60,20 @@ const BlockInputs = ({ onChange, value, name }: IBlockInputs) => {
         type='number'
         onChange={onChange}
       />
+      <TextInput
+        name={`${name}.videos`}
+        label='Video'
+        value={value.videos && value.videos.length > 0 ? value.videos[0] : ''}
+        onChange={event =>
+          onChange({
+            target: {
+              type: 'custom',
+              name: `${name}.videos`,
+              value: [event.target.value]
+            }
+          })
+        }
+      />
       <label>Blocks</label>
       {value.blocks && value.blocks.length > 0 && (
         <BlockInstanceInputs

+ 4 - 1
frontend/src/training/components/BlockSelector.tsx

@@ -45,10 +45,13 @@ const BlockSelector = ({
       <button
         type='button'
         onClick={event => {
+          const block =
+            blocks.data && blocks.data.blocks.find(block => block.id === state)
+          if (!block) return
           const changeEvent: CustomChangeEvent = {
             target: {
               type: 'custom',
-              value: { id: state },
+              value: block,
               name
             }
           }

+ 5 - 7
frontend/src/training/components/EditTraining.tsx

@@ -4,12 +4,12 @@ import {
   emptyTraining,
   emptyBlockInstance,
   transformArrayToDB,
-  KdiffDB
+  diffDB
 } from '../utils'
 import TrainingTypeSelector from './TrainingTypeSelector'
 import BlockInstanceInputs from './BlockInstanceInputs'
 import { TTraining } from '../types'
-import { transform } from 'lodash'
+import { transform, isEqual, isObject } from 'lodash'
 import Registrations from './Registrations'
 import Ratings from './Ratings'
 
@@ -29,11 +29,9 @@ const EditTraining = ({ training }: { training?: TTraining }) => {
           console.log({ newValues })
           createTraining({ variables: newValues })
         } else {
-          console.log(values, training, typeof KdiffDB)
-          const { id, ...changes } = KdiffDB(
-            values,
-            training || emptyTraining()
-          )
+          console.log(values, training, typeof diffDB)
+          const a = diffDB(values, training || emptyTraining())
+          const { id, ...changes } = a
           const newValues = transform(changes, transformArrayToDB)
           if (Object.keys(newValues).length > 0) {
             console.log('saving changes', changes, newValues)

+ 5 - 3
frontend/src/training/components/ExerciseSelector.tsx

@@ -45,12 +45,14 @@ const ExerciseSelector = ({
       <button
         type='button'
         onClick={event => {
+          const exercise =
+            exercises.data &&
+            exercises.data.exercises.find(exercise => exercise.id === state)
+          if (!exercise) return
           const changeEvent: CustomChangeEvent = {
             target: {
               type: 'custom',
-              value: exercises.data?.exercises.find(
-                exercise => exercise.id === state
-              ),
+              value: exercise,
               name
             }
           }

+ 18 - 2
frontend/src/training/components/Training.tsx

@@ -4,6 +4,7 @@ import TrainingBlock from './TrainingBlock'
 import Link from 'next/link'
 import { TTraining } from '../types'
 import TrainingMeta from './TrainingMeta'
+import { formatTime, calculateDuration } from '../utils'
 
 const Training = ({ training }: { training: TTraining }) => {
   return (
@@ -13,7 +14,12 @@ const Training = ({ training }: { training: TTraining }) => {
       <TrainingMeta training={training} />
 
       <section>
-        <h2>Program</h2>
+        <h2>
+          <span className='program-title'>{training.title}</span>{' '}
+          <span className='program-time'>
+            {formatTime(calculateDuration(training.blocks))}
+          </span>
+        </h2>
         <Link href='/timer/[id]' as={`/timer/${training.id}`}>
           <button type='button'>Start Timer</button>
         </Link>
@@ -50,7 +56,7 @@ const Training = ({ training }: { training: TTraining }) => {
           article > h2 {
             grid-area: title;
             font-weight: 900;
-            font-size: 120%;
+            font-size: 160%;
             background: ${theme.colors.darkerblue};
             color: ${theme.colors.offWhite};
           }
@@ -71,6 +77,16 @@ const Training = ({ training }: { training: TTraining }) => {
             padding: 0.5rem;
             cursor: pointer;
           }
+          .program-time {
+            color: gray;
+            font-size: 90%;
+          }
+          .program-time::before {
+            content: '(';
+          }
+          .program-time::after {
+            content: ')';
+          }
         `}
       </style>
     </article>

+ 12 - 7
frontend/src/training/components/TrainingBlock.tsx

@@ -11,14 +11,19 @@ const TrainingBlock = ({
   const { title, blocks, exercises } = blockInstance.block
   return (
     <div>
-      <h3>
-        <span className='block-title'>{title}</span>{' '}
-        <span className='block-time'>{formatTime(duration)}</span>
-      </h3>
-      {blocks && blocks.map(block => <TrainingBlock blockInstance={block} />)}
-      {exercises && (
-        <ExerciseComposition exercises={exercises} duration={duration} />
+      {title && (
+        <h3>
+          <span className='block-title'>{title}</span>{' '}
+          <span className='block-time'>{formatTime(duration)}</span>
+        </h3>
       )}
+      {blocks &&
+        blocks.map(block => (
+          <TrainingBlock key={block.id} blockInstance={block} />
+        ))}
+      {exercises?.length ? (
+        <ExerciseComposition exercises={exercises} duration={duration} />
+      ) : null}
 
       <style jsx>
         {`

+ 41 - 54
frontend/src/training/training.graphql

@@ -10,7 +10,7 @@ fragment exerciseContent on Exercise {
   baseExercise
 }
 
-fragment blockContent on Block {
+fragment blockWithoutBlocks on Block {
   id
   title
   description
@@ -23,42 +23,6 @@ fragment blockContent on Block {
     description
   }
   rest
-  blocks {
-    id
-    block {
-      ...blockHint
-    }
-    order
-    rounds
-    variation
-  }
-  exercises {
-    id
-    exercise {
-      ...exerciseContent
-    }
-    order
-    repetitions
-    variation
-  }
-}
-
-fragment blockHint on Block {
-  id
-  title
-  description
-  videos
-  pictures
-  duration
-  format {
-    id
-    name
-    description
-  }
-  rest
-  blocks {
-    id
-  }
   exercises {
     id
     exercise {
@@ -70,21 +34,8 @@ fragment blockHint on Block {
   }
 }
 
-fragment subBlock on BlockInstance {
-  id
-  block {
-    ...blockContent
-  }
-  order
-  rounds
-  variation
-}
-
-fragment subBlockHint on BlockInstance {
+fragment blockInstanceWithoutBlock on BlockInstance {
   id
-  block {
-    ...blockHint
-  }
   order
   rounds
   variation
@@ -105,7 +56,22 @@ query training($id: ID!) {
     attendance
     published
     blocks {
-      ...subBlock
+      ...blockInstanceWithoutBlock
+      block {
+        ...blockWithoutBlocks
+        blocks {
+          ...blockInstanceWithoutBlock
+          block {
+            ...blockWithoutBlocks
+            blocks {
+              ...blockInstanceWithoutBlock
+              block {
+                ...blockWithoutBlocks
+              }
+            }
+          }
+        }
+      }
     }
     registrations {
       id
@@ -220,7 +186,22 @@ query trainings {
     attendance
     published
     blocks {
-      ...subBlockHint
+      ...blockInstanceWithoutBlock
+      block {
+        ...blockWithoutBlocks
+        blocks {
+          ...blockInstanceWithoutBlock
+          block {
+            ...blockWithoutBlocks
+            blocks {
+              ...blockInstanceWithoutBlock
+              block {
+                ...blockWithoutBlocks
+              }
+            }
+          }
+        }
+      }
     }
     registrations {
       id
@@ -247,7 +228,13 @@ query formats {
 
 query blocks {
   blocks {
-    ...blockContent
+    ...blockWithoutBlocks
+    blocks {
+      ...blockInstanceWithoutBlock
+      block {
+        ...blockWithoutBlocks
+      }
+    }
   }
 }
 

+ 53 - 42
frontend/src/training/utils.ts

@@ -9,7 +9,6 @@ import {
 } from './types'
 import {
   TrainingQuery,
-  SubBlockFragment,
   Exercise,
   Block,
   ExerciseInstance,
@@ -76,19 +75,6 @@ export function calculateRating(ratings: TRating[]) {
   return numberOfRatings ? sumOfRatings / numberOfRatings : '-'
 }
 
-export function trainingDBToArray(DBTraining: TrainingQuery['training']) {
-  console.log({ DBTraining })
-  if (!DBTraining) return undefined
-  const { blocks, ...data } = DBTraining
-  return { ...data, blocks: blockDBToArray(blocks) }
-}
-
-export function blockDBToArray(DBSubBlock?: SubBlockFragment[]) {
-  console.log({ DBSubBlock })
-  if (!DBSubBlock) return undefined
-  return DBSubBlock
-}
-
 export function blockArrayToDB() {}
 
 const Weekdays = {
@@ -183,12 +169,12 @@ export function collectMutations(arr: any[]) {
   const connect: any[] = []
   const del: any[] = []
   const update: any[] = []
-  console.log('collect', arr)
   arr.forEach(val => {
-    if (typeof val === 'object' && val['connect']) connect.push(val['connect'])
-    if (typeof val === 'object' && val['create']) create.push(val['create'])
-    if (typeof val === 'object' && val['delete']) del.push(val['delete'])
-    if (typeof val === 'object' && val['update']) update.push(val['update'])
+    console.log(arr, val)
+    if (val?.connect) connect.push(val.connect)
+    if (val?.create) create.push(val.create)
+    if (val?.delete) del.push(val.delete)
+    if (val?.update) update.push(val.update)
   })
   const returnObject: {
     connect?: any
@@ -200,7 +186,10 @@ export function collectMutations(arr: any[]) {
   if (del.length > 0) returnObject.delete = del
   if (update.length > 0) returnObject.update = update
   if (create.length > 0) returnObject.create = create
-  return returnObject
+
+  if (Object.keys(returnObject).length > 0) return returnObject
+  else if (arr.length > 0) return { set: arr }
+  else return undefined
 }
 
 function isNotInDB(key: any, val: any) {
@@ -213,9 +202,11 @@ function isNotInDB(key: any, val: any) {
 export function transformArrayToDB(acc: any, val: any, key: any, object: any) {
   if (isNotInDB(key, val)) return
   if (isArray(val)) {
-    acc[key] = collectMutations(transform(val, transformArrayToDB))
+    const collectedArray = collectMutations(transform(val, transformArrayToDB))
+    if (collectedArray) {
+      acc[key] = collectedArray
+    }
   } else if (typeof val === 'object' && !!val) {
-    console.log('object', key, val)
     // we found an object!
     if (!!val['id'] && val['id'].startsWith('++')) {
       // values with placeholder IDs are preserved
@@ -246,34 +237,54 @@ export function transformArrayToDB(acc: any, val: any, key: any, object: any) {
   }
 }
 
-export function KdiffDB(newObject: any = {}, oldObject: any = {}) {
+function isScalarArray(object: any) {
+  return (
+    isArray(object) &&
+    object.every(
+      item =>
+        typeof item === 'string' ||
+        typeof item === 'number' ||
+        typeof item === 'boolean' ||
+        typeof item === 'bigint'
+    )
+  )
+}
+
+export function diffDB(newObject: any = {}, oldObject: any = {}) {
+  // IDs starting with -- are for deletion.
+  if (newObject.id && newObject.id.startsWith('--')) {
+    return { id: newObject['id'] }
+  }
+
+  // leave arrays of numbers and strings alone
+  if (isScalarArray(newObject)) {
+    if (!isEqual(newObject, oldObject)) return newObject
+    else return undefined
+  }
+
+  // transform everything else
   const transformResult = transform(
     newObject,
     (result: any, value: any, key: string) => {
-      /*if (key === 'id') {
-        result[key] = value
-      } else*/ if (
-        !isEqual(value, oldObject[key])
-      ) {
+      if (!isEqual(value, oldObject[key])) {
         const newValue =
           isObject(value) && isObject(oldObject[key])
-            ? KdiffDB(value, oldObject[key])
+            ? diffDB(value, oldObject[key])
             : value
-        if (newValue !== undefined && newValue !== null) {
-          if (isEqual(key, 'id')) {
-            if (result[key] !== undefined) return
-          } else {
-            result[key] = newValue
-            if (
-              oldObject['id'] !== undefined &&
-              oldObject['id'] === newObject['id']
-            ) {
-              result['id'] = `@@${oldObject['id']}`
-            }
-          }
+        if (newValue !== undefined) {
+          result[key] = newValue
         }
       }
     }
   )
-  return Object.keys(transformResult).length > 0 ? transformResult : undefined
+
+  // detects updates, return object if there was a change.
+  if (Object.keys(transformResult).length > 0) {
+    if (!transformResult.id && oldObject.id) {
+      transformResult.id = `@@${oldObject.id}`
+    }
+    return transformResult
+  } else {
+    return undefined
+  }
 }

+ 13 - 1
frontend/src/user/components/__tests__/DeleteUserButton.test.tsx

@@ -1,4 +1,5 @@
 import React from 'react'
+
 import { shallow } from 'enzyme'
 import { MockedProvider } from '@apollo/client/testing'
 
@@ -25,7 +26,18 @@ describe('testing delete user button', () => {
   it('renders properly', () => {
     const component = shallow(
       <MockedProvider mocks={mocks} addTypename={false}>
-        <DeleteUserButton title='Delete' user={{ id: '12' }} />
+        <DeleteUserButton
+          title='Delete'
+          user={{
+            id: '12',
+            email: 'a@b.c',
+            name: 'test',
+            password: '1234',
+            createdAt: '1.1.1111',
+            permissions: [],
+            interests: []
+          }}
+        />
       </MockedProvider>
     )
   })

+ 3 - 1
frontend/tsconfig.jest.json

@@ -1,6 +1,8 @@
 {
   "extends": "./tsconfig.json",
   "compilerOptions": {
-    "jsx": "react"
+    "jsx": "react",
+    "allowJs": true,
+    "target": "es5"
   }
 }