Match feature parity with Go api again

This commit is contained in:
Amber Sprenkels
2024-03-23 23:58:19 +01:00
parent bb143076a6
commit b0342bb4b9
4 changed files with 213 additions and 108 deletions

167
web/package-lock.json generated
View File

@@ -8,15 +8,17 @@
"name": "backpack-vite",
"version": "0.0.0",
"dependencies": {
"@syncedstore/core": "^0.6.0",
"@syncedstore/react": "^0.6.0",
"react": "^18.2.0",
"react-dom": "^18.2.0"
"react-dom": "^18.2.0",
"yjs": "^13.6.14"
},
"devDependencies": {
"@testing-library/react": "^13.4.0",
"@types/react": "^18.0.17",
"@types/react-dom": "^18.0.6",
"@vitejs/plugin-react": "^2.1.0",
"github-fork-ribbon-css": "^0.2.3",
"react-router-dom": "^6.4.0",
"typescript": "^4.6.4",
"vite": "^3.1.0",
@@ -513,6 +515,22 @@
"@jridgewell/sourcemap-codec": "^1.4.10"
}
},
"node_modules/@reactivedata/react": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/@reactivedata/react/-/react-0.2.2.tgz",
"integrity": "sha512-fJ8qoHRbicQnVcnwfHa9FO73mbHFfl590xpuGM4Mo1P2ClaDATfl9oUrrySE+tbA//M9cn8De8HTwjoNqt91sw==",
"dependencies": {
"@reactivedata/reactive": "^0.2.2"
},
"peerDependencies": {
"react": "^16.8.0 || ^17 || ^18"
}
},
"node_modules/@reactivedata/reactive": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/@reactivedata/reactive/-/reactive-0.2.2.tgz",
"integrity": "sha512-KnINM/Sng25QAv6sHkJO9q/XyslLegCF5jTsTSVu+AouY3uZDVf4Am99xNCqsfqFZFvnTBBDvCsHNdvTVGvPEA=="
},
"node_modules/@remix-run/router": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.0.0.tgz",
@@ -522,6 +540,37 @@
"node": ">=14"
}
},
"node_modules/@syncedstore/core": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/@syncedstore/core/-/core-0.6.0.tgz",
"integrity": "sha512-6TtjEoYJsceYi8u1oRecXwbbLmjHaU0S7HvVfOaEdDfphZLGm/faVuA2fpazqc28F0yIFGvYzvPEBUJn9vqRNw==",
"dependencies": {
"@reactivedata/reactive": "^0.2.0",
"@syncedstore/yjs-reactive-bindings": "^0.6.0"
},
"peerDependencies": {
"yjs": "^13.5.13"
}
},
"node_modules/@syncedstore/react": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/@syncedstore/react/-/react-0.6.0.tgz",
"integrity": "sha512-pc8ycnBuH2wp7Td5nGzP9Dn2mEW9Yg1qF0yGdjJ5UdgLEwfRkq+8/hdQl+alcoSribv9St/Bi5hJckX8GVsAbQ==",
"dependencies": {
"@reactivedata/react": "^0.2.1"
},
"peerDependencies": {
"@syncedstore/core": "*"
}
},
"node_modules/@syncedstore/yjs-reactive-bindings": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/@syncedstore/yjs-reactive-bindings/-/yjs-reactive-bindings-0.6.0.tgz",
"integrity": "sha512-VF78h0J4iOt79YU9d6j5E6bFKu7WXYuiI2ue9ZnA+T4SNVn8viRvg0AHm3NqHzudZZUgYT3dpnbv1/ZmU7yPZQ==",
"peerDependencies": {
"yjs": "^13.5.13"
}
},
"node_modules/@testing-library/dom": {
"version": "8.18.0",
"resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-8.18.0.tgz",
@@ -1360,12 +1409,6 @@
"node": "*"
}
},
"node_modules/github-fork-ribbon-css": {
"version": "0.2.3",
"resolved": "https://registry.npmjs.org/github-fork-ribbon-css/-/github-fork-ribbon-css-0.2.3.tgz",
"integrity": "sha512-cmGBV4sivRwmnteSOkqMjN2cnP5/J1SU5aDCVYsBWHmDokZ/JjwGEkduCxY9IULHdCPpw1WSk5Cy8N1LF6jOEw==",
"dev": true
},
"node_modules/globals": {
"version": "11.12.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz",
@@ -1408,6 +1451,15 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/isomorphic.js": {
"version": "0.2.5",
"resolved": "https://registry.npmjs.org/isomorphic.js/-/isomorphic.js-0.2.5.tgz",
"integrity": "sha512-PIeMbHqMt4DnUP3MA/Flc0HElYjMXArsw1qwJZcm9sqR8mq3l8NYizFMty0pWwE/tzIGH3EKK5+jes5mAr85yw==",
"funding": {
"type": "GitHub Sponsors ❤",
"url": "https://github.com/sponsors/dmonad"
}
},
"node_modules/js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
@@ -1437,6 +1489,26 @@
"node": ">=6"
}
},
"node_modules/lib0": {
"version": "0.2.93",
"resolved": "https://registry.npmjs.org/lib0/-/lib0-0.2.93.tgz",
"integrity": "sha512-M5IKsiFJYulS+8Eal8f+zAqf5ckm1vffW0fFDxfgxJ+uiVopvDdd3PxJmz0GsVi3YNO7QCFSq0nAsiDmNhLj9Q==",
"dependencies": {
"isomorphic.js": "^0.2.4"
},
"bin": {
"0ecdsa-generate-keypair": "bin/0ecdsa-generate-keypair.js",
"0gentesthtml": "bin/gentesthtml.js",
"0serve": "bin/0serve.js"
},
"engines": {
"node": ">=16"
},
"funding": {
"type": "GitHub Sponsors ❤",
"url": "https://github.com/sponsors/dmonad"
}
},
"node_modules/local-pkg": {
"version": "0.4.2",
"resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-0.4.2.tgz",
@@ -1907,6 +1979,22 @@
"optional": true
}
}
},
"node_modules/yjs": {
"version": "13.6.14",
"resolved": "https://registry.npmjs.org/yjs/-/yjs-13.6.14.tgz",
"integrity": "sha512-D+7KcUr0j+vBCUSKXXEWfA+bG4UQBviAwP3gYBhkstkgwy5+8diOPMx0iqLIOxNo/HxaREUimZRxqHGAHCL2BQ==",
"dependencies": {
"lib0": "^0.2.86"
},
"engines": {
"node": ">=16.0.0",
"npm": ">=8.0.0"
},
"funding": {
"type": "GitHub Sponsors ❤",
"url": "https://github.com/sponsors/dmonad"
}
}
},
"dependencies": {
@@ -2266,12 +2354,48 @@
"@jridgewell/sourcemap-codec": "^1.4.10"
}
},
"@reactivedata/react": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/@reactivedata/react/-/react-0.2.2.tgz",
"integrity": "sha512-fJ8qoHRbicQnVcnwfHa9FO73mbHFfl590xpuGM4Mo1P2ClaDATfl9oUrrySE+tbA//M9cn8De8HTwjoNqt91sw==",
"requires": {
"@reactivedata/reactive": "^0.2.2"
}
},
"@reactivedata/reactive": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/@reactivedata/reactive/-/reactive-0.2.2.tgz",
"integrity": "sha512-KnINM/Sng25QAv6sHkJO9q/XyslLegCF5jTsTSVu+AouY3uZDVf4Am99xNCqsfqFZFvnTBBDvCsHNdvTVGvPEA=="
},
"@remix-run/router": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.0.0.tgz",
"integrity": "sha512-SCR1cxRSMNKjaVYptCzBApPDqGwa3FGdjVHc+rOToocNPHQdIYLZBfv/3f+KvYuXDkUGVIW9IAzmPNZDRL1I4A==",
"dev": true
},
"@syncedstore/core": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/@syncedstore/core/-/core-0.6.0.tgz",
"integrity": "sha512-6TtjEoYJsceYi8u1oRecXwbbLmjHaU0S7HvVfOaEdDfphZLGm/faVuA2fpazqc28F0yIFGvYzvPEBUJn9vqRNw==",
"requires": {
"@reactivedata/reactive": "^0.2.0",
"@syncedstore/yjs-reactive-bindings": "^0.6.0"
}
},
"@syncedstore/react": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/@syncedstore/react/-/react-0.6.0.tgz",
"integrity": "sha512-pc8ycnBuH2wp7Td5nGzP9Dn2mEW9Yg1qF0yGdjJ5UdgLEwfRkq+8/hdQl+alcoSribv9St/Bi5hJckX8GVsAbQ==",
"requires": {
"@reactivedata/react": "^0.2.1"
}
},
"@syncedstore/yjs-reactive-bindings": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/@syncedstore/yjs-reactive-bindings/-/yjs-reactive-bindings-0.6.0.tgz",
"integrity": "sha512-VF78h0J4iOt79YU9d6j5E6bFKu7WXYuiI2ue9ZnA+T4SNVn8viRvg0AHm3NqHzudZZUgYT3dpnbv1/ZmU7yPZQ==",
"requires": {}
},
"@testing-library/dom": {
"version": "8.18.0",
"resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-8.18.0.tgz",
@@ -2798,12 +2922,6 @@
"integrity": "sha512-Hm0ixYtaSZ/V7C8FJrtZIuBBI+iSgL+1Aq82zSu8VQNB4S3Gk8e7Qs3VwBDJAhmRZcFqkl3tQu36g/Foh5I5ig==",
"dev": true
},
"github-fork-ribbon-css": {
"version": "0.2.3",
"resolved": "https://registry.npmjs.org/github-fork-ribbon-css/-/github-fork-ribbon-css-0.2.3.tgz",
"integrity": "sha512-cmGBV4sivRwmnteSOkqMjN2cnP5/J1SU5aDCVYsBWHmDokZ/JjwGEkduCxY9IULHdCPpw1WSk5Cy8N1LF6jOEw==",
"dev": true
},
"globals": {
"version": "11.12.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz",
@@ -2834,6 +2952,11 @@
"has": "^1.0.3"
}
},
"isomorphic.js": {
"version": "0.2.5",
"resolved": "https://registry.npmjs.org/isomorphic.js/-/isomorphic.js-0.2.5.tgz",
"integrity": "sha512-PIeMbHqMt4DnUP3MA/Flc0HElYjMXArsw1qwJZcm9sqR8mq3l8NYizFMty0pWwE/tzIGH3EKK5+jes5mAr85yw=="
},
"js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
@@ -2851,6 +2974,14 @@
"integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==",
"dev": true
},
"lib0": {
"version": "0.2.93",
"resolved": "https://registry.npmjs.org/lib0/-/lib0-0.2.93.tgz",
"integrity": "sha512-M5IKsiFJYulS+8Eal8f+zAqf5ckm1vffW0fFDxfgxJ+uiVopvDdd3PxJmz0GsVi3YNO7QCFSq0nAsiDmNhLj9Q==",
"requires": {
"isomorphic.js": "^0.2.4"
}
},
"local-pkg": {
"version": "0.4.2",
"resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-0.4.2.tgz",
@@ -3136,6 +3267,14 @@
"tinyspy": "^1.0.2",
"vite": "^2.9.12 || ^3.0.0-0"
}
},
"yjs": {
"version": "13.6.14",
"resolved": "https://registry.npmjs.org/yjs/-/yjs-13.6.14.tgz",
"integrity": "sha512-D+7KcUr0j+vBCUSKXXEWfA+bG4UQBviAwP3gYBhkstkgwy5+8diOPMx0iqLIOxNo/HxaREUimZRxqHGAHCL2BQ==",
"requires": {
"lib0": "^0.2.86"
}
}
}
}

View File

@@ -10,8 +10,11 @@
"test": "vitest"
},
"dependencies": {
"@syncedstore/core": "^0.6.0",
"@syncedstore/react": "^0.6.0",
"react": "^18.2.0",
"react-dom": "^18.2.0"
"react-dom": "^18.2.0",
"yjs": "^13.6.14"
},
"devDependencies": {
"@testing-library/react": "^13.4.0",

View File

@@ -10,14 +10,14 @@ import { AppStateContext, SetAppStateContext } from './main';
function TagList(
props: {
allTags: string[],
selectedTags: Set<string>,
selectedTags: {[key: string]: true},
onSelectTag: (tag: string, enabled: boolean) => void,
}) {
let tagElems = props.allTags.map(
(tagName) => <Tag
key={tagName}
name={tagName}
selected={props.selectedTags.has(tagName)}
selected={props.selectedTags[tagName]}
onSelectTag={props.onSelectTag} />
)
return <ul className="BringListView-tagList">
@@ -44,9 +44,9 @@ function Tag(props: {
function BringList(props: {
bringList: BL,
filter: Filter,
checkedItems: Set<string>,
checkedItems: {[key: string]: true},
updateCheckedItems: (name: string, isChecked: boolean) => void,
strikedItems: Set<string>,
strikedItems: {[key: string]: true},
updateStrikedItems: (name: string, isStriked: boolean) => void,
}) {
let annotate = (cat: BLC): [BLC, ExprIsMatchResult] =>
@@ -77,9 +77,9 @@ function BringListCategory(props: {
blcIsTrue: string[],
blcIsFalse: string[],
filter: Filter,
checkedItems: Set<string>,
checkedItems: {[key: string]: true},
updateCheckedItems: (name: string, isChecked: boolean) => void,
strikedItems: Set<string>,
strikedItems: {[key: string]: true},
updateStrikedItems: (name: string, isStriked: boolean) => void,
}) {
let annotate = (item: Item): [Item, ExprIsMatchResult] =>
@@ -103,9 +103,9 @@ function BringListCategory(props: {
isTrue={isTrue}
isFalse={isFalse}
filter={props.filter}
isChecked={props.checkedItems.has(item.name)}
isChecked={props.checkedItems[item.name]}
setIsChecked={(isChecked) => props.updateCheckedItems(item.name, isChecked)}
isStriked={props.strikedItems.has(item.name)}
isStriked={props.strikedItems[item.name]}
setIsStriked={(isStriked) => props.updateStrikedItems(item.name, isStriked)}
/>)}
</ul>
@@ -193,8 +193,8 @@ function BootstrapCross(props: { className?: string, width?: number, height?: nu
function Settings(props: {
bringList: filterspec.BringList,
tags: Set<string>,
setTags: (tags: Set<string>) => void,
tags: {[key: string]: true},
setTags: (tags: {[key: string]: true}) => void,
nights: number,
setNights: (nights: number) => void,
doResetAll: () => void,
@@ -204,7 +204,7 @@ function Settings(props: {
const [confirmResetTimeout, setConfirmResetTimout] = useState<ReturnType<typeof setTimeout> | null>()
const tagList = useMemo(() => Array.from(filterspec.collectTagsFromDB(props.bringList)), [props.bringList])
let noneSelectedElement = props.tags.size === 0 ?
let noneSelectedElement = Object.keys(props.tags).length === 0 ?
<div className="BringListView-tagListNoneSelected">no tags selected</div> : <></>
let resetButton;
@@ -243,8 +243,15 @@ function Settings(props: {
<TagList
allTags={tagList}
selectedTags={props.tags}
onSelectTag={(tag: string, enabled: boolean) =>
props.setTags(setAssign(props.tags, tag, enabled))}
onSelectTag={(tag: string, enabled: boolean) =>{
let tags = { ...props.tags }
if (enabled) {
tags[tag] = true
} else {
delete tags[tag]
}
props.setTags(tags)
}}
/>
</div>
<div className="BringListView-nightsContainer BringListView-smallVerticalMargin">
@@ -270,11 +277,11 @@ function BringListView() {
let doResetAll = () => {
store.clearAllLocalStorage()
SetAppStore?.(store.fromSerializable(store.loadStoreLocal()))
SetAppStore?.(store.loadStoreLocal())
}
const bringList = useMemo(() => filterspec.parseDatabase(appStore?.bringListTemplate ?? ""), [appStore?.bringListTemplate])
const filter = { tags: appStore?.tags ?? new Set(), nights: appStore?.nights ?? 0 }
const filter: Filter = appStore ? { tags: new Set(Object.keys(appStore.tags)), nights: appStore.nights } : { tags: new Set(), nights: 0 }
return (
<div className="BringListView">
<Header
@@ -284,7 +291,7 @@ function BringListView() {
<Nav />
<Settings
bringList={bringList}
tags={appStore?.tags ?? new Set()}
tags={appStore?.tags ?? {}}
setTags={(tags) => SetAppStore!({ ...appStore!, tags: tags })}
nights={appStore?.nights ?? 0}
setNights={(nights) => SetAppStore!({ ...appStore!, nights: nights })}
@@ -293,27 +300,29 @@ function BringListView() {
<BringList
bringList={bringList}
filter={filter}
checkedItems={appStore?.checkedItems ?? new Set()}
checkedItems={appStore?.checkedItems ?? {}}
updateCheckedItems={(name: string, isChecked: boolean) => {
SetAppStore!({ ...appStore!, checkedItems: setAssign(appStore!.checkedItems, name, isChecked) })
let checkedItems = { ...appStore!.checkedItems }
if (isChecked) {
checkedItems[name] = true
} else {
delete checkedItems[name]
}
SetAppStore!({ ...appStore!, checkedItems })
}}
strikedItems={appStore?.strikedItems ?? new Set()}
strikedItems={appStore?.strikedItems ?? {}}
updateStrikedItems={(name: string, isStriked: boolean) => {
SetAppStore!({ ...appStore!, strikedItems: setAssign(appStore!.strikedItems, name, isStriked) })
let strikedItems = { ...appStore!.strikedItems }
if (isStriked) {
strikedItems[name] = true
} else {
delete strikedItems[name]
}
SetAppStore!({ ...appStore!, strikedItems })
}}
/>
</div>
);
}
function setAssign<T>(_set: Set<T>, key: T, enabled: boolean): Set<T> {
let set = new Set(_set)
if (enabled) {
set.add(key)
} else {
set.delete(key)
}
return set
}
export default BringListView;

View File

@@ -4,64 +4,27 @@ const LOCALSTORAGE_PREFIX = "nl.as8.backpack."
const LOCALSTORAGE_STORE = `${LOCALSTORAGE_PREFIX}store`
const DEFAULT_STORE = {
bringListTemplate: DEFAULT_BRINGLIST_TEMPLATE,
tags: new Set<string>(),
checkedItems: new Set<string>(),
strikedItems: new Set<string>(),
tags: {},
checkedItems: {},
strikedItems: {},
nights: 0,
header: "",
revision: 0,
updatedAt: undefined,
}
export interface Store {
bringListTemplate: string,
tags: Set<string>,
checkedItems: Set<string>,
strikedItems: Set<string>,
tags: {[key: string]: true},
checkedItems: { [key: string]: true },
strikedItems: { [key: string]: true},
nights: number,
header: string,
revision: number,
updatedAt?: Date,
}
export interface SerializableStore {
bringListTemplate: string,
tags: Array<string>,
checkedItems: Array<string>,
strikedItems: Array<string>,
nights: number,
header: string,
revision: number,
updatedAt?: string,
}
export function toSerializable(store: Store): SerializableStore {
return {
bringListTemplate: store.bringListTemplate,
tags: Array.from(store.tags),
checkedItems: Array.from(store.checkedItems),
strikedItems: Array.from(store.strikedItems),
nights: store.nights,
header: store.header,
revision: store.revision,
updatedAt: store.updatedAt?.toISOString(),
}
}
export function fromSerializable(store: SerializableStore): Store {
return {
bringListTemplate: store.bringListTemplate,
tags: new Set(store.tags),
checkedItems: new Set(store.checkedItems),
strikedItems: new Set(store.strikedItems),
nights: store.nights,
header: store.header,
revision: store.revision,
updatedAt: new Date(store.updatedAt ?? 0),
}
}
export async function loadStore(): Promise<Store> {
let remoteStore
let localStore = loadStoreLocal()
@@ -70,31 +33,30 @@ export async function loadStore(): Promise<Store> {
if (typeof remoteStore === "object" && remoteStore.revision > localStore.revision) {
console.info("remote store is newer, overwriting local store")
saveStoreLocal(remoteStore)
return fromSerializable(remoteStore)
return remoteStore
}
}
catch (error) {
console.warn("error loading store from server, using local store", error)
}
return fromSerializable(localStore)
return localStore
}
export function loadStoreLocal(): SerializableStore {
export function loadStoreLocal(): Store {
return loadValue(LOCALSTORAGE_STORE, (json?: any) => json ?? DEFAULT_STORE)
}
export function saveStore(store: Store) {
let serializableStore = toSerializable(store)
serializableStore.revision++
saveStoreLocal(serializableStore)
saveStoreOnServer(serializableStore)
store.revision++
saveStoreLocal(store)
saveStoreOnServer(store)
}
export function saveStoreLocal(store: SerializableStore) {
export function saveStoreLocal(store: Store) {
saveValue(LOCALSTORAGE_STORE, store)
}
export async function saveStoreOnServer(store: SerializableStore) {
export async function saveStoreOnServer(store: Store) {
console.debug("saving store on server", store)
let controller = new AbortController()
let timeoutID = setTimeout(() => {
@@ -116,7 +78,7 @@ export async function saveStoreOnServer(store: SerializableStore) {
clearTimeout(timeoutID)
}
export async function loadStoreFromServer(): Promise<SerializableStore> {
export async function loadStoreFromServer(): Promise<Store> {
let controller = new AbortController()
let timeoutID = setTimeout(() => {
console.warn("timeout loading store from server")
@@ -133,21 +95,13 @@ export async function loadStoreFromServer(): Promise<SerializableStore> {
throw response
}
clearTimeout(timeoutID)
return await response.json() as SerializableStore
return await response.json() as Store
}
export function clearAllLocalStorage() {
localStorage.removeItem(LOCALSTORAGE_STORE)
}
function decodeStringSet(json?: any): Set<string> {
return new Set<string>(json)
}
function encodeStringSet(set: Set<string>): Array<string> {
return Array.from(set)
}
function loadValue<T>(key: string, constructor: (json?: any) => T): T {
let json = localStorage.getItem(key)
if (json === null) {