Add support for items list as notes are "liked"

This commit is contained in:
Dan Wahlin 2021-08-27 15:42:57 -07:00
Родитель debbb697d8
Коммит c1a4789d11
16 изменённых файлов: 316 добавлений и 65 удалений

54
package-lock.json сгенерированный
Просмотреть файл

@ -10,6 +10,7 @@
"license": "MIT",
"dependencies": {
"@fluentui/react": "^8.29.2",
"@fluentui/react-icons-mdl2": "^1.2.1",
"@fluidframework/azure-client": "^0.46.0",
"@fluidframework/test-runtime-utils": "0.46.0",
"@microsoft/mgt-element": "^2.2.1",
@ -2226,6 +2227,37 @@
"react": ">=16.8.0 <18.0.0"
}
},
"node_modules/@fluentui/react-icon-provider": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@fluentui/react-icon-provider/-/react-icon-provider-1.2.1.tgz",
"integrity": "sha512-P6Agy31ZF/S+4xDNBJzLQejkncKvblvhSwl0er4sLgPqID2fcIT6y0MGS4kOqMib9sC+aejpIav2pHqd+EMChg==",
"dependencies": {
"@fluentui/set-version": "^8.1.4",
"@fluentui/style-utilities": "^8.3.1",
"tslib": "^2.1.0"
},
"peerDependencies": {
"@types/react": ">=16.8.0 <18.0.0",
"@types/react-dom": ">=16.8.0 <18.0.0",
"react": ">=16.8.0 <18.0.0",
"react-dom": ">=16.8.0 <18.0.0"
}
},
"node_modules/@fluentui/react-icons-mdl2": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@fluentui/react-icons-mdl2/-/react-icons-mdl2-1.2.1.tgz",
"integrity": "sha512-vQRrgeDfmDxZJCeh/iW8yCAtpwfUrpHI0QU2NojXl5U0uv4KuOHp39UJvu8dzdp1zZ7arEYMoIgcq0eNqssnQA==",
"dependencies": {
"@fluentui/react-icon-provider": "^1.2.1",
"@fluentui/set-version": "^8.1.4",
"@fluentui/utilities": "^8.3.1",
"@microsoft/load-themed-styles": "^1.10.26",
"tslib": "^2.1.0"
},
"peerDependencies": {
"react": ">=16.8.0 <18.0.0"
}
},
"node_modules/@fluentui/react-window-provider": {
"version": "2.1.4",
"resolved": "https://registry.npmjs.org/@fluentui/react-window-provider/-/react-window-provider-2.1.4.tgz",
@ -28073,6 +28105,28 @@
"tslib": "^2.1.0"
}
},
"@fluentui/react-icon-provider": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@fluentui/react-icon-provider/-/react-icon-provider-1.2.1.tgz",
"integrity": "sha512-P6Agy31ZF/S+4xDNBJzLQejkncKvblvhSwl0er4sLgPqID2fcIT6y0MGS4kOqMib9sC+aejpIav2pHqd+EMChg==",
"requires": {
"@fluentui/set-version": "^8.1.4",
"@fluentui/style-utilities": "^8.3.1",
"tslib": "^2.1.0"
}
},
"@fluentui/react-icons-mdl2": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@fluentui/react-icons-mdl2/-/react-icons-mdl2-1.2.1.tgz",
"integrity": "sha512-vQRrgeDfmDxZJCeh/iW8yCAtpwfUrpHI0QU2NojXl5U0uv4KuOHp39UJvu8dzdp1zZ7arEYMoIgcq0eNqssnQA==",
"requires": {
"@fluentui/react-icon-provider": "^1.2.1",
"@fluentui/set-version": "^8.1.4",
"@fluentui/utilities": "^8.3.1",
"@microsoft/load-themed-styles": "^1.10.26",
"tslib": "^2.1.0"
}
},
"@fluentui/react-window-provider": {
"version": "2.1.4",
"resolved": "https://registry.npmjs.org/@fluentui/react-window-provider/-/react-window-provider-2.1.4.tgz",

Просмотреть файл

@ -45,6 +45,7 @@
},
"dependencies": {
"@fluentui/react": "^8.29.2",
"@fluentui/react-icons-mdl2": "^1.2.1",
"fluid-framework": "^0.46.0",
"@fluidframework/azure-client": "^0.46.0",
"@fluidframework/test-runtime-utils": "0.46.0",

Просмотреть файл

@ -1,6 +1,6 @@
import { FluidContainer, ISharedMap, SharedMap } from "fluid-framework";
import { AzureMember } from "@fluidframework/azure-client";
import { NoteData, Position } from "./Types";
import { LikedNote, NoteData, Position } from "./Types";
const c_NoteIdPrefix = "noteId_";
const c_PositionPrefix = "position_";
@ -19,6 +19,7 @@ export type BrainstormModel = Readonly<{
GetNoteLikedUsers(noteId: string): AzureMember[];
DeleteNote(noteId: string): void;
NoteIds: string[];
LikedNotes: LikedNote[];
setChangeListener(listener: () => void): void;
removeChangeListener(listener: () => void): void;
}>;
@ -49,6 +50,12 @@ export function createBrainstormModel(fluid: FluidContainer): BrainstormModel {
sharedMap.set(c_ColorPrefix + noteId, noteColor);
};
const numLikesCalculated = (noteId: string) => {
return Array.from(sharedMap
.keys())
.filter((key: string) => key.includes(c_votePrefix + noteId))
.filter((key: string) => sharedMap.get(key) !== undefined).length;
};
return {
CreateNote(noteId: string, myAuthor: AzureMember): NoteData {
@ -57,10 +64,7 @@ export function createBrainstormModel(fluid: FluidContainer): BrainstormModel {
text: sharedMap.get(c_TextPrefix + noteId),
position: sharedMap.get(c_PositionPrefix + noteId)!,
author: sharedMap.get(c_AuthorPrefix + noteId)!,
numLikesCalculated: Array.from(sharedMap
.keys())
.filter((key: string) => key.includes(c_votePrefix + noteId))
.filter((key: string) => sharedMap.get(key) !== undefined).length,
numLikesCalculated: numLikesCalculated(noteId),
didILikeThisCalculated:
Array.from(sharedMap
.keys())
@ -127,6 +131,38 @@ export function createBrainstormModel(fluid: FluidContainer): BrainstormModel {
);
},
get LikedNotes(): LikedNote[] {
return (
Array.from(sharedMap
.keys())
// Only look at keys which represent if a note exists or not
.filter((key: String) => key.includes(c_NoteIdPrefix))
// Modify the note ids to not expose the prefix
.map((noteIdWithPrefix) =>
noteIdWithPrefix.substring(c_NoteIdPrefix.length)
)
// Remove notes which are incomplete or deleted
.filter((noteId) =>
!IsDeletedNote(noteId) && numLikesCalculated(noteId) > 0 &&
sharedMap.get(c_TextPrefix + noteId)
)
.map((noteId) => {
const text = sharedMap.get(c_TextPrefix + noteId);
const color = sharedMap.get(c_ColorPrefix + noteId);
const author = sharedMap.get(c_AuthorPrefix + noteId);
return {
text,
color,
author,
numLikesCalculated: numLikesCalculated(noteId)
};
})
.sort((a: LikedNote, b: LikedNote) => {
return b.numLikesCalculated - a.numLikesCalculated;
})
);
},
setChangeListener(listener: () => void): void {
sharedMap.on("valueChanged", listener);
},

Просмотреть файл

@ -1,6 +1,6 @@
import { AzureConnectionConfig, InsecureTokenProvider } from "@fluidframework/azure-client";
import { SharedMap } from "fluid-framework";
import { generateUser } from './utils';
import { generateUser } from './Utils';
export const useAzureFrs = process.env.REACT_APP_FLUID_CLIENT === "frs";
export const user = generateUser();

Просмотреть файл

@ -1,9 +1,9 @@
export function Navbar() {
return (
<header>
<div className="container">
<div className="title">Let's Brainstorm</div>
<div className="login"></div>
<div className="grid-container">
<div className="left title">Let's Brainstorm</div>
<div className="right login end"></div>
</div>
</header>
);

Просмотреть файл

@ -19,3 +19,10 @@ export type ColorId =
| "Pink"
| "Purple"
| "Orange";
export type LikedNote = {
text: string,
color: string,
author: AzureMember,
numLikesCalculated: number
};

Просмотреть файл

@ -4,17 +4,16 @@
*/
import { initializeIcons, ThemeProvider } from "@fluentui/react";
import { AzureClient, AzureResources } from '@fluidframework/azure-client';
import React from 'react';
import ReactDOM from 'react-dom';
import { BrainstormView } from './view/BrainstormView';
import "./view/index.css";
import "./view/App.css";
import { themeNameToTheme } from './view/Themes';
import { connectionConfig, containerSchema } from "./Config";
import { Navbar } from './Navbar';
import { Providers } from '@microsoft/mgt-element';
import { Msal2Provider } from '@microsoft/mgt-msal2-provider';
import { getFluidContainer } from "./Utils";
Providers.globalProvider = new Msal2Provider({
clientId: '26fa7fdf-ae13-4db0-84f8-8249376812dc'
@ -23,37 +22,7 @@ Providers.globalProvider = new Msal2Provider({
export async function start() {
initializeIcons();
async function createOrGetContainer() :
Promise<{ containerId: string, azureResources: AzureResources}> {
let containerId = '';
// Check if there's a previous containerId (user may have simply logged out)
const prevContainerId = sessionStorage.getItem("containerId");
if (location.hash.length === 0) {
if (prevContainerId) {
location.hash = prevContainerId;
containerId = prevContainerId;
}
}
else {
containerId = location.hash.substring(1);
}
const client = new AzureClient(connectionConfig);
let azureResources: AzureResources;
if (containerId) {
azureResources = await client.getContainer(containerId, containerSchema);
}
else {
azureResources = await client.createContainer(containerSchema);
// Temporary until attach() is available (per Fluid engineering)
containerId = azureResources.fluidContainer.id;
location.hash = containerId;
}
sessionStorage.setItem("containerId", containerId);
return {containerId, azureResources};
}
let {azureResources} = await createOrGetContainer();
let {azureResources} = await getFluidContainer();
if (!azureResources.fluidContainer.connected) {
await new Promise<void>((resolve) => {

Просмотреть файл

@ -1,3 +1,36 @@
import { AzureClient, AzureResources } from "@fluidframework/azure-client";
import { connectionConfig, containerSchema } from "./Config";
export async function getFluidContainer() :
Promise<{ containerId: string, azureResources: AzureResources}> {
let containerId = '';
// Check if there's a previous containerId (user may have simply logged out)
const prevContainerId = sessionStorage.getItem("containerId");
if (location.hash.length === 0) {
if (prevContainerId) {
location.hash = prevContainerId;
containerId = prevContainerId;
}
}
else {
containerId = location.hash.substring(1);
}
const client = new AzureClient(connectionConfig);
let azureResources: AzureResources;
if (containerId) {
azureResources = await client.getContainer(containerId, containerSchema);
}
else {
azureResources = await client.createContainer(containerSchema);
// Temporary until attach() is available (per Fluid engineering)
containerId = azureResources.fluidContainer.id;
location.hash = containerId;
}
sessionStorage.setItem("containerId", containerId);
return {containerId, azureResources};
}
export function generateUser() {
const randomUser = {
id: uuidv4(),

Просмотреть файл

@ -28,24 +28,96 @@ header {
height: 40px;
}
.container {
.grid-container {
display: grid;
grid-template-columns: repeat(2, 1fr);
grid-column-gap: 0px;
grid-template-areas: "title login";
grid-template-areas: "left right";
align-items: center;
height: 100%;
color: white;
margin-left: 25px;
margin-right: 25px;
}
.left {
grid-area: left;
}
.right {
grid-area: right;
}
.end {
justify-self: end;
}
.start {
justify-self: start;
}
.title {
grid-area: title;
margin-left: 25px;
font-size: 20px;
color: white;
}
.login {
grid-area: login;
justify-self: end;
}
color: white;
margin-right: 25px;
}
.white {
color: white;
}
.mr-15 {
margin-right: 15px;
}
.mb-5 {
margin-bottom: 5px;
}
.selected-items {
background-color: #efefef;
}
.selected-items h2 {
height: 15px;
background-color: #ccc;
margin-bottom: 0px;
}
.selected-items h2 div {
margin-left: 10px;
}
.items-list ul {
list-style-type: none;
padding: 0;
margin: 5px 0px 0px 10px;
font-size: 12px;
padding-bottom: 2px;
}
.items-list li {
margin-bottom: 5px;
}
.icon-wrapper {
position: relative;
text-align: center;
height: 20px;
margin-right: 5px;
}
.circle-icon {
font-size: 18px;
}
.circle-icon-overlay {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: #fff;
padding-bottom: 2px;
}

Просмотреть файл

@ -5,6 +5,7 @@ import { DndProvider } from 'react-dnd'
import { HTML5Backend } from 'react-dnd-html5-backend'
import { BrainstormModel, createBrainstormModel } from "../BrainstormModel";
import { Header } from "./Header";
import { ItemsList } from "./ItemsList";
import { NoteSpace } from "./NoteSpace";
export const BrainstormView = (props: { frsResources: AzureResources }) => {
@ -47,12 +48,17 @@ export const BrainstormView = (props: { frsResources: AzureResources }) => {
author={authorInfo}
members={members}
/>
<DndProvider backend={HTML5Backend}>
<NoteSpace
model={model}
author={authorInfo}
/>
</DndProvider>
<div className="items-list">
<ItemsList model={model} />
</div>
<div>
<DndProvider backend={HTML5Backend}>
<NoteSpace
model={model}
author={authorInfo}
/>
</DndProvider>
</div>
</div>
);
};

Просмотреть файл

@ -10,7 +10,7 @@ import { DefaultColor } from "./Color";
import { ColorPicker } from "./ColorPicker";
import { NoteData } from "../Types";
import { NOTE_SIZE } from "./Note.style";
import { uuidv4 } from '../utils';
import { uuidv4 } from '../Utils';
export interface HeaderProps {
model: BrainstormModel;

59
src/view/ItemsList.tsx Normal file
Просмотреть файл

@ -0,0 +1,59 @@
import { useEffect, useState } from "react";
import { BrainstormModel } from "../BrainstormModel";
import { ColorId, LikedNote } from "../Types";
import { CircleFillIcon } from '@fluentui/react-icons-mdl2';
import { ColorOptions } from "./Color";
export type ItemsListProps = Readonly<{
model: BrainstormModel;
}>;
export function ItemsList(props: ItemsListProps) {
const { model } = props;
const [notes, setNotes] = useState<readonly LikedNote[]>([]);
// This runs when via model changes whether initiated by user or from external
useEffect(() => {
const syncLocalAndFluidState = () => {
const likedNotes: LikedNote[] = model.LikedNotes;
setNotes(likedNotes);
};
syncLocalAndFluidState();
model.setChangeListener(syncLocalAndFluidState);
return () => model.removeChangeListener(syncLocalAndFluidState);
}, [model]);
return (
<div className="items-list">
<div className="selected-items">
<h2 className="grid-container">
<div className="left">Selected Items</div>
<div className="right end mr-15">Votes</div>
</h2>
{!!notes.length &&
<ul>
{notes.map((note, i) => {
const iconColor = ColorOptions[note.color as ColorId].base;
return (
<li key={i}>
<div className="grid-container">
<div className="left">{note.text}</div>
<div className="right end mr-15">
<div className="icon-wrapper">
<CircleFillIcon className="circle-icon"
style={{color: iconColor}} />
<span className="circle-icon-overlay">{note.numLikesCalculated}</span>
</div>
</div>
</div>
</li>
)
})}
</ul>
}
</div>
</div>
);
}

Просмотреть файл

@ -3,8 +3,8 @@ import { ColorOptions } from "./Color";
import { ColorId } from "../Types";
export const NOTE_SIZE = {
width: 300,
height: 100
width: 250,
height: 75
}
export const tooltipHostStyle: Partial<ITooltipHostStyles> = {
@ -38,7 +38,13 @@ export const likesButtonStyle: IButtonStyles = {
root: { backgroundColor: "transparent" },
rootHovered: { backgroundColor: "transparent", fontSize: "18px" },
rootPressed: { backgroundColor: "transparent" },
iconHovered: { fontSize: "18px" }
iconHovered: { fontSize: "18px" },
};
export const likesButtonAuthorStyle: IButtonStyles = {
root: { backgroundColor: "transparent" },
rootHovered: { backgroundColor: "transparent", fontSize: "14px" },
rootPressed: { backgroundColor: "transparent" },
};
export function getRootStyleForColor(color: ColorId): IStyle {
@ -49,7 +55,7 @@ export function getRootStyleForColor(color: ColorId): IStyle {
boxShadow:
"rgb(0 0 0 / 13%) 0px 1.6px 3.6px 0px, rgb(0 0 0 / 11%) 0px 0.3px 0.9px 0px",
width: NOTE_SIZE.width,
minHeight: NOTE_SIZE.height
minHeight: NOTE_SIZE.height,
};
}

Просмотреть файл

@ -14,6 +14,7 @@ import { NoteBody } from "./NoteBody";
export type NoteProps = Readonly<{
id: string;
user: AzureMember;
setPosition: (position: Position) => void;
onLike: () => void;
getLikedUsers: () => AzureMember[];

Просмотреть файл

@ -17,12 +17,14 @@ import {
colorButtonStyle,
likesButtonStyle,
tooltipHostStyle,
likesButtonAuthorStyle,
} from "./Note.style";
import { ReactionListCallout } from "./ReactionListCallout";
import { NoteProps } from "./Note"
const HeaderComponent = (props: NoteProps) => {
const colorButtonRef = React.useRef();
const { user } = props;
const headerProps = {
className: mergeStyles(getHeaderStyleForColor(props.color)),
@ -84,7 +86,7 @@ const HeaderComponent = (props: NoteProps) => {
iconProps: {
iconName: props.didILikeThisCalculated ? "LikeSolid" : "Like",
},
buttonStyles: likesButtonStyle,
buttonStyles: isAuthorNote() ? likesButtonAuthorStyle : likesButtonStyle,
commandBarButtonAs: (props) => {
return (
<TooltipHost
@ -123,9 +125,13 @@ const HeaderComponent = (props: NoteProps) => {
title: "Delete Note",
onClick: props.onDelete,
buttonStyles: deleteButtonStyle,
},
}
];
function isAuthorNote() {
return user.userId && props.author.userId === user.userId;
}
const nonResizingGroup = (props: IResizeGroupProps) => (
<div>
<div style={{ position: "relative" }}>

Просмотреть файл

@ -92,6 +92,7 @@ export function NoteSpace(props: NoteSpaceProps) {
id={note.id}
key={note.id}
text={note.text}
user={props.author}
setPosition={setPosition}
onLike={onLike}
getLikedUsers={getLikedUsers}