Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
116 changes: 116 additions & 0 deletions samples/Dataverse/src/App.css
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,41 @@ header p {
position: relative;
}

/* Page Navigation Tabs */
.page-tabs {
display: flex;
gap: 8px;
margin-bottom: 24px;
border-bottom: 2px solid #c5b4e3;
padding-bottom: 0;
}

.tab-btn {
padding: 10px 28px;
border: none;
background: transparent;
font-size: 1rem;
font-family: 'Segoe UI', 'Segoe Sans Text', sans-serif;
font-weight: 500;
color: #702573;
cursor: pointer;
border-bottom: 3px solid transparent;
margin-bottom: -2px;
border-radius: 8px 8px 0 0;
transition: background 0.15s, color 0.15s;
}

.tab-btn:hover {
background: rgba(197, 180, 227, 0.2);
}

.tab-btn.active {
color: #c03bc4;
border-bottom-color: #c03bc4;
background: rgba(192, 59, 196, 0.08);
font-weight: 600;
}

/* Error message */
.error-message {
background: linear-gradient(135deg, #fee 0%, #fdd 100%);
Expand Down Expand Up @@ -465,6 +500,87 @@ footer code {
font-weight: 500;
}

/* Attachment sub-form */
.attachment-subform {
margin-top: 32px;
padding: 20px 24px;
background: rgba(197, 180, 227, 0.12);
border: 1px solid rgba(197, 180, 227, 0.4);
border-radius: 12px;
}

.attachment-subform h3 {
margin: 0 0 16px 0;
font-size: 1rem;
font-weight: 600;
color: #702573;
text-transform: uppercase;
letter-spacing: 0.5px;
}

.attachment-actions {
margin-top: 8px;
}

.file-current {
display: flex;
align-items: center;
gap: 8px;
padding: 10px 14px;
background: rgba(197, 180, 227, 0.15);
border: 1px solid rgba(197, 180, 227, 0.5);
border-radius: 8px;
font-size: 0.9rem;
}

.file-current-icon {
font-size: 1.1rem;
flex-shrink: 0;
}

.file-current-name {
color: #702573;
font-weight: 500;
word-break: break-all;
}

.file-empty {
margin: 0;
color: rgba(112, 37, 115, 0.5);
font-size: 0.88rem;
font-style: italic;
}

.required-mark {
color: #c03bc4;
margin-left: 2px;
}

/* Upload toast */
.upload-toast {
position: sticky;
top: 0;
z-index: 100;
padding: 14px 20px;
border-radius: 10px;
margin-bottom: 16px;
font-weight: 500;
font-size: 0.95rem;
animation: fadeIn 0.25s ease-out;
}

.upload-toast--success {
background: linear-gradient(135deg, #e6f9f0 0%, #d4f5e5 100%);
border: 1px solid #a3d9b8;
color: #1a6640;
}

.upload-toast--error {
background: linear-gradient(135deg, #fee 0%, #fdd 100%);
border: 1px solid #fcc;
color: #c33;
}

/* Smooth animations */
@keyframes fadeIn {
from {
Expand Down
145 changes: 104 additions & 41 deletions samples/Dataverse/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@
* Dataverse Demo App - Main Application Component
*
* This app demonstrates how to use Power Apps code apps with Dataverse:
* - CRUD operations (Create, Read, Update, Delete) on Contact entities
* - CRUD operations (Create, Read, Update, Delete) on Contact and Account entities
* - Lookup fields (linking Contacts to Accounts)
* - File upload (AccountForm attachment sub-form)
* - Error handling and best practices
* - Component-based architecture
*
Expand All @@ -14,12 +15,13 @@
* 1. COMPONENTS (Presentation Layer):
* - Pure presentational components (UI only, no business logic)
* - Receive data via props, emit events via callbacks
* - Examples: Header, ContactList, ContactForm, ErrorMessage
* - Examples: Header, ContactList, ContactForm, AccountList, AccountForm,
* ErrorMessage
*
* 2. HOOKS (Business Logic Layer):
* - Custom hooks that manage state and orchestrate services
* - Handle error handling, loading states, and data transformations
* - Examples: useContacts, useAccounts, useLookupResolver
* - Examples: useContacts, useAccounts, useAccountsCrud, useLookupResolver
*
* 3. SERVICES (Data Access Layer):
* - Auto-generated by PAC CLI from Dataverse metadata
Expand All @@ -38,35 +40,56 @@
* - Clarity: Clear boundaries between presentation, logic, and data access
*/

import { useState } from 'react';
import {
Header,
ErrorMessage,
ContactList,
ContactForm,
AccountList,
AccountForm,
Footer,
} from "./components";
import { useContacts, useAccounts } from "./hooks";
import "./App.css";
} from './components';
import { useContacts, useAccounts, useAccountsCrud } from './hooks';
import './App.css';

type ActivePage = 'contacts' | 'accounts';

function App() {
const [activePage, setActivePage] = useState<ActivePage>('contacts');

// PATTERN: Custom hooks handle all the business logic and state management
// The component stays simple and focused on rendering UI
const {
contacts, // Array of contact records
loading, // Loading state for initial data fetch
error, // Error message string (null if no error)
selectedContact, // Currently selected contact for editing (null if none)
isCreating, // Flag indicating if we're in "create new" mode
startCreate, // Function to enter create mode
selectContact, // Function to select a contact for editing
cancelForm, // Function to cancel create/edit and return to list
handleFormSubmit, // Function to handle form submission (routes to create or update)
deleteContact, // Function to delete a contact
contacts,
loading: contactsLoading,
error: contactsError,
selectedContact,
isCreating: isCreatingContact,
startCreate: startCreateContact,
selectContact,
cancelForm: cancelContactForm,
handleFormSubmit: handleContactFormSubmit,
deleteContact,
} = useContacts();

// Load accounts for the Managing Partner lookup dropdown
// Load accounts for the Managing Partner lookup dropdown in the Contact form
const { accounts } = useAccounts();

// Full CRUD hook for the Accounts page
const {
accounts: accountsList,
loading: accountsLoading,
error: accountsError,
selectedAccount,
isCreating: isCreatingAccount,
startCreate: startCreateAccount,
selectAccount,
cancelForm: cancelAccountForm,
handleFormSubmit: handleAccountFormSubmit,
deleteAccount,
} = useAccountsCrud();

return (
<div className="app-container">
{/* Header Section */}
Expand All @@ -75,32 +98,72 @@ function App() {
description="Demonstrating CRUD operations with Power Apps Code Apps"
/>

{/* Error Display - Shows only when error exists */}
<ErrorMessage error={error} />
{/* Page Navigation Tabs */}
<nav className="page-tabs">
<button
className={`tab-btn ${activePage === 'contacts' ? 'active' : ''}`}
onClick={() => setActivePage('contacts')}
>
Contacts
</button>
<button
className={`tab-btn ${activePage === 'accounts' ? 'active' : ''}`}
onClick={() => setActivePage('accounts')}
>
Accounts
</button>
</nav>

{/* Main Content Grid - Two column layout (list + form) */}
<div className="content-grid">
{/* Left Column: Contacts List */}
<ContactList
contacts={contacts}
selectedContact={selectedContact}
loading={loading}
onSelect={selectContact}
onCreateNew={startCreate}
/>
{/* Contacts Page */}
{activePage === 'contacts' && (
<>
<ErrorMessage error={contactsError} />
<div className="content-grid">
<ContactList
contacts={contacts}
selectedContact={selectedContact}
loading={contactsLoading}
onSelect={selectContact}
onCreateNew={startCreateContact}
/>
{(isCreatingContact || selectedContact) && (
<ContactForm
selectedContact={selectedContact}
isCreating={isCreatingContact}
accounts={accounts}
onSubmit={handleContactFormSubmit}
onCancel={cancelContactForm}
onDelete={deleteContact}
/>
)}
</div>
</>
)}

{/* Right Column: Contact Form (shown only when creating or editing) */}
{(isCreating || selectedContact) && (
<ContactForm
selectedContact={selectedContact}
isCreating={isCreating}
accounts={accounts}
onSubmit={handleFormSubmit}
onCancel={cancelForm}
onDelete={deleteContact}
/>
)}
</div>
{/* Accounts Page */}
{activePage === 'accounts' && (
<>
<ErrorMessage error={accountsError} />
<div className="content-grid">
<AccountList
accounts={accountsList}
selectedAccount={selectedAccount}
loading={accountsLoading}
onSelect={selectAccount}
onCreateNew={startCreateAccount}
/>
{(isCreatingAccount || selectedAccount) && (
<AccountForm
selectedAccount={selectedAccount}
isCreating={isCreatingAccount}
onSubmit={handleAccountFormSubmit}
onCancel={cancelAccountForm}
onDelete={deleteAccount}
/>
)}
</div>
</>
)}

{/* Footer Section */}
<Footer />
Expand Down
42 changes: 42 additions & 0 deletions samples/Dataverse/src/components/AccountCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/**
* AccountCard Component
* Displays a single account in a card format with actions
*
* PATTERN: Pure Presentational Component
* - Minimal component focused on a single UI concern
* - No state or business logic
* - Delegates all actions via callback props
*/

import type { Accounts } from '../generated/models/AccountsModel';

interface AccountCardProps {
account: Accounts;
isSelected: boolean;
onSelect: (account: Accounts) => void;
}

export function AccountCard({ account, isSelected, onSelect }: AccountCardProps) {
return (
<div
className={`contact-card ${isSelected ? 'selected' : ''}`}
onClick={() => onSelect(account)}
>
<h3>{account.name}</h3>

{account.emailaddress1 && (
<p className="email">{account.emailaddress1}</p>
)}

{account.telephone1 && !account.emailaddress1 && (
<p className="email">{account.telephone1}</p>
)}

{(account.address1_city || account.address1_country) && (
<p className="email">
{[account.address1_city, account.address1_country].filter(Boolean).join(', ')}
</p>
)}
</div>
);
}
Loading