Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
83f74c3
feat(mobile contacts): implement mobile view card and routes for hous…
RyanMG Mar 11, 2025
0c20082
feat(mobile contacts): implement first pass mobile contacts listing UI
RyanMG Mar 11, 2025
d3ae54f
chore: remove previously created mobile contacts example out of /src/…
RyanMG Mar 11, 2025
bba655e
chore: previous mobile contacts app code now placed in new /examples/…
RyanMG Mar 11, 2025
2d77257
ui: copy over favorite button SCSS file
RyanMG Mar 12, 2025
aa19fd2
Merge branch 'develop' into contact-app-mobile
RyanMG Mar 12, 2025
f098783
fix: entry point app name change contactMobile -> mobileContacts
RyanMG Mar 12, 2025
4df8677
feat(mobile contacts): hide / show contact details
RyanMG Mar 12, 2025
1e1a5f8
feat(mobile contacts): clear grid / tile selection when the details …
RyanMG Mar 12, 2025
8aa46fd
ui(mobile contacts): contact details animate properly in and out
RyanMG Mar 12, 2025
f03878b
fix(mobile contacts): explicit height on content details to allow scr…
RyanMG Mar 12, 2025
ccc571a
feat(mobile contacts): implement contact details bottom bar
RyanMG Mar 12, 2025
79768c8
ui(mobile contacts): add contact data into ui
RyanMG Mar 12, 2025
f8c2574
feat(mobile contacts): persist user favorites in localstorage
RyanMG Mar 12, 2025
4bf46b0
ui(mobile contacts): CSS cleanup
RyanMG Mar 12, 2025
b344237
refactor(mobile contacts): better semantic name for entrypoint
RyanMG Mar 12, 2025
4104855
chore(mobile contacts): remove contact service code bootstrapped into…
RyanMG Mar 13, 2025
e0fc1fb
ui(mobile contacts): styling pattern adjustments / comments
RyanMG Mar 13, 2025
8f4f800
feat(mobile contacts): replace localstorage favorites with DB persist…
RyanMG Mar 13, 2025
45d7a4e
feat(mobile contacts): TODO cleanups
RyanMG Mar 13, 2025
5ea7796
feat(mobile contacts): grid view grouping
RyanMG Mar 13, 2025
6b0ada2
refactor(mobile contacts): nest mobile and desktop under "contact" pa…
RyanMG Mar 14, 2025
75e2192
Merge branch 'develop' into contact-app-mobile
TomTirapani Mar 26, 2025
9bc654c
feat(mobile contacts): set up mobile contacts to use native app navig…
RyanMG Mar 29, 2025
b735ac7
feat(mobile contacts): migrate contact data store up to AppModel
RyanMG Mar 31, 2025
835f7c2
fix(mobile contacts): import name fixes
RyanMG Apr 1, 2025
0733793
+ Use AppModel.contacts as the sole observable source of truth
TomTirapani Apr 1, 2025
76b6ca3
Merge pull request #756 from xh/contact-app-mobile-tweaks
RyanMG Apr 1, 2025
efc4c7e
Update hoist-core to 30.0-SNAPSHOT
amcclain Apr 1, 2025
f4edffe
fix(mobile contacts): get the domain off window.location, and remove …
RyanMG Apr 1, 2025
84af811
fix(mobile contacts): tags were not being persisted / rendered proper…
RyanMG Apr 1, 2025
7923f43
refactor(mobile contacts): mobile details page no longer needs its ow…
RyanMG Apr 1, 2025
6c7ce8a
fix: isFavorites / favorites button was not properly toggling and per…
RyanMG Apr 1, 2025
75e84e1
Merge branch 'develop' into contact-app-mobile
TomTirapani Apr 2, 2025
06b92f8
Exit contact save event early if no new data was provided to be saved
RyanMG Apr 2, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions client-app/src/apps/contact.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import '../Bootstrap';

import {XH} from '@xh/hoist/core';
import {AppContainer} from '@xh/hoist/desktop/appcontainer';
import {AppComponent} from '../examples/contact/AppComponent';
import {AppModel} from '../examples/contact/AppModel';
import {AppComponent} from '../examples/contact/desktop/AppComponent';
Copy link
Contributor Author

Choose a reason for hiding this comment

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

You will se this update across this PR.
After some back and forth, Lee suggested that I nest the new mobile contacts under the parent /examples/contacts, and do the same with desktop.
This means that some of this PR will be just file path adjustments to accomodate this change.

import {AppModel} from '../examples/contact/desktop/AppModel';
import {AuthModel} from '../core/AuthModel';

XH.renderApp({
Expand Down
19 changes: 19 additions & 0 deletions client-app/src/apps/contactMobile.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import '../Bootstrap';

import {XH} from '@xh/hoist/core';
import {AppContainer} from '@xh/hoist/mobile/appcontainer';
import {AppComponent} from '../examples/contact/mobile/AppComponent';
import AppModel from '../examples/contact/mobile/AppModel';
import {AuthModel} from '../core/AuthModel';

XH.renderApp({
clientAppCode: 'contactMobile',
clientAppName: 'XH Contact Mobile',
componentClass: AppComponent,
modelClass: AppModel,
containerClass: AppContainer,
authModelClass: AuthModel,
isMobileApp: true,
enableLogout: true,
checkAccess: () => true
});
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,27 @@ import {hoistCmp, uses} from '@xh/hoist/core';
import {appBar} from '@xh/hoist/desktop/cmp/appbar';
import {panel} from '@xh/hoist/desktop/cmp/panel';
import {Icon} from '@xh/hoist/icon';
import {a} from '@xh/hoist/cmp/layout';
import {AppModel} from './AppModel';
import {directoryPanel} from './DirectoryPanel';
import '../../core/Toolbox.scss';
import '../../../core/Toolbox.scss';

export const AppComponent = hoistCmp({
displayName: 'App',
model: uses(AppModel),

render() {
const domain = window.location.origin;
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Unsure if there is a 'hoist' way to access the URI.

return panel({
tbar: appBar({
icon: Icon.contact({size: '2x', prefix: 'fal'}),
leftItems: [
a({
Copy link
Contributor Author

Choose a reason for hiding this comment

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

New link to let users access mobile contacts from the existing toolbox UI

item: 'View contacts for mobile',
href: `${domain}/contactMobile`,
target: '_blank'
})
],
appMenuButtonProps: {hideLogoutItem: false}
}),
item: directoryPanel()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import {XH} from '@xh/hoist/core';
import {ContactService} from './svc/ContactService';
import {BaseAppModel} from '../../BaseAppModel';

export const PERSIST_APP = {prefKey: 'contactAppState'};
Copy link
Contributor Author

Choose a reason for hiding this comment

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

As desktop AppModel is no longer the root for all of contacts, I moved this to here, which is common to both versions.

import {ContactService} from '../svc/ContactService';
import {BaseAppModel} from '../../../BaseAppModel';

export class AppModel extends BaseAppModel {
static instance: AppModel;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@ import {toolbar, toolbarSep} from '@xh/hoist/desktop/cmp/toolbar';
import {Icon} from '@xh/hoist/icon';
import {tileView} from './cmp/TileView';
import {detailsPanel} from './details/DetailsPanel';
import './DirectoryPanel.scss';

import {DirectoryPanelModel} from './DirectoryPanelModel';
import '../DirectoryPanel.scss';

export const directoryPanel = hoistCmp.factory({
model: creates(DirectoryPanelModel),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@ import {GridModel} from '@xh/hoist/cmp/grid';
import {withFilterByField, withFilterByKey} from '@xh/hoist/data';
import {isEmpty, uniq, without} from 'lodash';

import {PERSIST_APP} from './AppModel';
import {PERSIST_APP} from '../svc/ContactService';
import {favoriteButton} from './cmp/FavoriteButton';
import {DetailsPanelModel} from './details/DetailsPanelModel';
import {cellPhoneCol, emailCol, locationCol, nameCol, workPhoneCol} from '../../core/columns';
import {cellPhoneCol, emailCol, locationCol, nameCol, workPhoneCol} from '../../../core/columns';
import {FilterLike} from '@xh/hoist/data/filter/Types';

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import {Icon} from '@xh/hoist/icon/Icon';
import {consumeEvent} from '@xh/hoist/utils/js';
import {button} from '@xh/hoist/desktop/cmp/button';

import './FavoriteButton.scss';
import {DirectoryPanelModel} from '../DirectoryPanelModel';
import '../../FavoriteButton.scss';

export const favoriteButton = hoistCmp.factory<DirectoryPanelModel>(({model, record}) => {
const {isFavorite} = record.data;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import {div, filler, span, tileFrame, vbox} from '@xh/hoist/cmp/layout';
import {hoistCmp} from '@xh/hoist/core';
import {favoriteButton} from './FavoriteButton';
import './TileView.scss';

import {DirectoryPanelModel} from '../DirectoryPanelModel';
import '../../TileView.scss';

export const tileView = hoistCmp.factory<DirectoryPanelModel>({
render({model}) {
Expand Down
27 changes: 27 additions & 0 deletions client-app/src/examples/contact/mobile/AppComponent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import {hoistCmp, uses, XH} from '@xh/hoist/core';
import {appBar} from '@xh/hoist/mobile/cmp/header';
import {panel} from '@xh/hoist/mobile/cmp/panel';
import {Icon} from '@xh/hoist/icon';
import AppModel from './AppModel';
import {navigator} from '@xh/hoist/mobile/cmp/navigator';
import '../../../core/Toolbox.scss';

export const AppComponent = hoistCmp({
displayName: 'App',
model: uses(AppModel),

render() {
return panel({
tbar: appBar({
omit: XH.isLandscape,
icon: Icon.boxFull({size: 'lg', prefix: 'fal'}),
hideRefreshButton: false,
appMenuButtonProps: {
hideLogoutItem: false,
hideThemeItem: true
}
}),
item: navigator()
});
}
});
99 changes: 99 additions & 0 deletions client-app/src/examples/contact/mobile/AppModel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import {managed, PlainObject, XH} from '@xh/hoist/core';
import {ContactService} from '../svc/ContactService';
import {BaseAppModel} from '../../../BaseAppModel';
import {NavigatorModel} from '@xh/hoist/mobile/cmp/navigator';
import directoryPanel from './DirectoryPanel';
import detailsPanel from './details/DetailsPanel';
import DetailsPanelModel from './details/DetailsPanelModel';
import DirectoryPanelModel from './DirectoryPanelModel';
import {action, makeObservable, observable, runInAction} from '@xh/hoist/mobx';
import {castArray, isEmpty, uniq} from 'lodash';

import {
autoRefreshAppOption,
sizingModeAppOption,
themeAppOption
} from '@xh/hoist/mobile/cmp/appOption';

export default class AppModel extends BaseAppModel {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

App model, as the root model for mobile contacts, now does a. lot more management of its children - including storing the fetched contact data and instantiating the models for the child views.

static instance: AppModel;

@observable.ref tagList: string[] = [];
@observable.ref contacts: PlainObject[] = [];

@managed directoryPanelModel: DirectoryPanelModel;
@managed detailsPanelModel: DetailsPanelModel;

@managed
navigatorModel: NavigatorModel = new NavigatorModel({
track: true,
pages: [
{id: 'default', content: directoryPanel},
{id: 'details', content: detailsPanel}
]
});

override getRoutes() {
return [
{
name: 'default',
path: '/contactMobile',
children: [
{
name: 'details',
path: '/:id'
}
]
}
];
}

constructor() {
super();
makeObservable(this);
this.directoryPanelModel = new DirectoryPanelModel(this);
this.detailsPanelModel = new DetailsPanelModel(this);
}

async updateContactAsync(id: string, data: PlainObject) {
if (!data || isEmpty(data)) return;
// Mobile select only allows single selections, while desktop allows multiple
// This means we receive an array on the desktop picker, and a single value here.
// Convert single result into an array of one to keep data consistent.
if (data.tags) {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Comment above explains this. At some point maybe mobile needs to be enhanced to support multi-select, but that seems out of scope here.

data.tags = castArray(data.tags);
}
await XH.contactService.updateContactAsync(id, data);
await this.refreshAsync();
}

@action
async toggleFavorite(id: string) {
await XH.contactService.toggleFavorite(id);
this.contacts = this.contacts.map(contact =>
contact.id === id ? {...contact, isFavorite: !contact.isFavorite} : contact
);
}

override getAppOptions() {
return [themeAppOption(), sizingModeAppOption(), autoRefreshAppOption()];
}

override async initAsync() {
await super.initAsync();
await XH.installServicesAsync(ContactService);
await this.loadAsync();
}

override async doLoadAsync() {
try {
const contacts = await XH.contactService.getContactsAsync();
runInAction(() => {
this.contacts = contacts;
this.tagList = uniq(contacts.flatMap(it => it.tags ?? [])).sort() as string[];
});
} catch (e) {
XH.handleException(e);
}
}
}
72 changes: 72 additions & 0 deletions client-app/src/examples/contact/mobile/DirectoryPanel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import {hoistCmp, uses} from '@xh/hoist/core';
import {grid} from '@xh/hoist/cmp/grid';
import {hframe, filler} from '@xh/hoist/cmp/layout';
import {storeCountLabel, storeFilterField} from '@xh/hoist/cmp/store';
import {button} from '@xh/hoist/mobile/cmp/button';
import {buttonGroupInput} from '@xh/hoist/mobile/cmp/input';
import {panel} from '@xh/hoist/mobile/cmp/panel';
import {Icon} from '@xh/hoist/icon';
import {toolbar} from '@xh/hoist/mobile/cmp/toolbar';

import tileView from './cmp/TileView';
import DirectoryPanelModel from './DirectoryPanelModel';
import '../DirectoryPanel.scss';

const directoryPanel = hoistCmp.factory({
model: uses(DirectoryPanelModel),

render({model}) {
return panel({
className: 'tb-directory-panel',
item: hframe(
panel({
tbar: tbar(),
item: model.displayMode === 'grid' ? grid() : tileView(),
bbar: bbar()
})
)
});
}
});

const tbar = hoistCmp.factory<DirectoryPanelModel>(({model}) => {
return toolbar({
className: 'tb-directory-panel__tbar',
items: [
storeFilterField({
leftIcon: Icon.search(),
maxWidth: 400,
minWidth: 200
}),
filler(),
buttonGroupInput({
outlined: true,
bind: 'displayMode',
value: model.displayMode,
intent: 'primary',
items: [
button({
icon: Icon.list(),
value: 'grid',
width: 50
}),
button({
icon: Icon.users(),
value: 'tiles',
width: 50
})
]
})
]
});
});

const bbar = hoistCmp.factory<DirectoryPanelModel>(({model}) => {
const {
gridModel: {store}
} = model;

return toolbar(filler(), storeCountLabel({store, unit: 'contact'}));
});

export default directoryPanel;
Loading
Loading