diff --git a/src/components/Home/ConsumptionView.tsx b/src/components/Home/ConsumptionView.tsx index a73b51aeb13e205e12699f880f2f92c64c992533..5e903ed14a55142e1a796945b7445ed3a6a72320 100644 --- a/src/components/Home/ConsumptionView.tsx +++ b/src/components/Home/ConsumptionView.tsx @@ -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 && ( diff --git a/src/components/Home/releaseNotesModal.scss b/src/components/Home/releaseNotesModal.scss new file mode 100644 index 0000000000000000000000000000000000000000..83ac0850d4ca6d9abdfe26064eaa0510a6959282 --- /dev/null +++ b/src/components/Home/releaseNotesModal.scss @@ -0,0 +1,63 @@ +@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; +} diff --git a/src/components/Home/releaseNotesModal.tsx b/src/components/Home/releaseNotesModal.tsx new file mode 100644 index 0000000000000000000000000000000000000000..981bc4fd59f20493aac5a0d31a2479e87778062b --- /dev/null +++ b/src/components/Home/releaseNotesModal.tsx @@ -0,0 +1,79 @@ +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 diff --git a/src/components/Splash/SplashRoot.tsx b/src/components/Splash/SplashRoot.tsx index fef56daabf71cbce4720402166270bd6043fcb30..97463335e8a959b89df7404d66e838dbbc4544bb 100644 --- a/src/components/Splash/SplashRoot.tsx +++ b/src/components/Splash/SplashRoot.tsx @@ -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)) diff --git a/src/locales/fr.json b/src/locales/fr.json index 1f868421c47485d78a51c18ba7de27260ee6b845..b8a2566735beb25f2ce749ada42f98dcc76516c1 100644 --- a/src/locales/fr.json +++ b/src/locales/fr.json @@ -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": { diff --git a/src/migrations/migration.data.ts b/src/migrations/migration.data.ts index 5170b93c3370d2a506e77e7b86f5658351e53486..06da77d55ebb924f2486556ee120e110659b9368 100644 --- a/src/migrations/migration.data.ts +++ b/src/migrations/migration.data.ts @@ -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 + }) + }, + }, ] diff --git a/src/migrations/migration.service.ts b/src/migrations/migration.service.ts index d23a0c16d9b5ef36a5237de0d36a9cbf1f01b949..41c97963f7c504233a18e1aee289d6799911125e 100644 --- a/src/migrations/migration.service.ts +++ b/src/migrations/migration.service.ts @@ -1,8 +1,12 @@ 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 } } diff --git a/src/migrations/migration.ts b/src/migrations/migration.ts index fb9e1961468f484e111d3df8fbcc52ed7b1b912a..0e690905a0810202adce11cfb205e1d342680039 100644 --- a/src/migrations/migration.ts +++ b/src/migrations/migration.ts @@ -1,4 +1,8 @@ -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) diff --git a/src/migrations/migration.type.ts b/src/migrations/migration.type.ts index 480539c36d4167b2ef40ab13393f56825b92684b..ae576864b506317f7df54c84706dc4d6f9332c8a 100644 --- a/src/migrations/migration.type.ts +++ b/src/migrations/migration.type.ts @@ -1,4 +1,5 @@ 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 +} diff --git a/src/models/global.model.ts b/src/models/global.model.ts index fb74219f1e127f50f898c36c6b56838251eb21d5..d36194024ae97df7e45bd86411130b7809b76dbd 100644 --- a/src/models/global.model.ts +++ b/src/models/global.model.ts @@ -1,9 +1,11 @@ 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 diff --git a/src/models/releaseNotes.model.ts b/src/models/releaseNotes.model.ts new file mode 100644 index 0000000000000000000000000000000000000000..79c8688a90296d75c89a08b3f814d26f2a9b2012 --- /dev/null +++ b/src/models/releaseNotes.model.ts @@ -0,0 +1,9 @@ +export interface ReleaseNotes { + show: boolean + notes: Notes[] +} + +export interface Notes { + title: string + description: string +} diff --git a/src/store/global/global.actions.ts b/src/store/global/global.actions.ts index eb323d21002da86d65d1c5fcb4d705a968548e93..072b7fddcb4a1f234e6a2107824927ca19727562 100644 --- a/src/store/global/global.actions.ts +++ b/src/store/global/global.actions.ts @@ -1,8 +1,10 @@ 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 { diff --git a/src/store/global/global.reducer.ts b/src/store/global/global.reducer.ts index 232f9bcfdb7b03c19b5ed607bb98867c29f82aa6..e4e408f9989ab5121e8a26bcceaec90e9dcf2c96 100644 --- a/src/store/global/global.reducer.ts +++ b/src/store/global/global.reducer.ts @@ -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]