From 753a05945812508f6b8eb0effab09d7e5f21e5da Mon Sep 17 00:00:00 2001 From: Hunter Miller Date: Wed, 18 Aug 2021 18:36:55 -0500 Subject: [PATCH 1/5] leap: added recent lists to search home --- pkg/grid/.eslintrc.js | 1 + pkg/grid/package-lock.json | 15 +++ pkg/grid/package.json | 1 + pkg/grid/src/components/AppLink.tsx | 52 ++++++++++ pkg/grid/src/components/AppList.tsx | 46 +++++++++ pkg/grid/src/components/ProviderLink.tsx | 46 +++++++++ pkg/grid/src/components/ProviderList.tsx | 55 +++++++++++ pkg/grid/src/components/ShipName.tsx | 12 ++- pkg/grid/src/nav/search/AppInfo.tsx | 9 +- pkg/grid/src/nav/search/Apps.tsx | 73 ++++---------- pkg/grid/src/nav/search/Home.tsx | 115 +++++++++++++++++++++-- pkg/grid/src/nav/search/Providers.tsx | 54 ++--------- pkg/grid/src/tiles/Tile.tsx | 4 + 13 files changed, 373 insertions(+), 110 deletions(-) create mode 100644 pkg/grid/src/components/AppLink.tsx create mode 100644 pkg/grid/src/components/AppList.tsx create mode 100644 pkg/grid/src/components/ProviderLink.tsx create mode 100644 pkg/grid/src/components/ProviderList.tsx diff --git a/pkg/grid/.eslintrc.js b/pkg/grid/.eslintrc.js index 37ca88bb1..02ef1346d 100644 --- a/pkg/grid/.eslintrc.js +++ b/pkg/grid/.eslintrc.js @@ -30,6 +30,7 @@ module.exports = { 'no-unused-vars': 'off', '@typescript-eslint/no-unused-vars': ['error'], 'no-use-before-define': 'off', + 'no-param-reassign': ['error', { props: true, ignorePropertyModificationsFor: ['draft'] }], '@typescript-eslint/no-use-before-define': 'off', '@typescript-eslint/no-empty-function': 'off', 'react/jsx-filename-extension': ['warn', { extensions: ['.tsx'] }], diff --git a/pkg/grid/package-lock.json b/pkg/grid/package-lock.json index 09315fe17..ec6b82d44 100644 --- a/pkg/grid/package-lock.json +++ b/pkg/grid/package-lock.json @@ -17,6 +17,7 @@ "clipboard-copy": "^4.0.1", "color2k": "^1.2.4", "fuzzy": "^0.1.3", + "immer": "^9.0.5", "lodash-es": "^4.17.21", "mousetrap": "^1.6.5", "postcss-import": "^14.0.2", @@ -3957,6 +3958,15 @@ "node": ">= 4" } }, + "node_modules/immer": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/immer/-/immer-9.0.5.tgz", + "integrity": "sha512-2WuIehr2y4lmYz9gaQzetPR2ECniCifk4ORaQbU3g5EalLt+0IVTosEPJ5BoYl/75ky2mivzdRzV8wWgQGOSYQ==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, "node_modules/import-cwd": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/import-cwd/-/import-cwd-3.0.0.tgz", @@ -10321,6 +10331,11 @@ "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", "dev": true }, + "immer": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/immer/-/immer-9.0.5.tgz", + "integrity": "sha512-2WuIehr2y4lmYz9gaQzetPR2ECniCifk4ORaQbU3g5EalLt+0IVTosEPJ5BoYl/75ky2mivzdRzV8wWgQGOSYQ==" + }, "import-cwd": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/import-cwd/-/import-cwd-3.0.0.tgz", diff --git a/pkg/grid/package.json b/pkg/grid/package.json index 87e5f5f73..d206eb0ba 100644 --- a/pkg/grid/package.json +++ b/pkg/grid/package.json @@ -23,6 +23,7 @@ "clipboard-copy": "^4.0.1", "color2k": "^1.2.4", "fuzzy": "^0.1.3", + "immer": "^9.0.5", "lodash-es": "^4.17.21", "mousetrap": "^1.6.5", "postcss-import": "^14.0.2", diff --git a/pkg/grid/src/components/AppLink.tsx b/pkg/grid/src/components/AppLink.tsx new file mode 100644 index 000000000..d768d503e --- /dev/null +++ b/pkg/grid/src/components/AppLink.tsx @@ -0,0 +1,52 @@ +import classNames from 'classnames'; +import React from 'react'; +import { Link, LinkProps } from 'react-router-dom'; +import { Docket } from '../state/docket-types'; + +export type AppLinkProps = Omit & { + app: Docket; + small?: boolean; + selected?: boolean; + to?: (app: Docket) => LinkProps['to']; +}; + +export const AppLink = ({ + app, + to, + small = false, + selected = false, + className, + ...props +}: AppLinkProps) => { + return ( + +
+ {app.img && ( + + )} +
+
+

{app.title}

+ {app.info && !small &&

{app.info}

} +
+ + ); +}; diff --git a/pkg/grid/src/components/AppList.tsx b/pkg/grid/src/components/AppList.tsx new file mode 100644 index 000000000..40e2309a5 --- /dev/null +++ b/pkg/grid/src/components/AppList.tsx @@ -0,0 +1,46 @@ +import React, { MouseEvent, useCallback } from 'react'; +import { MatchItem } from '../nav/Nav'; +import { useRecentsStore } from '../nav/search/Home'; +import { Docket } from '../state/docket-types'; +import { AppLink, AppLinkProps } from './AppLink'; + +type AppListProps = { + apps: Docket[]; + labelledBy: string; + matchAgainst?: MatchItem; + onClick?: (e: MouseEvent, app: Docket) => void; +} & Omit; + +export function appMatches(target: Docket, match?: MatchItem): boolean { + if (!match) { + return false; + } + + const matchValue = match.display || match.value; + return target.title === matchValue || target.base === matchValue; +} + +export const AppList = ({ apps, labelledBy, matchAgainst, onClick, ...props }: AppListProps) => { + const addRecentApp = useRecentsStore((state) => state.addRecentApp); + const selected = useCallback((app: Docket) => appMatches(app, matchAgainst), [matchAgainst]); + + return ( +
    + {apps.map((app) => ( +
  • + { + addRecentApp(app); + if (onClick) { + onClick(e, app); + } + }} + /> +
  • + ))} +
+ ); +}; diff --git a/pkg/grid/src/components/ProviderLink.tsx b/pkg/grid/src/components/ProviderLink.tsx new file mode 100644 index 000000000..39a645380 --- /dev/null +++ b/pkg/grid/src/components/ProviderLink.tsx @@ -0,0 +1,46 @@ +import classNames from 'classnames'; +import React from 'react'; +import { Link, LinkProps } from 'react-router-dom'; +import { Provider } from '../state/docket-types'; +import { ShipName } from './ShipName'; + +export type ProviderLinkProps = Omit & { + provider: Provider; + small?: boolean; + selected?: boolean; + to?: (p: Provider) => LinkProps['to']; +}; + +export const ProviderLink = ({ + provider, + to, + selected = false, + small = false, + className, + ...props +}: ProviderLinkProps) => { + return ( + +
+ {/* TODO: Handle sigils */} +
+
+

{provider.nickname || }

+ {provider.status && !small &&

{provider.status}

} +
+ + ); +}; diff --git a/pkg/grid/src/components/ProviderList.tsx b/pkg/grid/src/components/ProviderList.tsx new file mode 100644 index 000000000..6299383ca --- /dev/null +++ b/pkg/grid/src/components/ProviderList.tsx @@ -0,0 +1,55 @@ +import React, { MouseEvent, useCallback } from 'react'; +import { MatchItem } from '../nav/Nav'; +import { useRecentsStore } from '../nav/search/Home'; +import { Provider } from '../state/docket-types'; +import { ProviderLink, ProviderLinkProps } from './ProviderLink'; + +export type ProviderListProps = { + providers: Provider[]; + labelledBy: string; + matchAgainst?: MatchItem; + onClick?: (e: MouseEvent, p: Provider) => void; +} & Omit; + +export function providerMatches(target: Provider, match?: MatchItem): boolean { + if (!match) { + return false; + } + + const matchValue = match.display || match.value; + return target.nickname === matchValue || target.shipName === matchValue; +} + +export const ProviderList = ({ + providers, + labelledBy, + matchAgainst, + onClick, + ...props +}: ProviderListProps) => { + const addRecentDev = useRecentsStore((state) => state.addRecentDev); + const selected = useCallback( + (provider: Provider) => providerMatches(provider, matchAgainst), + [matchAgainst] + ); + + return ( +
    + {providers.map((p) => ( +
  • + { + addRecentDev(p); + if (onClick) { + onClick(e, p); + } + }} + /> +
  • + ))} +
+ ); +}; diff --git a/pkg/grid/src/components/ShipName.tsx b/pkg/grid/src/components/ShipName.tsx index 99c2a3feb..34f743023 100644 --- a/pkg/grid/src/components/ShipName.tsx +++ b/pkg/grid/src/components/ShipName.tsx @@ -5,16 +5,20 @@ type ShipNameProps = { } & HTMLAttributes; export const ShipName = ({ name, ...props }: ShipNameProps) => { - const parts = name.replace('~', '').split(/[_^-]/); + const parts = name.replace('~', '').split(/([_^-])/); return ( ~ {/* sig */} {parts[0]} - - - {/* hep */} - {parts[1]} + {parts.length > 1 && ( + <> + {parts[1]} + {/* hep */} + {parts[2]} + + )} ); }; diff --git a/pkg/grid/src/nav/search/AppInfo.tsx b/pkg/grid/src/nav/search/AppInfo.tsx index ec6fd251d..96dac4c4f 100644 --- a/pkg/grid/src/nav/search/AppInfo.tsx +++ b/pkg/grid/src/nav/search/AppInfo.tsx @@ -8,8 +8,10 @@ import { TreatyMeta } from '../../components/TreatyMeta'; import { useTreaty } from '../../logic/useTreaty'; import { chargesKey, fetchCharges } from '../../state/docket'; import { useLeapStore } from '../Nav'; +import { useRecentsStore } from './Home'; export const AppInfo = () => { + const addRecentApp = useRecentsStore((state) => state.addRecentApp); const select = useLeapStore((state) => state.select); const { ship, desk, treaty, installStatus, copyApp, installApp } = useTreaty(); const { data: charges } = useQuery(chargesKey(), fetchCharges); @@ -37,7 +39,12 @@ export const AppInfo = () => {
{installed && ( - + addRecentApp(treaty)} + > Open App )} diff --git a/pkg/grid/src/nav/search/Apps.tsx b/pkg/grid/src/nav/search/Apps.tsx index 3b3b2ca7c..6609fc64a 100644 --- a/pkg/grid/src/nav/search/Apps.tsx +++ b/pkg/grid/src/nav/search/Apps.tsx @@ -1,16 +1,20 @@ import React, { useCallback, useEffect, useMemo } from 'react'; import { useQuery, useQueryClient } from 'react-query'; -import { Link, RouteComponentProps } from 'react-router-dom'; +import { RouteComponentProps } from 'react-router-dom'; import fuzzy from 'fuzzy'; import slugify from 'slugify'; -import classNames from 'classnames'; import { ShipName } from '../../components/ShipName'; import { fetchProviderTreaties, treatyKey } from '../../state/docket'; -import { Treaty } from '../../state/docket-types'; -import { useLeapStore } from '../Nav'; +import { Docket } from '../../state/docket-types'; +import { MatchItem, useLeapStore } from '../Nav'; +import { AppList } from '../../components/AppList'; type AppsProps = RouteComponentProps<{ ship: string }>; +export function appMatch(app: Docket): MatchItem { + return { value: app.base, display: app.title }; +} + export const Apps = ({ match }: AppsProps) => { const queryClient = useQueryClient(); const { searchInput, selectedMatch, select } = useLeapStore((state) => ({ @@ -53,30 +57,18 @@ export const Apps = ({ match }: AppsProps) => { useEffect(() => { if (results) { useLeapStore.setState({ - matches: results.map((treaty) => ({ value: treaty.desk, display: treaty.title })) + matches: results.map(appMatch) }); } }, [results]); const preloadApp = useCallback( - (app: Treaty) => { - queryClient.setQueryData(treatyKey([provider, app.desk]), app); + (app: Docket) => { + queryClient.setQueryData(treatyKey([provider, app.base]), app); }, [queryClient] ); - const isSelected = useCallback( - (target: Treaty) => { - if (!selectedMatch) { - return false; - } - - const matchValue = selectedMatch.display || selectedMatch.value; - return target.title === matchValue || target.desk === matchValue; - }, - [selectedMatch] - ); - return (
@@ -88,42 +80,13 @@ export const Apps = ({ match }: AppsProps) => {

{results && ( -
    - {results.map((app) => ( -
  • - preloadApp(app)} - > -
    - {app.img && ( - - )} -
    -
    -

    {app.title}

    - {app.info &&

    {app.info}

    } -
    - -
  • - ))} -
+ `${match?.path.replace(':ship', provider)}/${slugify(app.base)}`} + onClick={(e, app) => preloadApp(app)} + /> )}

That's it!

diff --git a/pkg/grid/src/nav/search/Home.tsx b/pkg/grid/src/nav/search/Home.tsx index 3a305f2a0..43b4507c0 100644 --- a/pkg/grid/src/nav/search/Home.tsx +++ b/pkg/grid/src/nav/search/Home.tsx @@ -1,13 +1,116 @@ -import React from 'react'; +import produce from 'immer'; +import create from 'zustand'; +import React, { useEffect } from 'react'; +import { persist } from 'zustand/middleware'; +import { useQuery } from 'react-query'; +import { take } from 'lodash-es'; +import { Docket, Provider } from '../../state/docket-types'; +import { MatchItem, useLeapStore } from '../Nav'; +import { appMatch } from './Apps'; +import { providerMatch } from './Providers'; +import { AppList } from '../../components/AppList'; +import { ProviderList } from '../../components/ProviderList'; +import { AppLink } from '../../components/AppLink'; +import { ShipName } from '../../components/ShipName'; +import { ProviderLink } from '../../components/ProviderLink'; +import { chargesKey, fetchCharges } from '../../state/docket'; + +interface RecentsStore { + recentApps: Docket[]; + recentDevs: Provider[]; + addRecentApp: (docket: Docket) => void; + addRecentDev: (dev: Provider) => void; +} + +export const useRecentsStore = create( + persist( + (set) => ({ + recentApps: [], + recentDevs: [], + addRecentApp: (docket) => { + set( + produce((draft: RecentsStore) => { + const hasApp = draft.recentApps.find((app) => app.base === docket.base); + if (!hasApp) { + draft.recentApps.unshift(docket); + } + + draft.recentApps = take(draft.recentApps, 3); + }) + ); + }, + addRecentDev: (dev) => { + set( + produce((draft: RecentsStore) => { + const hasDev = draft.recentDevs.find((p) => p.shipName === dev.shipName); + if (!hasDev) { + draft.recentDevs.unshift(dev); + } + + draft.recentDevs = take(draft.recentDevs, 3); + }) + ); + } + }), + { + whitelist: ['recentApps', 'recentDevs'], + name: 'recents-store' + } + ) +); export const Home = () => { + const selectedMatch = useLeapStore((state) => state.selectedMatch); + const { recentApps, recentDevs, addRecentApp, addRecentDev } = useRecentsStore(); + const { data: charges } = useQuery(chargesKey(), () => fetchCharges()); + const groups = charges?.groups; + const zod = { shipName: '~zod' }; + + useEffect(() => { + const apps = recentApps.map(appMatch); + const devs = recentDevs.map(providerMatch); + + useLeapStore.setState({ + matches: ([] as MatchItem[]).concat(apps, devs) + }); + }, [recentApps, recentDevs]); + return ( -
-

Recent Apps

-
+
+

+ Recent Apps +

+ {recentApps.length === 0 && ( +
+

Apps you use will be listed here, in the order you used them.

+

You can click/tap/keyboard on a listed app to open it.

+ {groups && addRecentApp(groups)} />} +
+ )} + {recentApps.length > 0 && ( + + )}
-

Recent Developers

-
+

+ Recent Developers +

+ {recentDevs.length === 0 && ( +
+

Urbit app developers you search for will be listed here.

+

+ Try out app discovery by visiting below. +

+ addRecentDev(zod)} /> +
+ )} + {recentDevs.length > 0 && ( + + )}
); }; diff --git a/pkg/grid/src/nav/search/Providers.tsx b/pkg/grid/src/nav/search/Providers.tsx index 9385c4090..81a8a2837 100644 --- a/pkg/grid/src/nav/search/Providers.tsx +++ b/pkg/grid/src/nav/search/Providers.tsx @@ -1,15 +1,18 @@ import fuzzy from 'fuzzy'; -import classNames from 'classnames'; -import React, { useCallback, useEffect, useMemo } from 'react'; +import React, { useEffect, useMemo } from 'react'; import { useQuery } from 'react-query'; -import { Link, RouteComponentProps } from 'react-router-dom'; -import { ShipName } from '../../components/ShipName'; +import { RouteComponentProps } from 'react-router-dom'; import { fetchProviders, providersKey } from '../../state/docket'; import { Provider } from '../../state/docket-types'; -import { useLeapStore } from '../Nav'; +import { MatchItem, useLeapStore } from '../Nav'; +import { ProviderList } from '../../components/ProviderList'; type ProvidersProps = RouteComponentProps<{ ship: string }>; +export function providerMatch(provider: Provider): MatchItem { + return { value: provider.shipName, display: provider.nickname }; +} + export const Providers = ({ match }: ProvidersProps) => { const { selectedMatch, select } = useLeapStore((state) => ({ select: state.select, @@ -48,23 +51,11 @@ export const Providers = ({ match }: ProvidersProps) => { useEffect(() => { if (results) { useLeapStore.setState({ - matches: results.map((p) => ({ value: p.shipName, display: p.nickname })) + matches: results.map(providerMatch) }); } }, [JSON.stringify(results)]); - const isSelected = useCallback( - (target: Provider) => { - if (!selectedMatch) { - return false; - } - - const matchValue = selectedMatch.display || selectedMatch.value; - return target.nickname === matchValue || target.shipName === matchValue; - }, - [selectedMatch] - ); - return (
@@ -74,32 +65,7 @@ export const Providers = ({ match }: ProvidersProps) => {

{results && ( -
    - {results.map((p) => ( -
  • - -
    - {/* TODO: Handle sigils */} -
    -
    -

    {p.nickname || }

    - {p.status &&

    {p.status}

    } -
    - -
  • - ))} -
+ )}

That's it!

diff --git a/pkg/grid/src/tiles/Tile.tsx b/pkg/grid/src/tiles/Tile.tsx index ad0601336..5cf32734f 100644 --- a/pkg/grid/src/tiles/Tile.tsx +++ b/pkg/grid/src/tiles/Tile.tsx @@ -3,6 +3,7 @@ import React, { FunctionComponent } from 'react'; import { darken, hsla, lighten, parseToHsla, readableColorIsBlack } from 'color2k'; import { TileMenu } from './TileMenu'; import { Docket } from '../state/docket-types'; +import { useRecentsStore } from '../nav/search/Home'; type TileProps = { docket: Docket; @@ -22,6 +23,7 @@ function getMenuColor(color: string, lightText: boolean, active: boolean): strin } export const Tile: FunctionComponent = ({ docket, desk }) => { + const addRecentApp = useRecentsStore((state) => state.addRecentApp); const { title, base, color, img, status } = docket; const active = status === 'active'; const lightText = !readableColorIsBlack(color); @@ -37,6 +39,8 @@ export const Tile: FunctionComponent = ({ docket, desk }) => { !active && 'cursor-default' )} style={{ backgroundColor: active ? color || 'purple' : suspendColor }} + onClick={() => addRecentApp(docket)} + onAuxClick={() => addRecentApp(docket)} >
Date: Mon, 23 Aug 2021 14:18:07 -0500 Subject: [PATCH 2/5] dialogs: updates to design and mobile tweaks --- pkg/grid/src/components/Button.tsx | 10 +++--- pkg/grid/src/nav/Nav.tsx | 4 +-- pkg/grid/src/nav/search/AppInfo.tsx | 49 +++++++++++++++++++++-------- pkg/grid/src/pages/Grid.tsx | 6 ++-- pkg/grid/src/state/docket.ts | 37 +++++++++++----------- pkg/grid/src/state/mock-data.ts | 9 +++--- pkg/grid/src/tiles/RemoveApp.tsx | 24 ++++++++------ pkg/grid/src/tiles/SuspendApp.tsx | 20 +++++++----- pkg/grid/src/tiles/Tile.tsx | 2 +- 9 files changed, 96 insertions(+), 65 deletions(-) diff --git a/pkg/grid/src/components/Button.tsx b/pkg/grid/src/components/Button.tsx index e8f6c88eb..df1d00c38 100644 --- a/pkg/grid/src/components/Button.tsx +++ b/pkg/grid/src/components/Button.tsx @@ -2,7 +2,7 @@ import React from 'react'; import type * as Polymorphic from '@radix-ui/react-polymorphic'; import classNames from 'classnames'; -type ButtonVariant = 'primary' | 'secondary' | 'destructive'; +type ButtonVariant = 'primary' | 'secondary' | 'destructive' | 'alt-primary' | 'alt-secondary'; type PolymorphicButton = Polymorphic.ForwardRefComponent< 'button', @@ -12,9 +12,11 @@ type PolymorphicButton = Polymorphic.ForwardRefComponent< >; const variants: Record = { - primary: 'text-white bg-blue-400', - secondary: 'text-blue-400 bg-blue-100', - destructive: 'text-white bg-red-400' + primary: 'text-white bg-black', + secondary: 'text-black bg-gray-100', + destructive: 'text-white bg-red-400', + 'alt-primary': 'text-white bg-blue-400', + 'alt-secondary': 'text-blue-400 bg-blue-100' }; export const Button = React.forwardRef( diff --git a/pkg/grid/src/nav/Nav.tsx b/pkg/grid/src/nav/Nav.tsx index 5a15d5d82..798c43372 100644 --- a/pkg/grid/src/nav/Nav.tsx +++ b/pkg/grid/src/nav/Nav.tsx @@ -163,13 +163,13 @@ export const Nav: FunctionComponent = ({ menu }) => { -
+
{
{installed && ( - + Open App )} {!installed && ( - - {installing ? ( - <> - - Installing... - - ) : ( - 'Get App' - )} - + + + {installing ? ( + <> + + Installing... + + ) : ( + 'Get App' + )} + + +

Install “{treaty.title}”

+

+ This application will be able to view and interact with the contents of your + Urbit. Only install if you trust the developer. +

+
+ + Cancel + + + Get “{treaty.title}” + +
+
+
)} - + Copy App Link
diff --git a/pkg/grid/src/pages/Grid.tsx b/pkg/grid/src/pages/Grid.tsx index 4d33cfd1d..3e56b2059 100644 --- a/pkg/grid/src/pages/Grid.tsx +++ b/pkg/grid/src/pages/Grid.tsx @@ -26,14 +26,14 @@ export const Grid: FunctionComponent = ({ match }) => { return (
-
+
-
+
{!chargesLoaded && Loading...} {chargesLoaded && ( -
+
{charges && map(omit(charges, 'grid'), (charge, desk) => ( diff --git a/pkg/grid/src/state/docket.ts b/pkg/grid/src/state/docket.ts index 9940c7758..f60ff5faf 100644 --- a/pkg/grid/src/state/docket.ts +++ b/pkg/grid/src/state/docket.ts @@ -1,9 +1,7 @@ import create from 'zustand'; import produce from 'immer'; import { useCallback, useEffect } from 'react'; -import { omit } from 'lodash-es'; -import api from './api'; -import { mockAllies, mockCharges, mockTreaties } from './mock-data'; +import { mapValues, omit, pick } from 'lodash-es'; import { Allies, Charge, @@ -21,8 +19,9 @@ import { docketInstall, ChargeUpdate } from '@urbit/api/docket'; -import _ from 'lodash'; -import {kilnRevive, kilnSuspend} from '@urbit/api/hood'; +import { kilnRevive, kilnSuspend } from '@urbit/api/hood'; +import api from './api'; +import { mockAllies, mockCharges, mockTreaties } from './mock-data'; const useMockData = import.meta.env.MODE === 'mock'; @@ -67,8 +66,10 @@ const useDocketState = create((set, get) => ({ return allies; }, fetchAllyTreaties: async (ally: string) => { - let treaties = useMockData ? mockTreaties : (await api.scry(scryAllyTreaties(ally))).ini; - treaties = _.mapValues(treaties, normalizeDocket); + let treaties = useMockData + ? mockTreaties + : (await api.scry(scryAllyTreaties(ally))).ini; + treaties = mapValues(treaties, normalizeDocket); set((s) => ({ treaties: { ...s.treaties, ...treaties } })); return treaties; }, @@ -97,10 +98,9 @@ const useDocketState = create((set, get) => ({ throw new Error('Bad install'); } if (useMockData) { - - set((state) => addCharge(state, desk, {...treaty, chad: { install: null }})); + set((state) => addCharge(state, desk, { ...treaty, chad: { install: null } })); await new Promise((res) => setTimeout(() => res(), 5000)); - set((state) => addCharge(state, desk, {...treaty, chad: { glob: null }})); + set((state) => addCharge(state, desk, { ...treaty, chad: { glob: null } })); } return api.poke(docketInstall(ship, desk)); @@ -117,7 +117,7 @@ const useDocketState = create((set, get) => ({ }); }, toggleDocket: async (desk: string) => { - if(useMockData) { + if (useMockData) { set( produce((draft) => { const charge = draft.charges[desk]; @@ -127,11 +127,11 @@ const useDocketState = create((set, get) => ({ } const { charges } = get(); const charge = charges[desk]; - if(!charge) { + if (!charge) { return; } const suspended = 'suspend' in charge.chad; - if(suspended) { + if (suspended) { await api.poke(kilnRevive(desk)); } else { await api.poke(kilnSuspend(desk)); @@ -175,15 +175,14 @@ api.subscribe({ path: '/charges', event: (data: ChargeUpdate) => { useDocketState.setState((state) => { - if ('add-charge' in data) { - const { desk, charge } = data['add-charge'] - return addCharge(state, desk, charge) + const { desk, charge } = data['add-charge']; + return addCharge(state, desk, charge); } if ('del-charge' in data) { const desk = data['del-charge']; - return delCharge(state, desk) + return delCharge(state, desk); } return { charges: state.charges }; @@ -222,7 +221,7 @@ export function useAllyTreaties(ship: string) { useCallback( (s) => { const charter = s.allies[ship]; - return _.pick(s.treaties, ...(charter || [])); + return pick(s.treaties, ...(charter || [])); }, [ship] ) @@ -242,6 +241,6 @@ export function useTreaty(host: string, desk: string) { } // xx useful for debugging -//window.docket = useDocketState.getState; +// window.docket = useDocketState.getState; export default useDocketState; diff --git a/pkg/grid/src/state/mock-data.ts b/pkg/grid/src/state/mock-data.ts index c18cf6998..6f6728eaf 100644 --- a/pkg/grid/src/state/mock-data.ts +++ b/pkg/grid/src/state/mock-data.ts @@ -1,6 +1,6 @@ -import systemUrl from '../assets/system.png'; -import _ from 'lodash'; +import _ from 'lodash-es'; import { Allies, Charges, DocketHrefGlob, Treaties, Treaty } from '@urbit/api/docket'; +import systemUrl from '../assets/system.png'; export const appMetaData: Pick = { cass: '~2021.8.11..05.11.10..b721', @@ -127,9 +127,8 @@ export const mockCharges: Charges = _.reduce( mockTreaties, (acc, val, key) => { const [, desk] = key.split('/'); - const chad = desk === 'uniswap' - ? { install: null } : { glob : null }; - if(desk === 'inbox') { + const chad = { glob: null }; + if (desk === 'inbox') { return acc; } diff --git a/pkg/grid/src/tiles/RemoveApp.tsx b/pkg/grid/src/tiles/RemoveApp.tsx index 4239c4644..3325ce7cb 100644 --- a/pkg/grid/src/tiles/RemoveApp.tsx +++ b/pkg/grid/src/tiles/RemoveApp.tsx @@ -1,7 +1,7 @@ import React, { useCallback } from 'react'; import { useHistory, useParams } from 'react-router-dom'; import { Button } from '../components/Button'; -import { Dialog, DialogContent } from '../components/Dialog'; +import { Dialog, DialogClose, DialogContent } from '../components/Dialog'; import useDocketState, { useCharges } from '../state/docket'; export const RemoveApp = () => { @@ -14,19 +14,23 @@ export const RemoveApp = () => { // TODO: add optimistic updates const handleRemoveApp = useCallback(() => { uninstallDocket(desk); - history.push('/'); - }, []); + }, [desk]); return ( !open && history.push('/')}> - -

Remove “{docket?.title || ''}”

-

- Explanatory writing about what data will be kept. + +

Remove “{docket?.title || ''}”?

+

+ This will remove the software's tile from your home screen.

- +
+ + Cancel + + + Remove “{docket?.title}” + +
); diff --git a/pkg/grid/src/tiles/SuspendApp.tsx b/pkg/grid/src/tiles/SuspendApp.tsx index 748a29862..c7cfcdf57 100644 --- a/pkg/grid/src/tiles/SuspendApp.tsx +++ b/pkg/grid/src/tiles/SuspendApp.tsx @@ -1,7 +1,7 @@ import React, { useCallback } from 'react'; import { Redirect, useHistory, useParams } from 'react-router-dom'; import { Button } from '../components/Button'; -import { Dialog, DialogContent } from '../components/Dialog'; +import { Dialog, DialogClose, DialogContent } from '../components/Dialog'; import useDocketState, { useCharges } from '../state/docket'; export const SuspendApp = () => { @@ -13,7 +13,6 @@ export const SuspendApp = () => { // TODO: add optimistic updates const handleSuspendApp = useCallback(() => { useDocketState.getState().toggleDocket(desk); - history.push('/'); }, [desk]); if ('suspend' in charge.chad) { @@ -22,14 +21,19 @@ export const SuspendApp = () => { return ( !open && history.push('/')}> - -

Suspend “{charge?.title || ''}”

-

+ +

Suspend “{charge?.title || ''}”

+

Suspending an app will freeze its current state, and render it unable

- +
+ + Cancel + + + Suspend “{charge?.title}” + +
); diff --git a/pkg/grid/src/tiles/Tile.tsx b/pkg/grid/src/tiles/Tile.tsx index 6b52f30d2..3cbfa2653 100644 --- a/pkg/grid/src/tiles/Tile.tsx +++ b/pkg/grid/src/tiles/Tile.tsx @@ -38,7 +38,7 @@ export const Tile: FunctionComponent = ({ charge, desk }) => { href={active ? link : undefined} target={desk} className={classNames( - 'group relative font-semibold aspect-w-1 aspect-h-1 rounded-xl default-ring', + 'group relative font-semibold aspect-w-1 aspect-h-1 rounded-3xl default-ring', !active && 'cursor-default' )} style={{ backgroundColor: active ? color || 'purple' : suspendColor }} From 0ebfc72ea2a605bf08cbbeb6e0238eeadb363e73 Mon Sep 17 00:00:00 2001 From: Hunter Miller Date: Tue, 24 Aug 2021 14:59:19 -0500 Subject: [PATCH 3/5] styles: more exact dialog pos to remove jump --- pkg/grid/src/nav/Nav.tsx | 23 ++++++++++++++--------- pkg/grid/src/styles/utilities.css | 7 +++++++ 2 files changed, 21 insertions(+), 9 deletions(-) diff --git a/pkg/grid/src/nav/Nav.tsx b/pkg/grid/src/nav/Nav.tsx index 798c43372..22fadc543 100644 --- a/pkg/grid/src/nav/Nav.tsx +++ b/pkg/grid/src/nav/Nav.tsx @@ -85,11 +85,7 @@ export const Nav: FunctionComponent = ({ menu }) => { const dialogNavRef = useRef(null); const [systemMenuOpen, setSystemMenuOpen] = useState(false); const [dialogContentOpen, setDialogContentOpen] = useState(false); - const { selection, select } = useLeapStore((state) => ({ - selectedMatch: state.selectedMatch, - selection: state.selection, - select: state.select - })); + const select = useLeapStore((state) => state.select); const menuState = menu || 'closed'; const isOpen = menuState !== 'closed'; @@ -99,10 +95,8 @@ export const Nav: FunctionComponent = ({ menu }) => { if (!isOpen) { select(null); setDialogContentOpen(false); - } else { - inputRef.current?.focus(); } - }, [selection, isOpen]); + }, [isOpen]); const onOpen = useCallback( (event: Event) => { @@ -124,6 +118,15 @@ export const Nav: FunctionComponent = ({ menu }) => { } }, []); + const disableCloseWhenDropdownOpen = useCallback( + (e: Event) => { + if (systemMenuOpen) { + e.preventDefault(); + } + }, + [systemMenuOpen] + ); + return ( <> = ({ menu }) => { = ({ menu }) => { Date: Tue, 24 Aug 2021 18:02:07 -0500 Subject: [PATCH 4/5] notifications: adding initial structure --- pkg/grid/index.html | 11 ++- pkg/grid/src/components/icons/Elbow.tsx | 14 ++++ pkg/grid/src/nav/Notifications.tsx | 51 +++++++++++-- .../nav/notifications/BasicNotification.tsx | 10 +++ .../nav/notifications/SystemNotification.tsx | 37 ++++++++++ pkg/grid/src/state/docket.ts | 9 +-- pkg/grid/src/state/hark-types.ts | 18 +++++ pkg/grid/src/state/hark.ts | 12 +++ pkg/grid/src/state/mock-data.ts | 6 ++ pkg/grid/src/state/util.ts | 3 +- pkg/grid/src/styles/components.css | 4 + pkg/grid/tailwind.config.js | 73 +++++++++---------- 12 files changed, 187 insertions(+), 61 deletions(-) create mode 100644 pkg/grid/src/components/icons/Elbow.tsx create mode 100644 pkg/grid/src/nav/notifications/BasicNotification.tsx create mode 100644 pkg/grid/src/nav/notifications/SystemNotification.tsx create mode 100644 pkg/grid/src/state/hark-types.ts create mode 100644 pkg/grid/src/state/hark.ts diff --git a/pkg/grid/index.html b/pkg/grid/index.html index 3d2721deb..ee7d7eeed 100644 --- a/pkg/grid/index.html +++ b/pkg/grid/index.html @@ -5,11 +5,14 @@ Landscape • Home - - - + + + - +
diff --git a/pkg/grid/src/components/icons/Elbow.tsx b/pkg/grid/src/components/icons/Elbow.tsx new file mode 100644 index 000000000..6b50598f2 --- /dev/null +++ b/pkg/grid/src/components/icons/Elbow.tsx @@ -0,0 +1,14 @@ +import React, { HTMLAttributes } from 'react'; + +type ElbowProps = HTMLAttributes; + +export const Elbow = (props: ElbowProps) => ( + + + +); diff --git a/pkg/grid/src/nav/Notifications.tsx b/pkg/grid/src/nav/Notifications.tsx index faee3838c..e99fafffd 100644 --- a/pkg/grid/src/nav/Notifications.tsx +++ b/pkg/grid/src/nav/Notifications.tsx @@ -1,20 +1,57 @@ import React, { useEffect } from 'react'; +import { Link } from 'react-router-dom'; +import { Button } from '../components/Button'; +import { useHarkStore } from '../state/hark'; +import { Notification } from '../state/hark-types'; import { useLeapStore } from './Nav'; +import { BasicNotification } from './notifications/BasicNotification'; +import { SystemNotification } from './notifications/SystemNotification'; + +function renderNotification(notification: Notification) { + if (notification.type === 'system-updates-blocked') { + return ; + } + + return ; +} + +const Empty = () => ( +
+ All clear! +
+); export const Notifications = () => { - const select = useLeapStore((state) => state.select); + const select = useLeapStore((s) => s.select); + const notifications = useHarkStore((s) => s.notifications); + const hasNotifications = notifications.length > 0; useEffect(() => { select('Notifications'); }, []); return ( -
-

Recent Apps

-
-
-

Recent Developers

-
+
+
+ + +
+ + {!hasNotifications && } + {hasNotifications && ( +
+ {notifications.map((n) => renderNotification(n))} +
+ )}
); }; diff --git a/pkg/grid/src/nav/notifications/BasicNotification.tsx b/pkg/grid/src/nav/notifications/BasicNotification.tsx new file mode 100644 index 000000000..64e9741c7 --- /dev/null +++ b/pkg/grid/src/nav/notifications/BasicNotification.tsx @@ -0,0 +1,10 @@ +import React from 'react'; +import { BasicNotification as BasicNotificationType } from '../../state/hark-types'; + +interface BasicNotificationProps { + notification: BasicNotificationType; +} + +export const BasicNotification = ({ notification }: BasicNotificationProps) => ( +
{notification.message}
+); diff --git a/pkg/grid/src/nav/notifications/SystemNotification.tsx b/pkg/grid/src/nav/notifications/SystemNotification.tsx new file mode 100644 index 000000000..e4be4ece1 --- /dev/null +++ b/pkg/grid/src/nav/notifications/SystemNotification.tsx @@ -0,0 +1,37 @@ +import { pick } from 'lodash-es'; +import React from 'react'; +import { AppList } from '../../components/AppList'; +import { Elbow } from '../../components/icons/Elbow'; +import { useCharges } from '../../state/docket'; +import { SystemNotification as SystemNotificationType } from '../../state/hark-types'; + +interface SystemNotificationProps { + notification: SystemNotificationType; +} + +export const SystemNotification = ({ notification }: SystemNotificationProps) => { + const keys = notification.charges; + const charges = useCharges(); + const blockedCharges = Object.values(pick(charges, keys)); + + return ( +
+
+
+ + Landscape +
+
+ +

+ The following ({blockedCharges.length}) apps blocked a System Update: +

+ +
+
+
+ ); +}; diff --git a/pkg/grid/src/state/docket.ts b/pkg/grid/src/state/docket.ts index f60ff5faf..be035ea24 100644 --- a/pkg/grid/src/state/docket.ts +++ b/pkg/grid/src/state/docket.ts @@ -22,6 +22,7 @@ import { import { kilnRevive, kilnSuspend } from '@urbit/api/hood'; import api from './api'; import { mockAllies, mockCharges, mockTreaties } from './mock-data'; +import { fakeRequest } from './util'; const useMockData = import.meta.env.MODE === 'mock'; @@ -38,14 +39,6 @@ interface DocketState { uninstallDocket: (desk: string) => Promise; } -async function fakeRequest(data: T, time = 300): Promise { - return new Promise((resolve) => { - setTimeout(() => { - resolve(data); - }, time); - }); -} - const useDocketState = create((set, get) => ({ fetchCharges: async () => { const charg = useMockData diff --git a/pkg/grid/src/state/hark-types.ts b/pkg/grid/src/state/hark-types.ts new file mode 100644 index 000000000..d17c1f7d1 --- /dev/null +++ b/pkg/grid/src/state/hark-types.ts @@ -0,0 +1,18 @@ +/** + * I know this doesn't match our current hark type scheme, but since we're talking + * about changing that I decided to just throw something together to at least test + * this flow for updates. + */ + +export interface SystemNotification { + type: 'system-updates-blocked'; + charges: string[]; +} + +export interface BasicNotification { + type: 'basic'; + time: string; + message: string; +} + +export type Notification = BasicNotification | SystemNotification; diff --git a/pkg/grid/src/state/hark.ts b/pkg/grid/src/state/hark.ts new file mode 100644 index 000000000..b878cffba --- /dev/null +++ b/pkg/grid/src/state/hark.ts @@ -0,0 +1,12 @@ +import create from 'zustand'; +import { Notification } from './hark-types'; +import { mockBlockedChargeNotification } from './mock-data'; +import { useMockData } from './util'; + +interface HarkStore { + notifications: Notification[]; +} + +export const useHarkStore = create(() => ({ + notifications: useMockData ? [mockBlockedChargeNotification] : [] +})); diff --git a/pkg/grid/src/state/mock-data.ts b/pkg/grid/src/state/mock-data.ts index 6f6728eaf..185962a2e 100644 --- a/pkg/grid/src/state/mock-data.ts +++ b/pkg/grid/src/state/mock-data.ts @@ -1,6 +1,7 @@ import _ from 'lodash-es'; import { Allies, Charges, DocketHrefGlob, Treaties, Treaty } from '@urbit/api/docket'; import systemUrl from '../assets/system.png'; +import { SystemNotification } from './hark-types'; export const appMetaData: Pick = { cass: '~2021.8.11..05.11.10..b721', @@ -149,3 +150,8 @@ export const mockAllies: Allies = [ '~nalrex_bannus', '~nalrys' ].reduce((acc, val) => ({ ...acc, [val]: charter }), {}); + +export const mockBlockedChargeNotification: SystemNotification = { + type: 'system-updates-blocked', + charges: ['~zod/groups', '~zod/pomodoro'] +}; diff --git a/pkg/grid/src/state/util.ts b/pkg/grid/src/state/util.ts index 2ad28781f..1bfd52bdf 100644 --- a/pkg/grid/src/state/util.ts +++ b/pkg/grid/src/state/util.ts @@ -1,4 +1,4 @@ -import { DocketHref } from "@urbit/api/docket"; +import { DocketHref } from '@urbit/api/docket'; export function makeKeyFn(key: string) { return (childKeys: string[] = []) => { @@ -16,7 +16,6 @@ export async function fakeRequest(data: T, time = 300): Promise { }); } - export function getAppHref(href: DocketHref) { return 'site' in href ? href.site : `/apps/${href.glob.base}`; } diff --git a/pkg/grid/src/styles/components.css b/pkg/grid/src/styles/components.css index 7ad7a9e3f..7f62df764 100644 --- a/pkg/grid/src/styles/components.css +++ b/pkg/grid/src/styles/components.css @@ -22,6 +22,10 @@ @apply min-w-52 p-4 rounded-xl; } +.notification { + @apply p-4 bg-gray-100 rounded-xl; +} + .spinner { @apply inline-flex items-center w-6 h-6 animate-spin; } diff --git a/pkg/grid/tailwind.config.js b/pkg/grid/tailwind.config.js index 65122483b..555e92bcb 100644 --- a/pkg/grid/tailwind.config.js +++ b/pkg/grid/tailwind.config.js @@ -7,68 +7,61 @@ module.exports = { theme: { extend: { colors: { - transparent: "transparent", - white: "#FFFFFF", - black: "#000000", + transparent: 'transparent', + white: '#FFFFFF', + black: '#000000', gray: { ...colors.trueGray, - 100: "#F2F2F2", - 200: "#CCCCCC", - 300: "#B3B3B3", - 400: "#808080", - 500: "#666666", + 100: '#F2F2F2', + 200: '#CCCCCC', + 300: '#B3B3B3', + 400: '#808080', + 500: '#666666' }, blue: { - 100: "#E9F5FF", - 200: "#D3EBFF", - 300: "#BCE2FF", - 400: "#219DFF", + 100: '#E9F5FF', + 200: '#D3EBFF', + 300: '#BCE2FF', + 400: '#219DFF' }, red: { - 100: "#FFF6F5", - 200: "#FFC6C3", - 400: "#FF4136", + 100: '#FFF6F5', + 200: '#FFC6C3', + 400: '#FF4136' }, green: { - 100: "#E6F5F0", - 200: "#B3E2D1", - 400: "#009F65", + 100: '#E6F5F0', + 200: '#B3E2D1', + 400: '#009F65' }, yellow: { - 100: "#FFF9E6", - 200: "#FFEEB3", - 300: "#FFDD66", - 400: "#FFC700", + 100: '#FFF9E6', + 200: '#FFEEB3', + 300: '#FFDD66', + 400: '#FFC700' }, + orange: colors.orange }, fontFamily: { sans: [ '"Inter"', '"Inter UI"', - "-apple-system", - "BlinkMacSystemFont", + '-apple-system', + 'BlinkMacSystemFont', '"San Francisco"', '"Helvetica Neue"', - "Arial", - "sans-serif", - ], - mono: [ - '"Source Code Pro"', - '"Roboto mono"', - '"Courier New"', - "monospace", + 'Arial', + 'sans-serif' ], + mono: ['"Source Code Pro"', '"Roboto mono"', '"Courier New"', 'monospace'] }, - minWidth: theme => theme('spacing'), - }, + minWidth: (theme) => theme('spacing') + } }, variants: { extend: { opacity: ['hover-none'] - }, + } }, - plugins: [ - require('@tailwindcss/aspect-ratio'), - require('tailwindcss-touch')() - ], -} + plugins: [require('@tailwindcss/aspect-ratio'), require('tailwindcss-touch')()] +}; From 8f8a09f4dc1fd106ccded22f5ac6b5f4c16bee88 Mon Sep 17 00:00:00 2001 From: Hunter Miller Date: Tue, 24 Aug 2021 18:40:23 -0500 Subject: [PATCH 5/5] system-notification: layout and dialogs --- pkg/grid/src/components/AppLink.tsx | 21 ++-- pkg/grid/src/components/AppList.tsx | 12 ++- pkg/grid/src/components/Button.tsx | 9 +- pkg/grid/src/nav/Notifications.tsx | 8 +- .../nav/notifications/NotificationButton.tsx | 36 +++++++ .../nav/notifications/SystemNotification.tsx | 96 +++++++++++++++++-- pkg/grid/src/state/mock-data.ts | 2 +- 7 files changed, 162 insertions(+), 22 deletions(-) create mode 100644 pkg/grid/src/nav/notifications/NotificationButton.tsx diff --git a/pkg/grid/src/components/AppLink.tsx b/pkg/grid/src/components/AppLink.tsx index 3981ab79b..9cb939048 100644 --- a/pkg/grid/src/components/AppLink.tsx +++ b/pkg/grid/src/components/AppLink.tsx @@ -4,17 +4,25 @@ import { Link, LinkProps } from 'react-router-dom'; import { Docket } from '@urbit/api'; import { getAppHref } from '../state/util'; +type Sizes = 'xs' | 'small' | 'default'; + export type AppLinkProps = Omit & { app: Docket; - small?: boolean; + size?: Sizes; selected?: boolean; to?: (app: Docket) => LinkProps['to']; }; +const sizeMap: Record = { + xs: 'w-6 h-6 mr-2 rounded', + small: 'w-8 h-8 mr-3 rounded-lg', + default: 'w-12 h-12 mr-3 rounded-lg' +}; + export const AppLink = ({ app, to, - small = false, + size = 'default', selected = false, className, ...props @@ -23,17 +31,14 @@ export const AppLink = ({
{app.image && ( @@ -46,7 +51,7 @@ export const AppLink = ({

{app.title}

- {app.info && !small &&

{app.info}

} + {app.info && size === 'default' &&

{app.info}

}
); diff --git a/pkg/grid/src/components/AppList.tsx b/pkg/grid/src/components/AppList.tsx index 2ba5810c5..901f4437c 100644 --- a/pkg/grid/src/components/AppList.tsx +++ b/pkg/grid/src/components/AppList.tsx @@ -9,6 +9,7 @@ type AppListProps = { labelledBy: string; matchAgainst?: MatchItem; onClick?: (e: MouseEvent, app: Docket) => void; + listClass?: string; } & Omit; export function appMatches(target: Docket, match?: MatchItem): boolean { @@ -20,12 +21,19 @@ export function appMatches(target: Docket, match?: MatchItem): boolean { return target.title === matchValue; // TODO: need desk name or something || target.href === matchValue; } -export const AppList = ({ apps, labelledBy, matchAgainst, onClick, ...props }: AppListProps) => { +export const AppList = ({ + apps, + labelledBy, + matchAgainst, + onClick, + listClass = 'space-y-8', + ...props +}: AppListProps) => { const addRecentApp = useRecentsStore((state) => state.addRecentApp); const selected = useCallback((app: Docket) => appMatches(app, matchAgainst), [matchAgainst]); return ( -
    +
      {apps.map((app) => (
    • = { primary: 'text-white bg-black', secondary: 'text-black bg-gray-100', + caution: 'text-white bg-orange-500', destructive: 'text-white bg-red-400', 'alt-primary': 'text-white bg-blue-400', 'alt-secondary': 'text-blue-400 bg-blue-100' diff --git a/pkg/grid/src/nav/Notifications.tsx b/pkg/grid/src/nav/Notifications.tsx index e99fafffd..c1ed9927f 100644 --- a/pkg/grid/src/nav/Notifications.tsx +++ b/pkg/grid/src/nav/Notifications.tsx @@ -7,12 +7,12 @@ import { useLeapStore } from './Nav'; import { BasicNotification } from './notifications/BasicNotification'; import { SystemNotification } from './notifications/SystemNotification'; -function renderNotification(notification: Notification) { +function renderNotification(notification: Notification, key: string) { if (notification.type === 'system-updates-blocked') { - return ; + return ; } - return ; + return ; } const Empty = () => ( @@ -49,7 +49,7 @@ export const Notifications = () => { {!hasNotifications && } {hasNotifications && (
      - {notifications.map((n) => renderNotification(n))} + {notifications.map((n, index) => renderNotification(n, index.toString()))}
      )}
diff --git a/pkg/grid/src/nav/notifications/NotificationButton.tsx b/pkg/grid/src/nav/notifications/NotificationButton.tsx new file mode 100644 index 000000000..dbd4706d5 --- /dev/null +++ b/pkg/grid/src/nav/notifications/NotificationButton.tsx @@ -0,0 +1,36 @@ +import React from 'react'; +import type * as Polymorphic from '@radix-ui/react-polymorphic'; +import classNames from 'classnames'; + +type NotificationButtonVariant = 'primary' | 'secondary' | 'destructive'; + +type PolymorphicButton = Polymorphic.ForwardRefComponent< + 'button', + { + variant?: NotificationButtonVariant; + } +>; + +const variants: Record = { + primary: 'text-blue bg-white', + secondary: 'text-black bg-white', + destructive: 'text-red-400 bg-white' +}; + +export const NotificationButton = React.forwardRef( + ({ as: Comp = 'button', variant = 'primary', children, className, ...props }, ref) => { + return ( + + {children} + + ); + } +) as PolymorphicButton; diff --git a/pkg/grid/src/nav/notifications/SystemNotification.tsx b/pkg/grid/src/nav/notifications/SystemNotification.tsx index e4be4ece1..e65ca03cd 100644 --- a/pkg/grid/src/nav/notifications/SystemNotification.tsx +++ b/pkg/grid/src/nav/notifications/SystemNotification.tsx @@ -1,9 +1,12 @@ import { pick } from 'lodash-es'; -import React from 'react'; +import React, { useCallback } from 'react'; import { AppList } from '../../components/AppList'; +import { Button } from '../../components/Button'; +import { Dialog, DialogClose, DialogContent, DialogTrigger } from '../../components/Dialog'; import { Elbow } from '../../components/icons/Elbow'; import { useCharges } from '../../state/docket'; import { SystemNotification as SystemNotificationType } from '../../state/hark-types'; +import { NotificationButton } from './NotificationButton'; interface SystemNotificationProps { notification: SystemNotificationType; @@ -13,10 +16,19 @@ export const SystemNotification = ({ notification }: SystemNotificationProps) => const keys = notification.charges; const charges = useCharges(); const blockedCharges = Object.values(pick(charges, keys)); + const count = blockedCharges.length; + + const handlePauseOTAs = useCallback(() => { + console.log('pause updates'); + }, []); + + const handleArchiveApps = useCallback(() => { + console.log('archive apps'); + }, []); return (
@@ -26,12 +38,84 @@ export const SystemNotification = ({ notification }: SystemNotificationProps) =>
-

- The following ({blockedCharges.length}) apps blocked a System Update: -

- +

The following ({count}) apps blocked a System Update:

+ +
+

+ In order to proceed with the System Update, you’ll need to temporarily archive these apps, + which will render them unusable, but with data intact. +

+

+ Archived apps will automatically un-archive and resume operation when their developer + provides an app update. +

+
+
+ + Dismiss + +

Skip System Update

+

+ Skipping the application fo an incoming System Update will grant you the ability to + continue using incompatible apps at the cost of an urbit that's not up to date. +

+

+ You can choose to apply System Updates from System Preferences any time.{' '} + + Learn More + +

+
+ + Cancel + + + Pause OTAs + +
+
+
+ + + Archive ({count}) apps and Apply System Update + + +

Archive ({count}) Apps and Apply System Update

+

+ The following apps will be archived until their developer provides a compatible update + to your system. +

+ +
+ + Cancel + + + Archive Apps + +
+
+
+
); }; diff --git a/pkg/grid/src/state/mock-data.ts b/pkg/grid/src/state/mock-data.ts index 185962a2e..63997f3fc 100644 --- a/pkg/grid/src/state/mock-data.ts +++ b/pkg/grid/src/state/mock-data.ts @@ -153,5 +153,5 @@ export const mockAllies: Allies = [ export const mockBlockedChargeNotification: SystemNotification = { type: 'system-updates-blocked', - charges: ['~zod/groups', '~zod/pomodoro'] + charges: ['groups', 'pomodoro'] };