diff --git a/samples/Dataverse/src/App.css b/samples/Dataverse/src/App.css index dda1952..634b647 100644 --- a/samples/Dataverse/src/App.css +++ b/samples/Dataverse/src/App.css @@ -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%); @@ -465,6 +500,129 @@ 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; + flex: 1; +} + +.file-action-btn { + flex-shrink: 0; + background: none; + border: none; + cursor: pointer; + color: rgba(112, 37, 115, 0.6); + font-size: 1rem; + line-height: 1; + padding: 0 2px; +} + +.file-action-btn:hover:not(:disabled) { + color: #702573; +} + +.file-action-btn:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.file-clear-btn { + margin-left: auto; + flex-shrink: 0; + background: none; + border: none; + cursor: pointer; + color: rgba(112, 37, 115, 0.6); + font-size: 1.2rem; + line-height: 1; + padding: 0 2px; +} + +.file-clear-btn:hover:not(:disabled) { + color: #c0392b; +} + +.file-clear-btn:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.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 { diff --git a/samples/Dataverse/src/App.tsx b/samples/Dataverse/src/App.tsx index 28babd5..4ea3b1f 100644 --- a/samples/Dataverse/src/App.tsx +++ b/samples/Dataverse/src/App.tsx @@ -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 * @@ -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 @@ -38,35 +40,57 @@ * - 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('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, + loadAccounts, + } = useAccountsCrud(); + return (
{/* Header Section */} @@ -75,32 +99,73 @@ function App() { description="Demonstrating CRUD operations with Power Apps Code Apps" /> - {/* Error Display - Shows only when error exists */} - + {/* Page Navigation Tabs */} + - {/* Main Content Grid - Two column layout (list + form) */} -
- {/* Left Column: Contacts List */} - + {/* Contacts Page */} + {activePage === 'contacts' && ( + <> + +
+ + {(isCreatingContact || selectedContact) && ( + + )} +
+ + )} - {/* Right Column: Contact Form (shown only when creating or editing) */} - {(isCreating || selectedContact) && ( - - )} -
+ {/* Accounts Page */} + {activePage === 'accounts' && ( + <> + +
+ + {(isCreatingAccount || selectedAccount) && ( + + )} +
+ + )} {/* Footer Section */}