mirror of
https://github.com/ilyakooo0/urbit.git
synced 2024-09-20 23:18:00 +03:00
Merge branch 'release/next-userspace' into lf/global-skeleton-links
This commit is contained in:
commit
8acabefcc5
@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:06808af2c089441d2cb497fc95e3292b6229b3dfa034272d46c7c41f34eb6a3b
|
||||
size 6268465
|
||||
oid sha256:eab360913b845f8775002cfe1830defcd252b490ac90e8dfa093297b56531392
|
||||
size 19090656
|
||||
|
@ -22,8 +22,10 @@
|
||||
state-5
|
||||
state-6
|
||||
state-7
|
||||
state-8
|
||||
==
|
||||
::
|
||||
+$ state-8 [%8 state-base]
|
||||
+$ state-7 [%7 state-base]
|
||||
+$ state-6 [%6 state-base]
|
||||
+$ state-5 [%5 state-base]
|
||||
@ -54,7 +56,7 @@
|
||||
$% [%chat-update update:store]
|
||||
==
|
||||
--
|
||||
=| state-7
|
||||
=| state-8
|
||||
=* state -
|
||||
::
|
||||
%- agent:dbug
|
||||
@ -83,8 +85,33 @@
|
||||
=/ old !<(versioned-state old-vase)
|
||||
=| cards=(list card)
|
||||
|-
|
||||
?: ?=(%7 -.old)
|
||||
?: ?=(%8 -.old)
|
||||
[cards this(state old)]
|
||||
?: ?=(%7 -.old)
|
||||
=/ subscribers=(jug path ship)
|
||||
%+ roll ~(val by sup.bol)
|
||||
|= [[=ship =path] out=(jug path ship)]
|
||||
:: /(mailbox|backlog)/~ship/resource.name
|
||||
::
|
||||
?. ?=([@ @ @ *] path) out
|
||||
=/ pax=^path [i.t.path i.t.t.path ~]
|
||||
(~(put ju out) pax ship)
|
||||
=/ group ~(. grpl bol)
|
||||
=. cards
|
||||
%+ weld cards
|
||||
^- (list card)
|
||||
%+ murn ~(tap in ~(key by synced.old))
|
||||
|= =path
|
||||
^- (unit card)
|
||||
?> ?=([@ @ ~] path)
|
||||
=/ group-path (group-from-chat:cc path)
|
||||
=/ members (members-from-path:group group-path)
|
||||
?: (is-managed-path:group group-path) ~
|
||||
=/ ships=(set ship) (~(get ju subscribers) path)
|
||||
%- some
|
||||
=+ [%invite path (~(dif in members) ships)]
|
||||
[%pass /inv %agent [our.bol %chat-view] %poke %chat-view-action !>(-)]
|
||||
$(-.old %8)
|
||||
?: ?=(%6 -.old)
|
||||
=. cards
|
||||
%+ weld cards
|
||||
|
@ -218,7 +218,6 @@
|
||||
;~(plug (cold %ur lus) parse-url)
|
||||
;~(plug (cold %ge lus) parse-model)
|
||||
;~(plug (cold %te hep) sym (star ;~(pfix ace parse-source)))
|
||||
;~(plug (cold %as pad) sym ;~(pfix ace parse-source))
|
||||
;~(plug (cold %do cab) parse-hoon ;~(pfix ace parse-source))
|
||||
parse-value
|
||||
==
|
||||
@ -284,6 +283,7 @@
|
||||
==
|
||||
++ parse-value
|
||||
;~ pose
|
||||
;~(plug (cold %as pad) sym ;~(pfix ace parse-source))
|
||||
(stag %sa ;~(pfix tar pad sym))
|
||||
(stag %ex parse-hoon)
|
||||
(stag %tu (ifix [lac rac] (most ace parse-source)))
|
||||
|
@ -5,7 +5,7 @@
|
||||
/- glob
|
||||
/+ default-agent, verb, dbug
|
||||
|%
|
||||
++ hash 0v3.u1ets.ipgbo.eo23m.md70h.djpj0
|
||||
++ hash 0v5.6e3d0.3hm4q.iib09.rb2jb.9h4k4
|
||||
+$ state-0 [%0 hash=@uv glob=(unit (each glob:glob tid=@ta))]
|
||||
+$ all-states
|
||||
$% state-0
|
||||
|
@ -473,6 +473,22 @@
|
||||
:+ %0
|
||||
now.bowl
|
||||
[%add-graph [ship term] `graph:store`p.u.result q.u.result]
|
||||
::
|
||||
:: note: near-duplicate of /x/graph
|
||||
::
|
||||
[%x %archive @ @ ~]
|
||||
=/ =ship (slav %p i.t.t.path)
|
||||
=/ =term i.t.t.t.path
|
||||
=/ result=(unit marked-graph:store)
|
||||
(~(get by archive) [ship term])
|
||||
?~ result
|
||||
~& no-archived-graph+[ship term]
|
||||
[~ ~]
|
||||
:- ~ :- ~ :- %graph-update
|
||||
!> ^- update:store
|
||||
:+ %0
|
||||
now.bowl
|
||||
[%add-graph [ship term] `graph:store`p.u.result q.u.result]
|
||||
::
|
||||
[%x %graph-subset @ @ @ @ ~]
|
||||
=/ =ship (slav %p i.t.t.path)
|
||||
|
@ -24,7 +24,7 @@
|
||||
<div id="portal-root"></div>
|
||||
<script src="/~landscape/js/channel.js"></script>
|
||||
<script src="/~landscape/js/session.js"></script>
|
||||
<script src="/~landscape/js/bundle/index.df81e597349a655b83f2.js"></script>
|
||||
<script src="/~landscape/js/bundle/index.9f00eb9b1c58d2b1bd3c.js"></script>
|
||||
<script src="https://sdk.amazonaws.com/js/aws-sdk-2.1.12.min.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
@ -36,9 +36,15 @@
|
||||
^- card
|
||||
[%pass wire %agent [ship term] %leave ~]
|
||||
::
|
||||
++ on-arvo
|
||||
|= [=wire =sign-arvo]
|
||||
^- (quip card _this)
|
||||
?+ sign-arvo (on-arvo:def wire sign-arvo)
|
||||
[%b *] [~ this]
|
||||
==
|
||||
::
|
||||
++ on-agent on-agent:def
|
||||
++ on-poke on-poke:def
|
||||
++ on-arvo on-arvo:def
|
||||
++ on-peek on-peek:def
|
||||
++ on-watch on-watch:def
|
||||
++ on-leave on-leave:def
|
||||
|
@ -1,63 +1,11 @@
|
||||
:: link [landscape]:
|
||||
::
|
||||
:: social bookmarking
|
||||
::
|
||||
:: the paths under which links are submitted are generally expected to
|
||||
:: correspond to existing group paths. for strictly-local collections of
|
||||
:: links, arbitrary paths are probably fair game, but could trip up
|
||||
:: primitive ui implementations.
|
||||
::
|
||||
:: urls in paths are expected to be encoded using +wood, for @ta sanity.
|
||||
:: generally, use /lib/link's +build-discussion-path.
|
||||
::
|
||||
:: see link-listen-hook to see what's synced in, and similarly
|
||||
:: see link-proxy-hook to see what's exposed.
|
||||
::
|
||||
:: scry and subscription paths:
|
||||
::
|
||||
:: (map path pages) %local-pages
|
||||
:: /local-pages our saved pages
|
||||
:: /local-pages/some-path our saved pages on path
|
||||
::
|
||||
:: (map path submissions) %submissions
|
||||
:: /submissions all submissions we've seen
|
||||
:: /submissions/some-path all submissions we've seen on path
|
||||
::
|
||||
:: (map path (map url notes)) %annotations
|
||||
:: /annotations our comments
|
||||
:: /annotations/wood-url our comments on url
|
||||
:: /annotations/wood-url/some-path our comments on url on path
|
||||
:: /annotations//some-path our comments on path
|
||||
::
|
||||
:: (map path (map url comments)) %discussions
|
||||
:: /discussions all comments
|
||||
:: /discussions/wood-url all comments on url
|
||||
:: /discussions/wood-url/some-path all comments on url on path
|
||||
:: /discussions//some-path all comments on path
|
||||
::
|
||||
:: subscription-only paths:
|
||||
::
|
||||
:: [path url] %observation
|
||||
:: /seen updates whenever an item is seen
|
||||
::
|
||||
:: scry-only paths:
|
||||
::
|
||||
::
|
||||
:: (map path (set url))
|
||||
:: /unseen the ones we haven't seen yet
|
||||
::
|
||||
:: (set url)
|
||||
:: /unseen/some-path the ones we haven't seen here yet
|
||||
::
|
||||
:: ?
|
||||
:: /seen/wood-url/some-path have we seen this here
|
||||
::
|
||||
/- *link, gra=graph-store, *resource
|
||||
/+ store=link-store, graph-store, default-agent, verb, dbug
|
||||
::
|
||||
|%
|
||||
+$ state-any $%(state-1 state-0)
|
||||
+$ state-1 [%1 ~]
|
||||
+$ state-1 [%1 cards=(list card)]
|
||||
+$ state-0
|
||||
$: %0
|
||||
by-group=(map path links)
|
||||
@ -96,9 +44,15 @@
|
||||
++ on-load
|
||||
|= old=vase
|
||||
^- (quip card _this)
|
||||
::
|
||||
=/ s !<(state-any old)
|
||||
?: ?=(%1 -.s)
|
||||
[~ this(state s)]
|
||||
:: defer card emission to later event
|
||||
::
|
||||
=; [cards=(list card) that=_this]
|
||||
:_ that(state [%1 cards])
|
||||
[%pass /load %arvo %b %wait now.bowl]~
|
||||
::
|
||||
:_ this(state *state-1)
|
||||
=/ orm orm:graph-store
|
||||
@ -107,9 +61,9 @@
|
||||
%+ turn ~(tap by by-group.s)
|
||||
|= [=path =links]
|
||||
^- (list card)
|
||||
?. ?=([@ @ *] path)
|
||||
?. ?=([@ ~] path)
|
||||
(on-bad-path path links)
|
||||
=/ =resource [(slav %p i.path) i.t.path]
|
||||
=/ =resource [our.bowl i.path]
|
||||
:_ [(archive-graph resource)]~
|
||||
%+ add-graph resource
|
||||
^- graph:gra
|
||||
@ -153,7 +107,7 @@
|
||||
++ on-bad-path
|
||||
|= [=path =links]
|
||||
^- (list card)
|
||||
~| discarding-malformed-links+[path links]
|
||||
~& discarding-malformed-links+[path links]
|
||||
~
|
||||
::
|
||||
++ add-graph
|
||||
@ -181,6 +135,12 @@
|
||||
++ on-watch on-watch:def
|
||||
++ on-leave on-leave:def
|
||||
++ on-agent on-agent:def
|
||||
++ on-arvo on-arvo:def
|
||||
++ on-arvo
|
||||
|= [=wire =sign-arvo]
|
||||
^- (quip card _this)
|
||||
?+ sign-arvo (on-arvo:def wire sign-arvo)
|
||||
[%b %wake *]
|
||||
[cards.state this]
|
||||
==
|
||||
++ on-fail on-fail:def
|
||||
--
|
||||
|
@ -55,15 +55,17 @@
|
||||
+$ state-1 [%1 base-state-0]
|
||||
+$ state-2 [%2 base-state-0]
|
||||
+$ state-3 [%3 base-state-1]
|
||||
+$ state-4 [%4 base-state-1]
|
||||
+$ versioned-state
|
||||
$% state-0
|
||||
state-1
|
||||
state-2
|
||||
state-3
|
||||
state-4
|
||||
==
|
||||
--
|
||||
::
|
||||
=| state-3
|
||||
=| state-4
|
||||
=* state -
|
||||
%+ verb |
|
||||
%- agent:dbug
|
||||
@ -82,8 +84,21 @@
|
||||
=/ old !<(versioned-state vase)
|
||||
=| cards=(list card)
|
||||
|^
|
||||
?: ?=(%3 -.old)
|
||||
?: ?=(%4 -.old)
|
||||
[cards this(state old)]
|
||||
?: ?=(%3 -.old)
|
||||
%_ $
|
||||
-.old %4
|
||||
::
|
||||
resource-indices.old
|
||||
(rebuild-resource-indices associations.old)
|
||||
::
|
||||
app-indices.old
|
||||
(rebuild-app-indices associations.old)
|
||||
::
|
||||
group-indices.old
|
||||
(rebuild-group-indices associations.old)
|
||||
==
|
||||
?: ?=(%2 -.old)
|
||||
=/ new-state=state-3
|
||||
%* . *state-3
|
||||
@ -119,6 +134,41 @@
|
||||
==
|
||||
$(old new-state-1)
|
||||
::
|
||||
++ rebuild-app-indices
|
||||
=| app-indices=(jug app-name [group-path app-path])
|
||||
|= =^associations
|
||||
^- (jug app-name [group-path app-path])
|
||||
?~ associations app-indices
|
||||
=. app-indices
|
||||
%+ ~(put ju app-indices) app-name.p.n.associations
|
||||
[-.p.n.associations app-path.p.n.associations]
|
||||
%- ~(uni by $(associations l.associations))
|
||||
$(associations r.associations)
|
||||
::
|
||||
++ rebuild-group-indices
|
||||
=| group-indices=(jug group-path md-resource)
|
||||
|= =^associations
|
||||
^- (jug group-path md-resource)
|
||||
?~ associations group-indices
|
||||
=. group-indices
|
||||
%+ ~(put ju group-indices)
|
||||
-.p.n.associations
|
||||
+.p.n.associations
|
||||
%- ~(uni by $(associations l.associations))
|
||||
$(associations r.associations)
|
||||
::
|
||||
++ rebuild-resource-indices
|
||||
=| resource-indices=(jug md-resource group-path)
|
||||
|= =^associations
|
||||
^- (jug md-resource group-path)
|
||||
?~ associations resource-indices
|
||||
=. resource-indices
|
||||
%+ ~(put ju resource-indices)
|
||||
+.p.n.associations
|
||||
-.p.n.associations
|
||||
%- ~(uni by $(associations l.associations))
|
||||
$(associations r.associations)
|
||||
::
|
||||
++ poke-md-hook
|
||||
|= act=metadata-hook-action
|
||||
^- card
|
||||
|
@ -1,8 +0,0 @@
|
||||
:- ~[comments+&]
|
||||
;>
|
||||
|
||||
# Static
|
||||
|
||||
You can put static files in here to serve them to the web. Actually, you can put static files anywhere in `/web` and see them in a browser.
|
||||
|
||||
Docs on static publishing with urbit are forthcoming — but feel free to drop markdown files in `/web` to try it out.
|
14
pkg/arvo/gen/graph-store/export-graph.hoon
Normal file
14
pkg/arvo/gen/graph-store/export-graph.hoon
Normal file
@ -0,0 +1,14 @@
|
||||
/+ graph-store
|
||||
::
|
||||
:- %say
|
||||
|= $: [now=@da eny=@uvJ bec=beak]
|
||||
[[=ship graph=term ~] ~]
|
||||
==
|
||||
:- %graph-update
|
||||
=/ our (scot %p p.bec)
|
||||
=/ wen (scot %da now)
|
||||
=/ who (scot %p ship)
|
||||
::
|
||||
.^ update:graph-store
|
||||
/gx/[our]/graph-store/[wen]/archive/[who]/[graph]/graph-update
|
||||
==
|
9
pkg/arvo/gen/graph-store/import-graph.hoon
Normal file
9
pkg/arvo/gen/graph-store/import-graph.hoon
Normal file
@ -0,0 +1,9 @@
|
||||
/+ graph-store
|
||||
::
|
||||
:- %say
|
||||
|= $: [now=@da eny=@uvJ bec=beak]
|
||||
[[graph=term =path ~] ~]
|
||||
==
|
||||
:- %graph-update
|
||||
=- ~& update=- -
|
||||
.^(=update:graph-store %cx path)
|
@ -21,10 +21,11 @@
|
||||
:: ## Pokes
|
||||
::
|
||||
:: %push-hook-action: Add/remove a resource from pushing.
|
||||
:: [update-mark.config]: A poke to proxy to the local store
|
||||
:: [update-mark.config]: A poke to proxy to the local store or a
|
||||
:: foreign push-hook
|
||||
::
|
||||
/- *push-hook
|
||||
/+ default-agent, resource
|
||||
/+ default-agent, resource, verb
|
||||
|%
|
||||
+$ card card:agent:gall
|
||||
::
|
||||
@ -182,6 +183,9 @@
|
||||
[cards this]
|
||||
::
|
||||
?: =(mark update-mark.config)
|
||||
?: (team:title [our src]:bowl)
|
||||
:_ this
|
||||
(forward-update:hc vase)
|
||||
=^ cards state
|
||||
(poke-update:hc vase)
|
||||
[cards this]
|
||||
@ -201,7 +205,7 @@
|
||||
=/ =resource
|
||||
(de-path:resource t.path)
|
||||
=/ =vase
|
||||
(initial-watch:og t.t.t.path resource)
|
||||
(initial-watch:og t.t.t.t.path resource)
|
||||
:_ this
|
||||
[%give %fact ~ update-mark.config vase]~
|
||||
::
|
||||
@ -333,5 +337,19 @@
|
||||
=/ =path
|
||||
resource+(en-path:resource u.rid)
|
||||
[%give %fact ~[path] update-mark.config vase]~
|
||||
::
|
||||
++ forward-update
|
||||
|= =vase
|
||||
^- (list card:agent:gall)
|
||||
=/ rid=(unit resource)
|
||||
(resource-for-update:og vase)
|
||||
?~ rid ~
|
||||
=/ =path
|
||||
resource+(en-path:resource u.rid)
|
||||
=/ =wire
|
||||
(make-wire resource+(en-path:resource u.rid))
|
||||
=/ dap=term
|
||||
?:(=(our.bowl entity.u.rid) store-name.config dap.bowl)
|
||||
[%pass wire %agent [entity.u.rid dap] %poke update-mark.config vase]~
|
||||
--
|
||||
--
|
||||
|
@ -1,15 +1,19 @@
|
||||
/+ *graph-store
|
||||
=* as-octs as-octs:mimes:html
|
||||
::
|
||||
|_ upd=update
|
||||
++ grad %noun
|
||||
++ grow
|
||||
|%
|
||||
++ noun upd
|
||||
++ json (update:enjs upd)
|
||||
++ mime [/application/x-urb-graph-update (as-octs (jam upd))]
|
||||
--
|
||||
::
|
||||
++ grab
|
||||
|%
|
||||
++ noun update
|
||||
++ json update:dejs
|
||||
++ mime |=([* =octs] ;;(update (cue q.octs)))
|
||||
--
|
||||
--
|
||||
|
@ -53,7 +53,7 @@
|
||||
::
|
||||
;< ~ bind:m
|
||||
%+ poke-our %metadata-hook
|
||||
metadata-hook-action+!>([%add-synced ship.action rid.action])
|
||||
metadata-hook-action+!>([%add-synced ship.action (en-path:resource rid.action)])
|
||||
::
|
||||
;< ~ bind:m
|
||||
%+ poke-our %graph-pull-hook
|
||||
|
37
pkg/arvo/ted/graph/restore.hoon
Normal file
37
pkg/arvo/ted/graph/restore.hoon
Normal file
@ -0,0 +1,37 @@
|
||||
/- spider, graph=graph-store, *metadata-store, *group, group-store
|
||||
/+ strandio, resource, graph-view
|
||||
=>
|
||||
|%
|
||||
++ strand strand:spider
|
||||
++ poke poke:strandio
|
||||
++ poke-our poke-our:strandio
|
||||
--
|
||||
=, strand=strand:spider
|
||||
^- thread:spider
|
||||
|= arg=vase
|
||||
=/ m (strand ,vase)
|
||||
^- form:m
|
||||
=+ !<([rid=resource title=@t description=@t group=resource module=@t ~] arg)
|
||||
;< =bowl:spider bind:m get-bowl:strandio
|
||||
:: unarchive graph and share it
|
||||
;< ~ bind:m
|
||||
(poke-our %graph-store %graph-update !>([%0 now.bowl %unarchive-graph rid]))
|
||||
;< ~ bind:m
|
||||
(poke-our %graph-push-hook %push-hook-action !>([%add rid]))
|
||||
::
|
||||
:: Setup metadata
|
||||
::
|
||||
=/ =metadata
|
||||
%* . *metadata
|
||||
title title
|
||||
description description
|
||||
date-created now.bowl
|
||||
creator our.bowl
|
||||
module module
|
||||
==
|
||||
=/ act=metadata-action
|
||||
[%add (en-path:resource group) graph+(en-path:resource rid) metadata]
|
||||
;< ~ bind:m (poke-our %metadata-hook %metadata-action !>(act))
|
||||
;< ~ bind:m
|
||||
(poke-our %metadata-hook %metadata-hook-action !>([%add-owned (en-path:resource group)]))
|
||||
(pure:m !>(~))
|
@ -44,6 +44,8 @@ let devServer = {
|
||||
contentBase: path.join(__dirname, '../dist'),
|
||||
hot: true,
|
||||
port: 9000,
|
||||
host: '0.0.0.0',
|
||||
disableHostCheck: true,
|
||||
historyApiFallback: true
|
||||
};
|
||||
|
||||
|
BIN
pkg/interface/package-lock.json
generated
BIN
pkg/interface/package-lock.json
generated
Binary file not shown.
@ -9,7 +9,7 @@
|
||||
"@reach/menu-button": "^0.10.5",
|
||||
"@reach/tabs": "^0.10.5",
|
||||
"@tlon/indigo-light": "^1.0.3",
|
||||
"@tlon/indigo-react": "github:liam-fitzgerald/indigo-react#lf/1.1.17",
|
||||
"@tlon/indigo-react": "1.2.6",
|
||||
"aws-sdk": "^2.726.0",
|
||||
"classnames": "^2.2.6",
|
||||
"codemirror": "^5.55.0",
|
||||
|
@ -28,7 +28,7 @@ export default class GraphApi extends BaseApi<StoreState> {
|
||||
}
|
||||
|
||||
private hookAction(ship: Patp, action: any): Promise<any> {
|
||||
return this.action('graph-push-hook', 'graph-update', action, deSig(ship));
|
||||
return this.action('graph-push-hook', 'graph-update', action);
|
||||
}
|
||||
|
||||
createManagedGraph(
|
||||
|
@ -5,7 +5,8 @@ import { Path, Patp } from '~/types/noun';
|
||||
|
||||
export default class MetadataApi extends BaseApi<StoreState> {
|
||||
|
||||
metadataAdd(appName: string, appPath: Path, groupPath: Path, title: string, description: string, dateCreated: string, color: string, module: string = '') {
|
||||
|
||||
metadataAdd(appName: string, appPath: Path, groupPath: Path, title: string, description: string, dateCreated: string, color: string, moduleName: string) {
|
||||
const creator = `~${this.ship}`;
|
||||
return this.metadataAction({
|
||||
add: {
|
||||
@ -20,7 +21,7 @@ export default class MetadataApi extends BaseApi<StoreState> {
|
||||
color,
|
||||
'date-created': dateCreated,
|
||||
creator,
|
||||
module
|
||||
'module': moduleName
|
||||
}
|
||||
}
|
||||
});
|
||||
|
22
pkg/interface/src/logic/lib/OrderedMap.ts
Normal file
22
pkg/interface/src/logic/lib/OrderedMap.ts
Normal file
@ -0,0 +1,22 @@
|
||||
|
||||
export class OrderedMap<V> extends Map<number, V>
|
||||
implements Iterable<[number, V]> {
|
||||
|
||||
[Symbol.iterator](): IterableIterator<[number, V]> {
|
||||
const sorted = Array.from(super[Symbol.iterator]()).sort(
|
||||
([a], [b]) => b - a
|
||||
);
|
||||
|
||||
let index = 0;
|
||||
return {
|
||||
[Symbol.iterator]: this[Symbol.iterator],
|
||||
next: (): IteratorResult<[number, V]> => {
|
||||
if (index < sorted.length) {
|
||||
return { value: sorted[index++], done: false };
|
||||
} else {
|
||||
return { done: true, value: null };
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
@ -103,6 +103,10 @@ export default function index(associations, apps) {
|
||||
app = 'groups';
|
||||
};
|
||||
|
||||
if (each['app-name'] === 'graph') {
|
||||
app = each.metadata.module;
|
||||
}
|
||||
|
||||
const shipStart = each['app-path'].substr(each['app-path'].indexOf('~'));
|
||||
|
||||
if (app === 'groups') {
|
||||
@ -116,7 +120,8 @@ export default function index(associations, apps) {
|
||||
} else {
|
||||
const obj = result(
|
||||
title,
|
||||
`/~${each['app-name']}/join${each['app-path']}`,
|
||||
`/~${each['app-name']}/join${each['app-path']}${
|
||||
(each.metadata.module && '/' + each.metadata.module) || ''}`,
|
||||
app.charAt(0).toUpperCase() + app.slice(1),
|
||||
(associations?.contacts?.[each['group-path']]?.metadata?.title || null)
|
||||
);
|
||||
|
@ -1,4 +1,5 @@
|
||||
import _ from 'lodash';
|
||||
import { OrderedMap } from "~/logic/lib/OrderedMap";
|
||||
|
||||
|
||||
export const GraphReducer = (json, state) => {
|
||||
@ -17,11 +18,6 @@ const keys = (json, state) => {
|
||||
if (data) {
|
||||
state.graphKeys = new Set(data.map((res) => {
|
||||
let resource = res.ship + '/' + res.name;
|
||||
|
||||
if (!(resource in state.graphs)) {
|
||||
state.graphs[resource] = new Map();
|
||||
}
|
||||
|
||||
return resource;
|
||||
}));
|
||||
}
|
||||
@ -32,12 +28,12 @@ const addGraph = (json, state) => {
|
||||
const _processNode = (node) => {
|
||||
// is empty
|
||||
if (!node.children) {
|
||||
node.children = new Map();
|
||||
node.children = new OrderedMap();
|
||||
return node;
|
||||
}
|
||||
|
||||
// is graph
|
||||
let converted = new Map();
|
||||
let converted = new OrderedMap();
|
||||
for (let i in node.children) {
|
||||
let item = node.children[i];
|
||||
let index = item[0].split('/').slice(1).map((ind) => {
|
||||
@ -62,7 +58,7 @@ const addGraph = (json, state) => {
|
||||
}
|
||||
|
||||
let resource = data.resource.ship + '/' + data.resource.name;
|
||||
state.graphs[resource] = new Map();
|
||||
state.graphs[resource] = new OrderedMap();
|
||||
|
||||
for (let i in data.graph) {
|
||||
let item = data.graph[i];
|
||||
@ -86,12 +82,19 @@ const removeGraph = (json, state) => {
|
||||
if (!('graphs' in state)) {
|
||||
state.graphs = {};
|
||||
}
|
||||
let resource = data.ship + '/' + data.name;
|
||||
let resource = data.resource.ship + '/' + data.resource.name;
|
||||
delete state.graphs[resource];
|
||||
state.graphKeys.delete(resource);
|
||||
}
|
||||
};
|
||||
|
||||
const mapifyChildren = (children) => {
|
||||
return new OrderedMap(
|
||||
children.map(([idx, node]) => {
|
||||
const nd = {...node, children: mapifyChildren(node.children || []) };
|
||||
return [parseInt(idx.slice(1), 10), nd];
|
||||
}));
|
||||
};
|
||||
|
||||
const addNodes = (json, state) => {
|
||||
const _addNode = (graph, index, node) => {
|
||||
// set child of graph
|
||||
@ -128,8 +131,8 @@ const addNodes = (json, state) => {
|
||||
|
||||
if (index.length === 0) { return; }
|
||||
|
||||
// TODO: support adding nodes with children
|
||||
item[1].children = new Map();
|
||||
item[1].children = mapifyChildren(item[1].children || []);
|
||||
|
||||
|
||||
state.graphs[resource] = _addNode(
|
||||
state.graphs[resource],
|
||||
@ -167,4 +170,3 @@ const removeNodes = (json, state) => {
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -56,7 +56,6 @@ export default class GlobalStore extends BaseStore<StoreState> {
|
||||
this.localReducer.dehydrate(this.state);
|
||||
}
|
||||
|
||||
|
||||
initialState(): StoreState {
|
||||
return {
|
||||
pendingMessages: new Map(),
|
||||
|
@ -28,7 +28,6 @@ const groupSubscriptions: AppSubscription[] = [
|
||||
];
|
||||
|
||||
const graphSubscriptions: AppSubscription[] = [
|
||||
['/keys', 'graph-store'],
|
||||
['/updates', 'graph-store']
|
||||
];
|
||||
|
||||
@ -58,6 +57,7 @@ export default class GlobalSubscription extends BaseSubscription<StoreState> {
|
||||
this.subscribe('/all', 's3-store');
|
||||
this.subscribe('/all', 'launch');
|
||||
this.subscribe('/all', 'weather');
|
||||
this.subscribe('/keys', 'graph-store');
|
||||
}
|
||||
|
||||
restart() {
|
||||
|
2
pkg/interface/src/types/util.ts
Normal file
2
pkg/interface/src/types/util.ts
Normal file
@ -0,0 +1,2 @@
|
||||
|
||||
export type PropFunc<T extends (...args: any[]) => any> = Parameters<T>[0];
|
@ -156,6 +156,7 @@ class App extends React.Component {
|
||||
/>
|
||||
</Router>
|
||||
</Root>
|
||||
<div id="portal-root" />
|
||||
</ThemeProvider>
|
||||
);
|
||||
}
|
||||
|
@ -2,7 +2,7 @@ import React, { Component } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Spinner } from '../../../components/Spinner';
|
||||
import urbitOb from 'urbit-ob';
|
||||
import { Box, Text, Input, Button } from '@tlon/indigo-react';
|
||||
import { Box, Text, ManagedTextInputField as Input, Button } from '@tlon/indigo-react';
|
||||
import { Formik, Form } from 'formik'
|
||||
import * as Yup from 'yup';
|
||||
|
||||
|
@ -100,6 +100,9 @@ export default class ChatWindow extends Component<ChatWindowProps, ChatWindowSta
|
||||
|
||||
handleWindowFocus() {
|
||||
this.setState({ idle: false });
|
||||
if (this.virtualList?.window?.scrollTop === 0) {
|
||||
this.dismissUnread();
|
||||
}
|
||||
}
|
||||
|
||||
initialFetch() {
|
||||
@ -121,7 +124,7 @@ export default class ChatWindow extends Component<ChatWindowProps, ChatWindowSta
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps: ChatWindowProps, prevState) {
|
||||
const { isChatMissing, history, envelopes, mailboxSize, stationPendingMessages } = this.props;
|
||||
const { isChatMissing, history, envelopes, mailboxSize, stationPendingMessages, unreadCount } = this.props;
|
||||
|
||||
if (isChatMissing) {
|
||||
history.push("/~chat");
|
||||
@ -134,6 +137,12 @@ export default class ChatWindow extends Component<ChatWindowProps, ChatWindowSta
|
||||
this.stayLockedIfActive();
|
||||
}
|
||||
|
||||
if (unreadCount > prevProps.unreadCount && this.state.idle) {
|
||||
this.setState({
|
||||
lastRead: unreadCount ? mailboxSize - unreadCount : Infinity,
|
||||
});
|
||||
}
|
||||
|
||||
if (stationPendingMessages.length !== prevProps.stationPendingMessages.length) {
|
||||
this.virtualList?.calculateVisibleItems();
|
||||
}
|
||||
|
@ -98,6 +98,7 @@ export class SettingsScreen extends Component {
|
||||
association={association}
|
||||
resource="chat"
|
||||
app="chat"
|
||||
module=""
|
||||
/>
|
||||
<Spinner
|
||||
awaiting={this.state.awaiting}
|
||||
|
@ -44,8 +44,12 @@ export default class GraphApp extends PureComponent {
|
||||
setTimeout(autoJoin, 2000);
|
||||
}
|
||||
};
|
||||
autoJoin();
|
||||
|
||||
if(!graphKeys.has(resource)) {
|
||||
autoJoin();
|
||||
} else if(props.match.params.module) {
|
||||
props.history.push(`/~${props.match.params.module}/${resource}`);
|
||||
}
|
||||
return (
|
||||
<Center width="100%" height="100%">
|
||||
<Text fontSize={1}>Redirecting...</Text>
|
||||
|
@ -3,16 +3,11 @@ import { AsyncButton } from "../../../../components/AsyncButton";
|
||||
import * as Yup from "yup";
|
||||
import {
|
||||
Box,
|
||||
Input,
|
||||
Checkbox,
|
||||
ManagedTextInputField as Input,
|
||||
Col,
|
||||
InputLabel,
|
||||
InputCaption,
|
||||
Button,
|
||||
Center,
|
||||
Label,
|
||||
Text,
|
||||
} from "@tlon/indigo-react";
|
||||
|
||||
import { Formik, Form, useFormikContext, FormikHelpers } from "formik";
|
||||
import GlobalApi from "~/logic/api/global";
|
||||
import { FormError } from "~/views/components/FormError";
|
||||
@ -79,9 +74,9 @@ export function ChannelSettings(props: ChannelSettingsProps) {
|
||||
>
|
||||
<Col mb={3}>
|
||||
<Text fontWeight="bold">Channel Host Settings</Text>
|
||||
<InputCaption>
|
||||
<Label>
|
||||
Adjust channel settings, only available for channel's hosts
|
||||
</InputCaption>
|
||||
</Label>
|
||||
</Col>
|
||||
<Input
|
||||
id="title"
|
||||
|
@ -1,13 +1,17 @@
|
||||
import React, { Component } from "react";
|
||||
import React from "react";
|
||||
import { Sigil } from "~/logic/lib/sigil";
|
||||
import * as Yup from "yup";
|
||||
|
||||
import { Link } from "react-router-dom";
|
||||
import { EditElement } from "./edit-element";
|
||||
import { Spinner } from "~/views/components/Spinner";
|
||||
import { uxToHex } from "~/logic/lib/util";
|
||||
import { Col, Input, Box, Text, Row } from "@tlon/indigo-react";
|
||||
import { Formik, Form, FormikHelpers } from "formik";
|
||||
import {
|
||||
ManagedForm as Form,
|
||||
Col,
|
||||
ManagedTextInputField as Input,
|
||||
Box,
|
||||
Text,
|
||||
Row,
|
||||
} from "@tlon/indigo-react";
|
||||
import { Formik, FormikHelpers } from "formik";
|
||||
import { Contact } from "~/types/contact-update";
|
||||
import { AsyncButton } from "~/views/components/AsyncButton";
|
||||
import { ColorInput } from "~/views/components/ColorInput";
|
||||
@ -103,30 +107,34 @@ export function ContactCard(props: ContactCardProps) {
|
||||
initialValues={contact || emptyContact}
|
||||
onSubmit={onSubmit}
|
||||
>
|
||||
<Form>
|
||||
<Col>
|
||||
<Row
|
||||
borderBottom={1}
|
||||
borderBottomColor="washedGray"
|
||||
pb={3}
|
||||
alignItems="center"
|
||||
>
|
||||
<Sigil size={32} classes="" color={hexColor} ship={us} />
|
||||
<Box ml={2}>
|
||||
<Text fontFamily="mono">{us}</Text>
|
||||
</Box>
|
||||
</Row>
|
||||
<ImageInput mt={3} id="avatar" label="Avatar" s3={props.s3} />
|
||||
<ColorInput id="color" label="Sigil Color" />
|
||||
<Input id="nickname" label="Nickname" />
|
||||
<Input id="email" label="Email" />
|
||||
<Input id="phone" label="Phone" />
|
||||
<Input id="website" label="Website" />
|
||||
<Input id="notes" label="Notes" />
|
||||
<AsyncButton primary loadingText="Updating..." border>
|
||||
Save
|
||||
</AsyncButton>
|
||||
</Col>
|
||||
<Form
|
||||
display="grid"
|
||||
gridAutoRows="auto"
|
||||
gridTemplateColumns="100%"
|
||||
gridRowGap="5"
|
||||
maxWidth="400px"
|
||||
>
|
||||
<Row
|
||||
borderBottom={1}
|
||||
borderBottomColor="washedGray"
|
||||
pb={3}
|
||||
alignItems="center"
|
||||
>
|
||||
<Sigil size={32} classes="" color={hexColor} ship={us} />
|
||||
<Box ml={2}>
|
||||
<Text fontFamily="mono">{us}</Text>
|
||||
</Box>
|
||||
</Row>
|
||||
<ImageInput id="avatar" label="Avatar" s3={props.s3} />
|
||||
<ColorInput id="color" label="Sigil Color" />
|
||||
<Input id="nickname" label="Nickname" />
|
||||
<Input id="email" label="Email" />
|
||||
<Input id="phone" label="Phone" />
|
||||
<Input id="website" label="Website" />
|
||||
<Input id="notes" label="Notes" />
|
||||
<AsyncButton primary loadingText="Updating..." border>
|
||||
Save
|
||||
</AsyncButton>
|
||||
</Form>
|
||||
</Formik>
|
||||
</Box>
|
||||
|
@ -3,13 +3,11 @@ import { AsyncButton } from "../../../../components/AsyncButton";
|
||||
import * as Yup from "yup";
|
||||
import {
|
||||
Box,
|
||||
Input,
|
||||
Checkbox,
|
||||
ManagedTextInputField as Input,
|
||||
ManagedCheckboxField as Checkbox,
|
||||
Col,
|
||||
InputLabel,
|
||||
InputCaption,
|
||||
Label,
|
||||
Button,
|
||||
Center,
|
||||
} from "@tlon/indigo-react";
|
||||
import { Formik, Form, useFormikContext, FormikHelpers } from "formik";
|
||||
import { FormError } from "~/views/components/FormError";
|
||||
@ -87,12 +85,12 @@ export function GroupSettings(props: GroupSettingsProps) {
|
||||
mx={4}
|
||||
>
|
||||
<Col mb={4}>
|
||||
<InputLabel>Delete Group</InputLabel>
|
||||
<InputCaption>
|
||||
<Label>Delete Group</Label>
|
||||
<Label gray mt="2">
|
||||
Permanently delete this group. (All current members will no
|
||||
longer see this group.)
|
||||
</InputCaption>
|
||||
<Button onClick={onDelete} mt={1} border error>
|
||||
</Label>
|
||||
<Button onClick={onDelete} mt={1} destructive>
|
||||
Delete this group
|
||||
</Button>
|
||||
</Col>
|
||||
|
@ -1,5 +1,11 @@
|
||||
import React, { useCallback } from "react";
|
||||
import { Box, Input, Col, InputLabel, Radio, Text } from "@tlon/indigo-react";
|
||||
import {
|
||||
Box,
|
||||
ManagedTextInputField as Input,
|
||||
Col,
|
||||
ManagedRadioButtonField as Radio,
|
||||
Text,
|
||||
} from "@tlon/indigo-react";
|
||||
import { Formik, Form } from "formik";
|
||||
import * as Yup from "yup";
|
||||
import GlobalApi from "~/logic/api/global";
|
||||
@ -59,7 +65,7 @@ export function NewChannel(props: NewChannelProps & RouteComponentProps) {
|
||||
appPath,
|
||||
groupPath,
|
||||
EMPTY_INVITE_POLICY,
|
||||
ships.map(s => `~${s}`),
|
||||
ships.map((s) => `~${s}`),
|
||||
true,
|
||||
false
|
||||
);
|
||||
|
@ -281,7 +281,8 @@ export class GroupDetail extends Component {
|
||||
this.state.title,
|
||||
association.metadata.description,
|
||||
association.metadata['date-created'],
|
||||
uxToHex(association.metadata.color)
|
||||
uxToHex(association.metadata.color),
|
||||
''
|
||||
).then(() => {
|
||||
this.setState({ awaiting: false });
|
||||
});
|
||||
|
@ -44,7 +44,7 @@ export default class LaunchApp extends React.Component {
|
||||
<Icon
|
||||
stroke="green"
|
||||
fill="rgba(0,0,0,0)"
|
||||
icon="CircleDot"
|
||||
icon="Circle"
|
||||
/>
|
||||
<Text ml="1" color="green">Home</Text>
|
||||
</Row>
|
||||
|
@ -137,9 +137,11 @@ export default class LinksApp extends Component {
|
||||
render={ (props) => {
|
||||
const resourcePath =
|
||||
`${props.match.params.ship}/${props.match.params.name}`;
|
||||
const metPath = `/ship/~${resourcePath}`;
|
||||
const resource =
|
||||
associations.graph[resourcePath] ?
|
||||
associations.graph[resourcePath] : { metadata: {} };
|
||||
associations.graph[metPath] ?
|
||||
associations.graph[metPath] : { metadata: {} };
|
||||
|
||||
const contactDetails = contacts[resource['group-path']] || {};
|
||||
const popout = props.match.url.includes('/popout/');
|
||||
const graph = graphs[resourcePath] || null;
|
||||
@ -158,8 +160,10 @@ export default class LinksApp extends Component {
|
||||
<LinkList
|
||||
{...props}
|
||||
api={api}
|
||||
s3={s3}
|
||||
graph={graph}
|
||||
graphResource={graphKeys.has(resourcePath)}
|
||||
resourcePath={resourcePath}
|
||||
popout={popout}
|
||||
metadata={resource.metadata}
|
||||
contacts={contactDetails}
|
||||
@ -177,10 +181,12 @@ export default class LinksApp extends Component {
|
||||
render={ (props) => {
|
||||
const resourcePath =
|
||||
`${props.match.params.ship}/${props.match.params.name}`;
|
||||
const metPath = `/ship/~${resourcePath}`;
|
||||
const resource =
|
||||
associations.graph[resourcePath] ?
|
||||
associations.graph[resourcePath] : { metadata: {} };
|
||||
associations.graph[metPath] ?
|
||||
associations.graph[metPath] : { metadata: {} };
|
||||
const popout = props.match.url.includes('/popout/');
|
||||
|
||||
const contactDetails = contacts[resource['group-path']] || {};
|
||||
|
||||
const indexArr = props.match.params.index.split('-');
|
||||
|
@ -6,14 +6,15 @@ import { getContactDetails } from '~/logic/lib/util';
|
||||
export const Comments = (props) => {
|
||||
const {
|
||||
hideNicknames,
|
||||
hideAvatars
|
||||
hideAvatars,
|
||||
remoteContentPolicy
|
||||
} = props;
|
||||
|
||||
const contacts = props.contacts ? props.contacts : {};
|
||||
|
||||
return (
|
||||
<div>
|
||||
{ Array.from(props.comments.values()).map((comment) => {
|
||||
{ Array.from(props.comments).map(([date, comment]) => {
|
||||
const { nickname, color, member, avatar } =
|
||||
getContactDetails(contacts[comment.post.author]);
|
||||
|
||||
@ -30,6 +31,7 @@ export const Comments = (props) => {
|
||||
member={member}
|
||||
hideNicknames={hideNicknames}
|
||||
hideAvatars={hideAvatars}
|
||||
remoteContentPolicy={remoteContentPolicy}
|
||||
/>
|
||||
);
|
||||
})
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React, { Component } from 'react';
|
||||
import React from 'react';
|
||||
import { Row, Col, Anchor, Box, Text } from '@tlon/indigo-react';
|
||||
|
||||
import { Sigil } from '~/logic/lib/sigil';
|
||||
@ -9,34 +9,32 @@ export const LinkItem = (props) => {
|
||||
const {
|
||||
node,
|
||||
nickname,
|
||||
color,
|
||||
avatar,
|
||||
resource,
|
||||
hideAvatars,
|
||||
hideNicknames
|
||||
} = props;
|
||||
|
||||
const URLparser = new RegExp(
|
||||
/((?:([\w\d\.-]+)\:\/\/?){1}(?:(www)\.?){0,1}(((?:[\w\d-]+\.)*)([\w\d-]+\.[\w\d]+))){1}(?:\:(\d+)){0,1}((\/(?:(?:[^\/\s\?]+\/)*))(?:([^\?\/\s#]+?(?:.[^\?\s]+){0,1}){0,1}(?:\?([^\s#]+)){0,1})){0,1}(?:#([^#\s]+)){0,1}/
|
||||
);
|
||||
|
||||
const author = node.post.author;
|
||||
const index = node.post.index.split('/').join('-');
|
||||
const size = node.children ? node.children.size : 0;
|
||||
const contents = node.post.contents;
|
||||
const hostname = URLparser.exec(contents[1].url) ? URLparser.exec(contents[1].url)[4] : null;
|
||||
|
||||
const showAvatar = props.avatar && !hideAvatars;
|
||||
const showAvatar = avatar && !hideAvatars;
|
||||
const showNickname = nickname && !hideNicknames;
|
||||
|
||||
const mono = showNickname ? 'inter white-d' : 'mono white-d';
|
||||
|
||||
const img = showAvatar
|
||||
? <img src={props.avatar} height={36} width={36} className="dib" />
|
||||
: <Sigil ship={`~${author}`} size={36} color={'#' + props.color} />;
|
||||
|
||||
const baseUrl = props.baseUrl || `/~link/${resource}`;
|
||||
|
||||
let hostname = '';
|
||||
try {
|
||||
const url = new URL(contents[1].url);
|
||||
hostname = url.hostname;
|
||||
} catch (e) {}
|
||||
|
||||
|
||||
return (
|
||||
<Row alignItems="center" py={3} bg="white">
|
||||
{img}
|
||||
@ -63,5 +61,5 @@ export const LinkItem = (props) => {
|
||||
</Col>
|
||||
</Row>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -1,21 +1,39 @@
|
||||
import React from 'react';
|
||||
import { cite } from '~/logic/lib/util';
|
||||
import RemoteContent from "~/views/components/RemoteContent";
|
||||
|
||||
import moment from 'moment';
|
||||
|
||||
|
||||
const URLparser = new RegExp(
|
||||
/((?:([\w\d\.-]+)\:\/\/?){1}(?:(www)\.?){0,1}(((?:[\w\d-]+\.)*)([\w\d-]+\.[\w\d]+))){1}(?:\:(\d+)){0,1}((\/(?:(?:[^\/\s\?]+\/)*))(?:([^\?\/\s#]+?(?:.[^\?\s]+){0,1}){0,1}(?:\?([^\s#]+)){0,1})){0,1}(?:#([^#\s]+)){0,1}/
|
||||
);
|
||||
|
||||
|
||||
export const LinkPreview = (props) => {
|
||||
const showNickname = props.nickname && !props.hideNicknames;
|
||||
const nameClass = showNickname ? 'inter' : 'mono';
|
||||
|
||||
const author = props.post.author;
|
||||
const title = props.post.contents[0].text;
|
||||
const url = props.post.contents[1].url;
|
||||
const hostname = URLparser.exec(url) ? URLparser.exec(url)[4] : null;
|
||||
|
||||
const timeSent =
|
||||
moment.unix(props.post['time-sent'] / 1000).format('hh:mm a');
|
||||
|
||||
const title = props.post.contents[0].text;
|
||||
const url = props.post.contents[1].url;
|
||||
|
||||
const embed = (
|
||||
<RemoteContent
|
||||
unfold={true}
|
||||
renderUrl={false}
|
||||
url={url}
|
||||
remoteContentPolicy={props.remoteContentPolicy}
|
||||
className="mw-100"
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="pb6 w-100">
|
||||
<div className='w-100 tc'>{embed}</div>
|
||||
<div className="flex flex-column ml2 pt6 flex-auto">
|
||||
<a href={url}
|
||||
className="w-100 flex"
|
||||
@ -23,7 +41,7 @@ export const LinkPreview = (props) => {
|
||||
rel="noopener noreferrer">
|
||||
<p className="f8 truncate">{title}</p>
|
||||
<span className="gray2 ml2 f8 dib v-btm flex-shrink-0">
|
||||
{url} ↗
|
||||
{hostname} ↗
|
||||
</span>
|
||||
</a>
|
||||
<div className="w-100 pt1">
|
||||
|
@ -1,126 +0,0 @@
|
||||
import React, { Component } from 'react';
|
||||
import { Spinner } from '~/views/components/Spinner';
|
||||
import { createPost } from '~/logic/api/graph';
|
||||
|
||||
|
||||
export class LinkSubmit extends Component {
|
||||
constructor() {
|
||||
super();
|
||||
this.state = {
|
||||
linkValue: '',
|
||||
linkTitle: '',
|
||||
submitFocus: false,
|
||||
disabled: false
|
||||
};
|
||||
this.setLinkValue = this.setLinkValue.bind(this);
|
||||
this.setLinkTitle = this.setLinkTitle.bind(this);
|
||||
}
|
||||
|
||||
onClickPost() {
|
||||
const link = this.state.linkValue;
|
||||
const title = this.state.linkTitle
|
||||
? this.state.linkTitle
|
||||
: this.state.linkValue;
|
||||
|
||||
const parentIndex = this.props.parentIndex || '';
|
||||
let post = createPost([
|
||||
{ text: title },
|
||||
{ url: link }
|
||||
], parentIndex);
|
||||
|
||||
this.setState({ disabled: true }, () => {
|
||||
this.props.api.graph.addPost(
|
||||
`~${this.props.ship}`,
|
||||
this.props.name,
|
||||
post
|
||||
).then((r) => {
|
||||
this.setState({
|
||||
disabled: false,
|
||||
linkValue: '',
|
||||
linkTitle: '',
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
setLinkValue(event) {
|
||||
this.setState({ linkValue: event.target.value });
|
||||
}
|
||||
|
||||
setLinkTitle(event) {
|
||||
this.setState({ linkTitle: event.target.value });
|
||||
}
|
||||
|
||||
render() {
|
||||
const activeClasses = (!this.state.disabled) ? 'green2 pointer' : 'gray2';
|
||||
|
||||
const focus = (this.state.submitFocus)
|
||||
? 'b--black b--white-d'
|
||||
: 'b--gray4 b--gray2-d';
|
||||
|
||||
return (
|
||||
<div className={'relative ba br1 w-100 mb6 ' + focus}>
|
||||
<textarea
|
||||
className="pl2 bg-gray0-d white-d w-100 f8"
|
||||
style={{
|
||||
resize: 'none',
|
||||
height: 40,
|
||||
paddingTop: 10
|
||||
}}
|
||||
placeholder="Paste link here"
|
||||
onChange={this.setLinkValue}
|
||||
onBlur={() => this.setState({ submitFocus: false })}
|
||||
onFocus={() => this.setState({ submitFocus: true })}
|
||||
spellCheck="false"
|
||||
rows={1}
|
||||
onKeyPress={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
this.onClickPost();
|
||||
}
|
||||
}}
|
||||
value={this.state.linkValue}
|
||||
/>
|
||||
<textarea
|
||||
className="pl2 bg-gray0-d white-d w-100 f8"
|
||||
style={{
|
||||
resize: 'none',
|
||||
height: 40,
|
||||
paddingTop: 16
|
||||
}}
|
||||
placeholder="Enter title"
|
||||
onChange={this.setLinkTitle}
|
||||
onBlur={() => this.setState({ submitFocus: false })}
|
||||
onFocus={() => this.setState({ submitFocus: true })}
|
||||
spellCheck="false"
|
||||
rows={1}
|
||||
onKeyPress={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
this.onClickPost();
|
||||
}
|
||||
}}
|
||||
value={this.state.linkTitle}
|
||||
/>
|
||||
<button
|
||||
className={
|
||||
'absolute bg-gray0-d f8 ml2 flex-shrink-0 ' + activeClasses
|
||||
}
|
||||
disabled={this.state.disabled}
|
||||
onClick={this.onClickPost.bind(this)}
|
||||
style={{
|
||||
bottom: 12,
|
||||
right: 8
|
||||
}}
|
||||
>
|
||||
Post
|
||||
</button>
|
||||
<Spinner
|
||||
awaiting={this.state.disabled}
|
||||
classes="mt3 absolute right-0"
|
||||
text="Posting to collection..." />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,300 @@
|
||||
import React, { Component } from 'react';
|
||||
import { hasProvider } from 'oembed-parser';
|
||||
|
||||
import { S3Upload, SubmitDragger } from '~/views/components/s3-upload';
|
||||
import { Spinner } from '~/views/components/Spinner';
|
||||
import { Icon } from "@tlon/indigo-react";
|
||||
import GlobalApi from '~/logic/api/global';
|
||||
import { S3State } from '~/types';
|
||||
|
||||
import { createPost } from '~/logic/api/graph';
|
||||
|
||||
|
||||
interface LinkSubmitProps {
|
||||
api: GlobalApi;
|
||||
s3: S3State;
|
||||
name: string;
|
||||
ship: string;
|
||||
}
|
||||
|
||||
interface LinkSubmitState {
|
||||
linkValue: string;
|
||||
linkTitle: string;
|
||||
linkValid: boolean;
|
||||
submitFocus: boolean;
|
||||
urlFocus: boolean;
|
||||
disabled: boolean;
|
||||
dragover: boolean;
|
||||
}
|
||||
|
||||
export class LinkSubmit extends Component<LinkSubmitProps, LinkSubmitState> {
|
||||
private s3Uploader: React.RefObject<S3Upload>;
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
linkValue: '',
|
||||
linkTitle: '',
|
||||
linkValid: false,
|
||||
submitFocus: false,
|
||||
urlFocus: false,
|
||||
disabled: false,
|
||||
dragover: false
|
||||
};
|
||||
this.setLinkValue = this.setLinkValue.bind(this);
|
||||
this.setLinkTitle = this.setLinkTitle.bind(this);
|
||||
this.onDragEnter = this.onDragEnter.bind(this);
|
||||
this.onDrop = this.onDrop.bind(this);
|
||||
this.onPaste = this.onPaste.bind(this);
|
||||
this.uploadFiles = this.uploadFiles.bind(this);
|
||||
this.s3Uploader = React.createRef();
|
||||
}
|
||||
|
||||
onClickPost() {
|
||||
const link = this.state.linkValue;
|
||||
const title = this.state.linkTitle
|
||||
? this.state.linkTitle
|
||||
: this.state.linkValue;
|
||||
this.setState({ disabled: true });
|
||||
|
||||
const parentIndex = this.props.parentIndex || '';
|
||||
let post = createPost([
|
||||
{ text: title },
|
||||
{ url: link }
|
||||
], parentIndex);
|
||||
|
||||
this.props.api.graph.addPost(
|
||||
`~${this.props.ship}`,
|
||||
this.props.name,
|
||||
post
|
||||
).then((r) => {
|
||||
this.setState({
|
||||
disabled: false,
|
||||
linkValue: '',
|
||||
linkTitle: '',
|
||||
linkValid: false
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
setLinkValid(linkValue) {
|
||||
const URLparser = new RegExp(
|
||||
/((?:([\w\d\.-]+)\:\/\/?){1}(?:(www)\.?){0,1}(((?:[\w\d-]+\.)*)([\w\d-]+\.[\w\d]+))){1}(?:\:(\d+)){0,1}((\/(?:(?:[^\/\s\?]+\/)*))(?:([^\?\/\s#]+?(?:.[^\?\s]+){0,1}){0,1}(?:\?([^\s#]+)){0,1})){0,1}(?:#([^#\s]+)){0,1}/
|
||||
);;
|
||||
|
||||
let linkValid = URLparser.test(linkValue);
|
||||
|
||||
if (!linkValid) {
|
||||
linkValid = URLparser.test(`http://${linkValue}`);
|
||||
if (linkValid) {
|
||||
linkValue = `http://${linkValue}`;
|
||||
}
|
||||
}
|
||||
|
||||
this.setState({ linkValid, linkValue });
|
||||
|
||||
if (linkValid) {
|
||||
if (hasProvider(linkValue)) {
|
||||
fetch(`https://noembed.com/embed?url=${linkValue}`)
|
||||
.then(response => response.json())
|
||||
.then((result) => {
|
||||
if (result.title && !this.state.linkTitle) {
|
||||
this.setState({ linkTitle: result.title });
|
||||
}
|
||||
}).catch((error) => {/*noop*/});
|
||||
} else if (!this.state.linkTitle) {
|
||||
this.setState({
|
||||
linkTitle: decodeURIComponent(linkValue
|
||||
.split('/')
|
||||
.pop()
|
||||
.split('.')
|
||||
.slice(0, -1)
|
||||
.join('.')
|
||||
.replace('_', ' ')
|
||||
.replace(/\d{4}\.\d{1,2}\.\d{2}\.\.\d{2}\.\d{2}\.\d{2}-/, '')
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setLinkValue(event) {
|
||||
this.setState({ linkValue: event.target.value });
|
||||
this.setLinkValid(event.target.value);
|
||||
}
|
||||
|
||||
setLinkTitle(event) {
|
||||
this.setState({ linkTitle: event.target.value });
|
||||
}
|
||||
|
||||
uploadSuccess(url) {
|
||||
this.setState({ linkValue: url });
|
||||
this.setLinkValid(url);
|
||||
}
|
||||
|
||||
uploadError(error) {
|
||||
// no-op for now
|
||||
}
|
||||
|
||||
readyToUpload(): boolean {
|
||||
return Boolean(this.s3Uploader.current && this.s3Uploader.current.inputRef.current);
|
||||
}
|
||||
|
||||
onDragEnter() {
|
||||
if (!this.readyToUpload()) {
|
||||
return;
|
||||
}
|
||||
this.setState({ dragover: true });
|
||||
}
|
||||
|
||||
onDrop(event: DragEvent) {
|
||||
this.setState({ dragover: false });
|
||||
if (!event.dataTransfer || !event.dataTransfer.files.length) {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
this.uploadFiles(event.dataTransfer.files);
|
||||
}
|
||||
|
||||
onPaste(event: ClipboardEvent) {
|
||||
if (!event.clipboardData || !event.clipboardData.files.length) {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
this.uploadFiles(event.clipboardData.files);
|
||||
}
|
||||
|
||||
uploadFiles(files: FileList) {
|
||||
if (!this.readyToUpload()) {
|
||||
return;
|
||||
}
|
||||
this.s3Uploader.current.inputRef.current.files = files;
|
||||
const fire = document.createEvent("HTMLEvents");
|
||||
fire.initEvent("change", true, true);
|
||||
this.s3Uploader.current?.inputRef.current?.dispatchEvent(fire);
|
||||
}
|
||||
|
||||
render() {
|
||||
const activeClasses = (this.state.linkValid && !this.state.disabled)
|
||||
? 'green2 pointer' : 'gray2';
|
||||
|
||||
const focus = (this.state.submitFocus)
|
||||
? 'b--black b--white-d'
|
||||
: 'b--gray4 b--gray2-d';
|
||||
|
||||
const isS3Ready =
|
||||
( this.props.s3.credentials.secretAccessKey &&
|
||||
this.props.s3.credentials.endpoint &&
|
||||
this.props.s3.credentials.accessKeyId
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`relative ba br1 w-100 mb6 ${focus}`}
|
||||
onDragEnter={this.onDragEnter.bind(this)}
|
||||
onDragOver={e => {
|
||||
e.preventDefault();
|
||||
if (isS3Ready) {
|
||||
this.setState({ dragover: true})
|
||||
}
|
||||
}}
|
||||
onDragLeave={() => this.setState({ dragover: false })}
|
||||
onDrop={this.onDrop}
|
||||
>
|
||||
{this.state.dragover ? <SubmitDragger /> : null}
|
||||
<div className="relative">
|
||||
{
|
||||
( this.state.linkValue ||
|
||||
this.state.urlFocus ||
|
||||
this.state.disabled
|
||||
) ? null : (
|
||||
isS3Ready ? (
|
||||
<span className="gray2 absolute pl2 pt3 pb2 f8"
|
||||
style={{pointerEvents: 'none'}}>
|
||||
Drop or
|
||||
<span className="pointer green2"
|
||||
style={{pointerEvents: 'all'}}
|
||||
onClick={(event) => {
|
||||
if (!this.readyToUpload()) {
|
||||
return;
|
||||
}
|
||||
this.s3Uploader.current.inputRef.current.click();
|
||||
}}> upload </span>
|
||||
a file, or paste a link here
|
||||
</span>
|
||||
) : (
|
||||
<span className="gray2 absolute pl2 pt3 pb2 f8"
|
||||
style={{pointerEvents: 'none'}}>
|
||||
Paste a link here
|
||||
</span>
|
||||
)
|
||||
)
|
||||
}
|
||||
{!this.state.disabled && isS3Ready ? <S3Upload
|
||||
ref={this.s3Uploader}
|
||||
configuration={this.props.s3.configuration}
|
||||
credentials={this.props.s3.credentials}
|
||||
uploadSuccess={this.uploadSuccess.bind(this)}
|
||||
uploadError={this.uploadError.bind(this)}
|
||||
className="dn absolute pt3 pb2 pl2 w-100"
|
||||
></S3Upload> : null}
|
||||
<input
|
||||
type="url"
|
||||
className="pl2 w-100 f8 pt3 pb2 white-d bg-transparent"
|
||||
onChange={this.setLinkValue}
|
||||
onBlur={() => this.setState({ submitFocus: false, urlFocus: false })}
|
||||
onFocus={() => this.setState({ submitFocus: true, urlFocus: true })}
|
||||
spellCheck="false"
|
||||
onPaste={this.onPaste}
|
||||
onKeyPress={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
this.onClickPost();
|
||||
}
|
||||
}}
|
||||
value={this.state.linkValue}
|
||||
/>
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
className="pl2 bg-transparent w-100 f8 white-d linkTitle"
|
||||
style={{
|
||||
resize: 'none',
|
||||
height: 40
|
||||
}}
|
||||
placeholder="Provide a title"
|
||||
onChange={this.setLinkTitle}
|
||||
onBlur={() => this.setState({ submitFocus: false })}
|
||||
onFocus={() => this.setState({ submitFocus: true })}
|
||||
spellCheck="false"
|
||||
onKeyPress={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
this.onClickPost();
|
||||
}
|
||||
}}
|
||||
value={this.state.linkTitle}
|
||||
/>
|
||||
{!this.state.disabled ? <button
|
||||
className={
|
||||
'bg-transparent f8 flex-shrink-0 pr2 pl2 pt2 pb3 ' + activeClasses
|
||||
}
|
||||
disabled={!this.state.linkValid || this.state.disabled}
|
||||
onClick={this.onClickPost.bind(this)}
|
||||
style={{
|
||||
bottom: 12,
|
||||
right: 8
|
||||
}}
|
||||
>
|
||||
Post link
|
||||
</button> : null}
|
||||
<Spinner awaiting={this.state.disabled} classes="nowrap flex items-center pr2 pl2 pt2 pb4" style={{flex: '1 1 14rem'}} text="Posting to collection..." />
|
||||
|
||||
|
||||
</div>
|
||||
) ;
|
||||
}
|
||||
}
|
||||
|
||||
export default LinkSubmit;
|
@ -48,8 +48,13 @@ export const LinkDetail = (props) => {
|
||||
popout={props.popout}
|
||||
api={props.api}
|
||||
/>
|
||||
<Link className="dib f9 fw4 pt2 gray2 lh-solid" to="/~link">
|
||||
{`<- ${title}`}
|
||||
<Link className="dib f9 fw4 pt2 gray2 lh-solid"
|
||||
to={`/~link/${resourcePath}`}>
|
||||
<h2
|
||||
className="dib f9 fw4 lh-solid v-top black white-d"
|
||||
style={{ width: 'max-content' }}>
|
||||
{`<- ${title}`}
|
||||
</h2>
|
||||
</Link>
|
||||
<TabBar
|
||||
location={props.location}
|
||||
@ -65,7 +70,9 @@ export const LinkDetail = (props) => {
|
||||
post={props.node.post}
|
||||
nickname={nickname}
|
||||
hideNicknames={props.hideNicknames}
|
||||
commentNumber={props.node.children.size} />
|
||||
commentNumber={props.node.children.size}
|
||||
remoteContentPolicy={props.remoteContentPolicy}
|
||||
/>
|
||||
<div className="flex">
|
||||
<CommentSubmit
|
||||
name={props.name}
|
||||
@ -80,7 +87,8 @@ export const LinkDetail = (props) => {
|
||||
popout={props.popout}
|
||||
api={props.api}
|
||||
hideAvatars={props.hideAvatars}
|
||||
hideNicknames={props.hideNicknames} />
|
||||
hideNicknames={props.hideNicknames}
|
||||
remoteContentPolicy={props.remoteContentPolicy} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,10 +1,10 @@
|
||||
import React, { Component, useEffect } from "react";
|
||||
|
||||
import { TabBar } from "~/views/components/chat-link-tabbar";
|
||||
import { SidebarSwitcher } from "~/views/components/SidebarSwitch";
|
||||
import { Link } from "react-router-dom";
|
||||
import { LinkItem } from "./lib/link-item";
|
||||
import { LinkSubmit } from "./lib/link-submit";
|
||||
import { TabBar } from '~/views/components/chat-link-tabbar';
|
||||
import { SidebarSwitcher } from '~/views/components/SidebarSwitch';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { LinkItem } from './lib/link-item';
|
||||
import LinkSubmit from './lib/link-submit';
|
||||
|
||||
import { getContactDetails } from "~/logic/lib/util";
|
||||
|
||||
@ -45,9 +45,12 @@ export const LinkList = (props) => {
|
||||
<SidebarSwitcher
|
||||
sidebarShown={props.sidebarShown}
|
||||
popout={props.popout}
|
||||
api={props.api}
|
||||
/>
|
||||
<h2 className="white-d dib f9 fw4 lh-solid v-top pt2">{title}</h2>
|
||||
api={props.api} />
|
||||
<h2
|
||||
className="dib f9 fw4 pt2 lh-solid v-top black white-d"
|
||||
style={{ width: 'max-content' }}>
|
||||
{title}
|
||||
</h2>
|
||||
<TabBar
|
||||
location={props.location}
|
||||
popout={props.popout}
|
||||
@ -58,19 +61,29 @@ export const LinkList = (props) => {
|
||||
<div className="w-100 mt6 flex justify-center overflow-y-scroll ph4 pb4">
|
||||
<div className="w-100 mw7">
|
||||
<div className="flex">
|
||||
<LinkSubmit name={props.name} ship={props.ship} api={props.api} />
|
||||
<LinkSubmit
|
||||
name={props.name}
|
||||
ship={props.ship}
|
||||
api={props.api}
|
||||
s3={props.s3} />
|
||||
</div>
|
||||
{Array.from(props.graph.values()).map((node) => {
|
||||
return (
|
||||
<LinkItem
|
||||
resource={resource}
|
||||
node={node}
|
||||
nickname={props.metadata.nickname}
|
||||
hideAvatars={props.hideAvatars}
|
||||
hideNicknames={props.hideNicknames}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{ Array.from(props.graph).map(([date, node]) => {
|
||||
const { nickname, color, avatar } =
|
||||
getContactDetails(props.contacts[ship]);
|
||||
|
||||
return (
|
||||
<LinkItem
|
||||
resource={resource}
|
||||
node={node}
|
||||
nickname={nickname}
|
||||
color={color}
|
||||
avatar={avatar}
|
||||
hideAvatars={props.hideAvatars}
|
||||
hideNicknames={props.hideNicknames}
|
||||
/>
|
||||
);
|
||||
})
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,6 +1,6 @@
|
||||
import React, { useCallback } from "react";
|
||||
import { RouteComponentProps } from "react-router-dom";
|
||||
import { Box, Input, Col } from "@tlon/indigo-react";
|
||||
import { Box, ManagedTextInputField as Input, Col } from "@tlon/indigo-react";
|
||||
import { Formik, Form } from "formik";
|
||||
import * as Yup from "yup";
|
||||
|
||||
@ -69,7 +69,7 @@ export function NewScreen(props: object) {
|
||||
<Box
|
||||
display="grid"
|
||||
gridTemplateRows="auto"
|
||||
gridRowGap={2}
|
||||
gridRowGap={4}
|
||||
gridTemplateColumns="300px">
|
||||
<Input
|
||||
id="name"
|
||||
|
@ -143,7 +143,8 @@ export class SettingsScreen extends Component {
|
||||
popout={this.props.popout}
|
||||
api={this.props.api}
|
||||
/>
|
||||
<Link to="/~link" className="pt2">
|
||||
<Link className="dib f9 fw4 pt2 gray2 lh-solid"
|
||||
to={`/~link/${props.resourcePath}`}>
|
||||
<h2
|
||||
className="dib f9 fw4 lh-solid v-top"
|
||||
style={{ width: 'max-content' }}>
|
||||
@ -153,8 +154,8 @@ export class SettingsScreen extends Component {
|
||||
<TabBar
|
||||
location={props.location}
|
||||
popout={props.popout}
|
||||
popoutHref={`/~link/popout/${props.resource}/settings`}
|
||||
settings={`/~link/${props.resource}/settings`}
|
||||
popoutHref={`/~link/popout/${props.resourcePath}/settings`}
|
||||
settings={`/~link/${props.resourcePath}/settings`}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-100 pl3 mt4 cf">
|
||||
@ -168,6 +169,7 @@ export class SettingsScreen extends Component {
|
||||
association={props.resource}
|
||||
resource="collection"
|
||||
app="graph"
|
||||
module="link"
|
||||
/>
|
||||
<Spinner
|
||||
awaiting={this.state.awaiting}
|
||||
|
@ -6,6 +6,10 @@
|
||||
padding-bottom: 56.25%;
|
||||
}
|
||||
|
||||
.linkTitle::placeholder {
|
||||
color: #7f7f7f;
|
||||
}
|
||||
|
||||
.links.embed-container iframe, .links.embed-container object, .links.embed-container embed {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
|
@ -1,9 +1,16 @@
|
||||
import React from 'react';
|
||||
import { Box, InputLabel, Radio, Input } from '@tlon/indigo-react';
|
||||
import React from "react";
|
||||
import {
|
||||
Box,
|
||||
Row,
|
||||
Label,
|
||||
Col,
|
||||
ManagedRadioButtonField as Radio,
|
||||
ManagedTextInputField as Input,
|
||||
} from "@tlon/indigo-react";
|
||||
|
||||
import GlobalApi from '~/logic/api/global';
|
||||
import { S3State } from '~/types';
|
||||
import { ImageInput } from '~/views/components/ImageInput';
|
||||
import GlobalApi from "~/logic/api/global";
|
||||
import { S3State } from "~/types";
|
||||
import { ImageInput } from "~/views/components/ImageInput";
|
||||
|
||||
export type BgType = "none" | "url" | "color";
|
||||
|
||||
@ -18,37 +25,33 @@ export function BackgroundPicker({
|
||||
api: GlobalApi;
|
||||
s3: S3State;
|
||||
}) {
|
||||
|
||||
const rowSpace = { my: 0, alignItems: 'center' };
|
||||
const radioProps = { my: 4, mr: 4, name: 'bgType' };
|
||||
return (
|
||||
<Box>
|
||||
<InputLabel>Landscape Background</InputLabel>
|
||||
<Box display="flex" alignItems="center">
|
||||
<Box mt={3} mr={7}>
|
||||
<Radio label="Image" id="url" name="bgType" />
|
||||
{bgType === "url" && (
|
||||
<ImageInput
|
||||
api={api}
|
||||
s3={s3}
|
||||
id="bgUrl"
|
||||
name="bgUrl"
|
||||
label="URL"
|
||||
url={bgUrl || ""}
|
||||
/>
|
||||
)}
|
||||
<Radio label="Color" id="color" name="bgType" />
|
||||
{bgType === "color" && (
|
||||
<Input
|
||||
ml={4}
|
||||
type="text"
|
||||
label="Color"
|
||||
id="bgColor"
|
||||
name="bgColor"
|
||||
/>
|
||||
)}
|
||||
<Radio label="None" id="none" name="bgType" />
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
<Col>
|
||||
<Label mb="2">Landscape Background</Label>
|
||||
<Row {...rowSpace}>
|
||||
<Radio {...radioProps} label="Image" id="url" />
|
||||
{bgType === "url" && (
|
||||
<ImageInput
|
||||
ml="3"
|
||||
api={api}
|
||||
s3={s3}
|
||||
id="bgUrl"
|
||||
name="bgUrl"
|
||||
label="URL"
|
||||
url={bgUrl || ""}
|
||||
/>
|
||||
)}
|
||||
</Row>
|
||||
<Row {...rowSpace}>
|
||||
<Radio label="Color" id="color" {...radioProps} />
|
||||
{bgType === "color" && (
|
||||
<Input ml={4} type="text" label="Color" id="bgColor" />
|
||||
)}
|
||||
</Row>
|
||||
<Radio label="None" id="none" {...radioProps} />
|
||||
</Col>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
@ -1,7 +1,8 @@
|
||||
import React, { useCallback } from "react";
|
||||
|
||||
import {
|
||||
Input,
|
||||
ManagedTextInputField as Input,
|
||||
ManagedForm as Form,
|
||||
Box,
|
||||
Button,
|
||||
Col,
|
||||
@ -11,9 +12,9 @@ import {
|
||||
MenuList,
|
||||
MenuItem,
|
||||
} from "@tlon/indigo-react";
|
||||
import { Formik, Form } from "formik";
|
||||
import { Formik } from "formik";
|
||||
|
||||
import GlobalApi from "../../../../api/global";
|
||||
import GlobalApi from "~/logic/api/global";
|
||||
|
||||
export function BucketList({
|
||||
buckets,
|
||||
@ -53,49 +54,48 @@ export function BucketList({
|
||||
|
||||
return (
|
||||
<Formik initialValues={{ newBucket: "" }} onSubmit={onSubmit}>
|
||||
<Form>
|
||||
<Col alignItems="start">
|
||||
{_buckets.map((bucket) => (
|
||||
<Box
|
||||
key={bucket}
|
||||
display="flex"
|
||||
justifyContent="space-between"
|
||||
alignItems="center"
|
||||
borderRadius={1}
|
||||
border={1}
|
||||
borderColor="washedGray"
|
||||
fontSize={1}
|
||||
pl={2}
|
||||
mb={2}
|
||||
width="100%"
|
||||
>
|
||||
<Text>{bucket}</Text>
|
||||
{bucket === selected && (
|
||||
<Text p={1} color="green">
|
||||
Active
|
||||
</Text>
|
||||
)}
|
||||
{bucket !== selected && (
|
||||
<Menu>
|
||||
<MenuButton sm>Options</MenuButton>
|
||||
<MenuList>
|
||||
<MenuItem onSelect={onSelect(bucket)}>Make Active</MenuItem>
|
||||
<MenuItem onSelect={onDelete(bucket)}>Delete</MenuItem>
|
||||
</MenuList>
|
||||
</Menu>
|
||||
)}
|
||||
</Box>
|
||||
))}
|
||||
<Input
|
||||
mt={2}
|
||||
type="text"
|
||||
label="New Bucket"
|
||||
id="newBucket"
|
||||
/>
|
||||
<Button border borderColor="washedGrey" type="submit">
|
||||
Add
|
||||
</Button>
|
||||
</Col>
|
||||
<Form
|
||||
display="grid"
|
||||
gridTemplateColumns="100%"
|
||||
gridAutoRows="auto"
|
||||
gridRowGap={2}
|
||||
>
|
||||
{_buckets.map((bucket) => (
|
||||
<Box
|
||||
key={bucket}
|
||||
display="flex"
|
||||
justifyContent="space-between"
|
||||
alignItems="center"
|
||||
borderRadius={1}
|
||||
border={1}
|
||||
borderColor="washedGray"
|
||||
fontSize={1}
|
||||
pl={2}
|
||||
mb={2}
|
||||
>
|
||||
<Text>{bucket}</Text>
|
||||
{bucket === selected && (
|
||||
<Text p={2} color="green">
|
||||
Active
|
||||
</Text>
|
||||
)}
|
||||
{bucket !== selected && (
|
||||
<Menu>
|
||||
<MenuButton border={0} cursor="pointer" width="auto">
|
||||
Options
|
||||
</MenuButton>
|
||||
<MenuList>
|
||||
<MenuItem onSelect={onSelect(bucket)}>Make Active</MenuItem>
|
||||
<MenuItem onSelect={onDelete(bucket)}>Delete</MenuItem>
|
||||
</MenuList>
|
||||
</Menu>
|
||||
)}
|
||||
</Box>
|
||||
))}
|
||||
<Input mt="2" label="New Bucket" id="newBucket" />
|
||||
<Button mt="2" borderColor="washedGrey" type="submit">
|
||||
Add
|
||||
</Button>
|
||||
</Form>
|
||||
</Formik>
|
||||
);
|
||||
|
@ -2,8 +2,8 @@ import React from "react";
|
||||
|
||||
import {
|
||||
Box,
|
||||
InputLabel,
|
||||
Checkbox,
|
||||
Label,
|
||||
ManagedCheckboxField as Checkbox,
|
||||
Button,
|
||||
} from "@tlon/indigo-react";
|
||||
import { Formik, Form } from "formik";
|
||||
@ -14,7 +14,7 @@ import GlobalApi from "../../../../api/global";
|
||||
import { LaunchState } from "../../../../types/launch-update";
|
||||
import { DropLaunchTiles } from "./DropLaunch";
|
||||
import { S3State, BackgroundConfig } from "../../../../types";
|
||||
import { BackgroundPicker, BgType } from './BackgroundPicker';
|
||||
import { BackgroundPicker, BgType } from "./BackgroundPicker";
|
||||
|
||||
const formSchema = Yup.object().shape({
|
||||
tileOrdering: Yup.array().of(Yup.string()),
|
||||
@ -47,14 +47,7 @@ interface DisplayFormProps {
|
||||
}
|
||||
|
||||
export default function DisplayForm(props: DisplayFormProps) {
|
||||
const {
|
||||
api,
|
||||
launch,
|
||||
background,
|
||||
hideAvatars,
|
||||
hideNicknames,
|
||||
s3
|
||||
} = props;
|
||||
const { api, launch, background, hideAvatars, hideNicknames, s3 } = props;
|
||||
|
||||
let bgColor, bgUrl;
|
||||
if (background?.type === "url") {
|
||||
@ -99,17 +92,17 @@ export default function DisplayForm(props: DisplayFormProps) {
|
||||
<Form>
|
||||
<Box
|
||||
display="grid"
|
||||
gridTemplateColumns="1fr"
|
||||
gridTemplateColumns="100%"
|
||||
gridTemplateRows="auto"
|
||||
gridRowGap={3}
|
||||
gridRowGap={5}
|
||||
>
|
||||
<Box color="black" fontSize={1} mb={3} fontWeight={900}>
|
||||
Display Preferences
|
||||
</Box>
|
||||
<Box mb={2}>
|
||||
<InputLabel display="block" pb={2}>
|
||||
<Label display="block" pb={2}>
|
||||
Tile Order
|
||||
</InputLabel>
|
||||
</Label>
|
||||
<DropLaunchTiles
|
||||
id="tileOrdering"
|
||||
name="tileOrdering"
|
||||
@ -123,22 +116,20 @@ export default function DisplayForm(props: DisplayFormProps) {
|
||||
api={api}
|
||||
s3={s3}
|
||||
/>
|
||||
<Box>
|
||||
<Checkbox
|
||||
label="Disable avatars"
|
||||
id="avatars"
|
||||
caption="Do not show user-set avatars"
|
||||
/>
|
||||
<Checkbox
|
||||
label="Disable nicknames"
|
||||
id="nicknames"
|
||||
caption="Do not show user-set nicknames"
|
||||
/>
|
||||
</Box>
|
||||
<Checkbox
|
||||
label="Disable avatars"
|
||||
id="avatars"
|
||||
caption="Do not show user-set avatars"
|
||||
/>
|
||||
<Checkbox
|
||||
label="Disable nicknames"
|
||||
id="nicknames"
|
||||
caption="Do not show user-set nicknames"
|
||||
/>
|
||||
<Button border={1} borderColor="washedGray" type="submit">
|
||||
Save
|
||||
</Button>
|
||||
</Box>
|
||||
<Button border={1} borderColor="washedGray" type="submit">
|
||||
Save
|
||||
</Button>
|
||||
</Form>
|
||||
)}
|
||||
</Formik>
|
||||
|
@ -4,11 +4,11 @@ import { usePreview } from "react-dnd-multi-backend";
|
||||
import { capitalize } from "lodash";
|
||||
import { TileTypeBasic, Tile } from "../../../../types/launch-update";
|
||||
|
||||
import { Box, Img as _Img, Text } from "@tlon/indigo-react";
|
||||
import { Box, Image as _Image, Text } from "@tlon/indigo-react";
|
||||
import styled from "styled-components";
|
||||
|
||||
// Need to change dojo image
|
||||
const Img = styled(_Img)<{ invert?: boolean }>`
|
||||
const Image = styled(_Image)<{ invert?: boolean }>`
|
||||
${(p) =>
|
||||
p.theme.colors.white !== "rgba(255,255,255,1)" ? `filter: invert(1);` : ``}
|
||||
|
||||
@ -83,7 +83,7 @@ function DragTileBasic(props: {
|
||||
}
|
||||
style={props.style}
|
||||
>
|
||||
<Img width="48px" height="48px" src={tile.iconUrl} invert={isDojo} />
|
||||
<Image width="48px" height="48px" src={tile.iconUrl} invert={isDojo} />
|
||||
<Text
|
||||
color={
|
||||
"black" // isDojo ? "white" : "black"
|
||||
|
@ -1,5 +1,9 @@
|
||||
import React from "react";
|
||||
import { Box, Button, Checkbox } from '@tlon/indigo-react';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
ManagedCheckboxField as Checkbox,
|
||||
} from "@tlon/indigo-react";
|
||||
import { Formik, Form } from "formik";
|
||||
import * as Yup from "yup";
|
||||
|
||||
@ -10,7 +14,7 @@ const formSchema = Yup.object().shape({
|
||||
imageShown: Yup.boolean(),
|
||||
audioShown: Yup.boolean(),
|
||||
videoShown: Yup.boolean(),
|
||||
oembedShown: Yup.boolean()
|
||||
oembedShown: Yup.boolean(),
|
||||
});
|
||||
|
||||
interface FormSchema {
|
||||
@ -39,7 +43,7 @@ export default function RemoteContentForm(props: RemoteContentFormProps) {
|
||||
imageShown,
|
||||
audioShown,
|
||||
videoShown,
|
||||
oembedShown
|
||||
oembedShown,
|
||||
} as FormSchema
|
||||
}
|
||||
onSubmit={(values, actions) => {
|
||||
@ -47,7 +51,7 @@ export default function RemoteContentForm(props: RemoteContentFormProps) {
|
||||
imageShown: values.imageShown,
|
||||
audioShown: values.audioShown,
|
||||
videoShown: values.videoShown,
|
||||
oembedShown: values.oembedShown
|
||||
oembedShown: values.oembedShown,
|
||||
});
|
||||
api.local.dehydrate();
|
||||
actions.setSubmitting(false);
|
||||
@ -59,36 +63,26 @@ export default function RemoteContentForm(props: RemoteContentFormProps) {
|
||||
display="grid"
|
||||
gridTemplateColumns="1fr"
|
||||
gridTemplateRows="audio"
|
||||
gridRowGap={3}
|
||||
gridRowGap={5}
|
||||
>
|
||||
<Box color="black" fontSize={1} mb={3} fontWeight={900}>
|
||||
<Box color="black" fontSize={1} fontWeight={900}>
|
||||
Remote Content
|
||||
</Box>
|
||||
<Box>
|
||||
<Checkbox
|
||||
label="Load images"
|
||||
id="imageShown"
|
||||
/>
|
||||
<Checkbox
|
||||
label="Load audio files"
|
||||
id="audioShown"
|
||||
/>
|
||||
<Checkbox
|
||||
label="Load video files"
|
||||
id="videoShown"
|
||||
/>
|
||||
<Checkbox
|
||||
label="Load embedded content"
|
||||
id="oembedShown"
|
||||
caption="Embedded content may contain scripts"
|
||||
/>
|
||||
</Box>
|
||||
<Checkbox label="Load images" id="imageShown" />
|
||||
<Checkbox label="Load audio files" id="audioShown" />
|
||||
<Checkbox label="Load video files" id="videoShown" />
|
||||
<Checkbox
|
||||
label="Load embedded content"
|
||||
id="oembedShown"
|
||||
caption="Embedded content may contain scripts"
|
||||
/>
|
||||
<Button border={1} borderColor="washedGray" type="submit">
|
||||
Save
|
||||
</Button>
|
||||
</Box>
|
||||
<Button border={1} borderColor="washedGray" type="submit">
|
||||
Save
|
||||
</Button>
|
||||
</Form>
|
||||
)}
|
||||
</Formik>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,17 +1,18 @@
|
||||
import React, { useCallback } from "react";
|
||||
|
||||
import {
|
||||
Input,
|
||||
ManagedTextInputField as Input,
|
||||
ManagedForm as Form,
|
||||
Box,
|
||||
Button,
|
||||
Col,
|
||||
Text,
|
||||
Menu
|
||||
Menu,
|
||||
} from "@tlon/indigo-react";
|
||||
|
||||
import { Formik, Form } from "formik";
|
||||
import { Formik } from "formik";
|
||||
import GlobalApi from "../../../../api/global";
|
||||
import { BucketList } from './BucketList';
|
||||
import { BucketList } from "./BucketList";
|
||||
import { S3State } from "../../../../types";
|
||||
|
||||
interface FormSchema {
|
||||
@ -49,9 +50,6 @@ export default function S3Form(props: S3FormProps) {
|
||||
return (
|
||||
<>
|
||||
<Col>
|
||||
<Box color="black" mb={4} fontSize={1} fontWeight={900}>
|
||||
S3 Credentials
|
||||
</Box>
|
||||
<Formik
|
||||
initialValues={
|
||||
{
|
||||
@ -64,23 +62,23 @@ export default function S3Form(props: S3FormProps) {
|
||||
}
|
||||
onSubmit={onSubmit}
|
||||
>
|
||||
<Form>
|
||||
<Input width="256px" type="text" label="Endpoint" id="s3endpoint" />
|
||||
<Form
|
||||
display="grid"
|
||||
gridTemplateColumns="100%"
|
||||
gridAutoRows="auto"
|
||||
gridRowGap={5}
|
||||
>
|
||||
<Box color="black" fontSize={1} fontWeight={900}>
|
||||
S3 Credentials
|
||||
</Box>
|
||||
<Input label="Endpoint" id="s3endpoint" />
|
||||
<Input label="Access Key ID" id="s3accessKeyId" />
|
||||
<Input
|
||||
width="256px"
|
||||
type="text"
|
||||
label="Access Key ID"
|
||||
id="s3accessKeyId"
|
||||
/>
|
||||
<Input
|
||||
width="256px"
|
||||
type="password"
|
||||
label="Secret Access Key"
|
||||
id="s3secretAccessKey"
|
||||
/>
|
||||
<Button border={1} type="submit">
|
||||
Submit
|
||||
</Button>
|
||||
<Button type="submit">Submit</Button>
|
||||
</Form>
|
||||
</Formik>
|
||||
</Col>
|
||||
|
@ -36,7 +36,6 @@ export default function Settings({
|
||||
return (
|
||||
<Box
|
||||
backgroundColor="white"
|
||||
fontSize={2}
|
||||
display="grid"
|
||||
gridTemplateRows="auto"
|
||||
gridTemplateColumns="1fr"
|
||||
|
@ -22,7 +22,7 @@ const SidebarItem = ({ children, view, current }) => {
|
||||
px={3}
|
||||
backgroundColor={selected ? "washedBlue" : "white"}
|
||||
>
|
||||
<Icon mr={2} display="inline-block" icon="Circle" fill={color} />
|
||||
<Icon mr={2} display="inline-block" icon="Circle" color={color} />
|
||||
<Text color={color} fontSize={0}>
|
||||
{children}
|
||||
</Text>
|
||||
|
@ -2,7 +2,7 @@ import React from "react";
|
||||
import * as Yup from "yup";
|
||||
import { Formik, FormikHelpers, Form, useFormikContext } from "formik";
|
||||
import { AsyncButton } from "../../../../components/AsyncButton";
|
||||
import { TextArea } from "@tlon/indigo-react";
|
||||
import { ManagedTextAreaField as TextArea } from "@tlon/indigo-react";
|
||||
|
||||
interface FormSchema {
|
||||
comment: string;
|
||||
@ -48,7 +48,7 @@ export default function CommentInput(props: CommentInputProps) {
|
||||
id="comment"
|
||||
placeholder={props.placeholder || ""}
|
||||
/>
|
||||
<AsyncButton loadingText={loading} border type="submit">
|
||||
<AsyncButton mt={2} loadingText={loading} border type="submit">
|
||||
{label}
|
||||
</AsyncButton>
|
||||
</Form>
|
||||
|
@ -38,7 +38,7 @@ export function EditPost(props: EditPostProps & RouteComponentProps) {
|
||||
<PostForm
|
||||
initial={initial}
|
||||
onSubmit={onSubmit}
|
||||
submitLabel={`Update ${note.title}`}
|
||||
submitLabel="Update"
|
||||
loadingText="Updating..."
|
||||
/>
|
||||
);
|
||||
|
@ -1,5 +1,5 @@
|
||||
import React, { useCallback, useState, useRef, useEffect } from "react";
|
||||
import { Col, Text, ErrorMessage } from "@tlon/indigo-react";
|
||||
import { Col, Text, ErrorLabel } from "@tlon/indigo-react";
|
||||
import { Spinner } from "~/views/components/Spinner";
|
||||
import { Notebooks } from "~/types/publish-update";
|
||||
import { useWaitForProps } from "~/logic/lib/useWaitForProps";
|
||||
@ -46,7 +46,7 @@ export function JoinScreen(props: JoinScreenProps & RouteComponentProps) {
|
||||
<Col p={4}>
|
||||
<Text fontSize={1}>Joining Notebook</Text>
|
||||
<Spinner awaiting text="Joining..." />
|
||||
{error && <ErrorMessage>Unable to join notebook</ErrorMessage>}
|
||||
{error && <ErrorLabel>Unable to join notebook</ErrorLabel>}
|
||||
</Col>
|
||||
);
|
||||
}
|
||||
|
@ -0,0 +1,73 @@
|
||||
import React, { useCallback } from "react";
|
||||
|
||||
import { UnControlled as CodeEditor } from "react-codemirror2";
|
||||
import { MOBILE_BROWSER_REGEX } from "~/logic/lib/util";
|
||||
import { PropFunc } from "~/types/util";
|
||||
import CodeMirror from "codemirror";
|
||||
|
||||
import "codemirror/mode/markdown/markdown";
|
||||
import "codemirror/addon/display/placeholder";
|
||||
|
||||
import "codemirror/lib/codemirror.css";
|
||||
import { Box } from "@tlon/indigo-react";
|
||||
|
||||
const MARKDOWN_CONFIG = {
|
||||
name: "markdown",
|
||||
};
|
||||
|
||||
interface MarkdownEditorProps {
|
||||
placeholder?: string;
|
||||
value: string;
|
||||
onChange: (s: string) => void;
|
||||
onBlur?: (e: any) => void;
|
||||
}
|
||||
|
||||
export function MarkdownEditor(
|
||||
props: MarkdownEditorProps & PropFunc<typeof Box>
|
||||
) {
|
||||
const { onBlur, placeholder, value, onChange, ...boxProps } = props;
|
||||
|
||||
const options = {
|
||||
mode: MARKDOWN_CONFIG,
|
||||
theme: "tlon",
|
||||
lineNumbers: false,
|
||||
lineWrapping: true,
|
||||
scrollbarStyle: "native",
|
||||
// cursorHeight: 0.85,
|
||||
placeholder: placeholder || "",
|
||||
};
|
||||
|
||||
const handleChange = useCallback(
|
||||
(_e, _d, v: string) => {
|
||||
onChange(v);
|
||||
},
|
||||
[onChange]
|
||||
);
|
||||
|
||||
const handleBlur = useCallback(
|
||||
(_i, e: any) => {
|
||||
onBlur && onBlur(e);
|
||||
},
|
||||
[onBlur]
|
||||
);
|
||||
|
||||
return (
|
||||
<Box
|
||||
flexGrow={1}
|
||||
position="static"
|
||||
className="publish"
|
||||
p={1}
|
||||
border={1}
|
||||
borderColor="lightGray"
|
||||
borderRadius={2}
|
||||
{...boxProps}
|
||||
>
|
||||
<CodeEditor
|
||||
onBlur={onBlur}
|
||||
value={value}
|
||||
options={options}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
}
|
@ -1,27 +1,43 @@
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { MarkdownEditor as _MarkdownEditor, Box, ErrorMessage } from '@tlon/indigo-react';
|
||||
import { useField } from 'formik';
|
||||
import React, { useCallback } from "react";
|
||||
import _ from "lodash";
|
||||
import { Box, ErrorLabel } from "@tlon/indigo-react";
|
||||
import { useField } from "formik";
|
||||
import { MarkdownEditor } from "./MarkdownEditor";
|
||||
|
||||
const MarkdownEditor = styled(_MarkdownEditor)`
|
||||
border: 1px solid ${(p) => p.theme.colors.lightGray};
|
||||
border-radius: ${(p) => p.theme.radii[2]}px;
|
||||
`;
|
||||
export const MarkdownField = ({
|
||||
id,
|
||||
...rest
|
||||
}: { id: string } & Parameters<typeof Box>[0]) => {
|
||||
const [{ value, onBlur }, { error, touched }, { setValue }] = useField(id);
|
||||
|
||||
export const MarkdownField = ({ id, ...rest }: { id: string; } & Parameters<typeof Box>[0]) => {
|
||||
const [{ value }, { error, touched }, { setValue, setTouched }] = useField(id);
|
||||
const handleBlur = useCallback(
|
||||
(e: any) => {
|
||||
_.set(e, "target.id", id);
|
||||
console.log(e);
|
||||
onBlur && onBlur(e);
|
||||
},
|
||||
[onBlur, id]
|
||||
);
|
||||
|
||||
const hasError = !!(error && touched);
|
||||
|
||||
return (
|
||||
<Box overflowY="hidden" width="100%" display="flex" flexDirection="column" {...rest}>
|
||||
<Box
|
||||
overflowY="hidden"
|
||||
width="100%"
|
||||
display="flex"
|
||||
flexDirection="column"
|
||||
{...rest}
|
||||
>
|
||||
<MarkdownEditor
|
||||
onFocus={() => setTouched(true)}
|
||||
onBlur={() => setTouched(false)}
|
||||
borderColor={hasError ? "red" : "lightGray"}
|
||||
onBlur={handleBlur}
|
||||
value={value}
|
||||
onBeforeChange={(e, d, v) => setValue(v)}
|
||||
onChange={setValue}
|
||||
/>
|
||||
<ErrorMessage>{touched && error}</ErrorMessage>
|
||||
<ErrorLabel mt="2" hasError={!!(error && touched)}>
|
||||
{error}
|
||||
</ErrorLabel>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
|
@ -3,11 +3,9 @@ import { AsyncButton } from "../../../../components/AsyncButton";
|
||||
import * as Yup from "yup";
|
||||
import {
|
||||
Box,
|
||||
Input,
|
||||
Checkbox,
|
||||
ManagedTextInputField as Input,
|
||||
ManagedCheckboxField as Checkbox,
|
||||
Col,
|
||||
InputLabel,
|
||||
InputCaption,
|
||||
Button,
|
||||
Center,
|
||||
} from "@tlon/indigo-react";
|
||||
|
@ -1,6 +1,11 @@
|
||||
import React from "react";
|
||||
import * as Yup from "yup";
|
||||
import { Box, Input } from "@tlon/indigo-react";
|
||||
import {
|
||||
Box,
|
||||
ManagedTextInputField as Input,
|
||||
Row,
|
||||
Col,
|
||||
} from "@tlon/indigo-react";
|
||||
import { AsyncButton } from "../../../../components/AsyncButton";
|
||||
import { Formik, Form, FormikHelpers } from "formik";
|
||||
import { MarkdownField } from "./MarkdownField";
|
||||
@ -29,32 +34,29 @@ export function PostForm(props: PostFormProps) {
|
||||
const { initial, onSubmit, submitLabel, loadingText } = props;
|
||||
|
||||
return (
|
||||
<Box
|
||||
width="100%"
|
||||
height="100%"
|
||||
p={[2, 4]}
|
||||
display="grid"
|
||||
justifyItems="start"
|
||||
gridTemplateRows={["64px 64px 1fr", "64px 1fr"]}
|
||||
gridTemplateColumns={["100%", "1fr 1fr"]}
|
||||
gridColumnGap={2}
|
||||
gridRowGap={2}
|
||||
>
|
||||
<Col width="100%" height="100%" p={[2, 4]}>
|
||||
<Formik
|
||||
validationSchema={formSchema}
|
||||
initialValues={initial}
|
||||
onSubmit={onSubmit}
|
||||
validateOnBlur
|
||||
>
|
||||
<Form style={{ display: "contents" }}>
|
||||
<Input width="100%" placeholder="Post Title" id="title" />
|
||||
<Box gridRow={["1/2", "auto"]} mt={1} justifySelf={["start", "end"]}>
|
||||
<AsyncButton primary loadingText={loadingText}>
|
||||
<Row flexDirection={["column-reverse", "row"]} mb={4} gapX={4} justifyContent='space-between'>
|
||||
<Input maxWidth='40rem' flexGrow={1} placeholder="Post Title" id="title" />
|
||||
<AsyncButton
|
||||
ml={[0,2]}
|
||||
mb={[4,0]}
|
||||
flexShrink={1}
|
||||
primary
|
||||
loadingText={loadingText}
|
||||
>
|
||||
{submitLabel}
|
||||
</AsyncButton>
|
||||
</Box>
|
||||
<MarkdownField gridColumn={["1/2", "1/3"]} id="body" />
|
||||
</Row>
|
||||
<MarkdownField flexGrow={1} id="body" />
|
||||
</Form>
|
||||
</Formik>
|
||||
</Box>
|
||||
</Col>
|
||||
);
|
||||
}
|
||||
|
@ -4,18 +4,9 @@ import { NotebookPosts } from "./NotebookPosts";
|
||||
import { Subscribers } from "./Subscribers";
|
||||
import { Settings } from "./Settings";
|
||||
import { Spinner } from "~/views/components/Spinner";
|
||||
import { Tabs, Tab } from "~/views/components/Tab";
|
||||
import { roleForShip } from "~/logic/lib/group";
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Text,
|
||||
Tab as _Tab,
|
||||
Tabs,
|
||||
TabList as _TabList,
|
||||
TabPanels,
|
||||
TabPanel,
|
||||
Row,
|
||||
} from "@tlon/indigo-react";
|
||||
import { Box, Button, Text, Row } from "@tlon/indigo-react";
|
||||
import { Notebook as INotebook } from "~/types/publish-update";
|
||||
import { Groups } from "~/types/group-update";
|
||||
import { Contacts, Rolodex } from "~/types/contact-update";
|
||||
@ -24,14 +15,6 @@ import styled from "styled-components";
|
||||
import { Associations } from "~/types";
|
||||
import { deSig } from "~/logic/lib/util";
|
||||
|
||||
const TabList = styled(_TabList)`
|
||||
margin-bottom: ${(p) => p.theme.space[4]}px;
|
||||
`;
|
||||
|
||||
const Tab = styled(_Tab)`
|
||||
flex-grow: 1;
|
||||
`;
|
||||
|
||||
interface NotebookProps {
|
||||
api: GlobalApi;
|
||||
ship: string;
|
||||
@ -48,17 +31,24 @@ interface NotebookProps {
|
||||
|
||||
interface NotebookState {
|
||||
isUnsubscribing: boolean;
|
||||
tab: string;
|
||||
}
|
||||
|
||||
export class Notebook extends PureComponent<
|
||||
NotebookProps & RouteComponentProps,
|
||||
NotebookState
|
||||
> {
|
||||
constructor(props: NotebookProps & RouteComponentProps) {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
isUnsubscribing: false,
|
||||
tab: "all",
|
||||
};
|
||||
this.setTab = this.setTab.bind(this);
|
||||
}
|
||||
|
||||
setTab(tab: string) {
|
||||
this.setState({ tab });
|
||||
}
|
||||
|
||||
render() {
|
||||
@ -73,6 +63,7 @@ export class Notebook extends PureComponent<
|
||||
hideNicknames,
|
||||
associations,
|
||||
} = this.props;
|
||||
const { state } = this;
|
||||
|
||||
const group = groups[notebook?.["writers-group-path"]];
|
||||
if (!group) return null; // Waitin on groups to populate
|
||||
@ -131,8 +122,7 @@ export class Notebook extends PureComponent<
|
||||
) : (
|
||||
<Button
|
||||
ml={isWriter ? 2 : 0}
|
||||
error
|
||||
border
|
||||
destructive
|
||||
onClick={() => {
|
||||
this.setState({ isUnsubscribing: true });
|
||||
api.publish
|
||||
@ -149,56 +139,74 @@ export class Notebook extends PureComponent<
|
||||
</Button>
|
||||
)
|
||||
) : null}
|
||||
{!isOwn && (
|
||||
<Button ml={isWriter ? 2 : 0} error border>
|
||||
Unsubscribe
|
||||
</Button>
|
||||
)}
|
||||
</Row>
|
||||
<Box gridColumn={["1/2", "1/3"]}>
|
||||
<Tabs>
|
||||
<TabList>
|
||||
<Tab>All Posts</Tab>
|
||||
<Tab>About</Tab>
|
||||
{isAdmin && <Tab>Subscribers</Tab>}
|
||||
{isOwn && <Tab>Settings</Tab>}
|
||||
</TabList>
|
||||
<TabPanels>
|
||||
<TabPanel>
|
||||
<NotebookPosts
|
||||
notes={notes}
|
||||
list={notesList}
|
||||
host={ship}
|
||||
book={book}
|
||||
contacts={notebookContacts}
|
||||
hideNicknames={hideNicknames}
|
||||
<Tab
|
||||
selected={state.tab}
|
||||
setSelected={this.setTab}
|
||||
label="All Posts"
|
||||
id="all"
|
||||
/>
|
||||
<Tab
|
||||
selected={state.tab}
|
||||
setSelected={this.setTab}
|
||||
label="About"
|
||||
id="about"
|
||||
/>
|
||||
{isAdmin && (
|
||||
<>
|
||||
<Tab
|
||||
selected={state.tab}
|
||||
setSelected={this.setTab}
|
||||
label="Subscribers"
|
||||
id="subscribers"
|
||||
/>
|
||||
</TabPanel>
|
||||
<TabPanel>
|
||||
<Box color="black">{notebook?.about}</Box>
|
||||
</TabPanel>
|
||||
<TabPanel>
|
||||
<Subscribers
|
||||
host={ship}
|
||||
book={book}
|
||||
notebook={notebook}
|
||||
api={api}
|
||||
groups={groups}
|
||||
<Tab
|
||||
selected={state.tab}
|
||||
setSelected={this.setTab}
|
||||
label="Settings"
|
||||
id="settings"
|
||||
/>
|
||||
</TabPanel>
|
||||
<TabPanel>
|
||||
<Settings
|
||||
host={ship}
|
||||
book={book}
|
||||
api={api}
|
||||
notebook={notebook}
|
||||
contacts={notebookContacts}
|
||||
associations={associations}
|
||||
groups={groups}
|
||||
/>
|
||||
</TabPanel>
|
||||
</TabPanels>
|
||||
</>
|
||||
)}
|
||||
</Tabs>
|
||||
{state.tab === "all" && (
|
||||
<NotebookPosts
|
||||
notes={notes}
|
||||
list={notesList}
|
||||
host={ship}
|
||||
book={book}
|
||||
contacts={notebookContacts}
|
||||
hideNicknames={hideNicknames}
|
||||
|
||||
/>
|
||||
)}
|
||||
{state.tab === "about" && (
|
||||
<Box mt="3" color="black">
|
||||
{notebook?.about}
|
||||
</Box>
|
||||
)}
|
||||
{state.tab === "subscribers" && (
|
||||
<Subscribers
|
||||
host={ship}
|
||||
book={book}
|
||||
notebook={notebook}
|
||||
api={api}
|
||||
groups={groups}
|
||||
/>
|
||||
)}
|
||||
{state.tab === "settings" && (
|
||||
<Settings
|
||||
host={ship}
|
||||
book={book}
|
||||
api={api}
|
||||
notebook={notebook}
|
||||
contacts={notebookContacts}
|
||||
associations={associations}
|
||||
groups={groups}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
|
@ -16,7 +16,7 @@ interface NotebookPostsProps {
|
||||
|
||||
export function NotebookPosts(props: NotebookPostsProps) {
|
||||
return (
|
||||
<Col>
|
||||
<Col mt="3">
|
||||
{props.list.map((noteId: NoteId) => {
|
||||
const note = props.notes[noteId];
|
||||
if (!note) {
|
||||
|
@ -1,5 +1,5 @@
|
||||
import React, { useEffect } from "react";
|
||||
import { Box, Col, Button, InputLabel, InputCaption } from "@tlon/indigo-react";
|
||||
import { Box, Col, Button, Label } from "@tlon/indigo-react";
|
||||
import GlobalApi from "~/logic/api/global";
|
||||
import { Notebook } from "~/types/publish-update";
|
||||
import { Contacts } from "~/types/contact-update";
|
||||
@ -20,7 +20,7 @@ interface SettingsProps {
|
||||
}
|
||||
|
||||
const Divider = (props) => (
|
||||
<Box {...props} mb={4} borderBottom={1} borderBottomColor="lightGray" />
|
||||
<Box {...props} borderBottom={1} borderBottomColor="lightGray" />
|
||||
);
|
||||
export function Settings(props: SettingsProps) {
|
||||
const history = useHistory();
|
||||
@ -36,10 +36,11 @@ export function Settings(props: SettingsProps) {
|
||||
<Box
|
||||
mx="auto"
|
||||
maxWidth="300px"
|
||||
mb={4}
|
||||
my={4}
|
||||
gridTemplateColumns="1fr"
|
||||
gridAutoRows="auto"
|
||||
display="grid"
|
||||
gridRowGap={5}
|
||||
>
|
||||
{isUnmanaged && (
|
||||
<>
|
||||
@ -50,12 +51,12 @@ export function Settings(props: SettingsProps) {
|
||||
<MetadataForm {...props} />
|
||||
<Divider />
|
||||
<Col mb={4}>
|
||||
<InputLabel>Delete Notebook</InputLabel>
|
||||
<InputCaption>
|
||||
<Label>Delete Notebook</Label>
|
||||
<Label gray mt="2">
|
||||
Permanently delete this notebook. (All current members will no longer
|
||||
see this notebook.)
|
||||
</InputCaption>
|
||||
<Button onClick={onDelete} mt={1} border error>
|
||||
</Label>
|
||||
<Button mt="2" onClick={onDelete} destructive>
|
||||
Delete this notebook
|
||||
</Button>
|
||||
</Col>
|
||||
|
@ -96,7 +96,7 @@ export function Sidebar(props: any) {
|
||||
);
|
||||
}
|
||||
|
||||
const display = props.path ? ['none', 'block'] : 'block';
|
||||
const display = props.hidden ? ['none', 'block'] : 'block';
|
||||
|
||||
return (
|
||||
<Col
|
||||
@ -106,7 +106,8 @@ export function Sidebar(props: any) {
|
||||
pt={[3, 0]}
|
||||
overflowY="auto"
|
||||
display={display}
|
||||
maxWidth={["none", "250px"]}
|
||||
flexShrink={0}
|
||||
width={["auto", "250px"]}
|
||||
>
|
||||
<Box>
|
||||
<Link to="/~publish/new" className="green2 pa4 f9 dib">
|
||||
|
@ -80,7 +80,7 @@ export class Subscribers extends Component<SubscribersProps> {
|
||||
const role = roleForShip(group, window.ship)
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Box mt="3">
|
||||
{ role === 'admin' && (
|
||||
<Button mb={3} border onClick={this.addAll}>
|
||||
Add all members as writers
|
||||
|
@ -59,7 +59,7 @@ export default function NewPost(props: NewPostProps & RouteComponentProps) {
|
||||
<PostForm
|
||||
initial={initialValues}
|
||||
onSubmit={onSubmit}
|
||||
submitLabel={`Publish to ${notebook?.title}`}
|
||||
submitLabel="Publish"
|
||||
loadingText="Posting..."
|
||||
/>
|
||||
);
|
||||
|
@ -1,5 +1,5 @@
|
||||
import React, { useCallback } from "react";
|
||||
import { Box, Input, Col } from "@tlon/indigo-react";
|
||||
import { Box, ManagedTextInputField as Input, Col } from "@tlon/indigo-react";
|
||||
import { Formik, Form } from "formik";
|
||||
import * as Yup from "yup";
|
||||
import GlobalApi from "~/logic/api/global";
|
||||
|
@ -89,7 +89,9 @@ export function Skeleton(props: SkeletonProps) {
|
||||
}, 1500);
|
||||
}, [noteId, notebook, ship, notebooks])
|
||||
|
||||
const panelDisplay = !path ? ["none", "block"] : "block";
|
||||
const hideSidebar = path || props.location.pathname.endsWith('/new')
|
||||
|
||||
const panelDisplay = !hideSidebar ? ["none", "block"] : "block";
|
||||
return (
|
||||
<Box height="100%" width="100%" px={[0, 3]} pb={[0, 3]}>
|
||||
<Box
|
||||
@ -105,6 +107,7 @@ export function Skeleton(props: SkeletonProps) {
|
||||
notebooks={props.notebooks}
|
||||
contacts={props.contacts}
|
||||
path={path}
|
||||
hidden={hideSidebar}
|
||||
invites={props.invites}
|
||||
associations={props.associations}
|
||||
api={props.api}
|
||||
|
@ -65,12 +65,13 @@
|
||||
|
||||
.publish .react-codemirror2 {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.publish .CodeMirror {
|
||||
padding: 12px;
|
||||
height: 100% !important;
|
||||
max-width: 700px;
|
||||
overflow-y: hidden;
|
||||
width: 100% !important;
|
||||
cursor: text;
|
||||
font-size: 12px;
|
||||
|
@ -1,8 +1,7 @@
|
||||
import React, { ReactNode, useState, useEffect } from "react";
|
||||
|
||||
import { Button } from "@tlon/indigo-react";
|
||||
import { Button, LoadingSpinner } from "@tlon/indigo-react";
|
||||
|
||||
import { Spinner } from "./Spinner";
|
||||
import { useFormikContext } from "formik";
|
||||
|
||||
interface AsyncButtonProps {
|
||||
@ -37,7 +36,12 @@ export function AsyncButton({
|
||||
return (
|
||||
<Button disabled={!isValid} type="submit" {...rest}>
|
||||
{isSubmitting ? (
|
||||
<Spinner awaiting text={loadingText} />
|
||||
<LoadingSpinner
|
||||
foreground={rest.primary ? "white" : 'black'}
|
||||
background="transparent"
|
||||
awaiting
|
||||
text={loadingText}
|
||||
/>
|
||||
) : success === true ? (
|
||||
"Done"
|
||||
) : success === false ? (
|
||||
|
@ -1,41 +1,34 @@
|
||||
import React from "react";
|
||||
import { useField } from "formik";
|
||||
import styled from "styled-components";
|
||||
import { Col, InputLabel, Row, Box, ErrorMessage } from "@tlon/indigo-react";
|
||||
import {
|
||||
Col,
|
||||
Label,
|
||||
Row,
|
||||
Box,
|
||||
ErrorLabel,
|
||||
StatelessTextInput as Input,
|
||||
} from "@tlon/indigo-react";
|
||||
|
||||
import { uxToHex, hexToUx } from "~/logic/lib/util";
|
||||
|
||||
const Input = styled.input`
|
||||
background-color: ${ p => p.theme.colors.white };
|
||||
color: ${ p => p.theme.colors.black };
|
||||
box-sizing: border-box;
|
||||
border: 1px solid;
|
||||
border-right: none;
|
||||
border-color: ${(p) => p.theme.colors.lightGray};
|
||||
border-top-left-radius: ${(p) => p.theme.radii[2]}px;
|
||||
border-bottom-left-radius: ${(p) => p.theme.radii[2]}px;
|
||||
padding: ${(p) => p.theme.space[2]}px;
|
||||
font-size: 12px;
|
||||
line-height: 1.2;
|
||||
`;
|
||||
|
||||
type ColorInputProps = Parameters<typeof Col>[0] & {
|
||||
id: string;
|
||||
label: string;
|
||||
}
|
||||
};
|
||||
|
||||
export function ColorInput(props: ColorInputProps) {
|
||||
const { id, label, ...rest } = props;
|
||||
const [{ value }, { error }, { setValue }] = useField(id);
|
||||
const { id, label, caption, ...rest } = props;
|
||||
const [{ value, onBlur }, meta, { setValue }] = useField(id);
|
||||
|
||||
const hex = value.substr(2).replace('.', '');
|
||||
const padded = hex.padStart(6, '0');
|
||||
const hex = value.substr(2).replace(".", "");
|
||||
const padded = hex.padStart(6, "0");
|
||||
|
||||
const onChange = (e: any) => {
|
||||
const { value: newValue } = e.target as HTMLInputElement;
|
||||
const valid = newValue.match(/^(\d|[a-f]|[A-F]){0,6}$/);
|
||||
|
||||
if(!valid) {
|
||||
if (!valid) {
|
||||
return;
|
||||
}
|
||||
const result = hexToUx(newValue);
|
||||
@ -43,10 +36,21 @@ export function ColorInput(props: ColorInputProps) {
|
||||
};
|
||||
|
||||
return (
|
||||
<Col {...rest}>
|
||||
<InputLabel htmlFor={id}>{label}</InputLabel>
|
||||
<Row mt={2}>
|
||||
<Input onChange={onChange} value={hex} />
|
||||
<Box display="flex" flexDirection="column" {...props}>
|
||||
<Label htmlFor={id}>{label}</Label>
|
||||
{caption ? (
|
||||
<Label mt="2" gray>
|
||||
{caption}
|
||||
</Label>
|
||||
) : null}
|
||||
<Row mt="2" alignItems="flex-end">
|
||||
<Input
|
||||
borderTopRightRadius={0}
|
||||
borderBottomRightRadius={0}
|
||||
onBlur={onBlur}
|
||||
onChange={onChange}
|
||||
value={hex}
|
||||
/>
|
||||
<Box
|
||||
borderBottomRightRadius={1}
|
||||
borderTopRightRadius={1}
|
||||
@ -58,7 +62,9 @@ export function ColorInput(props: ColorInputProps) {
|
||||
bg={`#${padded}`}
|
||||
/>
|
||||
</Row>
|
||||
<ErrorMessage mt="2">{error}</ErrorMessage>
|
||||
</Col>
|
||||
<ErrorLabel mt="2" hasError={!!(meta.touched && meta.error)}>
|
||||
{meta.error}
|
||||
</ErrorLabel>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
@ -10,9 +10,8 @@ import _ from "lodash";
|
||||
import Mousetrap from "mousetrap";
|
||||
import {
|
||||
Box,
|
||||
InputLabel,
|
||||
ErrorMessage,
|
||||
InputCaption,
|
||||
Label,
|
||||
ErrorLabel,
|
||||
} from "@tlon/indigo-react";
|
||||
import { useDropdown } from "~/logic/lib/useDropdown";
|
||||
import styled from "styled-components";
|
||||
@ -130,9 +129,9 @@ export function DropdownSearch<C>(props: DropdownSearchProps<C>) {
|
||||
}, [options, props.getKey, props.renderCandidate, selected]);
|
||||
|
||||
return (
|
||||
<Box position="relative">
|
||||
<InputLabel htmlFor={props.id}>{props.label}</InputLabel>
|
||||
{caption ? <InputCaption>{caption}</InputCaption> : null}
|
||||
<Box position="relative" zIndex={9}>
|
||||
<Label htmlFor={props.id}>{props.label}</Label>
|
||||
{caption ? <Label mt="2" gray>{caption}</Label> : null}
|
||||
{!props.disabled && (
|
||||
<TextArea
|
||||
ref={textarea}
|
||||
@ -166,7 +165,7 @@ export function DropdownSearch<C>(props: DropdownSearchProps<C>) {
|
||||
})}
|
||||
</Box>
|
||||
)}
|
||||
<ErrorMessage>{props.error}</ErrorMessage>
|
||||
<ErrorLabel>{props.error}</ErrorLabel>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
import React from "react";
|
||||
import { useFormikContext } from "formik";
|
||||
import { ErrorMessage } from "@tlon/indigo-react";
|
||||
import { ErrorLabel } from "@tlon/indigo-react";
|
||||
|
||||
export function FormError(props: { message: string }) {
|
||||
const { status } = useFormikContext();
|
||||
@ -8,6 +8,6 @@ export function FormError(props: { message: string }) {
|
||||
let s = status || {};
|
||||
|
||||
return (
|
||||
<ErrorMessage>{"error" in s ? props.message : null}</ErrorMessage>
|
||||
<ErrorLabel>{"error" in s ? props.message : null}</ErrorLabel>
|
||||
);
|
||||
}
|
||||
|
@ -25,16 +25,16 @@ class GroupMember extends Component<{ ship: Patp; options: any[] }, {}> {
|
||||
|
||||
return (
|
||||
<div className='flex justify-between f9 items-center'>
|
||||
<div className='flex flex-column'>
|
||||
<div className='flex flex-column flex-shrink-0'>
|
||||
<Text mono mr='2'>{`${cite(ship)}`}</Text>
|
||||
{children}
|
||||
</div>
|
||||
{options.length > 0 && (
|
||||
<Menu>
|
||||
<MenuButton sm>Options</MenuButton>
|
||||
<MenuButton width='min-content'>Options</MenuButton>
|
||||
<MenuList>
|
||||
{options.map(({ onSelect, text }) => (
|
||||
<MenuItem onSelect={onSelect}>{text}</MenuItem>
|
||||
<MenuItem onSelect={onSelect}><Text fontsize='0'>{text}</Text></MenuItem>
|
||||
))}
|
||||
</MenuList>
|
||||
</Menu>
|
||||
|
@ -1,7 +1,13 @@
|
||||
import React, { useRef, useCallback, useState } from "react";
|
||||
|
||||
import { Box, Input, Img, Button } from "@tlon/indigo-react";
|
||||
import GlobalApi from "~/api/global";
|
||||
import {
|
||||
Box,
|
||||
StatelessTextInput as Input,
|
||||
Row,
|
||||
Button,
|
||||
Label,
|
||||
ErrorLabel,
|
||||
} from "@tlon/indigo-react";
|
||||
import { useField } from "formik";
|
||||
import { S3State } from "~/types/s3-update";
|
||||
import { useS3 } from "~/logic/lib/useS3";
|
||||
@ -10,16 +16,17 @@ type ImageInputProps = Parameters<typeof Box>[0] & {
|
||||
id: string;
|
||||
label: string;
|
||||
s3: S3State;
|
||||
placeholder?: string;
|
||||
};
|
||||
|
||||
export function ImageInput(props: ImageInputProps) {
|
||||
const { id, label, s3, ...rest } = props;
|
||||
const { id, label, s3, caption, placeholder, ...rest } = props;
|
||||
|
||||
const { uploadDefault, canUpload } = useS3(s3);
|
||||
|
||||
const [uploading, setUploading] = useState(false);
|
||||
|
||||
const [, , { setValue, setError }] = useField(id);
|
||||
const [field, meta, { setValue, setError }] = useField(id);
|
||||
|
||||
const ref = useRef<HTMLInputElement | null>(null);
|
||||
|
||||
@ -44,29 +51,44 @@ export function ImageInput(props: ImageInputProps) {
|
||||
}, [ref]);
|
||||
|
||||
return (
|
||||
<Box {...rest} display="flex">
|
||||
<Input disabled={uploading} type="text" label={label} id={id} />
|
||||
{canUpload && (
|
||||
<>
|
||||
<Button
|
||||
ml={1}
|
||||
border={3}
|
||||
borderColor="washedGray"
|
||||
style={{ marginTop: "18px" }}
|
||||
onClick={onClick}
|
||||
>
|
||||
{uploading ? "Uploading" : "Upload"}
|
||||
</Button>
|
||||
<input
|
||||
style={{ display: "none" }}
|
||||
type="file"
|
||||
id="fileElement"
|
||||
ref={ref}
|
||||
accept="image/*"
|
||||
onChange={onImageUpload}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<Box display="flex" flexDirection="column" {...props}>
|
||||
<Label htmlFor={id}>{label}</Label>
|
||||
{caption ? (
|
||||
<Label mt="2" gray>
|
||||
{caption}
|
||||
</Label>
|
||||
) : null}
|
||||
<Row mt="2" alignItems="flex-end">
|
||||
<Input
|
||||
type={"text"}
|
||||
hasError={meta.touched && meta.error !== undefined}
|
||||
placeholder={placeholder}
|
||||
{...field}
|
||||
/>
|
||||
{canUpload && (
|
||||
<>
|
||||
<Button
|
||||
ml={1}
|
||||
border={1}
|
||||
borderColor="lightGray"
|
||||
onClick={onClick}
|
||||
>
|
||||
{uploading ? "Uploading" : "Upload"}
|
||||
</Button>
|
||||
<input
|
||||
style={{ display: "none" }}
|
||||
type="file"
|
||||
id="fileElement"
|
||||
ref={ref}
|
||||
accept="image/*"
|
||||
onChange={onImageUpload}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</Row>
|
||||
<ErrorLabel mt="2" hasError={!!(meta.touched && meta.error)}>
|
||||
{meta.error}
|
||||
</ErrorLabel>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
@ -48,7 +48,7 @@ export function SidebarItem(props: {
|
||||
const appPath = association?.["app-path"];
|
||||
const groupPath = association?.["group-path"];
|
||||
const app = apps[module];
|
||||
const isUnmanaged = groups[groupPath]?.hidden || false;
|
||||
const isUnmanaged = groups?.[groupPath]?.hidden || false;
|
||||
if (!app) {
|
||||
return null;
|
||||
}
|
||||
|
@ -1,6 +1,13 @@
|
||||
import React, { useCallback } from "react";
|
||||
import * as Yup from "yup";
|
||||
import { Row, Box, Icon, Radio, Col, Checkbox } from "@tlon/indigo-react";
|
||||
import {
|
||||
Row,
|
||||
Box,
|
||||
Icon,
|
||||
ManagedRadioButtonField as Radio,
|
||||
ManagedCheckboxField as Checkbox,
|
||||
Col,
|
||||
} from "@tlon/indigo-react";
|
||||
import { FormikOnBlur } from "./FormikOnBlur";
|
||||
import { Dropdown } from "./Dropdown";
|
||||
import { FormikHelpers } from "formik";
|
||||
@ -53,7 +60,7 @@ export function SidebarListHeader(props: {
|
||||
</FormikOnBlur>
|
||||
}
|
||||
>
|
||||
<Icon stroke="gray" icon="Menu" />
|
||||
<Icon stroke="gray" icon="Circle" />
|
||||
</Dropdown>
|
||||
</Row>
|
||||
);
|
||||
|
32
pkg/interface/src/views/components/Tab.tsx
Normal file
32
pkg/interface/src/views/components/Tab.tsx
Normal file
@ -0,0 +1,32 @@
|
||||
import React from "react";
|
||||
import { Box, Text, Row } from "@tlon/indigo-react";
|
||||
|
||||
export const Tab = ({ selected, id, label, setSelected }) => (
|
||||
<Box
|
||||
py={2}
|
||||
borderBottom={1}
|
||||
borderBottomColor={selected === id ? "black" : "washedGray"}
|
||||
px={2}
|
||||
cursor='pointer'
|
||||
flexGrow={1}
|
||||
display="flex"
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
onClick={() => setSelected(id)}
|
||||
>
|
||||
<Text color={selected === id ? "black" : "gray"}>{label}</Text>
|
||||
</Box>
|
||||
);
|
||||
|
||||
export const Tabs = ({ children, ...rest }: Parameters<typeof Row>[0]) => (
|
||||
<Row
|
||||
bg="white"
|
||||
mb={2}
|
||||
justifyContent="stretch"
|
||||
alignItems="flex-end"
|
||||
{...rest}
|
||||
>
|
||||
{children}
|
||||
</Row>
|
||||
);
|
||||
|
@ -117,7 +117,7 @@ export class Omnibox extends Component {
|
||||
const { props } = this;
|
||||
this.setState({ results: this.initialResults(), query: '' }, () => {
|
||||
props.api.local.setOmnibox();
|
||||
if (defaultApps.includes(app.toLowerCase()) || app === 'profile') {
|
||||
if (defaultApps.includes(app.toLowerCase()) || app === 'profile' || app === 'Links') {
|
||||
props.history.push(link);
|
||||
} else {
|
||||
window.location.href = link;
|
||||
@ -179,18 +179,15 @@ export class Omnibox extends Component {
|
||||
})
|
||||
);
|
||||
if (currentIndex > 0) {
|
||||
const nextApp = flattenedResults[currentIndex - 1].app;
|
||||
const nextLink = flattenedResults[currentIndex - 1].link;
|
||||
this.setState({ selected: [nextApp, nextLink] });
|
||||
const { app, link } = flattenedResults[currentIndex - 1];
|
||||
this.setState({ selected: [app, link] });
|
||||
} else {
|
||||
const nextApp = flattenedResults[totalLength - 1].app;
|
||||
const nextLink = flattenedResults[totalLength - 1].link;
|
||||
this.setState({ selected: [nextApp, nextLink] });
|
||||
const { app, link } = flattenedResults[totalLength - 1];
|
||||
this.setState({ selected: [app, link] });
|
||||
}
|
||||
} else {
|
||||
const nextApp = flattenedResults[totalLength - 1].app;
|
||||
const nextLink = flattenedResults[totalLength - 1].link;
|
||||
this.setState({ selected: [nextApp, nextLink] });
|
||||
const { app, link } = flattenedResults[totalLength - 1];
|
||||
this.setState({ selected: [app, link] });
|
||||
}
|
||||
}
|
||||
|
||||
@ -204,18 +201,15 @@ export class Omnibox extends Component {
|
||||
})
|
||||
);
|
||||
if (currentIndex < flattenedResults.length - 1) {
|
||||
const nextApp = flattenedResults[currentIndex + 1].app;
|
||||
const nextLink = flattenedResults[currentIndex + 1].link;
|
||||
this.setState({ selected: [nextApp, nextLink] });
|
||||
const { app, link } = flattenedResults[currentIndex + 1];
|
||||
this.setState({ selected: [app, link] });
|
||||
} else {
|
||||
const nextApp = flattenedResults[0].app;
|
||||
const nextLink = flattenedResults[0].link;
|
||||
this.setState({ selected: [nextApp, nextLink] });
|
||||
const { app, link } = flattenedResults[0];
|
||||
this.setState({ selected: [app, link] });
|
||||
}
|
||||
} else {
|
||||
const nextApp = flattenedResults[0].app;
|
||||
const nextLink = flattenedResults[0].link;
|
||||
this.setState({ selected: [nextApp, nextLink] });
|
||||
const { app, link } = flattenedResults[0];
|
||||
this.setState({ selected: [app, link] });
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -8,6 +8,10 @@ export class OmniboxInput extends Component {
|
||||
<input
|
||||
ref={(el) => {
|
||||
this.input = el;
|
||||
if (el && document.activeElement.isSameNode(el)) {
|
||||
el.blur();
|
||||
el.focus();
|
||||
}
|
||||
}
|
||||
}
|
||||
className='ba b--transparent w-100 br2 white-d bg-gray0-d inter f9 pa2'
|
||||
|
@ -12,7 +12,8 @@ export const MetadataSettings = (props) => {
|
||||
changeLoading,
|
||||
api,
|
||||
resource,
|
||||
app
|
||||
app,
|
||||
module
|
||||
} = props;
|
||||
|
||||
const title =
|
||||
@ -41,6 +42,7 @@ export const MetadataSettings = (props) => {
|
||||
val,
|
||||
association.metadata.description,
|
||||
association.metadata['date-created'],
|
||||
module,
|
||||
uxToHex(association.metadata.color)
|
||||
).then(() => {
|
||||
changeLoading(false, false, '', () => {});
|
||||
@ -61,7 +63,8 @@ export const MetadataSettings = (props) => {
|
||||
association.metadata.title,
|
||||
val,
|
||||
association.metadata['date-created'],
|
||||
uxToHex(association.metadata.color)
|
||||
uxToHex(association.metadata.color),
|
||||
module
|
||||
).then(() => {
|
||||
changeLoading(false, false, '', () => {});
|
||||
});
|
||||
@ -80,7 +83,8 @@ export const MetadataSettings = (props) => {
|
||||
association.metadata.title,
|
||||
association.metadata.description,
|
||||
association.metadata['date-created'],
|
||||
val
|
||||
val,
|
||||
module
|
||||
).then(() => {
|
||||
changeLoading(false, false, '', () => {});
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user