Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

UT-206: Adding soft-delete of thumbnails #78

Merged
merged 28 commits into from
Mar 9, 2023
Merged

Conversation

andr9528
Copy link
Contributor

@andr9528 andr9528 commented Feb 10, 2023

Created as a draft PR, so developers (mainly Kasper) can come with feedback on changes prior to full on PR review.

PR reviewed for:

  • UT-206
    • UT-207
    • UT-208
    • UT-209

… of thumbnail. Also refactors mediaReducer.media.

refactor: refactors away alot of duplication in mediaReducer.media.
feature: adds 'isHidden' to IThumbnail.
feature: adds 'isHidingState' to IOutput.
feature: adds handling of dispatching to redux store when Hiding state updates.
chore: renames 'LOOP_STATEUPDATE' to 'LOOP_STATE_UPDATE' to match other similar ones.
feature: adds the 'Hiding' button to the UI.
feature: adds a handful of missing redux store dispatches in connection with the new feature being developed.
feature: adds a handful of missing event listeners in connection with the new feature being developed.
feature: adds missing default values for the settings being stored in connection with the new feature being developed.
chore: adjusted name of of constant(s) for hiding, to use use boolean indicative pattern (IS_<X>).
refactor: refactors settingsReducer.settings to only have one return statement.
fix: fixes a few areas that wasn't renamed successfully from renaming in the the previous commit.
chore: renames new constants to match the pattern established by the existing constants.
feature: begins on adding temporary console.log statements to track down why the new button doesn't toggle.
fix: fixes incorrect name of property accessed on 'any' type causing the button to not toggle.
chore: removes all console.log's used to track down why it wasn't toggling.
chore: renames more properties to get in line with the pattern established by other similar properties.
return nextState
}

function conditionalApplyAction(

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is overengineered.
I see what you are trying to achieve, but it's enough to just extract the conditional:

function doesChannelExist(state: StateType, action: ActionType): boolean { // or whatever the conditional is trying to find out
  return state[0].output.length >= action.channelIndex
}

So now your switch cases would just look like this:

case: IO.SET_TIME:
  if (doesChannelExist(nextState, action)) {
    nextState[0].output[action.channelIndex].time = action.time
  }
  return nextState
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Noted, and Applied.

)
}

function isHidingStyle() {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isHidingStyle is using the naming convention for returning a boolean. This is returning an object, not a boolean.
Either rename the function or make it return a boolean.

Also, remember to specify return type

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Name updated to not include 'is', match new 'visibility' naming, and type added.

@@ -5,7 +5,8 @@ export const THUMB_UPDATE = 'thumbUpdate'
export const FOLDERS_UPDATE = 'foldersUpdate'
export const TAB_DATA_UPDATE = 'tabDataUpdate'
export const SETTINGS_UPDATE = 'settingsUpdate'
export const LOOP_STATEUPDATE = 'loopStateUpdate'
export const LOOP_STATE_UPDATE = 'loopStateUpdate'
export const HIDE_STATE_UPDATE = 'hideStateUpdate'

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

HIDE_STATE is a weird phrasing especially when the english language offers words that describe this very thing. (HIDDEN or VISIBILITY - I vote for visibility; then it would be UPDATE_VISIBILITY which is more like normal english).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated to use 'VISIBILITY' instead of 'HIDE'.

@@ -10,6 +10,7 @@ export const SET_MIX = 'setMix'
export const SET_WEB = 'setWeb'
export const SET_MANUAL_START = 'setManualStart'
export const SET_TIME = 'setTime'
export const SET_HIDE = 'setHide'

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you go with VISIBILITY instead of HIDE then this would be SET_VISIBILITY which would make more sense since hide is a verb and you don't set a verb, you do a verb.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated

@@ -97,3 +98,11 @@ export const setTime = (channelIndex: number, time: [number, number]) => {
time: time,
}
}

export const setHide = (channelIndex: number, hideState: boolean) => {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Continuing the visibility path, this would be:

export const setVisibility = (channelIndex: number, isVisible: boolean) => {
  return {
    type: SET_VISIBILITY,
    channelIndex,
    visibility: isVisible
  }
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated

chore: renames anything to do with 'Hide' yet again, to use 'Visibility' instead.
refactor: simplifies 'conditionalApplyAction' into new method 'doesChannelExist', to handle the reduced duplication more cleanly.
refactor: makes use of result from `useSelector(...)` instead of requesting the same data, the following line.
	- this also cuts down on the code size in many places where it was gotten twice.
refactor: converts a large if/else into a much simpler ternary operator.
	- this was one of the main beneficiaries from the `useSelector(...)` change.
refactor: adds types to many 'props' inside Thumbnail.tsx
feature: extracts current file being interacted with during click event in Thumbnail.tsx
feature: makes onClick of Thumbnail perform different action depending on if in 'edit visibility' mode
feauture: adds new events to handle toggling a thumbnails visibility.
refactor: extracts commonality between interfaces out to a common shared interface.
refactor: adds types to previously 'any' type.
chore: changes comparison used for UT-205 to use '===' instead of '.match'
fix: fixes some areas that was missed when renaming previously.
feature: adds saving of hiddenFiles as a json file to persist between sessions.
chore: eliminates the sideeffect of 'toggleHiddenFile' by copying the record, deleting from it, then returning it.
fix: fixes apparent long standing bug that doesn't appear to have had any negative impact.
Copy link
Contributor

@KvelaGorrrrnio KvelaGorrrrnio left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A lot of the comments are stuff we already talked about today :)

src/client/components/Header.tsx Outdated Show resolved Hide resolved
Comment on lines 49 to 50
(storeUpdate: any) =>
storeUpdate.media[0].hiddenFiles
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can be joined to a single line.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will try, but if the styling applied on commit is the one to do it, then i might not be able.

Comment on lines 52 to 55
const visibilityState = useSelector(
(storeUpdate: any) =>
storeUpdate.media[0].output[reduxState.appNav[0].activeTab].visibilityState
)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is visibilityState a boolean representing whether we are in "edit visibility" mode or not?
If so, then should this be per output or defined as an application state?

I'll suggest to have an enum like

enum operationsMode {
  CONTROL = 'control',
  EDIT_VISIBILITY = 'edit-visibility',
}

to ensure a only one mode of operation being active, even if we introduce another "edit XXX" mode.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should have succesfully updated the code to make use of the suggested 'OperationMode' enum, instead of a boolean value.

src/client/components/Thumbnail.tsx Outdated Show resolved Hide resolved
src/client/components/Thumbnail.tsx Outdated Show resolved Hide resolved
src/client/components/Thumbnail.tsx Outdated Show resolved Hide resolved
src/client/util/SocketClientHandlers.ts Outdated Show resolved Hide resolved
src/gateway/util/SocketGatewayHandlers.ts Outdated Show resolved Hide resolved
src/model/reducers/mediaActions.ts Outdated Show resolved Hide resolved
src/server/handlers/socketIOServerHandler.ts Outdated Show resolved Hide resolved
…enFiles when backing file is altered.

refactor: reworks 'visibilityState' into 'operationMode'
refactor: renames yet again just about anything with 'visibility' to something with 'operation'.
feature: adds 'operationMode' enum, allowing easier expansion in the future.
refactor: changes 'handleClickMedia' to use a switch rather than a ternary operator, to decide performed action.
chore: renames 'onClickPlayClicked' to 'playFile'.
chore: renames 'onClickToggleVisibility' to 'toggleVisibility'.
chore: renames 'IChangedInfo' to 'IHiddenFileInfo'
chore: moves 'size' from ICommonFile to IHiddenFileInfo.
feature: adds check to prevent hidding of currently playing file.
chore: adds types to a few places.
chore: extracts props type used multiple places in Thumbnail.tsx to an interface.
chore: renames 'item' to 'file'.
fix: fixes thumbnail not updating when backing file's thumbnail has changed.
…eview.

feature: adds Footer, giving clear indication that a different operation mode is in effect.
feature: improves application styling to fill screen better, to also place Footer truely at the bottom.
refactor: moves styling created inline to CSS files.
chore: removes unnecessary arrow function for onClick handlers.
feature: adds a button to the footer that exits whichever mode you are in back to OperationMode.Control always.
refactor: moves the new hiding button to be under settings, insted of the topbar.
chore: updated color of footer text to better represent a warning, rather than an error. (orangeish color vs redish color)
@andr9528 andr9528 marked this pull request as ready for review February 17, 2023 08:39
import { reduxState } from '../../model/reducers/store'
import * as IO from '../../model/SocketIoConstants'

import '../css/Footer.css'

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it really necessary to import the CSS file?

I guess it's because the CSS file has been moved to a separate folder than the tsx file

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Didn't work without the import. Also how it is setup in the other files, so it makes sense for it to be needed.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think that React imports the CSS file automatically. It requires an import statement (either in css or js).

import { socket } from '../util/SocketClientHandlers'

const footerDescriptions = new Map<OperationMode, string>([
[OperationMode.EDIT_VISIBILITY, 'You are currently editing visibility of files.'],

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the reasoning for having translations here? (And what's the reasoning for making a Map with one entry for translations?)

Ideally, components shouldn't know anything about translations (except for the translationId of the text it want to display).

Has it been requested that it should be translated?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd not exactly call it translations. It is just how i chose to keep the different strings, for the description and button text, with their Operation Mode key, in a manner that would be easily expandable, should the need arise.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the footer is where this should be living, then I would suggest creating an <OperationMode>Footer component, that encapsulates the logic for the corresponding operations mode.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Footer has been split into multiple smaller components. Among those a component whos sole purpose is to contain the logic and constants specific for OperationMode.EDIT_VISIBILITY`. This eliminates the use of the map.

? <div className='Footer-flex'>
<div className='Footer-text'>
{footerDescriptions.has(operationMode)
? footerDescriptions.get(operationMode).toUpperCase()

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't nest ternaries. They become way too complex to read way too fast.

Also, don't overcomplicate it more than necessary:

renderExitButton = () => {
  return (
    <div className='Footer-text'>
      You are currently editing visibility of files.
    </div>
    <button onClick={resetOperationMode} className='Footer-buton'>
      Exit Edit Visibility
    </button>
  )
}

return (
  <Footer className='Footer Footer-flex'>
    { if(operationMode !== OperationMode.CONTROL) {
        renderExitButton()
      }
    }
  </Footer>
)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have extracted the innards of the Footer to a seperate functional component, in this file. This eliminates the nested ternaries.
I am however keeping the Map usage, as it makes adding new operation modes easier, while not increasing the complexity by that THAT much, mostly due to the low size of the file.

On another note, if i were to return the capital letter Footer, i would be returning the same JSX Element, as the enclosing component, unless you also had a renaming of the component in mind?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With my comment on one of the other footer chains (#78 (comment)), my last comment has become outdated.

storeUpdate.media[0].output[reduxState.appNav[0].activeTab]?.operationMode)

return (
<footer className='Footer'>

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

React components are spelled with the first letter uppercased to differentiate them from the regular HTML tags.
CSS classes are all lowercased.
So this should be:
<Footer className='footer'>

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Anders pointed this out too, to which i pointed out that many other CSS classes are spelled with a capital letter in this application, at the moment.

I would very much like to after this PR, to go through and update the code styling to be unified in how it is defined, along with a few other things that Anders and I have talked with Kasper about.
I am fairly certain i know how we, as a team, want the style to be, but i would be happy to have a chat about it, if you'd like.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@LindvedKrvang Just a small correction to your suggestion. The tag name should still be <footer className="footer">, since it is a native HTML5 component/tag that is used. Lowercasing the class name should be done, but as @andr9528 points out, the currently used style for component class names are capitalized.

@@ -27,6 +31,23 @@ const selectorColorStyles = {
singleValue: (styles) => ({ ...styles, color: 'white' }),
}

function handleToggleEditVisibilityMode() {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are not toggling. You are changing OperationMode. Toggling is on/off.
A more accurate name for this method would be emitNewOperationMode or emitSelectedOperationMode or emitOperationModeChanged

Copy link
Contributor Author

@andr9528 andr9528 Feb 20, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If i am in any other mode, it turns on 'Edit Visibility' mode, otherwise it turns off 'Edit Visibility' mode, by setting the mode to the default one, being 'Control'.
Maybe creating a OperationModeService class which has function for setting the mode to each posible one, could help with simplifying this.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No matter what mode you are in you are just emitting an event here.
What the listeners of said event do (which may very well be toggling) has nothing to do with thehandleToggleEditVisibilityMode() function here, hence the name of this function should not indicate anything else than emitting an event.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Renamed to emitSetOperationMode. Still not entirely sure if that name is descriptive enough.

return nextState
}

function doesChannelExist(nextState: any, action: any) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't use any type

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is that, or using a type that would fill a couple of lines.
I am also not entirely sure if it would need to be a partial or not.

I would very much like to spend the time putting type of everything, while redoing the code structure.
This would be one of the areas that i'd spent some time improving.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be nice to at least have the minimum type declaration if not the State type or action type is avaiable.

function doesChannelExist(nextState: { output: unknown[] }[], action: { channelIndex: number }): boolean {
    return nextState[0].output.length > action.channelIndex
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have added what you suggested Anders, with a slight improvement, from an unknown[] to an IOutput[].

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is that, or using a type that would fill a couple of lines.

It's Typescript so just write the types needed for your actual function and nothing more (as Anders points out).
Ideally there would be an interface that all States uses and the same for Actions

@@ -128,10 +140,10 @@ export const settings = (
...action.channels.map(updateChannelConfigWithVideoFormat),
]
nextState[0].ccgConfig.path = action.path
return nextState
break

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What was wrong with returning immediatly?

Copy link
Contributor Author

@andr9528 andr9528 Feb 20, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

By returning immediatly AND removing the new single return at the bottom, a default case is needed, which only returns the nextState.
If the single return at the bottom remains, the default case is gone. However in this case immediatly returning has the same effect as simly breaking, but at the cost of a longer statement.

If there was just ONE case where the state was not returned, then yes i would be returning in each case. But as it is, it is ALWAYS returned, so having one return statement at the end simplifies the code, as there at the end is only one path out of the function.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are pros and cons for both approaches – personally I am in favor of the local returns. In tandem with my personal preference, I would like to point out that break indicates that something happens after the switch statement, which it is not the case here. So it weakens the locality of the execution flow, in contrast to a local return that clearly indicates that the execution flow for the function ends in that particular switch-case.

Using break with a single return also forces a stricter dependency on the variable nextState making the individual case less flexible in terms of what to return. It can both be a good or bad thing depending on how much control you want over the individual switch case.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would be nice to get input from @olzzon and @RasmusAlbrektsen on this one.
If either, also prefer the 'return-immediately' in this scenario, then i will change it back.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After a chat with Kasper, i found that the original structure that he had developed, was a replica of the structure used in Redux documentation, thus it now makes more sense to be why it was used. I will be reverting this switch change, anywhere where Redux is part of it.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If there was just ONE case where the state was not returned, then yes i would be returning in each case. But as it is, it is ALWAYS returned, so having one return statement at the end simplifies the code, as there at the end is only one path out of the function.

I disagree. As Anders points out, by not returning you indicate that more is going to happen so you have to read the rest of the method. It doesn't get much simpler than a return statement saying "now we are done"

@@ -103,6 +143,45 @@ export function socketIoHandlers(socket: any) {
})
}

function toggleHiddenFile(

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function is called toggle, but are you both CREATING and DELETING which is something completely different than toggling

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am toggleing, wheather the file in question is in the record of hidden files, by either deleting a record entry (turning off) or creating a record entry (turning on).

The method was build together with Anders, where we did extract the responsibility of building the new record out into another function.
Not sure how i'd better split the responsibilities in a more meaningfull way.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You could extract the logic for hiding and showing the files into their own functions like so:

type IHiddenFiles = Record<string, IHiddenFileInfo>

function toggleHiddenFile(fileName: string, channelIndex: number, hiddenFiles: IHiddenFiles): IHiddenFiles {
    return isFileHidden(fileName, hiddenFiles)
        ? showFile(fileName, hiddenFiles)
        : hideFile(fileName, channelIndex, hiddenFiles)
}

function isFileHidden(fileName: string, hiddenFiles: IHiddenFiles): boolean {
    return fileName in hiddenFiles
}

function showFile(fileName: string, hiddenFiles: IHiddenFiles): IHiddenFiles {
    const { [fileName], ...newHiddenFiles } = hiddenFiles
   return newHiddenFiles
}

function hideFile(fileName: string, channelIndex: number, hiddenFiles: IHiddenFiles): IHiddenFiles {
    const hiddenFile = getHiddenFileMetadataFromFileName(filename, channelIndex)
    return { ...hiddenFiles, [fileName]: hiddenFile }
}

Copy link
Contributor Author

@andr9528 andr9528 Feb 24, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reworked the function to use most of Anders' suggestion.

return true
}
}
for (let i = 0; i < newList.length; i++) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

export const hasThumbListChanged = (...): boolean => {
   ...
 
  return newList.some((entry, index) => entry.name !== previousList[i].size || entry.name !== previousList[i].size)
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good to learn that the Typescript Arrays' .some(...) is the quivialant of LINQ's .Any(...).

Have applied the suggest change, with a bit more styling, for readability.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FYI, most of the operations of LINQ is found on Array

@@ -56,3 +56,4 @@ ehthumbs.db
# Windows shortcuts #
#####################
*.lnk
/storage/hiddenFiles.json

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does this JSON do?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is a file generated by Cliptool now, that contains a Record of which files are hidden, with their size and changed values.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not just use localStorage?

chore: shrinks hasThumbListChanged(...) by usng function on array.
chore: extracts innards of Footer to another component, to eliminate nested ternaries
…utputs.

refactor: moves hiddenFiles from IMedia to IOutput.
refactor: reworks loadHiddenFiles to split the saved array out to the outputs.
refactor: moves call to loadHiddenFiles to just before initializing client in socketIOServerHandler.
refactor: adds channelIndex as input to updateHiddenFiles
…ltiple outputs."

This reverts commit 0bcd958.

# Conflicts:
#	src/client/components/Thumbnail.tsx
Copy link
Contributor

@KvelaGorrrrnio KvelaGorrrrnio left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You have made some good improvements and I found a little more that needs som love ❤️

src/client/components/Footer.tsx Outdated Show resolved Hide resolved
storeUpdate.media[0].output[reduxState.appNav[0].activeTab]?.operationMode)

return (
<footer className='Footer'>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@LindvedKrvang Just a small correction to your suggestion. The tag name should still be <footer className="footer">, since it is a native HTML5 component/tag that is used. Lowercasing the class name should be done, but as @andr9528 points out, the currently used style for component class names are capitalized.

import { socket } from '../util/SocketClientHandlers'

const footerDescriptions = new Map<OperationMode, string>([
[OperationMode.EDIT_VISIBILITY, 'You are currently editing visibility of files.'],
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the footer is where this should be living, then I would suggest creating an <OperationMode>Footer component, that encapsulates the logic for the corresponding operations mode.

interface ThumbnailProps {
file: IMediaFile
}

export const findThumbPix = (fileName: string, channelIndex: number) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Return type for this would be nice.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added return type and updated to be a function definition instead of constant.

src/client/components/Settings.tsx Outdated Show resolved Hide resolved
src/server/handlers/socketIOServerHandler.ts Outdated Show resolved Hide resolved
src/server/handlers/socketIOServerHandler.ts Outdated Show resolved Hide resolved
src/client/components/Thumbnail.tsx Outdated Show resolved Hide resolved
src/client/components/Thumbnail.tsx Outdated Show resolved Hide resolved
@@ -103,6 +143,45 @@ export function socketIoHandlers(socket: any) {
})
}

function toggleHiddenFile(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You could extract the logic for hiding and showing the files into their own functions like so:

type IHiddenFiles = Record<string, IHiddenFileInfo>

function toggleHiddenFile(fileName: string, channelIndex: number, hiddenFiles: IHiddenFiles): IHiddenFiles {
    return isFileHidden(fileName, hiddenFiles)
        ? showFile(fileName, hiddenFiles)
        : hideFile(fileName, channelIndex, hiddenFiles)
}

function isFileHidden(fileName: string, hiddenFiles: IHiddenFiles): boolean {
    return fileName in hiddenFiles
}

function showFile(fileName: string, hiddenFiles: IHiddenFiles): IHiddenFiles {
    const { [fileName], ...newHiddenFiles } = hiddenFiles
   return newHiddenFiles
}

function hideFile(fileName: string, channelIndex: number, hiddenFiles: IHiddenFiles): IHiddenFiles {
    const hiddenFile = getHiddenFileMetadataFromFileName(filename, channelIndex)
    return { ...hiddenFiles, [fileName]: hiddenFile }
}

refactor: moves Footer.tsx to new subfolder 'Footer'.
refactor: extract FooterContent component to its own file.
refactor: created 'EditVisibilityFooter' to cut away the use of a map for different text depending on operationMode.
refactor: adds return type to a few functions.
refactor: changes a few functions defined as constants with arrow functions, into function definitions.
chore: adds nullish coalescing to a number of 'useSelector(...)'.
chore: updates the order of import in App.tsx.
style: solidifies border-width and makes the color the thing that changes on selecting a file.
refactor: fixes a typo in a css class name.
refactor: changes the text show on the button to go into 'EDIT VISIBILITY' mode.
refactor: split up toggleHiddenFile function to better get in line with single responsibility.
refactor: extracted logic to figuring out the current active output to a function.
	note: could possibly be moved to a more centralized location, and be reused more places.
refactor: changes retrival of active output to use new function in all of Thumbnail.tsx.
refactor: extracted code to find file to its own function in socketIOServerHandler.
refactor: adds a logger log, with data, incase toggling the hidden state fails.
Copy link
Contributor

@KvelaGorrrrnio KvelaGorrrrnio left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have approved on the assumption that the following comments are addressed.
Nice work! 🚀

Comment on lines 4 to 5
const buttonText = 'Exit Edit Visibility'
const descriptionText = 'You are currently editing the visibility of files.'.toUpperCase()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since these are constants, they should be renamed to uppercased snake-case, e.g. BUTTON_TEXT.

Copy link
Contributor Author

@andr9528 andr9528 Feb 28, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have been renamed to use SNAKE_CASE.

) ?? {}

const classNames = [
"thumb",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a note for the refactor: Use single quotes for strings :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Think those were copied directly from the assignment to className, where it does make use of double quotes. Have updated it in 3 locations to use single quotes.

return nextState
}

function doesChannelExist(nextState: any, action: any) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be nice to at least have the minimum type declaration if not the State type or action type is avaiable.

function doesChannelExist(nextState: { output: unknown[] }[], action: { channelIndex: number }): boolean {
    return nextState[0].output.length > action.channelIndex
}

)
}

function buildHiddenFileMetadata(file: IMediaFile): IHiddenFileInfo {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will like to challenge the buildX naming convention. How is it different from getX?
At least we should have a consistent way of naming.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nothing is being built here. At most your are wrapping the parameter in an object.
I would suggest to use getMetaData(file: IMediaFile) which clearly indicates that you are extracting the meta data from the file your are passing.

Also, no reason to include 'Hidden' in the name. The function doesn't care about if the file is hidden or not, it just need to be a file.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have renamed the commented on method to getMetaData(...).

}
}

export function Footer(): JSX.Element {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your whole Footer components are way overdone. You are using three components to write:

export function Footer(): JSX.Element {
  resetOperationMode() {...}

  renderExitEditButton = () => {
    return (
      <div className='Footer-text'>
        You are currently editing visibility of files.
      </div>
      <button onClick={resetOperationMode} className='Footer-buton'>
        Exit Edit Visibility
      </button>
    )
  }
  
  return (
    <footer className='Footer Footer-flex'>
      { if(operationMode !== OperationMode.CONTROL) {
          renderExitEditButton()
        }
      }
    </footer>
  )
}

This is all you need. EditVisibilityFooter and FooterContent are way overkill. They do basically nothing.

}
}

export function Footer(): JSX.Element {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be nice if you would place the Component as the first function of the file and the place the helper functions below it in the order they are called.
There is a tendency in the JavaScript/TypeScript community to not do it like this so this is mostly a personal preference.
The idea behind this is to get a natural flow of reading in the file. When I go to a file called CarOverview.jsx I don't want to see the intriciaties of how they calculated the milage as the very first thing. I want to see the actual overview of the car and then start narrowing down to the specific thing I am after.
It's easier to express verbally so if you need a better explanation don't hesitate to ask

export function FooterContent(props: FooterContentProps): JSX.Element {
return (
<div className='Footer-flex'>
<div className='Footer-text'>

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This div should technically be a label.

Also, the identation seems to be misaligned (that may or may not just be GitHub showing it wrong, but I think it's not)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The file in question (and lines) have had mayor changes performed on the branch for UT-211.

Needs to question Kasper/Jacob/Kevin if we want to release the new and improved tabs too. If not, then changes to the Footer should very likely be cherry-picked onto this branch.

return thumb?.thumbnail ?? ''
}

export const isThumbWithTally = (thumbName: string): boolean => {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This class is called Thumbnail.tsx which means the only thing that should be exported from this class is the Thumbnail component, which this is not (same goes for findThumbPix)
If you need utility functions that are used several places you should create a dedicated class for that. If the functions are just used in here, they should not be exported.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure why Kasper added export to isThumbWithTally. If he had only one component per file, he'd need it to be exported as it is used by multiple components in this file. As it is, it isn't needed, and Cliptool builds just fine, if it is not exported.

In the case of findThumbPix (now renamed to findThumbnail), it is also used by the header. Havn't looked deeply into where, what or why. When splitting the file into multiple files for UT-210, this will be fixed.

return store.media[0].output[activeTab]
}

export function findThumbPix(fileName: string, channelIndex: number): string {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does 'Pix' mean? Is it Picture, Pixel or something third?
The function is returning a string which appears to be a thumbnail, so wouldn't the name for this method be 'findThumbnail'?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Renamed to findThumbnail.

size: number
}

interface ICommonFile extends IHiddenFileInfo {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This relationship seems to be flipped. CommonFile sounds like is stuff that is common for all Files, including hidden Files. Hence it should be HiddenFileInfo that extends CommonFile and not the other way around.

What you have done is make IMedaieFile and IThumbFile be hidden File which I hope was intentional. But if they need to be hidden aswell that just argues more in favour for my other comment about just adding the hidden attribute to the File interface

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, just call it File instead of CommonFile

Copy link
Contributor Author

@andr9528 andr9528 Mar 1, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have updated the name to IFile. Cleaning up the use of 'I' on interfaces will be handled during UT-210.

Attributes on HiddenFileInfo are data relevant to a hidden file, and nothings more. The Attributes of HiddenFileInfo also need to exist on the different files (IMediaFile / IThumbnailFile), as it is these attributes that gets extracted and persisted to figure out which version of a file is saved.

Attributes on IFile are data relevant to IMediaFile / IThumbnailFile, while in Cliptool. These attributes on IFile are irrelevant to determin the version of the file, when checking if the file has changed outside Cliptool.

changed: number
size: number
thumbnail?: any
export interface IThumbFile extends ICommonFile {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ThumbnailFile, not ThumbFile

Copy link
Contributor Author

@andr9528 andr9528 Mar 1, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Has been renamed. Keeping the I, for now, as it will be handled as part of UT-210.

size: number
thumbnail?: any
export interface IThumbFile extends ICommonFile {
thumbnail?: string

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You have an interface dedicated to only add this attribute. Because of that, this attribute should not be optional.
But since a big part of ClipTool is thumbnail management then there is no reasong for thumbnail not to be on the CommonFile interface

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure why Kasper originally made that attribute optional. Removing the ability for it to be optional doesn't appear to break anything.

As for moving it to the ICommonFile interface, i have some questions that would need answer before doing that. In case it is moved, then...

  • I take that thumbnailList on IOutput should have its type changed to ICommonFile?

When media files are retrived from CCG, they do not include the thumbnails, those are retrieved in a seperate call to CCG. The code would have to be updated to merge the thumbnail data from the one call, into the media files from the other call.

This is well out of scope for this task, i'd say, even if considered as boy-scouting.

Copy link
Contributor

@olzzon olzzon Mar 1, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@andr9528 I can't recall why i made it optional.
Could be related to whether the CasparCG scanner is running or not, or som other case. But it could also be an old reminiscence of something.
As you point out the reason for having it separate is that it's received separate from CCG. The other plan for it, was as far as i remember to have all media thumbnails available on the server, but only the one needed in the client. But it's been a long time since i looked at it, so if you wan't to know for sure, I need to spend a couple of hours digging in to it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@olzzon I'm not going to be moving that attribute for this PR.
It would be a nice-to-know for in the future, but no rush.

Also that was a quick response from you. I didn't even ping you.

}
export interface IMedia {
output: IOutput[]
folderList: string[]
hiddenFiles: Record<string, IHiddenFileInfo>

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this really need to be a Record?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried with a Map at first, but couldn't get it to work, so Anders came and helped me, and together we found out that Redux doesn't support maps. Redux does however support Records.

return nextState
}

function doesChannelExist(nextState: any, action: any) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is that, or using a type that would fill a couple of lines.

It's Typescript so just write the types needed for your actual function and nothing more (as Anders points out).
Ideally there would be an interface that all States uses and the same for Actions

refactor: renames a few constants to use SNAKE_CASE.
refactor: renames a function to a more generic name.
chore: changes use of double quotes for string to single quotes a few places.
refactor: adds a better type than 'any' to a few places.
refactor: renames 'findThumbPix' to 'findThumbnail'.
refactor: removes export on 'isThumbWithTally'.
refactor: renames 'IHiddenFileInfo' to 'HiddenFileInfo'.
refactor: renames 'IThumbFile' to 'IThumbnailFile'.
refactor: adds some types to better describe what is being manipulated.
style: removes some css that had no visual effect from what i could tell.
const tallyFileName = getActiveOutput(reduxState)
function isThumbnailWithTallyOnAnyOutput(thumbnailName: string): boolean {
const outputs = reduxState.media[0].output
for (const output of outputs) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In general you should try to avoid the use of loops (they do have their place, but it's becoming more rare). You can use Array.some() for the same behaviour

function isThumbnailWithAllayOnAnyOutput(...): boolean {
  return reduxState.media[0].ouput.some(output => getCleanTallyFile(output) === thumbnailName)
}

Copy link
Contributor Author

@andr9528 andr9528 Mar 6, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have applied the suggested change.

In the case of the loop, it did exit early, just as the .some(...) does, the loop just had a larger footprint. Not saying the loop is better - just pointing out that it did exit early.

@@ -91,8 +105,11 @@ function handleClickMedia(fileName: string): void {
}

function toggleVisibility(fileName: string) {
if (isThumbWithTally(fileName))
if (isThumbnailWithTallyOnAnyOutput(fileName)) {
alert('Unable to hide, as file is in use somewhere.')

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm taking a guess here and assume you want it to say as a file?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The alert is triggered when a user clicks to try and hide a file which currently is in use on another output. Thus it is 'Unable to hide, as the file is in use somewhere.' Added a 'the' to further specify that the file in question is the one clicked.

@@ -1,13 +1,27 @@
/* Main CSS file */
html {
height: 100%;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That still worries me and suggest to me that there is somewhere else the CSS isn't right, since this is needed...

changed: number
size: number
}

interface ICommonFile extends IHiddenFileInfo {
interface IFile extends HiddenFileInfo {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since you are already here refactoring, you should also remove the "I" prefix

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

'I' prefix has been removed.

@andr9528 andr9528 merged commit 73a60f6 into develop Mar 9, 2023
@andr9528 andr9528 deleted the UT-206/softDelete branch March 9, 2023 08:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants