Skip to content
This repository has been archived by the owner on Apr 17, 2023. It is now read-only.

Commit

Permalink
DataSync 2.0 (#420)
Browse files Browse the repository at this point in the history
Co-authored-by: Gianluca <[email protected]>
  • Loading branch information
wtrocki and kingsleyzissou authored Jul 20, 2020
1 parent 58297f5 commit 3ba0147
Show file tree
Hide file tree
Showing 56 changed files with 1,222 additions and 517 deletions.
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@ $RECYCLE.BIN/
Thumbs.db
UserInterfaceState.xcuserstate
package-lock.json
server/src/config/keycloak.json

server/website
yarn.lock
Expand Down
13 changes: 5 additions & 8 deletions .graphqlrc.yml
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
## GraphQL Config Generated by Graphback
## Please review configuration and adjust it for your own project
## Configuration is being used to generate client side queries
schema: ./server/src/schema/schema.graphql
documents: ./client/src/graphql/**/*.graphql
extensions:
graphback:
model: ./model
model: ./server/model/task.graphql
crud:
create: true
update: true
Expand All @@ -15,14 +15,11 @@ extensions:
subUpdate: true
subDelete: true
plugins:
## Schema for preview only - server is not using it as it is generated at runtime
graphback-schema:
format: graphql
outputPath: ./server/src/schema
graphback-resolvers:
format: 'ts'
outputPath: ./server/src/resolvers
## Client side queries
graphback-client:
format: 'ts'
outputPath: ./client/src/graphql
graphback-offix:
outputPath: ./server/src/resolvers
outputFile: ./client/src/graphql/generated.ts
14 changes: 8 additions & 6 deletions client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,19 +22,20 @@
"graphql-tag": "2.10.3",
"ionicons": "5.0.1",
"keycloak-js": "10.0.2",
"offix-cache": "0.15.1",
"offix-client": "0.15.1",
"offix-cache": "0.16.0-alpha2",
"offix-client": "0.16.0-alpha2",
"react": "16.13.1",
"react-dom": "16.13.1",
"react-offix-hooks": "0.15.1",
"react-offix-hooks": "0.16.0-alpha2",
"react-router": "5.2.0",
"react-router-dom": "5.2.0",
"react-scripts": "3.4.1",
"simpl-schema": "1.7.3",
"subscriptions-transport-ws": "0.9.16",
"typescript": "3.9.5",
"uniforms-ionic": "0.0.11",
"uniforms": "3.0.0-alpha.4",
"uniforms-bridge-simple-schema-2": "3.0.0-alpha.4"
"uniforms-bridge-simple-schema-2": "3.0.0-alpha.4",
"uniforms-ionic": "0.1.0"
},
"scripts": {
"start": "npm run build && cap serve",
Expand All @@ -61,7 +62,8 @@
]
},
"devDependencies": {
"@capacitor/cli": "2.1.2"
"@capacitor/cli": "2.1.2",
"@types/simpl-schema": "^0.2.7"
},
"description": "An Ionic project"
}
1 change: 1 addition & 0 deletions client/public/assets/icon/avatar.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 6 additions & 0 deletions client/public/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,12 @@
"type": "image/png",
"sizes": "512x512",
"purpose": "maskable"
},
{
"src": "assets/icon/avatar.svg",
"type": "image/svg",
"sizes": "512x512",
"purpose": "maskable"
}
],
"start_url": ".",
Expand Down
2 changes: 1 addition & 1 deletion client/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export const App: React.FC = () => {
useEffect(() => {
const conflictListener: ConflictListener = {
mergeOccurred() {
setShowConflict(true);
console.log("Merge occured! ")
},
conflictOccurred() {
setShowConflict(true);
Expand Down
10 changes: 6 additions & 4 deletions client/src/AppContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import React, { useState, useEffect } from 'react';
import { ApolloOfflineClient } from 'offix-client';
import { ApolloOfflineProvider } from 'react-offix-hooks';
import { ApolloProvider } from '@apollo/react-hooks';
import { AppContext } from './AppContext';
import { AuthContext } from './AuthContext';
import { clientConfig } from './config';
import { Loading } from './components/Loading';
import { IContainerProps } from './declarations';
Expand All @@ -20,7 +20,9 @@ export const AppContainer: React.FC<IContainerProps> = ({ app: App }) => {
const init = async () => {
keycloak = await getKeycloakInstance();
await apolloClient.init();

if (keycloak) {
await keycloak?.loadUserProfile();
}
setInitialized(true);
}
init();
Expand All @@ -30,13 +32,13 @@ export const AppContainer: React.FC<IContainerProps> = ({ app: App }) => {

// return container with keycloak provider
return (
<AppContext.Provider value={{ keycloak }}>
<AuthContext.Provider value={{ keycloak, profile: keycloak?.profile }}>
<ApolloOfflineProvider client={apolloClient}>
<ApolloProvider client={apolloClient}>
<App />
</ApolloProvider>
</ApolloOfflineProvider>
</AppContext.Provider>
</AuthContext.Provider>
);


Expand Down
4 changes: 0 additions & 4 deletions client/src/AppContext.tsx

This file was deleted.

9 changes: 9 additions & 0 deletions client/src/AuthContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import React from 'react';
import { KeycloakInstance, KeycloakProfile } from 'keycloak-js';

export interface IAuthContext {
keycloak?: KeycloakInstance | undefined
profile?: KeycloakProfile | undefined
}

export const AuthContext = React.createContext<IAuthContext>({ keycloak: undefined });
4 changes: 2 additions & 2 deletions client/src/components/Header.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React, { useContext, useState } from 'react';
import { IonHeader, IonToolbar, IonButtons, IonTitle, IonToast, IonButton, IonIcon } from '@ionic/react';
import { person, exit, arrowBack } from 'ionicons/icons';
import { AppContext } from '../AppContext';
import { AuthContext } from '../AuthContext';
import { logout } from '../auth/keycloakAuth';
import { useApolloOfflineClient } from 'react-offix-hooks';
import { Link } from 'react-router-dom';
Expand All @@ -11,7 +11,7 @@ export const Header : React.FC<{ title: string, backHref?: string, match: any, i
const { url } = match;

const client = useApolloOfflineClient();
const { keycloak } = useContext(AppContext);
const { keycloak } = useContext(AuthContext);
const [ showToast, setShowToast ] = useState(false);

const handleLogout = async () => {
Expand Down
14 changes: 8 additions & 6 deletions client/src/components/Router.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,25 @@
import React from 'react';
import {
HashRouter as AppRouter,
Switch,
Redirect,
Route,
Switch,
Redirect,
Route,
} from 'react-router-dom';
import { IonApp } from '@ionic/react';
import { TaskPage, AddTaskPage, OfflineQueuePage, UpdateTaskPage, ProfilePage } from '../pages';
import { ViewTaskPage } from '../pages/ViewTaskPage';

export const Router: React.FC = () => {
return (
<IonApp>
<AppRouter>
<Switch>
<Route
path="/addTask"
component={AddTaskPage}
<Route
path="/addTask"
component={AddTaskPage}
exact
/>
<Route path="/viewTask/:id" component={ViewTaskPage} exact />
<Route path="/updateTask/:id" component={UpdateTaskPage} exact />
<Route path="/offlineQueue" component={OfflineQueuePage} exact={true} />
<Route path="/tasks" component={TaskPage} exact={true} />
Expand Down
31 changes: 21 additions & 10 deletions client/src/components/Task.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,20 +9,25 @@ import {
IonCheckbox,
IonButtons
} from '@ionic/react';
import { create, trash } from 'ionicons/icons';
import { create, trash, navigate } from 'ionicons/icons';
import { ITask } from '../declarations';
import { Link } from 'react-router-dom';
import { Link, useHistory } from 'react-router-dom';

export const Task: React.FC<any> = ({ task, updateTask, deleteTask }) => {

const history = useHistory();
const onDeleteClick = (event: MouseEvent) => {
event.preventDefault();
deleteTask(task);
};

const onViewClick = (event: MouseEvent) => {
event.preventDefault();
history.push(`/viewTask/${task.id}`);
};

const check = (event: SyntheticEvent) => {
event.preventDefault();
let status = (task.status === 'COMPLETE') ? 'OPEN' : 'COMPLETE';
let status = (task.status === 'COMPLETE') ? 'OPEN' : 'COMPLETE';
updateTask({
...task,
status
Expand All @@ -40,26 +45,32 @@ export const Task: React.FC<any> = ({ task, updateTask, deleteTask }) => {
<IonItem>
<IonCheckbox checked={isChecked(task)} onClick={check} slot="start" className='ion-margin-end ion-align-items-start' />
<IonLabel>
<h2>{ task.title }</h2>
<h2>{task.title}</h2>
<IonNote item-start>
{ task.description }
{task.description}
</IonNote>
<br />
<IonNote>
<IonBadge color='primary'>
Server version: { task.version }
Server timestamp: {new Date(Number.parseInt(task.updatedAt)).toUTCString()}
</IonBadge>
</IonNote>
</IonLabel>
<IonButtons>
<IonButton onClick={onViewClick} item-start className='view-button' color='primary' fill="outline">
<IonIcon icon={navigate} />
</IonButton>
<Link to={`updateTask/${task.id}`}>
<IonButton item-start color='primary' fill="outline">
<IonIcon icon={create}/>
<IonButton item-start color='primary' fill="outline">
<IonIcon icon={create} />
</IonButton>
</Link>
<IonButton onClick={onDeleteClick} item-start className='trash-button' color='primary' fill="outline">
<IonIcon icon={trash}/>
<IonIcon icon={trash} />
</IonButton>



</IonButtons>
</IonItem>
);
Expand Down
30 changes: 13 additions & 17 deletions client/src/components/TaskList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,18 @@ import { useOfflineMutation } from 'react-offix-hooks';
import { ITask } from '../declarations';
import { Empty } from './Empty';
import { mutationOptions } from '../helpers';
import { updateTask } from '../graphql/mutations/updateTask';
import { deleteTask } from '../graphql/mutations/deleteTask';
import { updateTask, deleteTask } from '../graphql/generated';

export const TaskList: React.FC<any> = ({ tasks }) => {

const [updateTaskMutation] = useOfflineMutation(updateTask, mutationOptions.updateTask);
const [deleteTaskMutation] = useOfflineMutation(deleteTask, mutationOptions.deleteTask);

const [ showToast, setShowToast ] = useState<boolean>(false);
const [ errorMessage, setErrorMessage ] = useState<string>('');
const [showToast, setShowToast] = useState<boolean>(false);
const [errorMessage, setErrorMessage] = useState<string>('');

const handleError = (error: any) => {
if(error.offline) {
if (error.offline) {
error.watchOfflineChange();
}
if (error.graphQLErrors) {
Expand All @@ -26,26 +25,23 @@ export const TaskList: React.FC<any> = ({ tasks }) => {
setShowToast(true);
}
}

const handleDelete = (task: ITask) => {
const input = task;
delete input.__typename;
deleteTaskMutation({
const { comments, __typename, createdAt, ...input } = task as any;
deleteTaskMutation({
variables: { input }
})
.catch(handleError);
}).catch(handleError);
};

const handleUpdate = (task: ITask) => {
const input = task;
delete input.__typename;
const { comments, __typename, ...input } = task as any;
updateTaskMutation({
variables: { input }
})
.catch(handleError);
.catch(handleError);
}
if(tasks.length < 1) {

if (tasks.length < 1) {
const message = (<p>You currently have no tasks.</p>);
return <Empty message={message} />
};
Expand All @@ -54,7 +50,7 @@ export const TaskList: React.FC<any> = ({ tasks }) => {
<>
<IonList>
{
tasks.map((task : ITask) => {
tasks.map((task: ITask) => {
return <Task key={task.id} task={task} updateTask={handleUpdate} deleteTask={handleDelete} />;
})
}
Expand Down
6 changes: 6 additions & 0 deletions client/src/config/SimpleSchema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import SimpleSchema from "simpl-schema";

// add the uniforms property to SimpleSchema
SimpleSchema.extendOptions(['uniforms']);

export default SimpleSchema;
4 changes: 3 additions & 1 deletion client/src/config/clientConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { getAuthHeader } from '../auth/keycloakAuth';
import { ApolloOfflineClientOptions } from 'offix-client';
import { Capacitor } from '@capacitor/core';
import { CapacitorNetworkStatus } from '../helpers/CapacitorNetworkStatus';
import { TimeStampState } from './conflictStrategy';


let httpUri = 'http://localhost:4000/graphql';
Expand Down Expand Up @@ -84,7 +85,7 @@ const cache = new InMemoryCache({
// to query the cache for individual Task item
cacheRedirects: {
Query: {
findTasks: (_, { fields }, { getCacheKey }) => getCacheKey({ __typename: 'Task', id: fields.id }),
getTask: (_, { id }, { getCacheKey }) => getCacheKey({ __typename: 'Task', id }),
},
},
});
Expand All @@ -95,6 +96,7 @@ export const clientConfig: ApolloOfflineClientOptions = {
conflictListener: new ConflictLogger(),
mutationCacheUpdates: globalCacheUpdates,
networkStatus: new CapacitorNetworkStatus(),
conflictProvider: new TimeStampState(),
inputMapper: {
deserialize: (variables: any) => {
return (variables && variables.input) ? variables.input : variables;
Expand Down
19 changes: 19 additions & 0 deletions client/src/config/conflictStrategy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@

import { ObjectState, ConflictResolutionData } from "offix-client";

export class TimeStampState implements ObjectState {

public assignServerState(client: any, server: any): void {
client.updatedAt = server.updatedAt.toString();
}
public hasConflict(client: any, server: any): boolean {
return client.updatedAt !== server.updatedAt;
}
public getStateFields(): string[] {
return ["updatedAt"];
}

public currentState(currentObjectState: ConflictResolutionData) {
return currentObjectState.updatedAt;
}
}
Loading

0 comments on commit 3ba0147

Please sign in to comment.