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-210: Rebuilding of the code structure for Cliptool. #81

Merged
merged 114 commits into from
Jul 14, 2023

Conversation

andr9528
Copy link
Contributor

@andr9528 andr9528 commented Mar 7, 2023

A PR for changes related to UT-210 and UT-178.

  • Apply types to functions, constants, etc.
  • Define functions as functions, not constants with arrow functions.
  • Split React Components into multiple files.
  • Simplify Redux type, by eliminating the need to apply indexing, for as many of the contained types as possible.
  • Update Css class names to use kebab-case.
  • Update files and folders to use kebab-case.
  • Fix remaining errors/issues/todos.

refactor: divides down Thumbnail.tsx into files such that there is maximum one component per file.
refactor: extracts much of the common logic used between thumbnail components to mediaService.ts.
refactor: updates references to the Thumbnail.tsx to its new locations.
refactor: adds explicit types on constants and functions' arguments and return.
refactor: divides down Header.tsx into files such that there is maximum one component per file.
refactor: renames 'manualstartState' to 'manualStartState'.
fix: fixes header not showing the thumbnail as it was suppose too.
# Conflicts:
#	src/client/components/Tab/TabContent.tsx
refactor: splits last parts of Settings.tsx into other files.
refactor: renames the constant being exported from mediaService.ts to mediaService, so Intellisense is able to aid with adding import for it.
refactor: moves some styling applied inline to CSS.
…l crash at the moment, and toggling settings doesn't work..

refactor: renames interfaces in settingsReducer to not have 'I' in their names.
refactor adds placeholder user confirmations when saving/discarding settings changes.
refactor: extracts input layouts for settings to a reusable component.
refactor: fixes typo in 'transistionTime' to 'transitionTime'.
refactor: changes arrays of 'GenericSettings' into a single array of 'OutputSettings', each containing an attribute for the previous arrays.
refactor: removed most code in 'correctStructure(...)' and verifyStructure(...), as they have to be redone with the new settings structure.
@andr9528 andr9528 mentioned this pull request Mar 10, 2023
Base automatically changed from UT-211/reimplementTabs to develop March 10, 2023 13:21
# Conflicts:
#	src/client/components/Header.tsx
#	src/client/components/Settings.tsx
#	src/client/components/Thumbnail.tsx
…ved in redux

refactor: adds and implements Zod, to handle validation of the settings file loaded from drive.
refactor: updates used version of typescript.
refactor: reimplements validation of loaded settings, by use of Zod.
refactor: shifts many mediaActions over to settingsAction as the data they handle going to be stored in there instead.
refactor: create few service classes with shared logic.
refactor: updates a number of places to use the new service classes.
refactor: created type for reduxState, and applies it in a few dozen places.
refactor: renamed 'IThumbnailFile' to 'ThumbnailFile'.
refactor: renamed 'IMediaFile' to 'MediaFile'.
refactor: renamed 'IOutput' to 'Output'.
refactor: renamed 'IMedia' to 'Media'.
refactor: moves and renames Output.tallyFile to OutputSettings.selectedFile.
refactor: moves service classes from being client only to model level, as backend want many of the same functions.
refactor: extracts interfaces from mediaReducer to mediaModels, for consistencies sake.
refactor: implements the settings view sufficiently to not class cliptool.
refactor: makes SingleOutput only render, if it isn't undefined.
refactor: changes useState in SingleOutput to be one per attribute.
refactor: adds 'deep-copy-ts' to create a deep copy of settings, to allow discarding changes, should the user not want them anyways.
…g on if there is changes or not.

refactor: changes 'deep-copy-ts' for 'lodash' as lodash has other functions i need anyways.
…iminates some duplicate code.

refactor: changes functions defined as constants to function definitions.
refactor: removes/merges duplicate code to a single implementation(s).
refactor: adds return type to most, if not all, functions.
…eived. Applies changes for GeneralSettings that was missed.

refactor: updates GeneralSettings to support the new structure for allowing discarding changes.
refactor: adds a half dozen or so temp console.log used to try and track down why selectedFile is not set.
refactor: removes 'I' from interface name of 'TimeTallyPayload'.
refactor: renames 'handleOscMessage' to 'processOscMessage'.
refactor: renames 'updateThumbFileList' to 'updateThumbnailFileList' along with associated constants.
@andr9528
Copy link
Contributor Author

If any horses are found let me know. In that case i have propably missed renaming something back to it correct name, while doing the renaming.

… attributes.

refactor: changes types in reduxState to be ordinary objects, instead of array of said objects.
refactor: changes many commonly accessed attributes from reduxState to be retrieved via the service classes.
refactor: renamed one file i missed in the mass renaming to camelCase.
refactor: removes a few left over console.logs.
refactor: renames checkbox-input.tsx to checkbox.tsx.
refactor: renames css class 'save-button' to 'c-settings-actions-button'.
refactor: changes type of child input for button to a string.
refactor: changes used button in thumbnail-button.tsx to a 'div'.
refactor: extracts thumbnail file name display paragraph to new component - file-name-display.tsx.
…ails for list of changes.

refactor: renames 'models' layer to 'shared'.
refactor: moves action files from 'reducer' folder to new 'actions' folder.
refactor: moves model files from 'reducer' folder to new 'models' folder.

refactor: renames thumbnail-button to media-card.
refactor: renames image-thumbnail to image-media-card.
refactor: renames text-thumbnail to text-media-card.
refactor: renames thumbnails to media-overview.
refactor: renames client-handler-service to backend-observer.
refactor: renames CcgConfigChannel to CasparcgConfigChannel.
refactor: renames toggle to switch.
refactor: renames thumbnail-overlay-display to card-overlay-display.
refactor: renames thumbnail-overlay to card-overlay.

refactor: merges thumbnail-picture into image-media-card.
refactor: removes event-service by moving its usage to inline.
refactor: splits emits from socket-service out to multiple backend api services.
refactor: makes tab-title into a dumb component.
…t details for changes.

refactor: moves client specific logic of time-service to new client-time-service.
refactor: moves type definition for 'HiddenFiles' to media-models.
refactor: changes types related to HiddenFIles from 'Record<string, HiddenFileInfo>' to 'HiddenFiles'.
refactor: splits settings-persistence-service.load() into multiple methods to improve readability/flow.
refactor: splits hidden-files-persistence-service.load() into multiple methods to improve readability/flow.
refactor: renames a few arguements/enums to better match their content.
refactor: updates some enum values to use kebab-case.

fix: fixes casing of one argument of the old-settings-schema.
fix: fixes grayscale not being applied to hidden images.
fix: fixes backend-observer never being called, and thus nothing received from backend.
…gs-service.

refactor: renames tab-panel to main-page.
refactor: renames settings to settings-page.
refactor: moves logic in changing-settings-service to settings-actions and settings-page.
refactor: moves the task of displaying the header and footer from the overall app, to the individual pages, which is only main-page at the moment.
…nents and changes calls for events to be performed via bindings.
…ic attribute on each service and changes the import of the services to not use default import.
…type.

refactor: changes to checks for new layout structure before old.
refactor: removes defaults from new-settings-schema.
refactor: lowers amount of OutputSettings in the fallback default.
refactor: makes startup sequence fill in extra default OutputSettings, if file contained too few compared to configured channels.
refactor: reduces redux dispactches by changing used action, during startup.
refactor: updates usage of 'fs' to use promise based versions.
refactor: moves main part of forEach to new function for readability.
Copy link

@LindvedKrvang LindvedKrvang left a comment

Choose a reason for hiding this comment

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

There is a lot of things that needs to be addressed that I skipped for the sake of getting this PR merged.

There are however some things that must be addressed. I have commented many of them, but I need to emphasize two things:

The first thing, and by far the worst one: Your usage of singletons.
For some reason you have made an abundance of the classes singletons. I counted one place where a singleton might be suitable. Only one place.

A singleton is used to ensure that all consumers of a class calls the same data/state. This is not a need you have in almost all of your classses. Besides, this project used Redux which accomplies the same thing, so one more reason not to use singletons.

Many of the classes you have made singletons are "utility" classes. I.e. classes that just need an input and then provides an output. Classes that don't care about state at all. Since singletons is for classes that cares about state, then these "utility" classes should not be singletons.

What's even worse is that you call the instance method of your singletons over and over again in the classes that you use them in. This is no better than just making each method static and call the static method (especially because your classes allready don't care about state).

The second thing I need to address is the folder structure. It seems like you are on the right track, but haven't fully understood how it should be.

As we talked about, we want to separate our folders by feature/pages. That means that the folder structure should look like this:

-src/
  -featureX/
    -components/
      -componentA/
      -componentB/
    -services/
  -featureY/
    -components/
      -componentC/
      -componentD/
    -services/
      -someService/
      -someOtherService/
  -shared/
    -components/
      -componentE/
      -componentF/
    -services/

What this would like in this project would be something like this:

-src/
  -frontPage/
    -components/
      -FrontPageComponent
      ...
  -settings/
    -components/
      -SettingsPageComponent
      ...
  -shared/
    -components/
      -HeaderComponent
      -FooterComponent

import { reduxStore } from '../../../../shared/store'
import { toggleSettingsVisibility } from '../../../../shared/actions/app-navigation-action'

export default function ApplicationHeader(): JSX.Element {

Choose a reason for hiding this comment

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

Just call it Header. What information do Application give that is not allready in Header?

Copy link
Contributor

Choose a reason for hiding this comment

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

@LindvedKrvang Personally I think Header is too generic. I would expect some sort of generic header element if the component is called Header. Application might not be the best fit, but then we could have a look at what kind of header this is and give it an appropriate name. Since the primary focus of the header is to display information about what media is playing and how it is played, then perhaps MediaHeader, MediaPlayerHeader or MediaControlHeader? Other suggestions are welcomed 😁

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 it back to Header. Can't fully remember why Application was added, just that it was Anders who suggested it during our talk about a number of comments.

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 Anders late comment, i broke the Header appart such that:
1.Tthe header parts that should always be there are in a Header component under shared.
2. A more specific version exist under main-page called MediaControlHeader, which build upon the base and adds whats needed for a header specific to main-page.

Hope this satifies both parties.

const isHidden: boolean = props.file.name in hiddenFiles

return (
<div className={`c-image-media-card ${isHidden ? 'hidden' : ''}`}>

Choose a reason for hiding this comment

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

Why do you need a CSS class to hide the component? You could just not render it if it shouldn't be show:

return (
  {!isHidden && (
    <div>...</div>
  )}
)

Choose a reason for hiding this comment

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

But this is also ties into the discussion: "It's weird that you render a component that then decides not to render itself." It should be the calling component that figures out if it should render its child components or 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.

hidden is this case applies some css to indicate that the card in question is hidden, while not Editting Visibility of cards. For this PR, it is only some sizing, but already in #84 / UT-216 this is expanded to add more css styling.

Maybe the constant needs a better name, just not sure what...

} from '../../shared/actions/settings-action'
import { SocketService } from './socket-service'

export class BackendObserver {

Choose a reason for hiding this comment

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

Why is this called Backend? It's a service that lives in the frontend so it shouldn't be named "Backend" (it also should not be named "Frontend").

What does it observe?

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 observers, or maybe a better word listens, for event from the backend.

The new name was created following 1 or more comments made during the long PR review. You might have left before they left that/those comment(s) though...


public startBackendObserver(): void {
console.log('Socket Initialized', SocketService.instance.getSocket())
console.log('BackendObserver: Monitoring messages from socket...')

Choose a reason for hiding this comment

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

This message indicated that a more apt name for this class would be SocketObserver

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 class makes use of a socket for the communication at the moment, which does support your comment, but it is also a implementation detail, which afaik should not be part of the name.

import { ClientToServerCommand } from '../../shared/socket-io-constants'
import { SocketService } from './socket-service'

export class BackendPlayApi {

Choose a reason for hiding this comment

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

You should not be naming your files in the frontend BackendX. (goes for all files that you have named backend)
Instead you should have a form of service that does what you want.
In this case BackendPlayApi would be more aptly named PlayService.

First of, this is not the backend. The file tries to call something on the backend, but that just not justify calling this backend.
What if you don't need to call the backend at all in order to play files? Then you would have to rename your class.
It's an implementation detail whether or not this class calls the backend and we don't expose implementation details.

Second: This is not an API, so it shouldn't be called Api.

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 get what is being said, and will gladly make that change, but i would like a talk about it together with Anders too, as the names was sorta his suggestion, and also matches with how the domain communication/naming should be when UT-203 is eventually done.

import { SocketService } from './socket-service'

export class BackendPlayApi {
static readonly instance = new BackendPlayApi(

Choose a reason for hiding this comment

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

Why is this a singleton?
I get that you want to reuse the Socket connection, but you are allready using a singleton for the Socket, so there is absolutely no need for this also being a singleton (this applies to basically all the singletons you made).

Also, when you make a singleton you must make the constructor private as well.
There is currently nothing stopping me from writing new BackendPlayApi(SocketService.instance.getSocket()) and completely bypass your singleton.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

All but one service have been made not singleton.

src/server/index.ts Show resolved Hide resolved
'File containing hidden files not found, or not yet stored.'
)
}
PersistenceService.instance

Choose a reason for hiding this comment

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

You should not be calling you singletons instance methods every single time you need them. This is a very bad practice and this is basically the same as you just making all the methods on the class static.
How are you going to mock away the dependency in a unit test?
How are you going to provide a different implementation without having to change everywhere you use 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.

UT-221 exists to specifically address this.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

All but one service is no longer singleton.

class PersistanceService {
public loadFile(fileName: string): any {
return fs.readFileSync(path.resolve('storage', fileName))
export class PersistenceService {

Choose a reason for hiding this comment

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

This class is reading and writing to files. There is no state saved in the class. There is no need for this class to be a singleton.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

No longer a singleton.

@@ -0,0 +1,19 @@
export class ArrayService {
static readonly instance = new ArrayService()
fillWithDefault<T>(

Choose a reason for hiding this comment

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

This class is completely redundant. The class does one thing and that one thing is allready provided by default in Typescript as Array.fill().

This class should be deleted.

Also, you made it a singleton. Singleton should only be used when you want to ensure all consumers accesses the same data/state. This is a utility service, so there is neither class/nor state in here, so this should not be a singleton.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

First, it is no longer a singleton.

Secondly, does the Array.fill() work to fill up an existing array with more/new values?
Say you have an array with 3 numbers (eg. 1, 2 and 3) and then wants to fill in more numbers up to 8, with some default, does it work for that?
From what i can tell Array.fill() will not expand an array when used on an existing array, it will only overwrite existing slots.

refactor: removes singleton usage for all but one services.
refactor: renames backend-play-api to socket-play-service.
refactor: renames backend-settings-api to socket-settings-service.
refactor: renames backend-operation-api to socket-operation-service.
refactor: moves backend-observer to new observer folder, and splits it into connection-observer, settings-observer, operation-observer and play-observer.
refactor: renames array-service to utility-service.
refactor: renames fillWithDefault to expandArrayWithDefaultsIfNeeded
refactor: adds function on utility-service to convert Pascal case to Screaming Snake case and vice-versa.
refactor: changes the 'edit visibility' button to be a dropdown per output.
refactor: renames a function argument to better describe its content.
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 approve under the assumption that the comments I have provided are addressed.
Some of them might be outdated, in which case you can just ignore them. I had some comments I made some time ago, that I forgot to commit – sorry about that :/

.gitignore Outdated Show resolved Hide resolved
import { reduxStore } from '../../../../shared/store'
import { toggleSettingsVisibility } from '../../../../shared/actions/app-navigation-action'

export default function ApplicationHeader(): JSX.Element {
Copy link
Contributor

Choose a reason for hiding this comment

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

@LindvedKrvang Personally I think Header is too generic. I would expect some sort of generic header element if the component is called Header. Application might not be the best fit, but then we could have a look at what kind of header this is and give it an appropriate name. Since the primary focus of the header is to display information about what media is playing and how it is played, then perhaps MediaHeader, MediaPlayerHeader or MediaControlHeader? Other suggestions are welcomed 😁

@@ -1,4 +1,4 @@
.c-thumbnail-overlay-display {
.c-card-overlay-display {
Copy link
Contributor

Choose a reason for hiding this comment

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

The class names are inconsistent. Sometimes card is used as a prefix and at other times it is used as a suffix. This should be consistent.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

So we have the actual cards using card as suffix (image-media-card, text-media-card & media-card). Last of those is the base component used by the other two.

Then we have the ones that use card as prefix (card-overlay, card-overlay-display, and kinda selected-card-overlay).
card-overlay and selected-card-overlay are displayed by card-overlay-display when the correct conditions are met.

card-overlay-display are displayed on image-media-card and text-media-card.

When used as a prefix, it it to indicate that they are to be used one af card. Whereas when used as a suffix, it is to define the components as a card.

Not sure if the names still needs to change, and if so, to what...

Copy link
Contributor

Choose a reason for hiding this comment

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

The "card" flow seems a bit too complex. It will be a good candidate for a round 2 refactor at some later point :D

Comment on lines 73 to 74
if (new ReduxSettingsService().isCardSelectedOnAnyOutput(fileName, state.settings)
|| new ReduxSettingsService().isCardCuedOnAnyOutput(fileName, state.settings)) {
Copy link
Contributor

Choose a reason for hiding this comment

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

In this case it would make sense to store the ReduxSettingsService instance in a local variable, since it might be used multiple times.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Same resonse as this: #81 (comment)

Comment on lines 22 to 23
const cleanFileName: string = new ReduxSettingsService().getCleanSelectedFileName(settingsOutput, state.settings)
|| new ReduxSettingsService().getCleanCuedFileName(settingsOutput, state.settings)
Copy link
Contributor

Choose a reason for hiding this comment

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

It makes sense to store the ReduxSettingsService in a local variable, since it might get used multiple times in this scope.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

On one hand yes to reduce how many times it gets created.
But on the other hand no, cause then it will hardly ever get gc'ed, which was one of the reason for it not being singleton.

Copy link
Contributor

Choose a reason for hiding this comment

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

If you instantiate it as a local variable, then at the end of the scope, the variable along with the instance should be collected :)

@@ -1,19 +1,18 @@
import React from "react"
import './number-input.scss'
import '../label/label.scss'
import '../shared.scss'
Copy link
Contributor

Choose a reason for hiding this comment

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

Why is the file called shared.scss?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Because the css defined in it is shared between a handful of the different settings page components.
Not sure what other name might fit better.

Copy link
Contributor

Choose a reason for hiding this comment

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

Calling it shared and having multiple different parts defined in here invites for other unrelated stuff to crawl in. In my opinion it could be split. I can see that a dedicated file for input styling was removed and the contents moved to shared.scss.

I will leave it be for this PR, but it would be nice at some point to have the CSS properly organized :)

refactor: updates a number of css classes to better follow BEMIT
refactor: updates a number of arguemnts to better explain their content.
refactor: moves many end parentheses to new line.
refactor: split header component in two. One for base, and one specific for main-page.
refactor: deletes tab.tsx by moving what it did to main-page.tsx.
refactor: fixes typo.
refactor: extracts creation of most services to start of component/function/file.
refactor: re-adds switch to convert enum value to css class.
…al mass preventing startup.

refactor: converts express-handler into express-service.
refactor: converts socket-io-server-handler into socket-io-server-handler-service.
refactor: converts parts of caspar-cg-handler into casparcg-handler-service.
@andr9528 andr9528 merged commit 3a66048 into develop Jul 14, 2023
@andr9528 andr9528 deleted the UT-210/refactorStructure branch July 14, 2023 11:17
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