@ossy/workspaces 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +46 -0
- package/package.json +31 -0
- package/src/AuthenticationGuard.jsx +50 -0
- package/src/CreateWorkspacePage.jsx +70 -0
- package/src/Definition.js +23 -0
- package/src/GeneralSettings.jsx +87 -0
- package/src/Invitations.jsx +44 -0
- package/src/Invitations.page.jsx +23 -0
- package/src/Invite.jsx +105 -0
- package/src/Invite.page.jsx +23 -0
- package/src/SelectWorkspacePage.jsx +143 -0
- package/src/Users.jsx +47 -0
- package/src/Users.page.jsx +21 -0
- package/src/WorkspaceSettings.page.jsx +23 -0
- package/src/index.js +9 -0
- package/src/patchUserWorkspaceId.js +14 -0
package/README.md
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# @ossy/workspaces
|
|
2
|
+
|
|
3
|
+
Workspaces **feature package** for the Ossy platform — provides workspace creation, selection, user management, and invitation pages that are auto-discovered by `@ossy/app` when the package is installed.
|
|
4
|
+
|
|
5
|
+
## What's included
|
|
6
|
+
|
|
7
|
+
### Pages
|
|
8
|
+
|
|
9
|
+
| File | Page id | Default paths |
|
|
10
|
+
|---|---|---|
|
|
11
|
+
| `Invitations.page.jsx` | `workspace/invitations` | `/invitations` (en), `/inbjudningar` (sv) |
|
|
12
|
+
| `Invite.page.jsx` | `workspace/invitations/add` | `/invitations/add` (en), `/inbjudningar/lagg-till` (sv) |
|
|
13
|
+
| `Users.page.jsx` | `workspace/users` | `/users` (en), `/anvandare` (sv) |
|
|
14
|
+
| `WorkspaceSettings.page.jsx` | `workspace/settings` | `/settings` (en), `/installningar` (sv) |
|
|
15
|
+
|
|
16
|
+
### Components
|
|
17
|
+
|
|
18
|
+
| Export | Description |
|
|
19
|
+
|---|---|
|
|
20
|
+
| `CreateWorkspacePage` | Form to create a new workspace |
|
|
21
|
+
| `SelectWorkspacePage` | List and switch between workspaces |
|
|
22
|
+
| `GeneralSettings` | Workspace details and services settings |
|
|
23
|
+
| `Invitations` | List pending workspace invitations |
|
|
24
|
+
| `Invite` | Invite a user to the workspace by email |
|
|
25
|
+
| `Users` | List and manage workspace members |
|
|
26
|
+
| `AuthenticationGuard` | Wraps children, shows error for unauthenticated users |
|
|
27
|
+
| `patchUserWorkspaceId` | Utility to update the active workspace cookie |
|
|
28
|
+
| `Definition` | Feature metadata (id, actions, views) |
|
|
29
|
+
|
|
30
|
+
## Usage
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
npm install @ossy/workspaces
|
|
34
|
+
npm run build
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
`@ossy/app` discovers the pages automatically by reading the `"ossy": { "src": "./src" }` field in this package's `package.json`.
|
|
38
|
+
|
|
39
|
+
## Peer dependencies
|
|
40
|
+
|
|
41
|
+
| Package | Version |
|
|
42
|
+
|---|---|
|
|
43
|
+
| `@ossy/design-system` | `>=1.0.0` |
|
|
44
|
+
| `@ossy/router-react` | `>=1.0.0` |
|
|
45
|
+
| `@ossy/sdk-react` | `>=1.0.0` |
|
|
46
|
+
| `react` | `>=19.0.0 <20.0.0` |
|
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ossy/workspaces",
|
|
3
|
+
"description": "Workspaces feature package — create, select, and manage workspaces, users, and invitations",
|
|
4
|
+
"version": "1.1.0",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./src/index.js",
|
|
7
|
+
"module": "./src/index.js",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": "./src/index.js"
|
|
10
|
+
},
|
|
11
|
+
"author": "Ossy <yourfriends@ossy.se> (https://ossy.se)",
|
|
12
|
+
"license": "MIT",
|
|
13
|
+
"ossy": {
|
|
14
|
+
"src": "./src"
|
|
15
|
+
},
|
|
16
|
+
"peerDependencies": {
|
|
17
|
+
"@ossy/design-system": ">=1.0.0",
|
|
18
|
+
"@ossy/router-react": ">=1.0.0",
|
|
19
|
+
"@ossy/sdk-react": ">=1.0.0",
|
|
20
|
+
"react": ">=19.0.0 <20.0.0"
|
|
21
|
+
},
|
|
22
|
+
"publishConfig": {
|
|
23
|
+
"access": "public",
|
|
24
|
+
"registry": "https://registry.npmjs.org"
|
|
25
|
+
},
|
|
26
|
+
"files": [
|
|
27
|
+
"/src",
|
|
28
|
+
"README.md"
|
|
29
|
+
],
|
|
30
|
+
"gitHead": "eabc1ddca6f83721efa11f044422ab8628929c88"
|
|
31
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { AuthenticationStatus, useAuthentication } from '@ossy/sdk-react'
|
|
3
|
+
import { Switch, View, Guide, Icon } from '@ossy/design-system'
|
|
4
|
+
|
|
5
|
+
export const AuthenticationGuard = ({ children }) => {
|
|
6
|
+
const { status } = useAuthentication()
|
|
7
|
+
|
|
8
|
+
return (
|
|
9
|
+
<Switch on={status}>
|
|
10
|
+
|
|
11
|
+
<Switch.Case match={[AuthenticationStatus.NotAuthenticated]}>
|
|
12
|
+
<View layout="off-center" style={{ height: '100%' }}>
|
|
13
|
+
<Guide
|
|
14
|
+
style={{ width: '100vw', maxWidth: '300px' }}
|
|
15
|
+
slot="content"
|
|
16
|
+
title="Not Authenticated"
|
|
17
|
+
text="You need to login to view this page"
|
|
18
|
+
/>
|
|
19
|
+
</View>
|
|
20
|
+
</Switch.Case>
|
|
21
|
+
|
|
22
|
+
<Switch.Case match={[AuthenticationStatus.AuthenticationError]}>
|
|
23
|
+
<View layout="off-center" style={{ height: '100%' }}>
|
|
24
|
+
<Guide
|
|
25
|
+
style={{ width: '100vw', maxWidth: '300px' }}
|
|
26
|
+
slot="content"
|
|
27
|
+
title="Error"
|
|
28
|
+
text="An error occurred while trying to authenticate, try again in a few minutes"
|
|
29
|
+
/>
|
|
30
|
+
</View>
|
|
31
|
+
</Switch.Case>
|
|
32
|
+
|
|
33
|
+
<Switch.Case match={[AuthenticationStatus.Verifying, AuthenticationStatus.NoInitialized]}>
|
|
34
|
+
<View layout="off-center" style={{ height: '100%' }}>
|
|
35
|
+
<Icon
|
|
36
|
+
name="Cached"
|
|
37
|
+
style={{ width: '64px', height: '64px' }}
|
|
38
|
+
className="rotate fill-50"
|
|
39
|
+
slot="content"
|
|
40
|
+
/>
|
|
41
|
+
</View>
|
|
42
|
+
</Switch.Case>
|
|
43
|
+
|
|
44
|
+
<Switch.Case match={[AuthenticationStatus.Authenticated]}>
|
|
45
|
+
{children}
|
|
46
|
+
</Switch.Case>
|
|
47
|
+
|
|
48
|
+
</Switch>
|
|
49
|
+
)
|
|
50
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import React, { useState } from 'react'
|
|
2
|
+
import { useWorkspaces } from '@ossy/sdk-react'
|
|
3
|
+
import {
|
|
4
|
+
Button,
|
|
5
|
+
useInputValue,
|
|
6
|
+
Title,
|
|
7
|
+
Input,
|
|
8
|
+
View
|
|
9
|
+
} from '@ossy/design-system'
|
|
10
|
+
import { useRouter } from '@ossy/router-react'
|
|
11
|
+
import { AuthenticationGuard } from './AuthenticationGuard.jsx'
|
|
12
|
+
import { patchUserWorkspaceId } from './patchUserWorkspaceId.js'
|
|
13
|
+
|
|
14
|
+
export const CreateWorkspacePage = () => {
|
|
15
|
+
const router = useRouter()
|
|
16
|
+
const [error, setError] = useState()
|
|
17
|
+
const [workspaceName, setWorkspaceName] = useInputValue()
|
|
18
|
+
const { createWorkspace } = useWorkspaces()
|
|
19
|
+
|
|
20
|
+
const onCreateWorkspace = workspaceName => {
|
|
21
|
+
createWorkspace(workspaceName)
|
|
22
|
+
.then((workspace) =>
|
|
23
|
+
patchUserWorkspaceId(workspace.id).then((res) => {
|
|
24
|
+
if (!res.ok) throw new Error('Could not select the new workspace')
|
|
25
|
+
window.location.assign(
|
|
26
|
+
router.getHref({ id: '@storage/home', params: { workspaceId: workspace.id } })
|
|
27
|
+
)
|
|
28
|
+
})
|
|
29
|
+
)
|
|
30
|
+
.catch(err => { setError(err.message) })
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
<AuthenticationGuard>
|
|
35
|
+
<View gap="m">
|
|
36
|
+
|
|
37
|
+
<title>Create workspace</title>
|
|
38
|
+
<Title variant="primary" className="stack-m">Create a new workspace</Title>
|
|
39
|
+
|
|
40
|
+
<Input
|
|
41
|
+
id="workspaceName"
|
|
42
|
+
className={error ? 'stack-s' : 'stack-m'}
|
|
43
|
+
type="text"
|
|
44
|
+
placeholder="Workspace name"
|
|
45
|
+
required
|
|
46
|
+
onChange={setWorkspaceName}
|
|
47
|
+
style={{ width: '100%' }}
|
|
48
|
+
/>
|
|
49
|
+
|
|
50
|
+
<div className={error ? 'stack-m' : ''}>{error}</div>
|
|
51
|
+
|
|
52
|
+
<View gap="m" layout="row" justifyContent="flex-end">
|
|
53
|
+
|
|
54
|
+
<Button
|
|
55
|
+
variant="link"
|
|
56
|
+
id="cancel"
|
|
57
|
+
href={() => router.back()}>Cancel
|
|
58
|
+
</Button>
|
|
59
|
+
|
|
60
|
+
<Button
|
|
61
|
+
id="createWorkspace"
|
|
62
|
+
variant="cta"
|
|
63
|
+
onClick={() => onCreateWorkspace(workspaceName)}>Create
|
|
64
|
+
</Button>
|
|
65
|
+
|
|
66
|
+
</View>
|
|
67
|
+
</View>
|
|
68
|
+
</AuthenticationGuard>
|
|
69
|
+
)
|
|
70
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export const Definition = {
|
|
2
|
+
id: 'workspaces',
|
|
3
|
+
title: 'Workspaces',
|
|
4
|
+
description: 'Workspace lifecycle, members, invitations, and services.',
|
|
5
|
+
module: {
|
|
6
|
+
id: 'workspaces',
|
|
7
|
+
enabled: true
|
|
8
|
+
},
|
|
9
|
+
statuses: ['beta'],
|
|
10
|
+
actions: [
|
|
11
|
+
'workspaces.create',
|
|
12
|
+
'workspaces.invite-user',
|
|
13
|
+
'workspaces.enable-service',
|
|
14
|
+
'workspaces.disable-service'
|
|
15
|
+
],
|
|
16
|
+
views: [
|
|
17
|
+
'workspaces.get-all',
|
|
18
|
+
'workspaces.get',
|
|
19
|
+
'workspaces.get-current',
|
|
20
|
+
'workspaces.get-users'
|
|
21
|
+
],
|
|
22
|
+
tasks: []
|
|
23
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import React, { useState } from 'react'
|
|
2
|
+
import { useWorkspace, useUser, AsyncStatus } from '@ossy/sdk-react'
|
|
3
|
+
import { Switch, Title, Button, View, Text } from '@ossy/design-system'
|
|
4
|
+
|
|
5
|
+
const formatDate = (timestamp) => {
|
|
6
|
+
if (!timestamp) return ''
|
|
7
|
+
const date = new Date(timestamp)
|
|
8
|
+
return date.toLocaleDateString()
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const FlowStage = {
|
|
12
|
+
View: 'View',
|
|
13
|
+
Edit: 'Edit'
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const availableServices = [
|
|
17
|
+
'@ossy/jobs/visual-content-descriptors',
|
|
18
|
+
'@ossy/jobs/resize-common-web',
|
|
19
|
+
'@ossy/resumes',
|
|
20
|
+
'@ossy/consultancy',
|
|
21
|
+
'@ossy/apps',
|
|
22
|
+
'@ossy/domains',
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
export const GeneralSettings = () => {
|
|
26
|
+
const { workspace } = useWorkspace()
|
|
27
|
+
const { status: userStatus, user } = useUser()
|
|
28
|
+
const [flowStage, setFlowStage] = useState(FlowStage.View)
|
|
29
|
+
|
|
30
|
+
const services = workspace?.services || {}
|
|
31
|
+
|
|
32
|
+
const isWorkspaceCreator =
|
|
33
|
+
userStatus === AsyncStatus.Success &&
|
|
34
|
+
Boolean(workspace?.createdBy) &&
|
|
35
|
+
Boolean(user?.id) &&
|
|
36
|
+
workspace.createdBy === user.id
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<View gap="l" inset="m">
|
|
40
|
+
<View layout="row" gap="s" justifyContent="space-between">
|
|
41
|
+
<Title>Workspace details</Title>
|
|
42
|
+
|
|
43
|
+
<View>
|
|
44
|
+
<Switch on={flowStage}>
|
|
45
|
+
<Switch.Case match={[FlowStage.View]}>
|
|
46
|
+
<Button prefix={{ size: 's', name: 'pen' }} variant="cta" disabled>
|
|
47
|
+
Edit
|
|
48
|
+
</Button>
|
|
49
|
+
</Switch.Case>
|
|
50
|
+
</Switch>
|
|
51
|
+
</View>
|
|
52
|
+
</View>
|
|
53
|
+
|
|
54
|
+
<View gap="s" style={{ height: '100%' }}>
|
|
55
|
+
<View layout="row" gap="s">
|
|
56
|
+
<Text style={{ fontWeight: 'bold' }}>Id:</Text>
|
|
57
|
+
<Text>{workspace.id}</Text>
|
|
58
|
+
</View>
|
|
59
|
+
<View layout="row" gap="s">
|
|
60
|
+
<Text style={{ fontWeight: 'bold' }}>Name:</Text>
|
|
61
|
+
<Text>{workspace.name}</Text>
|
|
62
|
+
</View>
|
|
63
|
+
<View layout="row" gap="s">
|
|
64
|
+
<Text style={{ fontWeight: 'bold' }}>Created:</Text>
|
|
65
|
+
<Text>{formatDate(workspace?.created)}</Text>
|
|
66
|
+
</View>
|
|
67
|
+
</View>
|
|
68
|
+
|
|
69
|
+
{isWorkspaceCreator && (
|
|
70
|
+
<View gap="s">
|
|
71
|
+
<Title variant="secondary">Workspace services</Title>
|
|
72
|
+
<Text variant="small" style={{ maxWidth: '42rem' }}>
|
|
73
|
+
Turn features on or off for this workspace.
|
|
74
|
+
</Text>
|
|
75
|
+
{availableServices.map((x) => (
|
|
76
|
+
<View key={x}>
|
|
77
|
+
<Text>
|
|
78
|
+
{x} : {services[x] !== false ? 'On' : 'Off'}
|
|
79
|
+
</Text>
|
|
80
|
+
</View>
|
|
81
|
+
))}
|
|
82
|
+
</View>
|
|
83
|
+
)}
|
|
84
|
+
|
|
85
|
+
</View>
|
|
86
|
+
)
|
|
87
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { useWorkspace } from '@ossy/sdk-react'
|
|
3
|
+
import { Title, Text, Button, View, Icon2 } from '@ossy/design-system'
|
|
4
|
+
import { useRouter } from '@ossy/router-react'
|
|
5
|
+
|
|
6
|
+
export const Invitations = () => {
|
|
7
|
+
const router = useRouter()
|
|
8
|
+
const { workspace } = useWorkspace()
|
|
9
|
+
const invitations = workspace?.invitations || []
|
|
10
|
+
|
|
11
|
+
return (
|
|
12
|
+
<View gap="m">
|
|
13
|
+
|
|
14
|
+
<View inset="s" gap="s">
|
|
15
|
+
|
|
16
|
+
<View layout="row" justifyContent="space-between" alignItems="center">
|
|
17
|
+
<Title>Invitations</Title>
|
|
18
|
+
<Button variant="cta" href={router.getHref('@workspace/invitations/add')} prefix="user-add">
|
|
19
|
+
Invite user
|
|
20
|
+
</Button>
|
|
21
|
+
</View>
|
|
22
|
+
|
|
23
|
+
<Text style={{ maxWidth: '400px' }}>
|
|
24
|
+
Invite users to your workspace to collaborate.
|
|
25
|
+
</Text>
|
|
26
|
+
|
|
27
|
+
</View>
|
|
28
|
+
|
|
29
|
+
<View gap="xs">
|
|
30
|
+
{invitations.map(invite => (
|
|
31
|
+
<View layout="row" roundness="s" alignItems="center" inset="m" gap="m" selectable key={invite.email}>
|
|
32
|
+
<Icon2 name="mail" />
|
|
33
|
+
<View>
|
|
34
|
+
<Text variant="small" as="span">{invite.email}</Text>
|
|
35
|
+
</View>
|
|
36
|
+
</View>
|
|
37
|
+
))}
|
|
38
|
+
</View>
|
|
39
|
+
|
|
40
|
+
{invitations.length === 0 && <Text>No invitations</Text>}
|
|
41
|
+
|
|
42
|
+
</View>
|
|
43
|
+
)
|
|
44
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { View } from '@ossy/design-system'
|
|
3
|
+
import { Invitations } from './Invitations.jsx'
|
|
4
|
+
|
|
5
|
+
export const metadata = {
|
|
6
|
+
id: 'workspace/invitations',
|
|
7
|
+
path: {
|
|
8
|
+
sv: '/inbjudningar',
|
|
9
|
+
en: '/invitations',
|
|
10
|
+
},
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const InvitationsPage = () => {
|
|
14
|
+
return (
|
|
15
|
+
<View layout="off-center-m" style={{ height: '100%' }}>
|
|
16
|
+
<View slot="content" surface="primary" roundness="m" inset="m">
|
|
17
|
+
<Invitations />
|
|
18
|
+
</View>
|
|
19
|
+
</View>
|
|
20
|
+
)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export default InvitationsPage
|
package/src/Invite.jsx
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import React, { useState, useRef } from 'react'
|
|
2
|
+
import { useWorkspace } from '@ossy/sdk-react'
|
|
3
|
+
import { useRouter } from '@ossy/router-react'
|
|
4
|
+
import {
|
|
5
|
+
useInputValue,
|
|
6
|
+
Button,
|
|
7
|
+
Title,
|
|
8
|
+
Input,
|
|
9
|
+
Switch,
|
|
10
|
+
Guide,
|
|
11
|
+
Text,
|
|
12
|
+
View
|
|
13
|
+
} from '@ossy/design-system'
|
|
14
|
+
|
|
15
|
+
const FlowStage = {
|
|
16
|
+
EnterEmail: 'EnterEmail',
|
|
17
|
+
Success: 'Success',
|
|
18
|
+
Error: 'Error'
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const Invite = () => {
|
|
22
|
+
const router = useRouter()
|
|
23
|
+
const { inviteUser } = useWorkspace()
|
|
24
|
+
const [flowStage, setFlowStage] = useState(FlowStage.EnterEmail)
|
|
25
|
+
const [email, setEmail] = useInputValue('')
|
|
26
|
+
const formRef = useRef()
|
|
27
|
+
|
|
28
|
+
const sendInvite = event => {
|
|
29
|
+
event.preventDefault()
|
|
30
|
+
formRef.current.reportValidity() && inviteUser(email)
|
|
31
|
+
.then(() => setFlowStage(FlowStage.Success))
|
|
32
|
+
.catch(() => setFlowStage(FlowStage.Error))
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<Switch on={flowStage}>
|
|
37
|
+
|
|
38
|
+
<title>Invite user</title>
|
|
39
|
+
|
|
40
|
+
<Switch.Case match={[FlowStage.EnterEmail]}>
|
|
41
|
+
<View gap="m">
|
|
42
|
+
<Title variant="primary">Invite user</Title>
|
|
43
|
+
<Text>
|
|
44
|
+
Invite a user to manage and contribute content to this workspace
|
|
45
|
+
</Text>
|
|
46
|
+
<form
|
|
47
|
+
ref={formRef}
|
|
48
|
+
onSubmit={sendInvite}
|
|
49
|
+
style={{ width: '100%', display: 'flex', flexDirection: 'column' }}
|
|
50
|
+
>
|
|
51
|
+
<View gap="m">
|
|
52
|
+
|
|
53
|
+
<Input
|
|
54
|
+
id="registerField"
|
|
55
|
+
className="stack-m"
|
|
56
|
+
type="email"
|
|
57
|
+
placeholder="Email address"
|
|
58
|
+
required
|
|
59
|
+
value={email}
|
|
60
|
+
onChange={e => setEmail(e.target.value)}
|
|
61
|
+
/>
|
|
62
|
+
|
|
63
|
+
<View gap="m" layout="row" justifyContent="flex-end">
|
|
64
|
+
|
|
65
|
+
<Button
|
|
66
|
+
variant="link"
|
|
67
|
+
id="cancelButton"
|
|
68
|
+
href={() => router.back()}>Cancel
|
|
69
|
+
</Button>
|
|
70
|
+
|
|
71
|
+
<Button
|
|
72
|
+
variant="cta"
|
|
73
|
+
id="registerButton"
|
|
74
|
+
type="submit"
|
|
75
|
+
onClick={sendInvite}>Send invite
|
|
76
|
+
</Button>
|
|
77
|
+
</View>
|
|
78
|
+
</View>
|
|
79
|
+
</form>
|
|
80
|
+
</View>
|
|
81
|
+
</Switch.Case>
|
|
82
|
+
|
|
83
|
+
<Switch.Case match={[FlowStage.Success]}>
|
|
84
|
+
<Guide
|
|
85
|
+
title="Success"
|
|
86
|
+
titleVariant="primary"
|
|
87
|
+
>
|
|
88
|
+
<Text>
|
|
89
|
+
We sent them an invitation over email.
|
|
90
|
+
When they have accepted they will show up in the list of users.
|
|
91
|
+
<br/>
|
|
92
|
+
✌️
|
|
93
|
+
</Text>
|
|
94
|
+
</Guide>
|
|
95
|
+
</Switch.Case>
|
|
96
|
+
|
|
97
|
+
<Switch.Case match={[FlowStage.Error]}>
|
|
98
|
+
<Text>
|
|
99
|
+
Something went wrong, try again in a couple of minutes
|
|
100
|
+
</Text>
|
|
101
|
+
</Switch.Case>
|
|
102
|
+
|
|
103
|
+
</Switch>
|
|
104
|
+
)
|
|
105
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { View } from '@ossy/design-system'
|
|
3
|
+
import { Invite } from './Invite.jsx'
|
|
4
|
+
|
|
5
|
+
export const metadata = {
|
|
6
|
+
id: 'workspace/invitations/add',
|
|
7
|
+
path: {
|
|
8
|
+
sv: '/inbjudningar/lagg-till',
|
|
9
|
+
en: '/invitations/add',
|
|
10
|
+
},
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const InvitePage = () => {
|
|
14
|
+
return (
|
|
15
|
+
<View layout="off-center-m" style={{ height: '100%' }}>
|
|
16
|
+
<View slot="content" surface="primary" roundness="m" inset="m">
|
|
17
|
+
<Invite />
|
|
18
|
+
</View>
|
|
19
|
+
</View>
|
|
20
|
+
)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export default InvitePage
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { useWorkspaces, AsyncStatus } from '@ossy/sdk-react'
|
|
3
|
+
import { Title, Text, Switch, Button, View, Guide, Icon2, Stack } from '@ossy/design-system'
|
|
4
|
+
import { useRouter } from '@ossy/router-react'
|
|
5
|
+
import { AuthenticationGuard } from './AuthenticationGuard.jsx'
|
|
6
|
+
import { patchUserWorkspaceId } from './patchUserWorkspaceId.js'
|
|
7
|
+
|
|
8
|
+
const PageFlow = {
|
|
9
|
+
Error: AsyncStatus.Error,
|
|
10
|
+
Loading: AsyncStatus.Loading,
|
|
11
|
+
Success: AsyncStatus.Success,
|
|
12
|
+
NoWorkspacesCreated: 'NoWorkspacesCreated'
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const SelectWorkspacePage = () => {
|
|
16
|
+
const router = useRouter()
|
|
17
|
+
const { status: asyncStatus, workspaces } = useWorkspaces()
|
|
18
|
+
|
|
19
|
+
const pageFlow = (asyncStatus === AsyncStatus.Success && workspaces.length === 0)
|
|
20
|
+
? PageFlow.NoWorkspacesCreated
|
|
21
|
+
: asyncStatus
|
|
22
|
+
|
|
23
|
+
const setWorkspace = workspaceId => {
|
|
24
|
+
patchUserWorkspaceId(workspaceId).then((res) => {
|
|
25
|
+
if (!res.ok) return
|
|
26
|
+
window.location.assign(router.getHref('@home/home'))
|
|
27
|
+
})
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<AuthenticationGuard>
|
|
32
|
+
|
|
33
|
+
<title>Select workspace</title>
|
|
34
|
+
|
|
35
|
+
<Switch on={pageFlow}>
|
|
36
|
+
<Switch.Case match={[PageFlow.Success]}>
|
|
37
|
+
<div style={{
|
|
38
|
+
display: 'flex',
|
|
39
|
+
justifyContent: 'space-between',
|
|
40
|
+
alignItems: 'center',
|
|
41
|
+
marginBottom: 'var(--space-m)',
|
|
42
|
+
}}>
|
|
43
|
+
<Title variant="primary">
|
|
44
|
+
My workspaces
|
|
45
|
+
</Title>
|
|
46
|
+
<Button
|
|
47
|
+
prefix="math-plus"
|
|
48
|
+
variant="cta"
|
|
49
|
+
href={router.getHref('@profile/workspaces/create')}
|
|
50
|
+
className="mobile:d-none">
|
|
51
|
+
New workspace
|
|
52
|
+
</Button>
|
|
53
|
+
</div>
|
|
54
|
+
</Switch.Case>
|
|
55
|
+
</Switch>
|
|
56
|
+
|
|
57
|
+
<Switch on={pageFlow}>
|
|
58
|
+
|
|
59
|
+
<Switch.Case match={[PageFlow.Error]}>
|
|
60
|
+
<View layout="off-center" style={{ height: '100%' }}>
|
|
61
|
+
<Guide
|
|
62
|
+
slot="content"
|
|
63
|
+
style={{ width: '100vw', maxWidth: '500px' }}
|
|
64
|
+
title="We could not load your workspaces"
|
|
65
|
+
text={`
|
|
66
|
+
Something went wrong when loading your workspaces.
|
|
67
|
+
Maybe your session have expired, or we messed up somehow.
|
|
68
|
+
Try starting over from our home page.
|
|
69
|
+
`}
|
|
70
|
+
actions={[{
|
|
71
|
+
variant: 'cta',
|
|
72
|
+
href: '/',
|
|
73
|
+
label: 'Go to our home page'
|
|
74
|
+
}]}
|
|
75
|
+
/>
|
|
76
|
+
</View>
|
|
77
|
+
</Switch.Case>
|
|
78
|
+
|
|
79
|
+
<Switch.Case match={[PageFlow.Loading]}>
|
|
80
|
+
<Icon2
|
|
81
|
+
slot="content"
|
|
82
|
+
name="spinner"
|
|
83
|
+
style={{ width: '64px', height: '64px' }}
|
|
84
|
+
className="rotate"
|
|
85
|
+
/>
|
|
86
|
+
<Text>Loading...</Text>
|
|
87
|
+
</Switch.Case>
|
|
88
|
+
|
|
89
|
+
<Switch.Case match={[PageFlow.Success]}>
|
|
90
|
+
<View gap="m">
|
|
91
|
+
|
|
92
|
+
<Text style={{ maxWidth: '400px' }}>
|
|
93
|
+
Workspaces are separate environments where you can manage resources, templates, services and billing.
|
|
94
|
+
</Text>
|
|
95
|
+
|
|
96
|
+
<Stack gap="xs" style={{ overflowY: 'auto' }}>
|
|
97
|
+
{workspaces.map(workspace => (
|
|
98
|
+
<Stack.Item
|
|
99
|
+
key={workspace.id}
|
|
100
|
+
layout="row"
|
|
101
|
+
style={{ alignItems: 'center' }}
|
|
102
|
+
gap="m"
|
|
103
|
+
inset="m"
|
|
104
|
+
roundness="s"
|
|
105
|
+
selectable
|
|
106
|
+
onClick={() => setWorkspace(workspace.id)}
|
|
107
|
+
>
|
|
108
|
+
<View>
|
|
109
|
+
<Text style={{ fontWeight: 'bold' }}>{workspace.name}</Text>
|
|
110
|
+
<Text variant="small" as="span">{workspace.id}</Text>
|
|
111
|
+
</View>
|
|
112
|
+
<div style={{ flexGrow: '1' }} />
|
|
113
|
+
<Button prefix="more-vertical-alt" variant="command" style={{ cursor: 'not-allowed' }} />
|
|
114
|
+
</Stack.Item>
|
|
115
|
+
))}
|
|
116
|
+
</Stack>
|
|
117
|
+
</View>
|
|
118
|
+
</Switch.Case>
|
|
119
|
+
|
|
120
|
+
<Switch.Case match={[PageFlow.NoWorkspacesCreated]}>
|
|
121
|
+
<View layout="vertical-top" style={{ flexGrow: '1' }}>
|
|
122
|
+
<Guide
|
|
123
|
+
slot="content"
|
|
124
|
+
style={{ padding: 'var(--space-xl) var(--space-m)', width: '100vw', maxWidth: '500px' }}
|
|
125
|
+
title="🎉 Let's get the party started 🎉"
|
|
126
|
+
text={`
|
|
127
|
+
To get started we need to create a workspace.
|
|
128
|
+
A workspace is an encapsulation of resources, resource templates,
|
|
129
|
+
billing information, and what users have access to these things.
|
|
130
|
+
`}
|
|
131
|
+
actions={[{
|
|
132
|
+
variant: 'cta',
|
|
133
|
+
label: 'Create a workspace',
|
|
134
|
+
href: router.getHref('@profile/workspaces/create')
|
|
135
|
+
}]}
|
|
136
|
+
/>
|
|
137
|
+
</View>
|
|
138
|
+
</Switch.Case>
|
|
139
|
+
|
|
140
|
+
</Switch>
|
|
141
|
+
</AuthenticationGuard>
|
|
142
|
+
)
|
|
143
|
+
}
|
package/src/Users.jsx
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { useUsers, AsyncStatus } from '@ossy/sdk-react'
|
|
3
|
+
import { Text, View, Icon2, Title, Button } from '@ossy/design-system'
|
|
4
|
+
import { useRouter } from '@ossy/router-react'
|
|
5
|
+
|
|
6
|
+
export const Users = () => {
|
|
7
|
+
const router = useRouter()
|
|
8
|
+
const { users = [], status } = useUsers()
|
|
9
|
+
|
|
10
|
+
if (status === AsyncStatus.Loading) return <>Loading</>
|
|
11
|
+
|
|
12
|
+
return (
|
|
13
|
+
<View gap="m">
|
|
14
|
+
|
|
15
|
+
<View inset="s" gap="s">
|
|
16
|
+
|
|
17
|
+
<View layout="row" justifyContent="space-between" alignItems="center">
|
|
18
|
+
<Title>Users</Title>
|
|
19
|
+
<Button variant="cta" href={router.getHref('@workspace/invitations/add')} prefix="user-add">
|
|
20
|
+
Invite user
|
|
21
|
+
</Button>
|
|
22
|
+
</View>
|
|
23
|
+
|
|
24
|
+
<Text style={{ maxWidth: '400px' }}>
|
|
25
|
+
Manage users in your workspace.
|
|
26
|
+
</Text>
|
|
27
|
+
|
|
28
|
+
</View>
|
|
29
|
+
|
|
30
|
+
<View gap="xs">
|
|
31
|
+
{users.map(user => (
|
|
32
|
+
<View layout="row" roundness="s" alignItems="center" inset="m" gap="m" selectable key={user.id}>
|
|
33
|
+
<Icon2 name={user.type === 'Bot' ? 'bot' : 'user'} />
|
|
34
|
+
<View>
|
|
35
|
+
<View layout="row" gap="s">
|
|
36
|
+
<Text as="span" style={{ fontWeight: 'bold' }}>{user.firstName}</Text>
|
|
37
|
+
<Text as="span" style={{ fontWeight: 'bold' }}>{user.lastName}</Text>
|
|
38
|
+
</View>
|
|
39
|
+
<Text variant="small" as="span">{user.email}</Text>
|
|
40
|
+
</View>
|
|
41
|
+
</View>
|
|
42
|
+
))}
|
|
43
|
+
</View>
|
|
44
|
+
|
|
45
|
+
</View>
|
|
46
|
+
)
|
|
47
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { View } from '@ossy/design-system'
|
|
3
|
+
import { Users } from './Users.jsx'
|
|
4
|
+
|
|
5
|
+
export const metadata = {
|
|
6
|
+
id: 'workspace/users',
|
|
7
|
+
path: {
|
|
8
|
+
sv: '/anvandare',
|
|
9
|
+
en: '/users',
|
|
10
|
+
},
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const UsersPage = () => {
|
|
14
|
+
return (
|
|
15
|
+
<View gap="m" surface="primary" style={{ padding: 'var(--space-m) var(--space-l)', height: '100%', overflowY: 'auto' }}>
|
|
16
|
+
<Users />
|
|
17
|
+
</View>
|
|
18
|
+
)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export default UsersPage
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { View } from '@ossy/design-system'
|
|
3
|
+
import { GeneralSettings } from './GeneralSettings.jsx'
|
|
4
|
+
|
|
5
|
+
export const metadata = {
|
|
6
|
+
id: 'workspace/settings',
|
|
7
|
+
path: {
|
|
8
|
+
sv: '/installningar',
|
|
9
|
+
en: '/settings',
|
|
10
|
+
},
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const WorkspaceSettingsPage = () => {
|
|
14
|
+
return (
|
|
15
|
+
<View layout="off-center-m" style={{ height: '100%' }}>
|
|
16
|
+
<View slot="content" surface="primary" roundness="m" inset="m">
|
|
17
|
+
<GeneralSettings />
|
|
18
|
+
</View>
|
|
19
|
+
</View>
|
|
20
|
+
)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export default WorkspaceSettingsPage
|
package/src/index.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export { CreateWorkspacePage } from './CreateWorkspacePage.jsx'
|
|
2
|
+
export { SelectWorkspacePage } from './SelectWorkspacePage.jsx'
|
|
3
|
+
export { GeneralSettings } from './GeneralSettings.jsx'
|
|
4
|
+
export { Invitations } from './Invitations.jsx'
|
|
5
|
+
export { Invite } from './Invite.jsx'
|
|
6
|
+
export { Users } from './Users.jsx'
|
|
7
|
+
export { AuthenticationGuard } from './AuthenticationGuard.jsx'
|
|
8
|
+
export { patchUserWorkspaceId } from './patchUserWorkspaceId.js'
|
|
9
|
+
export { Definition } from './Definition.js'
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
function patchUserAppSettings(partial) {
|
|
2
|
+
return fetch('/@ossy/users/me/app-settings', {
|
|
3
|
+
method: 'PATCH',
|
|
4
|
+
body: JSON.stringify(partial),
|
|
5
|
+
headers: {
|
|
6
|
+
'Content-Type': 'application/json',
|
|
7
|
+
},
|
|
8
|
+
credentials: 'same-origin',
|
|
9
|
+
})
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function patchUserWorkspaceId(workspaceId) {
|
|
13
|
+
return patchUserAppSettings({ workspaceId })
|
|
14
|
+
}
|