back ported staging, switch to dev proto, updated proto

This commit is contained in:
Carter 2026-03-19 13:57:30 -06:00
commit 8ecb9e0ca0
14 changed files with 59 additions and 35 deletions

4
package-lock.json generated
View file

@ -43,7 +43,7 @@
"mui-tel-input": "^7.0.0", "mui-tel-input": "^7.0.0",
"notistack": "^3.0.1", "notistack": "^3.0.1",
"openweathermap-ts": "^1.2.10", "openweathermap-ts": "^1.2.10",
"protobuf-ts": "git+https://gitlab+deploy-token-50627:hv8mB4WkyvtjBpJKU1rN@gitlab.com/brandx/protobuf-ts.git#dev", "protobuf-ts": "git+https://gitlab+deploy-token-50627:hv8mB4WkyvtjBpJKU1rN@gitlab.com/brandx/protobuf-ts.git#staging",
"query-string": "^9.2.1", "query-string": "^9.2.1",
"react": "^18.3.1", "react": "^18.3.1",
"react-beautiful-dnd": "^13.1.1", "react-beautiful-dnd": "^13.1.1",
@ -11205,7 +11205,7 @@
}, },
"node_modules/protobuf-ts": { "node_modules/protobuf-ts": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "git+https://gitlab+deploy-token-50627:hv8mB4WkyvtjBpJKU1rN@gitlab.com/brandx/protobuf-ts.git#40e15b5ed18b10c97125260a2d53a81d1eec398e", "resolved": "git+https://gitlab+deploy-token-50627:hv8mB4WkyvtjBpJKU1rN@gitlab.com/brandx/protobuf-ts.git#9c0f668d4a56b8216dd71a44c3110a818aaf3541",
"dependencies": { "dependencies": {
"protobufjs": "^6.8.8" "protobufjs": "^6.8.8"
} }

View file

@ -21,13 +21,14 @@ const useStyles = makeStyles((_theme: Theme) => {
}) })
interface Props { interface Props {
objectKey: string; parent: string;
parentType: string;
type?: pond.NoteType; type?: pond.NoteType;
} }
export default function Chat(props: Props) { export default function Chat(props: Props) {
const [chats, setChats] = useState<pond.Chat[]>([]); const [chats, setChats] = useState<pond.Chat[]>([]);
const { objectKey, type } = props; const { parent, parentType, type } = props;
const noteAPI = useNoteAPI(); const noteAPI = useNoteAPI();
const [loaded, setLoaded] = useState<boolean>(false); const [loaded, setLoaded] = useState<boolean>(false);
const [loading, setLoading] = useState<boolean>(false); const [loading, setLoading] = useState<boolean>(false);
@ -38,7 +39,7 @@ export default function Chat(props: Props) {
const loadChats = () => { const loadChats = () => {
setLoading(true) setLoading(true)
// console.log("listing chats?") // console.log("listing chats?")
noteAPI.listChats(10, chats.length, "desc", "timestamp", objectKey).then(resp => { noteAPI.listChats(10, chats.length, "desc", "timestamp", parent, [parent], [parentType]).then(resp => {
setChats(resp.data.chats ? resp.data.chats.reverse().concat(chats) : []) setChats(resp.data.chats ? resp.data.chats.reverse().concat(chats) : [])
setTotalMessages(resp.data.total) setTotalMessages(resp.data.total)
}).finally(() => { }).finally(() => {
@ -81,13 +82,13 @@ export default function Chat(props: Props) {
setLoaded(false); setLoaded(false);
setLoading(false); setLoading(false);
setTotalMessages(0); setTotalMessages(0);
}, [objectKey]); }, [parent]);
useEffect(() => { useEffect(() => {
if (chats.length === 0 && !loaded) { if (chats.length === 0 && !loaded) {
loadChats(); loadChats();
} }
}, [objectKey, chats, loaded]); }, [parent, chats, loaded]);
const loadMore = () => { const loadMore = () => {
loadChats(); loadChats();
@ -111,7 +112,7 @@ export default function Chat(props: Props) {
</Box> </Box>
<Box style={{ flexShrink: 0 }}> <Box style={{ flexShrink: 0 }}>
<Divider /> <Divider />
<ChatInput newNoteMethod={showNewNotes} objectKey={objectKey} type={type} /> <ChatInput newNoteMethod={showNewNotes} parent={parent} type={type} parentType={parentType} />
</Box> </Box>
</Box> </Box>
); );

View file

@ -143,7 +143,7 @@ export function ChatDrawer(props: Props) {
<Typography>{selectedTeam.name()} Chat</Typography> <Typography>{selectedTeam.name()} Chat</Typography>
</Box> </Box>
<Box className={classes.chatContainer}> <Box className={classes.chatContainer}>
<Chat objectKey={selectedTeam.key()} type={pond.NoteType.NOTE_TYPE_TEAM} /> <Chat parent={selectedTeam.key()} parentType={"team"} type={pond.NoteType.NOTE_TYPE_TEAM} />
</Box> </Box>
</Box> </Box>
</Box> </Box>

View file

@ -40,8 +40,9 @@ const useStyles = makeStyles((theme: Theme) => ({
})) }))
interface Props { interface Props {
objectKey: string; parent: string;
newNoteMethod: (note: Note) => void; newNoteMethod: (note: Note) => void;
parentType: string; // the object type the note is for ie "bin", "team" etc.
type?: pond.NoteType; type?: pond.NoteType;
} }
@ -52,13 +53,13 @@ interface Props {
export default function ChatInput(props: Props) { export default function ChatInput(props: Props) {
const [message, setMessage] = useState(""); const [message, setMessage] = useState("");
const classes = useStyles(); const classes = useStyles();
const { objectKey, type } = props; const { parent, type, parentType } = props;
const [{ user }] = useGlobalState(); const [{ user }] = useGlobalState();
const noteAPI = useNoteAPI(); const noteAPI = useNoteAPI();
const { openSnack } = useSnackbar(); const { openSnack } = useSnackbar();
const [shift, setShift] = useState(false); const [shift, setShift] = useState(false);
const theme = useTheme(); const theme = useTheme();
const [uploadedFiles, setUploadedFiles] = useState<Map<string, string>>(new Map()); // const [uploadedFiles, setUploadedFiles] = useState<Map<string, string>>(new Map());
const clearEntry = () => { const clearEntry = () => {
setMessage(""); setMessage("");
@ -93,13 +94,13 @@ export default function ChatInput(props: Props) {
} }
valid ? submit() : openSnack("Invalid Message"); valid ? submit() : openSnack("Invalid Message");
clearEntry(); clearEntry();
setUploadedFiles(new Map()); // setUploadedFiles(new Map());
}; };
const submit = () => { const submit = () => {
if (message !== "" && message !== "\n") { if (message !== "" && message !== "\n") {
let newNote = Note.create(); let newNote = Note.create();
newNote.settings.objectKey = objectKey; newNote.settings.objectKey = parent;
newNote.settings.userId = user.id(); newNote.settings.userId = user.id();
if (type) { if (type) {
newNote.settings.objectType = type; newNote.settings.objectType = type;
@ -107,13 +108,14 @@ export default function ChatInput(props: Props) {
newNote.settings.timestamp = Date.now(); newNote.settings.timestamp = Date.now();
newNote.settings.content = message; newNote.settings.content = message;
noteAPI noteAPI
.addNote(newNote.settings, Array.from(uploadedFiles.keys())) .addNote(newNote.settings, undefined, [parent], [parentType])
.then(resp => { .then(resp => {
newNote.settings.key = resp.data.note newNote.settings.key = resp.data.note
props.newNoteMethod(newNote); props.newNoteMethod(newNote);
openSnack("Message Sent"); openSnack("Message Sent");
}) })
.catch(_err => { .catch(_err => {
console.error(_err)
openSnack("Message Failed to send"); openSnack("Message Failed to send");
}); });
} }
@ -162,7 +164,7 @@ export default function ChatInput(props: Props) {
</Box> </Box>
</Grid> </Grid>
</Grid> </Grid>
<Typography>{Array.from(uploadedFiles.values()).toString()}</Typography> {/* <Typography>{Array.from(uploadedFiles.values()).toString()}</Typography> */}
</React.Fragment> </React.Fragment>
); );
} }

View file

@ -151,7 +151,7 @@ export default function HarvestPlanDisplay(props: Props) {
onClose={() => setOpenNote(false)}> onClose={() => setOpenNote(false)}>
<Box height={isMobile ? "50vh" : "100vh"} padding={2}> <Box height={isMobile ? "50vh" : "100vh"} padding={2}>
<Typography style={{ fontWeight: 650 }}>Notes</Typography> <Typography style={{ fontWeight: 650 }}>Notes</Typography>
<Chat type={pond.NoteType.NOTE_TYPE_HARVEST_PLAN} objectKey={plan.key()} /> <Chat type={pond.NoteType.NOTE_TYPE_HARVEST_PLAN} parent={plan.key()} parentType={"harvestPlan"} />
</Box> </Box>
</Drawer> </Drawer>
); );

View file

@ -1,6 +1,9 @@
import { pond } from "protobuf-ts/pond"; import { pond } from "protobuf-ts/pond";
import { useEffect, useRef, useCallback, useMemo } from "react"; import { useEffect, useRef, useCallback, useMemo } from "react";
/** Off by default to reduce server load; set VITE_ENABLE_WEBSOCKETS=true to enable live streams. */
const WEBSOCKETS_ENABLED = import.meta.env.VITE_ENABLE_WEBSOCKETS === "true";
/** /**
* Derives the WebSocket base URL from VITE_APP_API_URL. * Derives the WebSocket base URL from VITE_APP_API_URL.
* Matches the logic in useWebSocket.ts. * Matches the logic in useWebSocket.ts.
@ -98,7 +101,7 @@ export function useDeviceStatusStreams(options: UseDeviceStatusStreamsOptions) {
); );
useEffect(() => { useEffect(() => {
if (!enabled || !token || deviceIds.length === 0) { if (!WEBSOCKETS_ENABLED || !enabled || !token || deviceIds.length === 0) {
wsMapRef.current.forEach((ws) => ws.close(1000, "disabled")); wsMapRef.current.forEach((ws) => ws.close(1000, "disabled"));
wsMapRef.current.clear(); wsMapRef.current.clear();
reconnectTimeoutsRef.current.forEach((t) => clearTimeout(t)); reconnectTimeoutsRef.current.forEach((t) => clearTimeout(t));

View file

@ -1,5 +1,8 @@
import { useEffect, useRef, useCallback } from "react"; import { useEffect, useRef, useCallback } from "react";
/** Off by default to reduce server load; set VITE_ENABLE_WEBSOCKETS=true to enable live streams. */
const WEBSOCKETS_ENABLED = import.meta.env.VITE_ENABLE_WEBSOCKETS === "true";
/** /**
* Derives the WebSocket base URL from VITE_APP_API_URL. * Derives the WebSocket base URL from VITE_APP_API_URL.
* e.g. "https://api.brandxtech.ca" -> "wss://api.brandxtech.ca" * e.g. "https://api.brandxtech.ca" -> "wss://api.brandxtech.ca"
@ -100,7 +103,7 @@ export function useWebSocket<T>(options: UseWebSocketOptions<T>) {
}, [path, token, rate]); }, [path, token, rate]);
useEffect(() => { useEffect(() => {
if (!enabled || !token) { if (!WEBSOCKETS_ENABLED || !enabled || !token) {
if (wsRef.current) { if (wsRef.current) {
wsRef.current.close(1000, "disabled"); wsRef.current.close(1000, "disabled");
wsRef.current = null; wsRef.current = null;

View file

@ -287,7 +287,7 @@ export default function FieldDrawer(props: Props) {
onClose={() => setOpenNote(false)}> onClose={() => setOpenNote(false)}>
<Box height={isMobile ? "50vh" : "100vh"} padding={2}> <Box height={isMobile ? "50vh" : "100vh"} padding={2}>
<Typography style={{ fontWeight: 650 }}>Notes</Typography> <Typography style={{ fontWeight: 650 }}>Notes</Typography>
<Chat type={pond.NoteType.NOTE_TYPE_FIELD} objectKey={field.key()} /> <Chat type={pond.NoteType.NOTE_TYPE_FIELD} parent={field.key()} parentType={"field"} />
</Box> </Box>
</Drawer> </Drawer>
); );

View file

@ -496,7 +496,7 @@ export default function Bin(props: Props) {
</Tabs> </Tabs>
<TabPanelMine value={value} index={0}> <TabPanelMine value={value} index={0}>
<Box height={isMobile || displayMobile ? "80vh" : "90vh"} padding={2}> <Box height={isMobile || displayMobile ? "80vh" : "90vh"} padding={2}>
<Chat objectKey={binID} type={pond.NoteType.NOTE_TYPE_BIN} /> <Chat parent={binID} parentType={"bin"} type={pond.NoteType.NOTE_TYPE_BIN} />
</Box> </Box>
</TabPanelMine> </TabPanelMine>
<TabPanelMine value={value} index={1}> <TabPanelMine value={value} index={1}>

View file

@ -281,7 +281,7 @@ export default function Contract() {
{contract.name()} - Notes {contract.name()} - Notes
</Typography> </Typography>
<Card style={{ padding: 10, marginTop: 15 }}> <Card style={{ padding: 10, marginTop: 15 }}>
<Chat objectKey={contract.key()} type={pond.NoteType.NOTE_TYPE_CONTRACT} /> <Chat parent={contract.key()} parentType={"contract"} type={pond.NoteType.NOTE_TYPE_CONTRACT} />
</Card> </Card>
</TabPanelMine> </TabPanelMine>
</React.Fragment> </React.Fragment>
@ -316,7 +316,7 @@ export default function Contract() {
<Box padding={2}> <Box padding={2}>
<Typography>Notes</Typography> <Typography>Notes</Typography>
<Card style={{ padding: 10 }}> <Card style={{ padding: 10 }}>
<Chat objectKey={contract.key()} type={pond.NoteType.NOTE_TYPE_CONTRACT} /> <Chat parent={contract.key()} parentType={"contract"} type={pond.NoteType.NOTE_TYPE_CONTRACT} />
</Card> </Card>
</Box> </Box>
</Drawer> </Drawer>

View file

@ -400,7 +400,7 @@ export default function Gate(props: Props) {
<Box padding={2}> <Box padding={2}>
<Typography>Notes</Typography> <Typography>Notes</Typography>
<Card style={{ padding: 10 }}> <Card style={{ padding: 10 }}>
<Chat objectKey={gate.key} type={pond.NoteType.NOTE_TYPE_GATE} /> <Chat parent={gate.key} type={pond.NoteType.NOTE_TYPE_GATE} parentType={"gate"} />
</Card> </Card>
</Box> </Box>
</Drawer> </Drawer>
@ -465,7 +465,7 @@ export default function Gate(props: Props) {
{/* tab for notes on mobile and the map drawer */} {/* tab for notes on mobile and the map drawer */}
<TabPanelMine value={displayTab} index={1}> <TabPanelMine value={displayTab} index={1}>
<Card style={{ padding: 10 }}> <Card style={{ padding: 10 }}>
<Chat objectKey={gate.key} type={pond.NoteType.NOTE_TYPE_GATE} /> <Chat parent={gate.key} parentType={"gate"} type={pond.NoteType.NOTE_TYPE_GATE} />
</Card> </Card>
</TabPanelMine> </TabPanelMine>
{/* drawer is for displaying notes on desktop */} {/* drawer is for displaying notes on desktop */}

View file

@ -251,7 +251,7 @@ export default function TeamPage() {
<Grid2 container spacing={1}> <Grid2 container spacing={1}>
<Grid2 size={{ xs: 12, sm: 4 }}> <Grid2 size={{ xs: 12, sm: 4 }}>
<Card className={ isMobile ? classes.cardMobile : classes.card }> <Card className={ isMobile ? classes.cardMobile : classes.card }>
<Chat objectKey={team.key()} type={pond.NoteType.NOTE_TYPE_TEAM}/> <Chat parent={team.key()} type={pond.NoteType.NOTE_TYPE_TEAM} parentType={"team"}/>
</Card> </Card>
</Grid2> </Grid2>
<Grid2 size={{ xs: 12, sm: 4 }}> <Grid2 size={{ xs: 12, sm: 4 }}>

View file

@ -6,7 +6,7 @@ import { createContext, PropsWithChildren, useContext } from "react";
import { pondURL } from "./pond"; import { pondURL } from "./pond";
export interface INoteAPIContext { export interface INoteAPIContext {
addNote: (note: pond.NoteSettings, attachments?: string[]) => Promise<any>; addNote: (note: pond.NoteSettings, attachments?: string[], keys?: string[], types?: string[]) => Promise<any>;
getNote: (noteID: string) => Promise<AxiosResponse<pond.Note>>; getNote: (noteID: string) => Promise<AxiosResponse<pond.Note>>;
updateNote: (note: pond.NoteSettings) => Promise<any>; updateNote: (note: pond.NoteSettings) => Promise<any>;
listNotes: ( listNotes: (
@ -16,7 +16,8 @@ export interface INoteAPIContext {
orderBy?: string, orderBy?: string,
search?: string, search?: string,
asRoot?: boolean, asRoot?: boolean,
otherTeam?: string // otherTeam?: string,
keys?: string[], types?: string[]
) => Promise<AxiosResponse<pond.ListNotesResponse>>; ) => Promise<AxiosResponse<pond.ListNotesResponse>>;
listChats: ( listChats: (
limit: number, limit: number,
@ -24,8 +25,9 @@ export interface INoteAPIContext {
order?: "asc" | "desc", order?: "asc" | "desc",
orderBy?: string, orderBy?: string,
search?: string, search?: string,
asRoot?: boolean, // asRoot?: boolean,
otherTeam?: string // otherTeam?: string
keys?: string[], types?: string[]
) => Promise<AxiosResponse<pond.ListChatsResponse>>; ) => Promise<AxiosResponse<pond.ListChatsResponse>>;
removeNote: (noteID: string) => Promise<AxiosResponse<pond.RemoveBinResponse>>; removeNote: (noteID: string) => Promise<AxiosResponse<pond.RemoveBinResponse>>;
} }
@ -39,8 +41,13 @@ export default function NoteProvider(props: PropsWithChildren<Props>) {
const { get, del, post, put } = useHTTP(); const { get, del, post, put } = useHTTP();
// const [{as}] = useGlobalState() // const [{as}] = useGlobalState()
const addNote = (note: pond.NoteSettings, attachments?: string[]) => { const addNote = (note: pond.NoteSettings, attachments?: string[], keys?: string[], types?: string[]) => {
let url = pondURL("/notes" + (attachments ? "?attachments=" + attachments.toString() : "")) let url = pondURL(
"/notes" +
// (attachments ? "?attachments=" + attachments.toString() : "") +
(keys ? "?keys=" + keys.join(",") : "") +
(types ? "&types=" + types.join(",") : "")
)
return new Promise<AxiosResponse>((resolve, reject) => { return new Promise<AxiosResponse>((resolve, reject) => {
post(url, note).then(resp => { post(url, note).then(resp => {
return resolve(resp) return resolve(resp)
@ -89,6 +96,8 @@ export default function NoteProvider(props: PropsWithChildren<Props>) {
search?: string, search?: string,
asRoot?: boolean, asRoot?: boolean,
// otherTeam?: string // otherTeam?: string
keys?: string[],
types?: string[]
) => { ) => {
// const view = otherTeam ? otherTeam : as // const view = otherTeam ? otherTeam : as
let url = pondURL( let url = pondURL(
@ -100,7 +109,9 @@ export default function NoteProvider(props: PropsWithChildren<Props>) {
("&order=" + (order ? order : "asc")) + ("&order=" + (order ? order : "asc")) +
("&by=" + (orderBy ? orderBy : "key")) + ("&by=" + (orderBy ? orderBy : "key")) +
(search ? "&search=" + search : "") + (search ? "&search=" + search : "") +
(asRoot ? "&asRoot=" + asRoot.toString() : ""), (asRoot ? "&asRoot=" + asRoot.toString() : "") +
(keys ? "&keys=" + keys.join(",") : "") +
(types ? "&types=" + types.join(",") : "")
// (view ? "&as=" + "" : "") // (view ? "&as=" + "" : "")
) )
// console.log(url) // console.log(url)
@ -122,6 +133,8 @@ export default function NoteProvider(props: PropsWithChildren<Props>) {
search?: string, search?: string,
// asRoot?: boolean, // asRoot?: boolean,
// otherTeam?: string // otherTeam?: string
keys?: string[],
types?: string[]
) => { ) => {
// const view = otherTeam ? otherTeam : as // const view = otherTeam ? otherTeam : as
let url = pondURL( let url = pondURL(
@ -132,7 +145,9 @@ export default function NoteProvider(props: PropsWithChildren<Props>) {
offset + offset +
("&order=" + (order ? order : "asc")) + ("&order=" + (order ? order : "asc")) +
("&by=" + (orderBy ? orderBy : "key")) + ("&by=" + (orderBy ? orderBy : "key")) +
(search ? "&search=" + search : ""), (search ? "&search=" + search : "") +
(keys ? "&keys=" + keys.join(",") : "") +
(types ? "&types=" + types.join(",") : "")
// (asRoot ? "&asRoot=" + asRoot.toString() : ""), // (asRoot ? "&asRoot=" + asRoot.toString() : ""),
// (view ? "&as=" + view : "") // (view ? "&as=" + view : "")
) )

View file

@ -194,7 +194,7 @@ export default function TaskDrawer(props: Props) {
onClose={() => setOpenNote(false)}> onClose={() => setOpenNote(false)}>
<Box height={isMobile ? "50vh" : "100vh"} padding={2}> <Box height={isMobile ? "50vh" : "100vh"} padding={2}>
<Typography style={{ fontWeight: 650 }}>Notes</Typography> <Typography style={{ fontWeight: 650 }}>Notes</Typography>
<Chat type={pond.NoteType.NOTE_TYPE_TASK} objectKey={task.key} /> <Chat type={pond.NoteType.NOTE_TYPE_TASK} parent={task.key} parentType={"task"} />
</Box> </Box>
</Drawer> </Drawer>
); );