-
Notifications
You must be signed in to change notification settings - Fork 3
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
Conversation
… 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.
src/model/reducers/mediaReducer.ts
Outdated
return nextState | ||
} | ||
|
||
function conditionalApplyAction( |
There was a problem hiding this comment.
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
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Noted, and Applied.
src/client/components/Header.tsx
Outdated
) | ||
} | ||
|
||
function isHidingStyle() { |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
src/model/SocketIoConstants.ts
Outdated
@@ -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' |
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
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'.
src/model/reducers/mediaActions.ts
Outdated
@@ -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' |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Updated
src/model/reducers/mediaActions.ts
Outdated
@@ -97,3 +98,11 @@ export const setTime = (channelIndex: number, time: [number, number]) => { | |||
time: time, | |||
} | |||
} | |||
|
|||
export const setHide = (channelIndex: number, hideState: boolean) => { |
There was a problem hiding this comment.
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
}
}
There was a problem hiding this comment.
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.
There was a problem hiding this 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/Thumbnail.tsx
Outdated
(storeUpdate: any) => | ||
storeUpdate.media[0].hiddenFiles |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
src/client/components/Thumbnail.tsx
Outdated
const visibilityState = useSelector( | ||
(storeUpdate: any) => | ||
storeUpdate.media[0].output[reduxState.appNav[0].activeTab].visibilityState | ||
) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
…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)
src/client/components/Footer.tsx
Outdated
import { reduxState } from '../../model/reducers/store' | ||
import * as IO from '../../model/SocketIoConstants' | ||
|
||
import '../css/Footer.css' |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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).
src/client/components/Footer.tsx
Outdated
import { socket } from '../util/SocketClientHandlers' | ||
|
||
const footerDescriptions = new Map<OperationMode, string>([ | ||
[OperationMode.EDIT_VISIBILITY, 'You are currently editing visibility of files.'], |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
src/client/components/Footer.tsx
Outdated
? <div className='Footer-flex'> | ||
<div className='Footer-text'> | ||
{footerDescriptions.has(operationMode) | ||
? footerDescriptions.get(operationMode).toUpperCase() |
There was a problem hiding this comment.
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>
)
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
src/client/components/Footer.tsx
Outdated
storeUpdate.media[0].output[reduxState.appNav[0].activeTab]?.operationMode) | ||
|
||
return ( | ||
<footer className='Footer'> |
There was a problem hiding this comment.
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'>
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
src/client/components/Settings.tsx
Outdated
@@ -27,6 +31,23 @@ const selectorColorStyles = { | |||
singleValue: (styles) => ({ ...styles, color: 'white' }), | |||
} | |||
|
|||
function handleToggleEditVisibilityMode() { |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
src/model/reducers/mediaReducer.ts
Outdated
return nextState | ||
} | ||
|
||
function doesChannelExist(nextState: any, action: any) { |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
}
There was a problem hiding this comment.
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[]
.
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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( |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 }
}
There was a problem hiding this comment.
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.
src/server/utils/ccgHandlerUtils.ts
Outdated
return true | ||
} | ||
} | ||
for (let i = 0; i < newList.length; i++) { |
There was a problem hiding this comment.
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)
}
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
…every hidden file inside one record.
There was a problem hiding this 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
storeUpdate.media[0].output[reduxState.appNav[0].activeTab]?.operationMode) | ||
|
||
return ( | ||
<footer className='Footer'> |
There was a problem hiding this comment.
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.
src/client/components/Footer.tsx
Outdated
import { socket } from '../util/SocketClientHandlers' | ||
|
||
const footerDescriptions = new Map<OperationMode, string>([ | ||
[OperationMode.EDIT_VISIBILITY, 'You are currently editing visibility of files.'], |
There was a problem hiding this comment.
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.
src/client/components/Thumbnail.tsx
Outdated
interface ThumbnailProps { | ||
file: IMediaFile | ||
} | ||
|
||
export const findThumbPix = (fileName: string, channelIndex: number) => { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
@@ -103,6 +143,45 @@ export function socketIoHandlers(socket: any) { | |||
}) | |||
} | |||
|
|||
function toggleHiddenFile( |
There was a problem hiding this comment.
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.
There was a problem hiding this 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! 🚀
const buttonText = 'Exit Edit Visibility' | ||
const descriptionText = 'You are currently editing the visibility of files.'.toUpperCase() |
There was a problem hiding this comment.
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
.
There was a problem hiding this comment.
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.
src/client/components/Thumbnail.tsx
Outdated
) ?? {} | ||
|
||
const classNames = [ | ||
"thumb", |
There was a problem hiding this comment.
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 :)
There was a problem hiding this comment.
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.
src/model/reducers/mediaReducer.ts
Outdated
return nextState | ||
} | ||
|
||
function doesChannelExist(nextState: any, action: any) { |
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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'> |
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
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.
src/client/components/Thumbnail.tsx
Outdated
return thumb?.thumbnail ?? '' | ||
} | ||
|
||
export const isThumbWithTally = (thumbName: string): boolean => { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
src/client/components/Thumbnail.tsx
Outdated
return store.media[0].output[activeTab] | ||
} | ||
|
||
export function findThumbPix(fileName: string, channelIndex: number): string { |
There was a problem hiding this comment.
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'?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Renamed to findThumbnail
.
src/model/reducers/mediaReducer.ts
Outdated
size: number | ||
} | ||
|
||
interface ICommonFile extends IHiddenFileInfo { |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
src/model/reducers/mediaReducer.ts
Outdated
changed: number | ||
size: number | ||
thumbnail?: any | ||
export interface IThumbFile extends ICommonFile { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ThumbnailFile
, not ThumbFile
There was a problem hiding this comment.
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.
src/model/reducers/mediaReducer.ts
Outdated
size: number | ||
thumbnail?: any | ||
export interface IThumbFile extends ICommonFile { | ||
thumbnail?: string |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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
onIOutput
should have its type changed toICommonFile
?
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
src/model/reducers/mediaReducer.ts
Outdated
} | ||
export interface IMedia { | ||
output: IOutput[] | ||
folderList: string[] | ||
hiddenFiles: Record<string, IHiddenFileInfo> |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
src/model/reducers/mediaReducer.ts
Outdated
return nextState | ||
} | ||
|
||
function doesChannelExist(nextState: any, action: any) { |
There was a problem hiding this comment.
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.
src/client/components/Thumbnail.tsx
Outdated
const tallyFileName = getActiveOutput(reduxState) | ||
function isThumbnailWithTallyOnAnyOutput(thumbnailName: string): boolean { | ||
const outputs = reduxState.media[0].output | ||
for (const output of outputs) { |
There was a problem hiding this comment.
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)
}
There was a problem hiding this comment.
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.
src/client/components/Thumbnail.tsx
Outdated
@@ -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.') |
There was a problem hiding this comment.
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
?
There was a problem hiding this comment.
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%; |
There was a problem hiding this comment.
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...
src/model/reducers/mediaReducer.ts
Outdated
changed: number | ||
size: number | ||
} | ||
|
||
interface ICommonFile extends IHiddenFileInfo { | ||
interface IFile extends HiddenFileInfo { |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
…or of .some(...).
Created as a draft PR, so developers (mainly Kasper) can come with feedback on changes prior to full on PR review.
PR reviewed for: