Commit 2a6e33a7 authored by Hugo NOUTS's avatar Hugo NOUTS
Browse files

Merge branch 'feat/Add-optional-modal-following-migrations' into 'v1.4.3'

feat(ReleaseNotes): Added a release note modal to show user new functional evolution after updating

See merge request web-et-numerique/llle_project/ecolyo!475
parents 42f19a5d 8df5cce3
......@@ -18,6 +18,8 @@ import KonnectorViewerCard from 'components/Konnector/KonnectorViewerCard'
import KonnectorViewerList from 'components/Konnector/KonnectorViewerList'
import classNames from 'classnames'
import { isKonnectorActive } from 'utils/utils'
import ReleaseNotesModal from './releaseNotesModal'
import { showReleaseNotes } from 'store/global/global.actions'
interface ConsumptionViewProps {
fluidType: FluidType
......@@ -29,8 +31,15 @@ const ConsumptionView: React.FC<ConsumptionViewProps> = ({
const { currentTimeStep, loading } = useSelector(
(state: AppStore) => state.ecolyo.chart
)
const { fluidStatus } = useSelector((state: AppStore) => state.ecolyo.global)
const { fluidStatus, releaseNotes } = useSelector(
(state: AppStore) => state.ecolyo.global
)
const [isFluidKonnected, setIsFluidKonnected] = useState<boolean>(false)
const [openReleaseNoteModal, setOpenReleaseNoteModal] = useState<boolean>(
releaseNotes.show
)
const [headerHeight, setHeaderHeight] = useState<number>(0)
const [isMulti] = useState<boolean>(
fluidType === FluidType.MULTIFLUID ? true : false
......@@ -48,6 +57,11 @@ const ConsumptionView: React.FC<ConsumptionViewProps> = ({
setHeaderHeight(height)
}, [])
const toggleReleaseNoteModal = useCallback(() => {
setOpenReleaseNoteModal(prev => !prev)
dispatch(showReleaseNotes(false, releaseNotes.notes))
}, [])
useEffect(() => {
setIsFluidKonnected(isKonnectorActive(fluidStatus, fluidType))
if (
......@@ -69,6 +83,12 @@ const ConsumptionView: React.FC<ConsumptionViewProps> = ({
<FluidButtons activeFluid={fluidType} key={lastDataDate} />
</Header>
<Content height={headerHeight}>
{openReleaseNoteModal && (
<ReleaseNotesModal
open={openReleaseNoteModal}
handleCloseClick={toggleReleaseNoteModal}
></ReleaseNotesModal>
)}
{isFluidKonnected ? (
<>
{loading && (
......
@import '../../styles/base/color';
@import '../../styles/base/breakpoint';
.release-root.black {
.modal-overlay {
.modal-close-button {
display: none;
}
}
}
.release-note-container {
border-radius: 4px;
margin-bottom: 1rem;
color: $grey-bright;
.em-content {
padding: 1rem;
}
.release-note-title {
color: $gold-shadow;
margin-bottom: 2rem;
}
.release-note-button {
display: flex;
justify-content: center;
margin-top: 2rem;
button {
&.btn-highlight,
&.btn-secondary-positive {
width: 45%;
margin-bottom: 0;
}
&.btn-secondary-positive {
padding: 0.5rem 1rem;
}
&.btn-highlight {
padding: 0.25rem 0.5rem;
}
}
@media #{$large-phone} {
flex-direction: column-reverse;
button {
&.btn-highlight,
&.btn-secondary-positive {
margin-bottom: 0;
width: 100%;
height: 45px;
}
}
}
}
.release-note-part {
margin-top: 0.5rem;
}
.release-note-description {
padding-top: 0.5rem;
padding-bottom: 0.5rem;
}
}
#accessibility-title {
display: none;
}
import React from 'react'
import { useI18n } from 'cozy-ui/transpiled/react/I18n'
import Button from '@material-ui/core/Button'
import './releaseNotesModal.scss'
import Dialog from '@material-ui/core/Dialog'
import { AppStore } from 'store'
import { useSelector } from 'react-redux'
interface ReleaseNotesModalProps {
open: boolean
handleCloseClick: () => void
}
const ReleaseNotesModal: React.FC<ReleaseNotesModalProps> = ({
open,
handleCloseClick,
}: ReleaseNotesModalProps) => {
const { t } = useI18n()
const { releaseNotes } = useSelector((state: AppStore) => state.ecolyo.global)
return (
<Dialog
open={open}
disableBackdropClick
disableEscapeKeyDown
onClose={handleCloseClick}
aria-labelledby={'accessibility-title'}
classes={{
root: 'modal-root',
paper: 'modal-paper',
}}
>
<div id={'accessibility-title'}>
{t(
'consumption_visualizer.release_notes_modal.accessibility.window_title'
)}
</div>
<div className="em-root release-note-container">
<div className="em-content">
<div className="release-note-title text-20-bold">
{t('consumption_visualizer.release_notes_modal.title')}
</div>
<div className="release-note-message text-16-bold">
{t('consumption_visualizer.release_notes_modal.message')}
</div>
<div className="release-note-message text-16-normal">
{releaseNotes.notes.length > 0 &&
releaseNotes.notes.map((note, index) => (
<div key={index} className="release-note-part">
<div className="release-note-message text-16-bold">
{note.title}
</div>
<div className="release-note-description text-16-normal">
{note.description}
</div>
</div>
))}
</div>
<div className="release-note-button">
<Button
aria-label={t(
'consumption_visualizer.release_notes_modal.accessibility.button_go_back'
)}
onClick={handleCloseClick}
classes={{
root: 'btn-highlight',
label: 'text-16-bold',
}}
>
{t('consumption_visualizer.release_notes_modal.go_back')}
</Button>
</div>
</div>
</div>
</Dialog>
)
}
export default ReleaseNotesModal
......@@ -16,6 +16,7 @@ import {
setFluidStatus,
GlobalActionTypes,
updateTermValidation,
showReleaseNotes,
} from 'store/global/global.actions'
import {
ProfileActionTypes,
......@@ -50,6 +51,7 @@ import UsageEventService from 'services/usageEvent.service'
import { UsageEventType } from 'enum/usageEvent.enum'
import { MigrationService } from 'migrations/migration.service'
import { migrations } from 'migrations/migration.data'
import { ReleaseNotes } from 'models/releaseNotes.model'
interface SplashRootProps {
fadeTimer?: number
......@@ -99,11 +101,16 @@ const SplashRoot = ({
async function loadData() {
const initializationService = new InitializationService(client)
const ms = new MigrationService(client)
await ms.runMigrations(migrations)
const migrationsResult: ReleaseNotes = await ms.runMigrations(migrations)
try {
// Init index
await initializationService.initIndex()
// Init last release notes when they exist
dispatch(
showReleaseNotes(migrationsResult.show, migrationsResult.notes)
)
//init Terms
const isLastTermAccepted = await initializationService.initConsent()
if (subscribed) dispatch(updateTermValidation(isLastTermAccepted))
......
......@@ -261,6 +261,15 @@
"list2": " : 1 kWh = 0,1121 €TTC (tarif réglementé de vente au 1/10/2021 pour un consommateur soutirant moins de 6 MWh par an)",
"list3": " : 1 litre d’eau = 0,00319 € TTC (prix constaté au 1/01/2021 pour un abonnement et une consommation de 120 m3/an sur la Métropole de Lyon)",
"part3": "À cela s'ajoute le coût de votre abonnement, le coût d'acheminement et les taxes qui représentent plus de 66% de votre facture."
},
"release_notes_modal": {
"title": "Du nouveau sur Ecolyo !",
"message": "Les mises à jour suivantes ont été effectuées sur votre application :",
"go_back": "Retour",
"accessibility": {
"window_title": "Fenêtre de notifications",
"button_go_back": "J'ai compris"
}
}
},
"duel": {
......
......@@ -3,9 +3,12 @@ import {
PROFILE_DOCTYPE,
PROFILETYPE_DOCTYPE,
USERCHALLENGE_DOCTYPE,
EGL_DAY_DOCTYPE,
EGL_MONTH_DOCTYPE,
EGL_YEAR_DOCTYPE,
} from 'doctypes'
import { Profile, ProfileType, UserChallenge } from 'models'
import { Client, Q, QueryDefinition, QueryResult } from 'cozy-client'
import { Client } from 'cozy-client'
import { DateTime } from 'luxon'
import { UserQuizState } from 'enum/userQuiz.enum'
......@@ -21,7 +24,8 @@ export const migrations: Migration[] = [
targetSchemaVersion: 1,
appVersion: '1.3.0',
description:
'Removes old profileType artifacts from users database : \n - Oldest profileType gets deleted \n - Removes insulation work form field prone to errors \n - Changes area & outsideFacingWalls form field to strings \n - Changes updateDate values of all existing profileType to match "created_at" entry (former updateDate values got corrupted and held no meaning).',
'Removes old profileType artifacts from users database : \n - Oldest profileType is deleted \n - Removes insulation work form fields that were prone to errors \n - Changes area and outsideFacingWalls form field to strings \n - Changes updateDate values of all existing profileType to match "created_at" entry (former updateDate values got corrupted and hold no meanings).',
releaseNotes: null,
docTypes: PROFILETYPE_DOCTYPE,
run: async (_client: Client, docs: any[]): Promise<ProfileType[]> => {
docs.sort(function(a, b) {
......@@ -57,6 +61,7 @@ export const migrations: Migration[] = [
appVersion: '1.3.0',
description: 'Removes old profileType and GCUApprovalDate from profile.',
docTypes: PROFILE_DOCTYPE,
releaseNotes: null,
run: async (_client: Client, docs: any[]): Promise<Profile[]> => {
return docs.map(doc => {
if (doc.GCUApprovalDate) {
......@@ -75,6 +80,7 @@ export const migrations: Migration[] = [
appVersion: '1.3.0',
description:
'Updates userChallenges to make sure no quiz results are overflowing.',
releaseNotes: null,
docTypes: USERCHALLENGE_DOCTYPE,
run: async (_client: Client, docs: any[]): Promise<UserChallenge[]> => {
return docs.map(doc => {
......@@ -91,4 +97,66 @@ export const migrations: Migration[] = [
})
},
},
{
baseSchemaVersion: 3,
targetSchemaVersion: 4,
appVersion: '1.4.3',
description: 'Correction de vos données Eau du Grandlyon journalières.',
releaseNotes: {
title:
"Des corrections sur la connexion aux données de consommation d'eau ont été apportées.",
description:
'Merci de mettre à jour votre connecteur après avoir fermé cette fenêtre. Pour mettre à jour votre connecteur, rendez-vous maintenant du côté de la page Conso, dans la page du fluide concerné.',
},
docTypes: EGL_DAY_DOCTYPE,
queryOptions: {
scope: 'conso',
tag: 'day',
limit: 120,
},
run: async (_client: Client, docs: any[]): Promise<any[]> => {
return docs.map(doc => {
doc.deleteAction = true
return doc
})
},
},
{
baseSchemaVersion: 4,
targetSchemaVersion: 5,
appVersion: '1.4.3',
description: 'Correction de vos données Eau du Grandlyon mensuelles.',
releaseNotes: null,
docTypes: EGL_MONTH_DOCTYPE,
queryOptions: {
scope: 'conso',
tag: 'month',
limit: 4,
},
run: async (_client: Client, docs: any[]): Promise<any[]> => {
return docs.map(doc => {
doc.deleteAction = true
return doc
})
},
},
{
baseSchemaVersion: 5,
targetSchemaVersion: 6,
appVersion: '1.4.3',
description: 'Correction de vos données Eau du Grandlyon annuelles.',
releaseNotes: null,
docTypes: EGL_YEAR_DOCTYPE,
queryOptions: {
scope: 'conso',
tag: 'year',
limit: 1,
},
run: async (_client: Client, docs: any[]): Promise<any[]> => {
return docs.map(doc => {
doc.deleteAction = true
return doc
})
},
},
]
import { Client } from 'cozy-client'
import { Migration, MigrationResult } from './migration.type'
import { migrationLog, migrate } from './migration'
import { MIGRATION_RESULT_FAILED } from './migration.data'
import {
MIGRATION_RESULT_COMPLETE,
MIGRATION_RESULT_FAILED,
} from './migration.data'
import log from 'utils/logger'
import { ReleaseNotes } from 'models/releaseNotes.model'
export class MigrationService {
private readonly _client: Client
......@@ -11,8 +15,18 @@ export class MigrationService {
this._client = _client
}
async runMigrations(migrations: Migration[]) {
async runMigrations(migrations: Migration[]): Promise<ReleaseNotes> {
log.info('[Migration] Running migrations...')
let releaseStatus = false
const releaseNotes: ReleaseNotes = {
show: releaseStatus,
notes: [
{
title: '',
description: '',
},
],
}
for (const migration of migrations) {
// First attempt
const migrationResult: MigrationResult = await migrate(
......@@ -34,7 +48,17 @@ export class MigrationService {
log.info(migrationLog(migration, result))
}
}
if (
migration.releaseNotes !== null &&
migrationResult.type === MIGRATION_RESULT_COMPLETE
) {
releaseNotes.notes.push(migration.releaseNotes)
releaseStatus = true
}
}
releaseNotes.show = releaseStatus
log.info('[Migration] Done')
return releaseNotes
}
}
import { Migration, MigrationResult } from './migration.type'
import {
Migration,
MigrationQueryOptions,
MigrationResult,
} from './migration.type'
import { Client, Q, QueryDefinition, QueryResult } from 'cozy-client'
import { SCHEMAS_DOCTYPE } from 'doctypes/com-grandlyon-ecolyo-schemas'
import log from 'utils/logger'
......@@ -31,8 +35,22 @@ async function currentSchemaVersion(_client: Client): Promise<number> {
* @param doctype
* @returns all documents of given doctype
*/
async function getDocs(_client: Client, doctype: string): Promise<any> {
const query: QueryDefinition = Q(doctype)
async function getDocs(
_client: Client,
doctype: string,
options?: MigrationQueryOptions
): Promise<any> {
let query: QueryDefinition
if (options && options.scope === 'conso') {
query = Q(doctype)
.where({})
.indexFields(['year', 'month', 'day'])
.sortBy([{ year: 'desc' }, { month: 'desc' }, { day: 'desc' }])
.limitBy(options.limit)
} else {
query = Q(doctype)
}
const data: QueryResult<any[]> = await _client.query(query)
return data.data
}
......@@ -118,7 +136,11 @@ export async function migrate(
} else {
let result: MigrationResult
try {
const docToUpdate: any[] = await getDocs(_client, migration.docTypes)
const docToUpdate: any[] = await getDocs(
_client,
migration.docTypes,
migration.queryOptions
)
if (docToUpdate.length) {
const migratedDocs = await migration.run(_client, docToUpdate)
if (migratedDocs.length) {
......@@ -132,6 +154,7 @@ export async function migrate(
switch (result.type) {
case MIGRATION_RESULT_NOOP:
await updateSchemaVersion(_client, migration.targetSchemaVersion)
break
case MIGRATION_RESULT_COMPLETE:
await updateSchemaVersion(_client, migration.targetSchemaVersion)
......
import { Client } from 'cozy-client'
import { Notes } from 'models/releaseNotes.model'
type SchemaVersion = number
......@@ -24,7 +25,15 @@ export type Migration = {
baseSchemaVersion: SchemaVersion
targetSchemaVersion: SchemaVersion
description: string
releaseNotes: Notes | null
docTypes: string
queryOptions?: MigrationQueryOptions
appVersion: string
run: (_client: Client, docs: any[]) => Promise<any[]>
}
export type MigrationQueryOptions = {
scope: string
tag: string
limit: number
}
import { FluidType } from 'enum/fluid.enum'
import { ScreenType } from 'enum/screen.enum'
import { FluidStatus } from './fluid.model'
import { ReleaseNotes } from './releaseNotes.model'
export interface GlobalState {
screenType: ScreenType
releaseNotes: ReleaseNotes
challengeExplorationNotification: boolean
challengeActionNotification: boolean
challengeDuelNotification: boolean
......
export interface ReleaseNotes {
show: boolean
notes: Notes[]
}
export interface Notes {
title: string
description: string
}
import { FluidType } from 'enum/fluid.enum'
import { ScreenType } from 'enum/screen.enum'
import { FluidConnection, FluidStatus } from 'models'
import { Notes } from 'models/releaseNotes.model'
export const CHANGE_SCREEN_TYPE = 'CHANGE_SCREEN_TYPE'
export const SHOW_RELEASE_NOTES = 'SHOW_RELEASE_NOTES'
export const TOGGLE_CHALLENGE_EXPLORATION_NOTIFICATION =
'TOGGLE_CHALLENGE_EXPLORATION_NOTIFICATION'
export const TOGGLE_CHALLENGE_ACTION_NOTIFICATION =
......@@ -54,6 +56,11 @@ interface UpdateTermValidation {
payload?: boolean
}
interface ShowReleaseNotes {
type: typeof SHOW_RELEASE_NOTES
payload?: { show: boolean; notes: Notes[] }
}
export type GlobalActionTypes =
| ChangeScreenType
| ToogleChallengeExplorationNotification
......@@ -63,6 +70,7 @@ export type GlobalActionTypes =
| SetFluidStatus
| UpdatedFluidConnection
| UpdateTermValidation
| ShowReleaseNotes
export function changeScreenType(screenType: ScreenType): GlobalActionTypes {
return {
......@@ -71,6 +79,16 @@ export function changeScreenType(screenType: ScreenType): GlobalActionTypes {
}
}
export function showReleaseNotes(
show: boolean,
notes: Notes[]
): GlobalActionTypes {
return {
type: SHOW_RELEASE_NOTES,
payload: { show, notes },
}
}
export function toggleChallengeExplorationNotification(
notif: boolean
): GlobalActionTypes {
......
......@@ -9,6 +9,7 @@ import {
UPDATE_FLUID_CONNECTION,
GlobalActionTypes,
UPDATE_TERMS_VALIDATION,
SHOW_RELEASE_NOTES,
} from 'store/global/global.actions'
import { FluidStatus, GlobalState } from 'models'
import { ScreenType } from 'enum/screen.enum'
......@@ -16,6 +17,15 @@ import { FluidState, FluidType } from 'enum/fluid.enum'
const initialState: GlobalState = {
screenType: ScreenType.MOBILE,
releaseNotes: {
show: false,
notes: [
{
title: '',
description: '',
},
],
},
challengeExplorationNotification: false,
challengeActionNotification: false,
challengeDuelNotification: false,
......@@ -155,6 +165,13 @@ export const globalReducer: Reducer<GlobalState> = (
isLastTermAccepted: action.payload,
}
: state
case SHOW_RELEASE_NOTES:
return action.payload != undefined
? {
...state,
releaseNotes: action.payload,
}
: state
case UPDATE_FLUID_CONNECTION:
if (action.payload !== undefined) {
const updatedFluidStatus = [...state.fluidStatus]
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment