✨ Start on error handling
♻ Use TypedDocumentNode's instead of apollo-react-typescript codegen
💄 Add toast
This commit is contained in:
1 parent
ed82ca3891
commit
0a435d3224
21 files changed
+561
-113
No files matched your search
@@ -1,14 +1,18 @@
|
||||
# Installation
|
||||
sudo apt-get install libmysqlclient-dev
|
||||
sudo apt-get build-dep python-imaging
|
||||
virtualenv --distribute venv
|
||||
. venv/bin/active
|
||||
pip install -r requirements
|
||||
python manage.py syncdb
|
||||
python manage.py runserver
|
||||
# Getting started
|
||||
Before starting, make sure you have a working python and nodejs setup with pyenv and nvm.
|
||||
|
||||
# TODO
|
||||
* gulpfile.js: reload on js update, csso
|
||||
```bash
|
||||
apt-get install libmysqlclient-dev
|
||||
poetry install
|
||||
poetry shell
|
||||
python manage.py migrate
|
||||
python manage.py runserver
|
||||
```
|
||||
|
||||
## References:
|
||||
- We use JS code generation to work with queries/mutations in our graphql API: https://github.com/dotansimha/graphql-typed-document-node
|
||||
|
||||
# TODO-old
|
||||
* Larger pictures
|
||||
* Compact GUI for max 768 vertical pixels
|
||||
* Search right of in title
|
||||
|
||||
Executable
+4
@@ -0,0 +1,4 @@
|
||||
#!/bin/bash
|
||||
set -eu
|
||||
python manage.py export_schema
|
||||
cd frontend && npm run generate
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "bbs-schema",
|
||||
"schemaPath": "schema.graphql",
|
||||
"schemaPath": "../schema.graphql",
|
||||
"extensions": {
|
||||
"endpoints": {
|
||||
"default": {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
schema: http://localhost:8000/graphql/
|
||||
schema: ../schema.graphql
|
||||
documents: ./src/**/*.graphql
|
||||
generates:
|
||||
./src/types.ts:
|
||||
plugins:
|
||||
- typescript
|
||||
- typescript-operations
|
||||
- typescript-react-apollo
|
||||
- typed-document-node
|
||||
Generated
+83
-3
@@ -20,8 +20,11 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@graphql-codegen/cli": "^2.2.2",
|
||||
"@graphql-codegen/typed-document-node": "^2.2.0",
|
||||
"@graphql-codegen/typescript-operations": "^2.2.0",
|
||||
"@graphql-codegen/typescript-react-apollo": "^3.2.0",
|
||||
"@graphql-typed-document-node/core": "^3.1.0",
|
||||
"@types/bootstrap": "^5.1.6",
|
||||
"@types/react": "^17.0.33",
|
||||
"@types/react-dom": "^17.0.10",
|
||||
"@types/react-router-dom": "^5.3.2",
|
||||
@@ -1606,6 +1609,22 @@
|
||||
"graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@graphql-codegen/typed-document-node": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@graphql-codegen/typed-document-node/-/typed-document-node-2.2.0.tgz",
|
||||
"integrity": "sha512-A/owHXw3f+OLCefY3qQnfQ5nEMbqQ5U/BEufRP3K15XXXUnoF1PpLvFrN0RcWRJtuzHfByt7Z2oKnNBEKyMUGg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@graphql-codegen/plugin-helpers": "^2.3.0",
|
||||
"@graphql-codegen/visitor-plugin-common": "2.5.0",
|
||||
"auto-bind": "~4.0.0",
|
||||
"change-case-all": "1.0.14",
|
||||
"tslib": "~2.3.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@graphql-codegen/typescript": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@graphql-codegen/typescript/-/typescript-2.3.0.tgz",
|
||||
@@ -2429,7 +2448,6 @@
|
||||
"version": "2.10.2",
|
||||
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.10.2.tgz",
|
||||
"integrity": "sha512-IXf3XA7+XyN7CP9gGh/XB0UxVMlvARGEgGXLubFICsUMGz6Q+DU+i4gGlpOxTjKvXjkJDJC8YdqdKkDj9qZHEQ==",
|
||||
"peer": true,
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/popperjs"
|
||||
@@ -2767,6 +2785,16 @@
|
||||
"@babel/types": "^7.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/bootstrap": {
|
||||
"version": "5.1.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/bootstrap/-/bootstrap-5.1.6.tgz",
|
||||
"integrity": "sha512-3L6IvOCKyoVd3e4bgQTH7VBPbuYEOG8IQbRcuZ0AbjfwPdRX+kVf5L/7mVt1EVM+D/BVw4+71rtp7Z8yYROlpQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@popperjs/core": "^2.9.2",
|
||||
"@types/jquery": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/graceful-fs": {
|
||||
"version": "4.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.5.tgz",
|
||||
@@ -2806,6 +2834,15 @@
|
||||
"@types/istanbul-lib-report": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/jquery": {
|
||||
"version": "3.5.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/jquery/-/jquery-3.5.8.tgz",
|
||||
"integrity": "sha512-cXk6NwqjDYg+UI9p2l3x0YmPa4m7RrXqmbK4IpVVpRJiYXU/QTo+UZrn54qfE1+9Gao4qpYqUnxm5ZCy2FTXAw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/sizzle": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/js-yaml": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.4.tgz",
|
||||
@@ -2928,6 +2965,12 @@
|
||||
"integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/sizzle": {
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.3.tgz",
|
||||
"integrity": "sha512-JYM8x9EGF163bEyhdJBpR2QX1R5naCJHC8ucJylJ3w9/CVBaskdQ8WqBf8MmQrd1kRvp/a4TS8HJ+bxzR7ZJYQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/stack-utils": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.1.tgz",
|
||||
@@ -13474,6 +13517,19 @@
|
||||
"tslib": "~2.3.0"
|
||||
}
|
||||
},
|
||||
"@graphql-codegen/typed-document-node": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@graphql-codegen/typed-document-node/-/typed-document-node-2.2.0.tgz",
|
||||
"integrity": "sha512-A/owHXw3f+OLCefY3qQnfQ5nEMbqQ5U/BEufRP3K15XXXUnoF1PpLvFrN0RcWRJtuzHfByt7Z2oKnNBEKyMUGg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@graphql-codegen/plugin-helpers": "^2.3.0",
|
||||
"@graphql-codegen/visitor-plugin-common": "2.5.0",
|
||||
"auto-bind": "~4.0.0",
|
||||
"change-case-all": "1.0.14",
|
||||
"tslib": "~2.3.0"
|
||||
}
|
||||
},
|
||||
"@graphql-codegen/typescript": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@graphql-codegen/typescript/-/typescript-2.3.0.tgz",
|
||||
@@ -14129,8 +14185,7 @@
|
||||
"@popperjs/core": {
|
||||
"version": "2.10.2",
|
||||
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.10.2.tgz",
|
||||
"integrity": "sha512-IXf3XA7+XyN7CP9gGh/XB0UxVMlvARGEgGXLubFICsUMGz6Q+DU+i4gGlpOxTjKvXjkJDJC8YdqdKkDj9qZHEQ==",
|
||||
"peer": true
|
||||
"integrity": "sha512-IXf3XA7+XyN7CP9gGh/XB0UxVMlvARGEgGXLubFICsUMGz6Q+DU+i4gGlpOxTjKvXjkJDJC8YdqdKkDj9qZHEQ=="
|
||||
},
|
||||
"@rollup/pluginutils": {
|
||||
"version": "4.1.1",
|
||||
@@ -14346,6 +14401,16 @@
|
||||
"@babel/types": "^7.3.0"
|
||||
}
|
||||
},
|
||||
"@types/bootstrap": {
|
||||
"version": "5.1.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/bootstrap/-/bootstrap-5.1.6.tgz",
|
||||
"integrity": "sha512-3L6IvOCKyoVd3e4bgQTH7VBPbuYEOG8IQbRcuZ0AbjfwPdRX+kVf5L/7mVt1EVM+D/BVw4+71rtp7Z8yYROlpQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@popperjs/core": "^2.9.2",
|
||||
"@types/jquery": "*"
|
||||
}
|
||||
},
|
||||
"@types/graceful-fs": {
|
||||
"version": "4.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.5.tgz",
|
||||
@@ -14385,6 +14450,15 @@
|
||||
"@types/istanbul-lib-report": "*"
|
||||
}
|
||||
},
|
||||
"@types/jquery": {
|
||||
"version": "3.5.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/jquery/-/jquery-3.5.8.tgz",
|
||||
"integrity": "sha512-cXk6NwqjDYg+UI9p2l3x0YmPa4m7RrXqmbK4IpVVpRJiYXU/QTo+UZrn54qfE1+9Gao4qpYqUnxm5ZCy2FTXAw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/sizzle": "*"
|
||||
}
|
||||
},
|
||||
"@types/js-yaml": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.4.tgz",
|
||||
@@ -14507,6 +14581,12 @@
|
||||
"integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/sizzle": {
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.3.tgz",
|
||||
"integrity": "sha512-JYM8x9EGF163bEyhdJBpR2QX1R5naCJHC8ucJylJ3w9/CVBaskdQ8WqBf8MmQrd1kRvp/a4TS8HJ+bxzR7ZJYQ==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/stack-utils": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.1.tgz",
|
||||
|
||||
@@ -27,8 +27,10 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@graphql-codegen/cli": "^2.2.2",
|
||||
"@graphql-codegen/typed-document-node": "^2.2.0",
|
||||
"@graphql-codegen/typescript-operations": "^2.2.0",
|
||||
"@graphql-codegen/typescript-react-apollo": "^3.2.0",
|
||||
"@graphql-typed-document-node/core": "^3.1.0",
|
||||
"@types/bootstrap": "^5.1.6",
|
||||
"@types/react": "^17.0.33",
|
||||
"@types/react-dom": "^17.0.10",
|
||||
"@types/react-router-dom": "^5.3.2",
|
||||
|
||||
@@ -7,14 +7,19 @@ import styles from "./App.module.css";
|
||||
import { Routes } from "./Routes";
|
||||
import client from "./client";
|
||||
import { Header } from "./components/Header";
|
||||
import { Toast } from "./components/Toast";
|
||||
import { ToastProvider } from "./components/ToastProvider";
|
||||
|
||||
const App: React.FC = () => (
|
||||
<ApolloProvider client={client}>
|
||||
<Router>
|
||||
<ToastProvider>
|
||||
<Header />
|
||||
<div className={styles.app}>
|
||||
<Routes />
|
||||
</div>
|
||||
<Toast />
|
||||
</ToastProvider>
|
||||
</Router>
|
||||
</ApolloProvider>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
.toast {
|
||||
position: absolute;
|
||||
bottom: 32px;
|
||||
right: 32px;
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import { Toast as BSToast } from "bootstrap";
|
||||
import clsx from "clsx";
|
||||
import React, { useEffect, useRef } from "react";
|
||||
|
||||
import styles from "./Toast.module.css";
|
||||
import { useToast } from "./ToastProvider";
|
||||
|
||||
export const Toast: React.FC = () => {
|
||||
const ref = useRef<HTMLInputElement>(null);
|
||||
const { visible, showToast, message } = useToast();
|
||||
|
||||
const classes = clsx("toast align-items-center text-white bg-primary border-0", styles.toast);
|
||||
|
||||
useEffect(() => {
|
||||
if (!ref.current) return;
|
||||
const bsToast = BSToast.getInstance(ref.current) || new BSToast(ref.current);
|
||||
|
||||
if (visible) {
|
||||
bsToast?.show();
|
||||
} else if (!visible) {
|
||||
bsToast?.hide();
|
||||
}
|
||||
return () => bsToast?.hide();
|
||||
}, [visible]);
|
||||
|
||||
return (
|
||||
<div ref={ref} className={classes} role="alert" aria-live="assertive" aria-atomic="true">
|
||||
<div className="d-flex">
|
||||
<div className="toast-body">{message}</div>
|
||||
<button
|
||||
type="button"
|
||||
className="btn-close btn-close-white me-2 m-auto"
|
||||
data-bs-dismiss="toast"
|
||||
aria-label="Close"
|
||||
onClick={() => showToast(message)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,45 @@
|
||||
import React, { ReactNode, useContext, useState } from "react";
|
||||
|
||||
interface ToastProps {
|
||||
visible: boolean;
|
||||
message: string;
|
||||
showToast: (message: string) => void;
|
||||
hide: () => void;
|
||||
}
|
||||
|
||||
const defaultState = {
|
||||
visible: false,
|
||||
message: "",
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
showToast: () => {},
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
hide: () => {},
|
||||
};
|
||||
|
||||
const ToastContext = React.createContext<ToastProps>(defaultState);
|
||||
|
||||
type ToastProviderProps = {
|
||||
children: ReactNode;
|
||||
};
|
||||
export const ToastProvider: React.FC<ToastProviderProps> = ({ children }) => {
|
||||
const [visible, setVisible] = useState(defaultState.visible);
|
||||
const [message, setMessage] = useState("");
|
||||
|
||||
const showToast = (newMessage: string) => {
|
||||
setMessage(newMessage);
|
||||
if (!visible) {
|
||||
setVisible(true);
|
||||
setTimeout(() => {
|
||||
setVisible(false);
|
||||
setMessage("");
|
||||
}, 3000);
|
||||
}
|
||||
};
|
||||
const hide = () => {
|
||||
setVisible(true);
|
||||
setMessage("");
|
||||
};
|
||||
return <ToastContext.Provider value={{ visible, showToast, message, hide }}>{children}</ToastContext.Provider>;
|
||||
};
|
||||
|
||||
export const useToast = (): ToastProps => useContext(ToastContext);
|
||||
@@ -1,21 +1,23 @@
|
||||
import { useMutation } from "@apollo/client";
|
||||
import React, { useState } from "react";
|
||||
|
||||
import { ProductCard } from "../../components/ProductCard";
|
||||
import { useToast } from "../../components/ToastProvider";
|
||||
import { UserCard } from "../../components/UserCard";
|
||||
import { Product, User } from "../../types";
|
||||
import { CreateOrderDocument, Product, User } from "../../types";
|
||||
import styles from "./Register.module.css";
|
||||
import { useRegister } from "./useRegister";
|
||||
|
||||
const USER_EXTERNAL = "external";
|
||||
|
||||
type OrderLine = {
|
||||
id: string;
|
||||
productId: string;
|
||||
amount: number;
|
||||
};
|
||||
|
||||
const sumTotal = (products: Product[], order: OrderLine[], isExternal: boolean) => {
|
||||
const prices = order.map((orderLine: OrderLine) => {
|
||||
const product = products.find((product: Product) => product.id === orderLine.id);
|
||||
const product = products.find((product: Product) => product.id === orderLine.productId);
|
||||
if (!product) return 0;
|
||||
return orderLine.amount * (isExternal ? product.salePriceExt : product.salePriceInt);
|
||||
});
|
||||
@@ -26,22 +28,26 @@ export const Register: React.FC = () => {
|
||||
const { users, products, loading, showAll, setShowAll } = useRegister();
|
||||
const [selectedUser, setSelectedUser] = useState("");
|
||||
const [order, setOrder] = useState<OrderLine[]>([]);
|
||||
const [createOrderMutation, { loading: mutateLoading }] = useMutation(CreateOrderDocument, {
|
||||
variables: { customerId: selectedUser, orderLines: order },
|
||||
});
|
||||
const { showToast } = useToast();
|
||||
|
||||
const reset = () => {
|
||||
setOrder([]);
|
||||
setSelectedUser("");
|
||||
};
|
||||
const addToOrder = (id: string) => {
|
||||
const addToOrder = (productId: string) => {
|
||||
let added = false;
|
||||
const newOrder = order.map((orderLine: OrderLine) => {
|
||||
if (orderLine.id === id) {
|
||||
if (orderLine.productId === productId) {
|
||||
added = true;
|
||||
return { ...orderLine, amount: orderLine.amount + 1 };
|
||||
}
|
||||
return orderLine;
|
||||
});
|
||||
if (!added) {
|
||||
newOrder.push({ id, amount: 1 });
|
||||
newOrder.push({ productId, amount: 1 });
|
||||
}
|
||||
setOrder(newOrder);
|
||||
};
|
||||
@@ -54,6 +60,22 @@ export const Register: React.FC = () => {
|
||||
return <div>Loading...</div>;
|
||||
}
|
||||
|
||||
const onSubmit = async () => {
|
||||
let res;
|
||||
try {
|
||||
res = await createOrderMutation();
|
||||
} catch (e) {
|
||||
showToast("Whoops");
|
||||
console.log("woops");
|
||||
return;
|
||||
}
|
||||
const { data } = res;
|
||||
if (data?.createOrder?.__typename === "CreateOrderSuccess") {
|
||||
showToast("Woop");
|
||||
return;
|
||||
}
|
||||
showToast("booo");
|
||||
};
|
||||
return (
|
||||
<div>
|
||||
<section>
|
||||
@@ -86,8 +108,8 @@ export const Register: React.FC = () => {
|
||||
key={product.id}
|
||||
product={product}
|
||||
onProduct={() => addToOrder(product.id)}
|
||||
amount={order.find((orderLine: OrderLine) => orderLine.id === product.id)?.amount || 0}
|
||||
active={order.some((orderLine: OrderLine) => orderLine.id === product.id)}
|
||||
amount={order.find((orderLine: OrderLine) => orderLine.productId === product.id)?.amount || 0}
|
||||
active={order.some((orderLine: OrderLine) => orderLine.productId === product.id)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
@@ -96,7 +118,7 @@ export const Register: React.FC = () => {
|
||||
<span className={styles.total}>
|
||||
Totalt: <span>{total}</span>
|
||||
</span>
|
||||
<button type="button" className="btn btn-primary btn-lg" onClick={() => console.log("kjøp", order)}>
|
||||
<button type="button" className="btn btn-primary btn-lg" onClick={onSubmit}>
|
||||
Kjøp
|
||||
</button>{" "}
|
||||
<button type="button" className="btn btn-outline-secondary" onClick={reset}>
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
mutation CreateOrder($customerId: ID!, $orderLines:[OrderLineInput!]!) {
|
||||
createOrder(customerId: $customerId, orderLines: $orderLines) {
|
||||
... on CreateOrderSuccess {
|
||||
order {
|
||||
id
|
||||
customer {
|
||||
id
|
||||
}
|
||||
orderSum
|
||||
}
|
||||
}
|
||||
... on InsufficientFunds {
|
||||
error
|
||||
message
|
||||
amountLacking
|
||||
}
|
||||
... on FormErrors {
|
||||
error
|
||||
message
|
||||
fields {
|
||||
field
|
||||
message
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
import { useQuery } from "@apollo/client";
|
||||
import { parseISO, subDays } from "date-fns";
|
||||
import { useState } from "react";
|
||||
|
||||
import { useAllProductsQuery, useAllUsersQuery, User, Product } from "../../types";
|
||||
import { User, Product, AllProductsDocument, AllUsersDocument } from "../../types";
|
||||
|
||||
const activeLimitDays = 365 / 2;
|
||||
|
||||
@@ -15,8 +16,8 @@ type useRegisterType = {
|
||||
|
||||
export const useRegister = (): useRegisterType => {
|
||||
const [showAll, setShowAll] = useState(false);
|
||||
const { data, loading } = useAllProductsQuery({ variables: { active: true } });
|
||||
const { data: usersData, loading: usersLoading } = useAllUsersQuery();
|
||||
const { data, loading } = useQuery(AllProductsDocument, { variables: { active: true } });
|
||||
const { data: usersData, loading: usersLoading } = useQuery(AllUsersDocument);
|
||||
|
||||
const allUsers = usersData?.allUsers || [];
|
||||
const users = showAll
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import { useCallback, useEffect, useRef } from "react";
|
||||
|
||||
export function usePrevious(value: any) {
|
||||
// The ref object is a generic container whose current property is mutable ...
|
||||
// ... and can hold any value, similar to an instance property on a class
|
||||
const ref = useRef();
|
||||
|
||||
// Store current value in ref
|
||||
useEffect(() => {
|
||||
ref.current = value;
|
||||
}, [value]); // Only re-run if value changes
|
||||
// Return previous value (happens before update in useEffect above)
|
||||
return ref.current;
|
||||
}
|
||||
|
||||
export function useToast() {
|
||||
const show = useCallback(() => {
|
||||
console.log("showing toast with element from root");
|
||||
}, []);
|
||||
return { show };
|
||||
}
|
||||
+69
-82
@@ -1,10 +1,8 @@
|
||||
import { gql } from '@apollo/client';
|
||||
import * as Apollo from '@apollo/client';
|
||||
import { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core';
|
||||
export type Maybe<T> = T | null;
|
||||
export type Exact<T extends { [key: string]: unknown }> = { [K in keyof T]: T[K] };
|
||||
export type MakeOptional<T, K extends keyof T> = Omit<T, K> & { [SubKey in K]?: Maybe<T[SubKey]> };
|
||||
export type MakeMaybe<T, K extends keyof T> = Omit<T, K> & { [SubKey in K]: Maybe<T[SubKey]> };
|
||||
const defaultOptions = {}
|
||||
/** All built-in and custom scalars, mapped to their actual values */
|
||||
export type Scalars = {
|
||||
ID: string;
|
||||
@@ -16,6 +14,64 @@ export type Scalars = {
|
||||
DateTime: any;
|
||||
};
|
||||
|
||||
export type CreateOrderResponse = CreateOrderSuccess | FormErrors | InsufficientFunds;
|
||||
|
||||
export type CreateOrderSuccess = {
|
||||
__typename?: 'CreateOrderSuccess';
|
||||
order: Order;
|
||||
};
|
||||
|
||||
export type Error = {
|
||||
error: Scalars['Boolean'];
|
||||
message: Scalars['String'];
|
||||
};
|
||||
|
||||
export type FieldError = Error & {
|
||||
__typename?: 'FieldError';
|
||||
error: Scalars['Boolean'];
|
||||
field: Scalars['String'];
|
||||
message: Scalars['String'];
|
||||
};
|
||||
|
||||
export type FormErrors = {
|
||||
__typename?: 'FormErrors';
|
||||
error: Scalars['Boolean'];
|
||||
fields: Array<FieldError>;
|
||||
message: Scalars['String'];
|
||||
};
|
||||
|
||||
export type InsufficientFunds = Error & {
|
||||
__typename?: 'InsufficientFunds';
|
||||
amountLacking: Scalars['Float'];
|
||||
error: Scalars['Boolean'];
|
||||
message: Scalars['String'];
|
||||
};
|
||||
|
||||
export type Mutation = {
|
||||
__typename?: 'Mutation';
|
||||
createOrder: CreateOrderResponse;
|
||||
};
|
||||
|
||||
|
||||
export type MutationCreateOrderArgs = {
|
||||
customerId: Scalars['ID'];
|
||||
isExternal?: Scalars['Boolean'];
|
||||
orderLines: Array<OrderLineInput>;
|
||||
};
|
||||
|
||||
export type Order = {
|
||||
__typename?: 'Order';
|
||||
created: Scalars['DateTime'];
|
||||
customer?: Maybe<User>;
|
||||
id: Scalars['ID'];
|
||||
orderSum: Scalars['Float'];
|
||||
};
|
||||
|
||||
export type OrderLineInput = {
|
||||
amount: Scalars['Int'];
|
||||
productId: Scalars['ID'];
|
||||
};
|
||||
|
||||
export type Product = {
|
||||
__typename?: 'Product';
|
||||
id: Scalars['ID'];
|
||||
@@ -32,6 +88,7 @@ export type ProductFilter = {
|
||||
|
||||
export type Query = {
|
||||
__typename?: 'Query';
|
||||
allOrders: Array<Order>;
|
||||
allProducts: Array<Product>;
|
||||
allUsers: Array<User>;
|
||||
};
|
||||
@@ -69,85 +126,15 @@ export type AllUsersQueryVariables = Exact<{ [key: string]: never; }>;
|
||||
|
||||
export type AllUsersQuery = { __typename?: 'Query', allUsers: Array<{ __typename?: 'User', id: string, firstName: string, lastName: string, profile?: { __typename?: 'UserProfile', id: string, balance: number, image: string, lastPurchaseDate: any } | null | undefined }> };
|
||||
|
||||
export type CreateOrderMutationVariables = Exact<{
|
||||
customerId: Scalars['ID'];
|
||||
orderLines: Array<OrderLineInput> | OrderLineInput;
|
||||
}>;
|
||||
|
||||
export const AllProductsDocument = gql`
|
||||
query allProducts($active: Boolean!) {
|
||||
allProducts(filters: {active: $active}) {
|
||||
id
|
||||
name
|
||||
imageUrl
|
||||
salePriceExt
|
||||
salePriceInt
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
/**
|
||||
* __useAllProductsQuery__
|
||||
*
|
||||
* To run a query within a React component, call `useAllProductsQuery` and pass it any options that fit your needs.
|
||||
* When your component renders, `useAllProductsQuery` returns an object from Apollo Client that contains loading, error, and data properties
|
||||
* you can use to render your UI.
|
||||
*
|
||||
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
|
||||
*
|
||||
* @example
|
||||
* const { data, loading, error } = useAllProductsQuery({
|
||||
* variables: {
|
||||
* active: // value for 'active'
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useAllProductsQuery(baseOptions: Apollo.QueryHookOptions<AllProductsQuery, AllProductsQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useQuery<AllProductsQuery, AllProductsQueryVariables>(AllProductsDocument, options);
|
||||
}
|
||||
export function useAllProductsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<AllProductsQuery, AllProductsQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useLazyQuery<AllProductsQuery, AllProductsQueryVariables>(AllProductsDocument, options);
|
||||
}
|
||||
export type AllProductsQueryHookResult = ReturnType<typeof useAllProductsQuery>;
|
||||
export type AllProductsLazyQueryHookResult = ReturnType<typeof useAllProductsLazyQuery>;
|
||||
export type AllProductsQueryResult = Apollo.QueryResult<AllProductsQuery, AllProductsQueryVariables>;
|
||||
export const AllUsersDocument = gql`
|
||||
query allUsers {
|
||||
allUsers {
|
||||
id
|
||||
firstName
|
||||
lastName
|
||||
profile {
|
||||
id
|
||||
balance
|
||||
image
|
||||
lastPurchaseDate
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
export type CreateOrderMutation = { __typename?: 'Mutation', createOrder: { __typename?: 'CreateOrderSuccess', order: { __typename?: 'Order', id: string, orderSum: number, customer?: { __typename?: 'User', id: string } | null | undefined } } | { __typename?: 'FormErrors', error: boolean, message: string, fields: Array<{ __typename?: 'FieldError', field: string, message: string }> } | { __typename?: 'InsufficientFunds', error: boolean, message: string, amountLacking: number } };
|
||||
|
||||
/**
|
||||
* __useAllUsersQuery__
|
||||
*
|
||||
* To run a query within a React component, call `useAllUsersQuery` and pass it any options that fit your needs.
|
||||
* When your component renders, `useAllUsersQuery` returns an object from Apollo Client that contains loading, error, and data properties
|
||||
* you can use to render your UI.
|
||||
*
|
||||
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
|
||||
*
|
||||
* @example
|
||||
* const { data, loading, error } = useAllUsersQuery({
|
||||
* variables: {
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useAllUsersQuery(baseOptions?: Apollo.QueryHookOptions<AllUsersQuery, AllUsersQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useQuery<AllUsersQuery, AllUsersQueryVariables>(AllUsersDocument, options);
|
||||
}
|
||||
export function useAllUsersLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<AllUsersQuery, AllUsersQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useLazyQuery<AllUsersQuery, AllUsersQueryVariables>(AllUsersDocument, options);
|
||||
}
|
||||
export type AllUsersQueryHookResult = ReturnType<typeof useAllUsersQuery>;
|
||||
export type AllUsersLazyQueryHookResult = ReturnType<typeof useAllUsersLazyQuery>;
|
||||
export type AllUsersQueryResult = Apollo.QueryResult<AllUsersQuery, AllUsersQueryVariables>;
|
||||
|
||||
export const AllProductsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"allProducts"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"active"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Boolean"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"allProducts"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"filters"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"active"},"value":{"kind":"Variable","name":{"kind":"Name","value":"active"}}}]}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"imageUrl"}},{"kind":"Field","name":{"kind":"Name","value":"salePriceExt"}},{"kind":"Field","name":{"kind":"Name","value":"salePriceInt"}}]}}]}}]} as unknown as DocumentNode<AllProductsQuery, AllProductsQueryVariables>;
|
||||
export const AllUsersDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"allUsers"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"allUsers"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"firstName"}},{"kind":"Field","name":{"kind":"Name","value":"lastName"}},{"kind":"Field","name":{"kind":"Name","value":"profile"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"balance"}},{"kind":"Field","name":{"kind":"Name","value":"image"}},{"kind":"Field","name":{"kind":"Name","value":"lastPurchaseDate"}}]}}]}}]}}]} as unknown as DocumentNode<AllUsersQuery, AllUsersQueryVariables>;
|
||||
export const CreateOrderDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"CreateOrder"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"customerId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"orderLines"}},"type":{"kind":"NonNullType","type":{"kind":"ListType","type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"OrderLineInput"}}}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"createOrder"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"customerId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"customerId"}}},{"kind":"Argument","name":{"kind":"Name","value":"orderLines"},"value":{"kind":"Variable","name":{"kind":"Name","value":"orderLines"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"CreateOrderSuccess"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"order"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"customer"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}},{"kind":"Field","name":{"kind":"Name","value":"orderSum"}}]}}]}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"InsufficientFunds"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"error"}},{"kind":"Field","name":{"kind":"Name","value":"message"}},{"kind":"Field","name":{"kind":"Name","value":"amountLacking"}}]}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"FormErrors"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"error"}},{"kind":"Field","name":{"kind":"Name","value":"message"}},{"kind":"Field","name":{"kind":"Name","value":"fields"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"field"}},{"kind":"Field","name":{"kind":"Name","value":"message"}}]}}]}}]}}]}}]} as unknown as DocumentNode<CreateOrderMutation, CreateOrderMutationVariables>;
|
||||
@@ -0,0 +1,22 @@
|
||||
import strawberry
|
||||
|
||||
|
||||
@strawberry.interface
|
||||
class Error:
|
||||
message: str
|
||||
|
||||
@strawberry.field()
|
||||
def error(self) -> bool:
|
||||
return True
|
||||
|
||||
|
||||
@strawberry.type
|
||||
class FieldError(Error):
|
||||
field: str
|
||||
|
||||
|
||||
@strawberry.type
|
||||
class FormErrors:
|
||||
fields: list[FieldError]
|
||||
message: str = ''
|
||||
error: bool = True
|
||||
@@ -0,0 +1,66 @@
|
||||
import strawberry
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.db import transaction
|
||||
from django.shortcuts import get_object_or_404
|
||||
from strawberry import ID
|
||||
|
||||
from main import models
|
||||
from main.api.errors import Error, FormErrors, FieldError
|
||||
from main.api.types import Order
|
||||
|
||||
UserModel = get_user_model()
|
||||
|
||||
|
||||
@strawberry.input
|
||||
class OrderLineInput:
|
||||
product_id: ID
|
||||
amount: int
|
||||
|
||||
|
||||
@strawberry.type
|
||||
class CreateOrderSuccess:
|
||||
order: Order
|
||||
|
||||
|
||||
@strawberry.type
|
||||
class InsufficientFunds(Error):
|
||||
amount_lacking: float
|
||||
|
||||
|
||||
Response = strawberry.union("CreateOrderResponse", [CreateOrderSuccess, InsufficientFunds, FormErrors])
|
||||
|
||||
|
||||
@strawberry.mutation
|
||||
@transaction.atomic
|
||||
def create_order(customer_id: ID, order_lines: list[OrderLineInput], is_external: bool = False) -> Response:
|
||||
# clean and validate
|
||||
try:
|
||||
customer = UserModel.objects.get(pk=customer_id)
|
||||
except (UserModel.DoesNotExist, ValueError):
|
||||
customer = None
|
||||
|
||||
if customer is None and not is_external:
|
||||
message = "Du har ikke valgt hvem som skal kjøpe."
|
||||
return FormErrors(
|
||||
message=message,
|
||||
fields=[FieldError(field="customer_id", message=message)],
|
||||
)
|
||||
|
||||
ols = []
|
||||
order_sum = 0
|
||||
for ol in order_lines:
|
||||
product = get_object_or_404(models.Product, pk=ol.product_id)
|
||||
price = product.sale_price_ext if is_external else product.sale_price_int
|
||||
ols.append(models.OrderLine(product=product, amount=ol.amount, unit_price=price))
|
||||
order_sum += ol.amount * price
|
||||
|
||||
if not is_external and customer.profile.balance <= order_sum:
|
||||
return InsufficientFunds(amount_lacking=abs(order_sum - customer.profile.balance))
|
||||
|
||||
# create
|
||||
order = models.Order.objects.create(customer=customer, order_sum=order_sum)
|
||||
for ol in ols:
|
||||
ol.order = order
|
||||
models.OrderLine.objects.bulk_create(ols)
|
||||
|
||||
return CreateOrderSuccess(order=order)
|
||||
+9
-2
@@ -1,13 +1,20 @@
|
||||
import strawberry
|
||||
import strawberry_django
|
||||
|
||||
from main.api.types import Product, User
|
||||
from main.api import mutations
|
||||
from main.api.types import Product, User, Order
|
||||
|
||||
|
||||
@strawberry.type
|
||||
class Query:
|
||||
all_products: list[Product] = strawberry_django.field()
|
||||
all_users: list[User] = strawberry_django.field()
|
||||
all_orders: list[Order] = strawberry_django.field()
|
||||
|
||||
|
||||
schema = strawberry.Schema(query=Query)
|
||||
@strawberry.type
|
||||
class Mutation:
|
||||
create_order = mutations.create_order
|
||||
|
||||
|
||||
schema = strawberry.Schema(query=Query, mutation=Mutation)
|
||||
@@ -43,3 +43,11 @@ class User:
|
||||
last_name: auto
|
||||
profile: UserProfile
|
||||
# todo
|
||||
|
||||
|
||||
@strawberry_django.type(models.Order)
|
||||
class Order:
|
||||
id: auto
|
||||
customer: 'User'
|
||||
order_sum: auto
|
||||
created: auto
|
||||
@@ -0,0 +1,23 @@
|
||||
from pathlib import Path
|
||||
|
||||
from django.core.management import BaseCommand, CommandParser
|
||||
from strawberry.printer import print_schema
|
||||
|
||||
from bbssite.settings import BASE_DIR
|
||||
from main.api.schema import schema as schema_symbol
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = " Write graphql schema to file or stdout"
|
||||
|
||||
def add_arguments(self, parser: CommandParser):
|
||||
parser.add_argument("--filename", default='schema.graphql', help='`-` prints to stdout')
|
||||
|
||||
def handle(self, *args, filename=None, **options):
|
||||
schema_output = print_schema(schema_symbol)
|
||||
if filename != '-':
|
||||
with Path(BASE_DIR, filename).open("w+") as fp:
|
||||
fp.write(schema_output)
|
||||
self.stdout.write(self.style.SUCCESS('OK'))
|
||||
else:
|
||||
print(schema_output)
|
||||
@@ -0,0 +1,80 @@
|
||||
union CreateOrderResponse = CreateOrderSuccess | InsufficientFunds | FormErrors
|
||||
|
||||
type CreateOrderSuccess {
|
||||
order: Order!
|
||||
}
|
||||
|
||||
"""Date with time (isoformat)"""
|
||||
scalar DateTime
|
||||
|
||||
interface Error {
|
||||
message: String!
|
||||
error: Boolean!
|
||||
}
|
||||
|
||||
type FieldError implements Error {
|
||||
message: String!
|
||||
error: Boolean!
|
||||
field: String!
|
||||
}
|
||||
|
||||
type FormErrors {
|
||||
fields: [FieldError!]!
|
||||
message: String!
|
||||
error: Boolean!
|
||||
}
|
||||
|
||||
type InsufficientFunds implements Error {
|
||||
message: String!
|
||||
error: Boolean!
|
||||
amountLacking: Float!
|
||||
}
|
||||
|
||||
type Mutation {
|
||||
createOrder(customerId: ID!, orderLines: [OrderLineInput!]!, isExternal: Boolean! = false): CreateOrderResponse!
|
||||
}
|
||||
|
||||
type Order {
|
||||
id: ID!
|
||||
customer: User
|
||||
orderSum: Float!
|
||||
created: DateTime!
|
||||
}
|
||||
|
||||
input OrderLineInput {
|
||||
productId: ID!
|
||||
amount: Int!
|
||||
}
|
||||
|
||||
type Product {
|
||||
id: ID!
|
||||
name: String!
|
||||
imageUrl: String!
|
||||
salePriceInt: Float!
|
||||
salePriceExt: Float!
|
||||
}
|
||||
|
||||
input ProductFilter {
|
||||
id: ID
|
||||
active: Boolean
|
||||
}
|
||||
|
||||
type Query {
|
||||
allProducts(filters: ProductFilter): [Product!]!
|
||||
allUsers: [User!]!
|
||||
allOrders: [Order!]!
|
||||
}
|
||||
|
||||
type User {
|
||||
id: ID!
|
||||
firstName: String!
|
||||
lastName: String!
|
||||
profile: UserProfile
|
||||
}
|
||||
|
||||
type UserProfile {
|
||||
id: ID!
|
||||
balance: Float!
|
||||
image: String!
|
||||
lastPurchaseDate: DateTime!
|
||||
}
|
||||
Reference in new issue
Block a user