Skip to content

Commit

Permalink
Upgraded to latest React MVVM [release]
Browse files Browse the repository at this point in the history
  • Loading branch information
Andrei15193 committed Oct 27, 2024
1 parent 759fe7b commit 303a758
Show file tree
Hide file tree
Showing 74 changed files with 6,612 additions and 7,858 deletions.
32 changes: 16 additions & 16 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,17 @@ jobs:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4

- name: Setup .NET
uses: actions/setup-dotnet@v1
uses: actions/setup-dotnet@v4
with:
dotnet-version: 6.0.x
dotnet-version: 8

- name: Setup NodeJS
uses: actions/setup-node@v4
with:
node-version: '20.x'

- name: Restore dependencies
run: |
Expand All @@ -27,7 +32,7 @@ jobs:
- name: Build
run: |
npm run build -- --mode=production
npm run build -- -- --mode=production
dotnet build --configuration Release --no-restore
- name: Test
Expand All @@ -45,16 +50,11 @@ jobs:
rm appsettings.Development.json
zip -r ../HintKeep.zip .
- name: Azure Authentication
if: github.event_name == 'push' && contains(toJSON(github.event.commits.*.message), '[release]')
uses: azure/login@v1
- uses: Azure/[email protected]
with:
creds: ${{ secrets.AZURE_CREDENTIALS }}

- name: Deploy Application
if: github.event_name == 'push' && contains(toJSON(github.event.commits.*.message), '[release]')
shell: pwsh
run: |
az webapp stop --resource-group HintKeep --name HintKeep
az webapp deployment source config-zip --resource-group HintKeep --name HintKeep --src ./Publish/HintKeep.zip
az webapp start --resource-group HintKeep --name HintKeep
app-name: ''
publish-profile: ${{ secrets.PUBLISH_PROFILE }}
package: ./Publish/HintKeep.zip
type: zip
clean: true
restart: true
12 changes: 6 additions & 6 deletions HintKeep.Tests/HintKeep.Tests.csproj
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>

<IsPackable>false</IsPackable>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="CloudStub.Azure.Cosmos.Table" Version="1.0.0-alpha.1" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="6.0.7" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.2.0" />
<PackageReference Include="NSubstitute" Version="5.0.0" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="8.0.10" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
<PackageReference Include="NSubstitute" Version="5.1.0" />
<PackageReference Include="xunit" Version="2.9.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
Expand Down
13 changes: 6 additions & 7 deletions HintKeep/Client/components/alerts/alert.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,21 @@
import type { AlertViewModel } from '../../view-models/alerts-view-model';
import React from 'react';
import classnames from 'classnames';
import { watchViewModel } from 'react-model-view-viewmodel';
import { Message } from '../i18n';
import { useViewModel } from 'react-model-view-viewmodel';

import Style from './../style.scss';

export interface IAlertProps {
readonly $vm: AlertViewModel;
readonly alertViewModel: AlertViewModel;
};

export function Alert({ $vm }: IAlertProps): JSX.Element {
watchViewModel($vm);
export function Alert({ alertViewModel }: IAlertProps): JSX.Element {
useViewModel(alertViewModel);

return (
<div className={classnames(Style.m2, Style.alert, Style.alertDanger, Style.alertDismissible)} role="alert">
<Message id={$vm.message} />
<button type="button" className={Style.btnClose} onClick={() => $vm.dismiss()} />
<Message id={alertViewModel.message} />
<button type="button" className={Style.btnClose} onClick={() => alertViewModel.dismiss()} />
</div>
);
}
11 changes: 5 additions & 6 deletions HintKeep/Client/components/alerts/alerts.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
import React from 'react';
import { Alert } from './alert';
import { requireViewModel } from '../use-view-model';
import { watchCollection } from 'react-model-view-viewmodel';
import { useObservableCollection, useViewModelDependency } from 'react-model-view-viewmodel';
import { AlertsViewModel } from '../../view-models/alerts-view-model';

export function Alerts(): JSX.Element {
const $vm = requireViewModel(({ alertsViewModel }) => alertsViewModel);
watchCollection($vm.alerts);
const alertsViewModel = useViewModelDependency(AlertsViewModel);
useObservableCollection(alertsViewModel.alerts);

return (
<>
{$vm.alerts.map((alert, index) => <Alert key={index} $vm={alert} />)}
{alertsViewModel.alerts.map((alert, index) => <Alert key={index} alertViewModel={alert} />)}
</>
);
}
8 changes: 4 additions & 4 deletions HintKeep/Client/components/app.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
import type { PropsWithChildren } from 'react';
import React from 'react';
import { BrowserRouter, Navigate, Route, Routes } from 'react-router-dom';
import classnames from 'classnames';
import { Message } from './i18n';
import { Alerts } from './alerts';
import { useViewModel } from './use-view-model';
import { Login, Register, Confirmation, Recovery, PasswordReset, Extra, Accounts, AddAccount, EditAccount, AccountHints, DeletedAccountDetails, DeletedAccounts, TermsOfService } from './pages';
import { useViewModelDependency } from 'react-model-view-viewmodel';
import { SessionViewModel } from '../view-models/session-view-model';

import Style from './style.scss';

export function App(): JSX.Element {
const $vm = useViewModel(({ sessionViewModel }) => sessionViewModel);
const sessionViewModel = useViewModelDependency(SessionViewModel);

return (
<div className={classnames(Style.app, Style.m3, Style.border, Style.dFlex, Style.flexColumn, Style.flexFill)}>
Expand All @@ -20,7 +20,7 @@ export function App(): JSX.Element {
<BrowserRouter>
<Routes>
<Route path="/">
{$vm.isSessionActive
{sessionViewModel.isSessionActive
? <>
<Route path="extra" element={<Extra />} />
<Route path="accounts">
Expand Down
1 change: 0 additions & 1 deletion HintKeep/Client/components/conditionals/else.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import type { PropsWithChildren } from "react";
import React from "react";

export function Else({ children }: PropsWithChildren<{}>): JSX.Element {
return <>{children}</>;
Expand Down
1 change: 0 additions & 1 deletion HintKeep/Client/components/conditionals/then.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import type { PropsWithChildren } from "react";
import React from "react";

export function Then({ children }: PropsWithChildren<{}>): JSX.Element {
return <>{children}</>;
Expand Down
7 changes: 3 additions & 4 deletions HintKeep/Client/components/forms/checkbox-form-input.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import type { PropsWithChildren } from 'react';
import type { IInputProps } from './input';
import React from 'react';
import classnames from 'classnames';
import { watchViewModel } from 'react-model-view-viewmodel';
import { Input } from './input';
import { Message } from '../i18n';

import Style from '../style.scss';
import { useViewModel } from 'react-model-view-viewmodel';

export interface IFormCheckboxInputProps extends IInputProps {
readonly label: string;
Expand All @@ -15,7 +14,7 @@ export interface IFormCheckboxInputProps extends IInputProps {
}

export function FormCheckboxInput({ label, description, field, id, className, type, children, ...inputProps }: PropsWithChildren<IFormCheckboxInputProps>): JSX.Element {
watchViewModel(field, ['value', 'error']);
useViewModel(field);

return (
<div className={classnames(className, Style.formCheck)}>
Expand All @@ -24,7 +23,7 @@ export function FormCheckboxInput({ label, description, field, id, className, ty
<Message id={label} />
{children}
</label>
<div id={`${id}Feedback`} className={Style.invalidFeedback}>{field.error !== undefined && <Message id={field.error} />}</div>
<div id={`${id}Feedback`} className={Style.invalidFeedback}>{field.error !== null && <Message id={field.error} />}</div>
{description && <div className={Style.mt2}><Message id={description} /></div>}
</div>
);
Expand Down
7 changes: 3 additions & 4 deletions HintKeep/Client/components/forms/form-input.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import type { IInputProps } from './input';
import React from 'react';
import { watchViewModel } from 'react-model-view-viewmodel';
import { useViewModel } from 'react-model-view-viewmodel';
import { Input } from './input';
import { Message } from '../i18n';

Expand All @@ -13,13 +12,13 @@ export interface IFormInputProps extends IInputProps {
}

export function FormInput({ label, description, field, id, className, ...inputProps }: IFormInputProps): JSX.Element {
watchViewModel(field, ['error']);
useViewModel(field);

return (
<div className={className}>
<label htmlFor={id}><Message id={label} /></label>
<Input field={field} id={id} {...inputProps} />
<div id={`${id}Feedback`} className={Style.invalidFeedback}>{field.error !== undefined && <Message id={field.error} />}</div>
<div id={`${id}Feedback`} className={Style.invalidFeedback}>{field.error !== null && <Message id={field.error} />}</div>
{description && <div className={Style.mt2}><Message id={description} /></div>}
</div>
);
Expand Down
7 changes: 3 additions & 4 deletions HintKeep/Client/components/forms/form-text-area.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import type { ITextAreaProps } from './text-area';
import React from 'react';
import { watchViewModel } from 'react-model-view-viewmodel';
import { useViewModel } from 'react-model-view-viewmodel';
import { TextArea } from './text-area';
import { Message } from '../i18n';

Expand All @@ -13,13 +12,13 @@ export interface IFormInputProps extends ITextAreaProps {
}

export function FormTextArea({ label, description, field, id, className, ...textAreaProps }: IFormInputProps): JSX.Element {
watchViewModel(field, ['isTouched', 'error']);
useViewModel(field);

return (
<div className={className}>
<label htmlFor={id}><Message id={label} /></label>
<TextArea field={field} {...textAreaProps} />
<div id={`${id}Feedback`} className={Style.invalidFeedback}>{field.error !== undefined && <Message id={field.error} />}</div>
<div id={`${id}Feedback`} className={Style.invalidFeedback}>{field.error !== null && <Message id={field.error} />}</div>
{description && <div className={Style.mt2}><Message id={description} /></div>}
</div>
);
Expand Down
4 changes: 2 additions & 2 deletions HintKeep/Client/components/forms/get-validation-classes.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { IFormFieldViewModel } from 'react-model-view-viewmodel';
import type { HintKeepFormField } from '../../view-models/forms';
import Style from '../style.scss';

export function getValidationClasses(field: IFormFieldViewModel<any>): { [className: string]: boolean } {
export function getValidationClasses(field: HintKeepFormField<unknown>): { [className: string]: boolean } {
return {
[Style.isValid]: field.isTouched && field.isValid,
[Style.isInvalid]: field.isTouched && field.isInvalid
Expand Down
30 changes: 22 additions & 8 deletions HintKeep/Client/components/forms/input.tsx
Original file line number Diff line number Diff line change
@@ -1,27 +1,41 @@
import type { InputHTMLAttributes } from 'react'
import type { IFormFieldViewModel } from 'react-model-view-viewmodel';
import React, { useContext } from 'react';
import type { ChangeEvent, InputHTMLAttributes } from 'react'
import { useCallback, useContext } from 'react';
import classnames from 'classnames';
import { watchViewModel } from 'react-model-view-viewmodel';
import { getValidationClasses } from './get-validation-classes';
import { I18nContext } from '../i18n';
import { useViewModel } from 'react-model-view-viewmodel';
import type { HintKeepFormField } from '../../view-models/forms';

import Style from '../style.scss';

export interface IInputProps extends InputHTMLAttributes<HTMLInputElement> {
field: IFormFieldViewModel<any>
readonly field: HintKeepFormField<any>
}

export function Input({ field, placeholder, ...inputProps }: IInputProps): JSX.Element {
const messageResolver = useContext(I18nContext);
watchViewModel(field, ['value', 'isTouched']);
useViewModel(field);

const onFocusCallback = useCallback(
() => {
field.isTouched = true;
},
[field]
);

const onChangeCallback = useCallback(
(event: ChangeEvent<HTMLInputElement>) => {
field.value = event.target.value;
},
[field]
)

return (
<input
className={classnames(Style.formControl, getValidationClasses(field))}
onFocus={() => field.isTouched = true}
value={field.value}
onChange={ev => field.value = ev.target.value}
onFocus={onFocusCallback}
onChange={onChangeCallback}
placeholder={placeholder ? messageResolver.resolve(placeholder) : undefined}
autoComplete="off"
{...inputProps} />
Expand Down
10 changes: 5 additions & 5 deletions HintKeep/Client/components/forms/text-area.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
import type { TextareaHTMLAttributes } from 'react';
import type { IFormFieldViewModel } from 'react-model-view-viewmodel';
import React, { useContext } from 'react';
import { watchViewModel } from 'react-model-view-viewmodel';
import type { HintKeepFormField } from '../../view-models/forms';
import { useContext } from 'react';
import classnames from 'classnames';
import { I18nContext } from '../i18n';
import { getValidationClasses } from './get-validation-classes';
import { useViewModel } from 'react-model-view-viewmodel';

import Style from '../style.scss';

export interface ITextAreaProps extends TextareaHTMLAttributes<HTMLTextAreaElement> {
field: IFormFieldViewModel<string | undefined>
field: HintKeepFormField<string | undefined>
}

export function TextArea({ field, placeholder, ...textAreaProps }: ITextAreaProps): JSX.Element {
const messageResolver = useContext(I18nContext);
watchViewModel(field, ['value']);
useViewModel(field);

return (
<textarea
Expand Down
1 change: 0 additions & 1 deletion HintKeep/Client/components/i18n/i18n-consumer.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import type { ConsumerProps } from 'react';
import type { IMessageResolver } from './i18n-context';
import React from 'react';
import { I18nContext } from './i18n-context';

export function I18nConsumer(props: ConsumerProps<IMessageResolver>): JSX.Element {
Expand Down
4 changes: 2 additions & 2 deletions HintKeep/Client/components/i18n/i18n-context.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { IMessages } from '../../translations/IMessages';
import React from 'react';
import { createContext } from 'react';

export interface IMessageResolver {
resolve(key: string, values?: any): string;
Expand Down Expand Up @@ -31,4 +31,4 @@ export class MessageResolver implements IMessageResolver {
}
}

export const I18nContext = React.createContext<IMessageResolver>(new EmptyMessageResolver());
export const I18nContext = createContext<IMessageResolver>(new EmptyMessageResolver());
6 changes: 3 additions & 3 deletions HintKeep/Client/components/i18n/i18n-provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@ import type { PropsWithChildren } from 'react';
import type { AxiosResponse } from 'axios';
import type { IMessages } from '../../translations/IMessages';
import type { IResponseData } from '../../api/users/preferences/preferred-languages/get';
import React, { useState, useEffect } from 'react';
import { useState, useEffect } from 'react';
import { enGB } from '../../translations';
import { Spinner } from '../loaders';
import { I18nContext, EmptyMessageResolver, MessageResolver } from './i18n-context';
import { Axios } from '../../services';
import { AxiosInstance } from '../../services';

const defaultLocale = 'en-GB';
const messagesByLanguage: { [locale: string]: IMessages } = {
Expand All @@ -20,7 +20,7 @@ export function I18nProvider({ children }: PropsWithChildren<{}>): JSX.Element {
useEffect(
() => {
setIsLoading(true);
Axios
AxiosInstance
.get('/api/users/preferences/preferred-languages')
.then((response: AxiosResponse<IResponseData>) => {
const locale = response.status === 200
Expand Down
1 change: 0 additions & 1 deletion HintKeep/Client/components/i18n/message.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import React from 'react';
import { I18nConsumer } from './i18n-consumer';

export interface IMessageProps {
Expand Down
Loading

0 comments on commit 303a758

Please sign in to comment.