@ossy/workspaces 1.17.2 → 3.0.1
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 +4 -0
- package/package.json +8 -4
- package/src/AuthenticationGuard.jsx +3 -3
- package/src/CreateWorkspacePage.jsx +7 -16
- package/src/Definition.js +2 -1
- package/src/GeneralSettings.jsx +77 -19
- package/src/Invitations.jsx +7 -7
- package/src/Invitations.page.jsx +1 -1
- package/src/Invite.jsx +13 -25
- package/src/Invite.page.jsx +1 -1
- package/src/SchemasReadBootstrap.jsx +20 -0
- package/src/SelectWorkspacePage.jsx +8 -11
- package/src/Users.jsx +7 -7
- package/src/Users.page.jsx +3 -3
- package/src/WorkspaceSettings.page.jsx +1 -1
- package/src/accept-invitation.action.js +1 -1
- package/src/accept-invitation.api.js +46 -0
- package/src/accept-invitation.task.js +6 -5
- package/src/create-api-token.action.js +1 -1
- package/src/create-api-token.task.js +1 -1
- package/src/create.action.js +1 -1
- package/src/create.task.js +1 -1
- package/src/disable-service.action.js +1 -1
- package/src/disable-service.task.js +3 -15
- package/src/en.translations.json +36 -30
- package/src/enable-service.action.js +1 -1
- package/src/enable-service.task.js +3 -15
- package/src/entitlements.js +199 -0
- package/src/entitlements.spec.js +88 -0
- package/src/get-api-tokens.action.js +1 -1
- package/src/get-api-tokens.task.js +1 -1
- package/src/get-invitations.action.js +1 -1
- package/src/get-invitations.task.js +1 -1
- package/src/get-schemas.action.js +1 -0
- package/src/get-schemas.task.js +14 -0
- package/src/get-users.action.js +1 -1
- package/src/get-users.task.js +1 -1
- package/src/get.action.js +1 -1
- package/src/get.task.js +1 -1
- package/src/import-schemas.action.js +1 -0
- package/src/import-schemas.task.js +26 -0
- package/src/index.js +3 -2
- package/src/invite-user.action.js +1 -1
- package/src/invite-user.task.js +4 -3
- package/src/list.action.js +1 -1
- package/src/list.task.js +1 -1
- package/src/merge-workspace-schemas.js +13 -0
- package/src/merge-workspace-schemas.spec.js +16 -0
- package/src/remove-member.action.js +1 -1
- package/src/remove-member.task.js +1 -1
- package/src/schema-ids.js +3 -0
- package/src/select-workspace.api.js +50 -0
- package/src/selectWorkspace.js +11 -0
- package/src/service-keys.js +1 -0
- package/src/sv.translations.json +36 -30
- package/src/sync-workspace-membership.task.js +7 -6
- package/src/workspace-invitation.email.jsx +1 -1
- package/src/workspace-users.component.jsx +3 -3
- package/src/workspace.aggregate.js +35 -47
- package/src/workspace.schema.js +9 -0
- package/src/workspaces.events.js +61 -155
- package/src/{workspaces.spec.js → workspaces.integration.spec.js} +40 -38
- package/src/workspaces.queries.js +27 -12
- package/src/get-resource-templates.action.js +0 -1
- package/src/get-resource-templates.task.js +0 -18
- package/src/import-resource-templates.action.js +0 -1
- package/src/import-resource-templates.task.js +0 -26
package/README.md
CHANGED
|
@@ -37,6 +37,10 @@ npm run build
|
|
|
37
37
|
|
|
38
38
|
`@ossy/app` discovers the pages automatically by reading the `"ossy": { "src": "./src" }` field in this package's `package.json`.
|
|
39
39
|
|
|
40
|
+
## Preview
|
|
41
|
+
|
|
42
|
+
Workspace pages are exercised in **`@ossy/app-test`** on the production build path. Open workspace routes from the sidebar or inspect capabilities at `/packages/workspaces`. See [`packages/app-test/README.md`](../app-test/README.md).
|
|
43
|
+
|
|
40
44
|
## Peer dependencies
|
|
41
45
|
|
|
42
46
|
| Package | Version |
|
package/package.json
CHANGED
|
@@ -1,17 +1,21 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ossy/workspaces",
|
|
3
3
|
"description": "Workspaces feature package - create, select, and manage workspaces, users, and invitations",
|
|
4
|
-
"version": "
|
|
4
|
+
"version": "3.0.1",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.js",
|
|
7
7
|
"module": "./src/index.js",
|
|
8
8
|
"exports": {
|
|
9
9
|
".": "./src/index.js",
|
|
10
10
|
"./server": "./src/server.js",
|
|
11
|
+
"./entitlements": "./src/entitlements.js",
|
|
12
|
+
"./merge-workspace-schemas": "./src/merge-workspace-schemas.js",
|
|
13
|
+
"./SchemasReadBootstrap": "./src/SchemasReadBootstrap.jsx",
|
|
11
14
|
"./workspace-invitation.email.jsx": "./src/workspace-invitation.email.jsx"
|
|
12
15
|
},
|
|
13
16
|
"scripts": {
|
|
14
|
-
"test": "NODE_OPTIONS=--experimental-vm-modules jest --verbose"
|
|
17
|
+
"test": "NODE_OPTIONS=--experimental-vm-modules jest --verbose",
|
|
18
|
+
"test:integration": "NODE_OPTIONS=--experimental-vm-modules jest --config jest.integration.config.js --verbose"
|
|
15
19
|
},
|
|
16
20
|
"author": "Ossy <yourfriends@ossy.se> (https://ossy.se)",
|
|
17
21
|
"license": "MIT",
|
|
@@ -26,7 +30,7 @@
|
|
|
26
30
|
},
|
|
27
31
|
"devDependencies": {
|
|
28
32
|
"@jest/globals": "^30.2.0",
|
|
29
|
-
"@ossy/platform": "^
|
|
33
|
+
"@ossy/platform": "^3.0.1",
|
|
30
34
|
"casual": "^1.6.2",
|
|
31
35
|
"jest": "^30.2.0"
|
|
32
36
|
},
|
|
@@ -38,5 +42,5 @@
|
|
|
38
42
|
"/src",
|
|
39
43
|
"README.md"
|
|
40
44
|
],
|
|
41
|
-
"gitHead": "
|
|
45
|
+
"gitHead": "4a70ec216448680d2fddc57b2a8a0077489720ae"
|
|
42
46
|
}
|
|
@@ -25,7 +25,7 @@ export const AuthenticationGuard = ({ children }) => {
|
|
|
25
25
|
<View layout="off-center" style={{ height: '100%' }}>
|
|
26
26
|
<Guide
|
|
27
27
|
style={{ width: '100vw', maxWidth: '300px' }}
|
|
28
|
-
|
|
28
|
+
data-region="content"
|
|
29
29
|
title={t('authentication.guard.notAuthenticated.title')}
|
|
30
30
|
text={t('authentication.guard.notAuthenticated.text')}
|
|
31
31
|
/>
|
|
@@ -36,7 +36,7 @@ export const AuthenticationGuard = ({ children }) => {
|
|
|
36
36
|
<View layout="off-center" style={{ height: '100%' }}>
|
|
37
37
|
<Guide
|
|
38
38
|
style={{ width: '100vw', maxWidth: '300px' }}
|
|
39
|
-
|
|
39
|
+
data-region="content"
|
|
40
40
|
title={t('authentication.guard.error.title')}
|
|
41
41
|
text={t('authentication.guard.error.text')}
|
|
42
42
|
/>
|
|
@@ -49,7 +49,7 @@ export const AuthenticationGuard = ({ children }) => {
|
|
|
49
49
|
name="spinner"
|
|
50
50
|
style={{ width: '64px', height: '64px' }}
|
|
51
51
|
className="rotate fill-50"
|
|
52
|
-
|
|
52
|
+
data-region="content"
|
|
53
53
|
/>
|
|
54
54
|
</View>
|
|
55
55
|
</Switch.Case>
|
|
@@ -1,17 +1,10 @@
|
|
|
1
1
|
import React, { useState } from 'react'
|
|
2
2
|
import { ListWorkspaces, CreateWorkspace } from '@ossy/workspaces'
|
|
3
3
|
import { useSdk, cacheKey } from '@ossy/sdk-react'
|
|
4
|
-
import {
|
|
5
|
-
Button,
|
|
6
|
-
useInputValue,
|
|
7
|
-
Title,
|
|
8
|
-
Input,
|
|
9
|
-
View,
|
|
10
|
-
useLocale,
|
|
11
|
-
} from '@ossy/design-system'
|
|
4
|
+
import { Button, useInputValue, Input, View, useLocale } from '@ossy/design-system'
|
|
12
5
|
import { useRouter } from '@ossy/router-react'
|
|
13
6
|
import { AuthenticationGuard } from './AuthenticationGuard.jsx'
|
|
14
|
-
import {
|
|
7
|
+
import { selectWorkspace } from './selectWorkspace.js'
|
|
15
8
|
import { metadata as CreateWorkspaceMeta } from './create.action.js'
|
|
16
9
|
|
|
17
10
|
export const CreateWorkspacePage = () => {
|
|
@@ -25,12 +18,10 @@ export const CreateWorkspacePage = () => {
|
|
|
25
18
|
sdk.invoke(CreateWorkspace, { name: workspaceName })
|
|
26
19
|
.then((workspace) => {
|
|
27
20
|
sdk.invalidate(cacheKey(ListWorkspaces))
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
)
|
|
33
|
-
})
|
|
21
|
+
selectWorkspace(
|
|
22
|
+
workspace.id,
|
|
23
|
+
router.getHref({ id: '@storage/home', params: { workspaceId: workspace.id } })
|
|
24
|
+
)
|
|
34
25
|
})
|
|
35
26
|
.catch(err => { setError(err.message) })
|
|
36
27
|
}
|
|
@@ -40,7 +31,7 @@ export const CreateWorkspacePage = () => {
|
|
|
40
31
|
<View gap="m">
|
|
41
32
|
|
|
42
33
|
<title>{t('profile/workspaces/create.documentTitle')}</title>
|
|
43
|
-
<
|
|
34
|
+
<Text variant="heading-primary" className="stack-m" as="h1" text="workspaces.create.title" />
|
|
44
35
|
|
|
45
36
|
<Input
|
|
46
37
|
id="workspaceName"
|
package/src/Definition.js
CHANGED
package/src/GeneralSettings.jsx
CHANGED
|
@@ -1,42 +1,100 @@
|
|
|
1
1
|
import React, { useState } from 'react'
|
|
2
|
-
import { GetWorkspace } from '@ossy/workspaces'
|
|
2
|
+
import { GetWorkspace, EnableService, DisableService } from '@ossy/workspaces'
|
|
3
3
|
import { GetCurrentUser } from '@ossy/authentication'
|
|
4
|
-
import { useSdk, AsyncStatus } from '@ossy/sdk-react'
|
|
5
|
-
import { Switch,
|
|
4
|
+
import { useSdk, AsyncStatus, cacheKey } from '@ossy/sdk-react'
|
|
5
|
+
import { Switch, Button, View, Text, ContentHeader, useLocale } from '@ossy/design-system'
|
|
6
6
|
|
|
7
7
|
const formatDate = (timestamp) => timestamp ? new Date(timestamp).toLocaleDateString() : ''
|
|
8
8
|
const FlowStage = { View: 'View', Edit: 'Edit' }
|
|
9
9
|
|
|
10
|
+
/** Built-in capabilities a workspace can enable (package name → locale label key). */
|
|
11
|
+
const MANAGED_SERVICES = []
|
|
12
|
+
|
|
13
|
+
function isServiceOn (services, packageName) {
|
|
14
|
+
return services?.[packageName]?.enabled === true
|
|
15
|
+
}
|
|
16
|
+
|
|
10
17
|
export const GeneralSettings = () => {
|
|
11
18
|
const { t } = useLocale()
|
|
12
19
|
const sdk = useSdk()
|
|
13
|
-
const { data: workspace } = sdk.read(GetWorkspace)
|
|
20
|
+
const { data: workspace, refetch: refetchWorkspace } = sdk.read(GetWorkspace)
|
|
14
21
|
const { status: userStatus } = sdk.read(GetCurrentUser)
|
|
15
22
|
const [flowStage, setFlowStage] = useState(FlowStage.View)
|
|
23
|
+
const [busyPackage, setBusyPackage] = useState(null)
|
|
16
24
|
const services = workspace?.services || {}
|
|
17
25
|
|
|
26
|
+
const toggleService = async (packageName, enable) => {
|
|
27
|
+
setBusyPackage(packageName)
|
|
28
|
+
try {
|
|
29
|
+
const action = enable ? EnableService : DisableService
|
|
30
|
+
await sdk.invoke(action, { service: packageName })
|
|
31
|
+
sdk.invalidate(cacheKey(GetWorkspace))
|
|
32
|
+
await refetchWorkspace()
|
|
33
|
+
} finally {
|
|
34
|
+
setBusyPackage(null)
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
18
38
|
return (
|
|
19
39
|
<View gap="l" inset="m">
|
|
20
|
-
<
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
40
|
+
<ContentHeader
|
|
41
|
+
title="workspace.settings.detailsTitle"
|
|
42
|
+
description="workspace.settings.detailsDescription"
|
|
43
|
+
inset="none"
|
|
44
|
+
far={(
|
|
45
|
+
<Switch on={flowStage}>
|
|
46
|
+
<Switch.Case match={[FlowStage.View]}>
|
|
47
|
+
<Button prefix={{ size: 's', name: 'pen' }} variant="cta" disabled label="workspace.settings.edit" />
|
|
48
|
+
</Switch.Case>
|
|
49
|
+
</Switch>
|
|
50
|
+
)}
|
|
51
|
+
/>
|
|
28
52
|
{userStatus === AsyncStatus.Loading ? (
|
|
29
53
|
<Text>{t('workspace.settings.loading')}</Text>
|
|
30
54
|
) : (
|
|
31
55
|
<View gap="s">
|
|
32
|
-
<View layout="row" gap="s"><Text style={{ fontWeight: 'bold' }}
|
|
33
|
-
<View layout="row" gap="s"><Text style={{ fontWeight: 'bold' }}
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
)
|
|
56
|
+
<View layout="row" gap="s"><Text style={{ fontWeight: 'bold' }} text="workspace.settings.name" /><Text>{workspace?.name}</Text></View>
|
|
57
|
+
<View layout="row" gap="s"><Text style={{ fontWeight: 'bold' }} text="workspace.settings.created" /><Text>{formatDate(workspace?.created)}</Text></View>
|
|
58
|
+
{MANAGED_SERVICES.length > 0 && (
|
|
59
|
+
<>
|
|
60
|
+
<Text variant="heading-tertiary" as="h3" text="workspace.settings.services" />
|
|
61
|
+
<View gap="m">
|
|
62
|
+
{MANAGED_SERVICES.map(({ package: packageName, labelKey }) => {
|
|
63
|
+
const enabled = isServiceOn(services, packageName)
|
|
64
|
+
return (
|
|
65
|
+
<View key={packageName} layout="row" gap="m" alignItems="center" justifyContent="space-between">
|
|
66
|
+
<Text data-service-status={packageName}>
|
|
67
|
+
{t(labelKey)}: {enabled ? t('workspace.settings.service.on') : t('workspace.settings.service.off')}
|
|
68
|
+
</Text>
|
|
69
|
+
{enabled ? (
|
|
70
|
+
<Button
|
|
71
|
+
variant="neutral"
|
|
72
|
+
size="s"
|
|
73
|
+
data-action={DisableService.id}
|
|
74
|
+
data-service={packageName}
|
|
75
|
+
disabled={busyPackage === packageName}
|
|
76
|
+
onClick={() => toggleService(packageName, false)}
|
|
77
|
+
>
|
|
78
|
+
{t('workspace.settings.service.disable')}
|
|
79
|
+
</Button>
|
|
80
|
+
) : (
|
|
81
|
+
<Button
|
|
82
|
+
variant="primary"
|
|
83
|
+
size="s"
|
|
84
|
+
data-action={EnableService.id}
|
|
85
|
+
data-service={packageName}
|
|
86
|
+
disabled={busyPackage === packageName}
|
|
87
|
+
onClick={() => toggleService(packageName, true)}
|
|
88
|
+
>
|
|
89
|
+
{t('workspace.settings.service.enable')}
|
|
90
|
+
</Button>
|
|
91
|
+
)}
|
|
92
|
+
</View>
|
|
93
|
+
)
|
|
94
|
+
})}
|
|
39
95
|
</View>
|
|
96
|
+
</>
|
|
97
|
+
)}
|
|
40
98
|
</View>
|
|
41
99
|
)}
|
|
42
100
|
</View>
|
package/src/Invitations.jsx
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import React from 'react'
|
|
2
2
|
import { GetWorkspace } from '@ossy/workspaces'
|
|
3
3
|
import { useSdk } from '@ossy/sdk-react'
|
|
4
|
-
import {
|
|
4
|
+
import { Text, Button, View, Icon, ContentHeader, useLocale } from '@ossy/design-system'
|
|
5
5
|
import { useRouter } from '@ossy/router-react'
|
|
6
6
|
|
|
7
7
|
export const Invitations = () => {
|
|
@@ -13,15 +13,15 @@ export const Invitations = () => {
|
|
|
13
13
|
|
|
14
14
|
return (
|
|
15
15
|
<View gap="m">
|
|
16
|
-
<
|
|
17
|
-
|
|
18
|
-
|
|
16
|
+
<ContentHeader
|
|
17
|
+
title="workspace.invitations.title"
|
|
18
|
+
description="workspace.invitations.description"
|
|
19
|
+
far={(
|
|
19
20
|
<Button variant="cta" href={router.getHref('@workspace/invitations/add')} prefix="user-add">
|
|
20
21
|
{t('workspace.invitations.invite')}
|
|
21
22
|
</Button>
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
</View>
|
|
23
|
+
)}
|
|
24
|
+
/>
|
|
25
25
|
<View gap="xs">
|
|
26
26
|
{invitations.map(invite => (
|
|
27
27
|
<View layout="row" roundness="s" alignItems="center" inset="m" gap="m" selectable key={invite.email}>
|
package/src/Invitations.page.jsx
CHANGED
|
@@ -13,7 +13,7 @@ export const metadata = {
|
|
|
13
13
|
export const InvitationsPage = () => {
|
|
14
14
|
return (
|
|
15
15
|
<View layout="off-center-m" style={{ height: '100%' }}>
|
|
16
|
-
<View
|
|
16
|
+
<View data-region="content" surface="primary" roundness="m" inset="m">
|
|
17
17
|
<Invitations />
|
|
18
18
|
</View>
|
|
19
19
|
</View>
|
package/src/Invite.jsx
CHANGED
|
@@ -2,17 +2,7 @@ import React, { useState, useRef } from 'react'
|
|
|
2
2
|
import { InviteUser } from '@ossy/workspaces'
|
|
3
3
|
import { useSdk } from '@ossy/sdk-react'
|
|
4
4
|
import { useRouter } from '@ossy/router-react'
|
|
5
|
-
import {
|
|
6
|
-
useInputValue,
|
|
7
|
-
Button,
|
|
8
|
-
Title,
|
|
9
|
-
Input,
|
|
10
|
-
Switch,
|
|
11
|
-
Guide,
|
|
12
|
-
Text,
|
|
13
|
-
View,
|
|
14
|
-
useLocale,
|
|
15
|
-
} from '@ossy/design-system'
|
|
5
|
+
import { useInputValue, Button, Input, Switch, Guide, Text, View, useLocale } from '@ossy/design-system'
|
|
16
6
|
|
|
17
7
|
const FlowStage = {
|
|
18
8
|
EnterEmail: 'EnterEmail',
|
|
@@ -42,10 +32,8 @@ export const Invite = () => {
|
|
|
42
32
|
|
|
43
33
|
<Switch.Case match={[FlowStage.EnterEmail]}>
|
|
44
34
|
<View gap="m">
|
|
45
|
-
<
|
|
46
|
-
<Text
|
|
47
|
-
{t('workspace.invite.description')}
|
|
48
|
-
</Text>
|
|
35
|
+
<Text variant="heading-primary" as="h1" text="workspace.invite.title" />
|
|
36
|
+
<Text text="workspace.invite.description" />
|
|
49
37
|
<form
|
|
50
38
|
ref={formRef}
|
|
51
39
|
onSubmit={sendInvite}
|
|
@@ -57,7 +45,7 @@ export const Invite = () => {
|
|
|
57
45
|
id="registerField"
|
|
58
46
|
className="stack-m"
|
|
59
47
|
type="email"
|
|
60
|
-
placeholder=
|
|
48
|
+
placeholder="workspace.invite.emailPlaceholder"
|
|
61
49
|
required
|
|
62
50
|
value={email}
|
|
63
51
|
onChange={e => setEmail(e.target.value)}
|
|
@@ -68,16 +56,18 @@ export const Invite = () => {
|
|
|
68
56
|
<Button
|
|
69
57
|
variant="link"
|
|
70
58
|
id="cancelButton"
|
|
71
|
-
href={() => router.back()}
|
|
72
|
-
|
|
59
|
+
href={() => router.back()}
|
|
60
|
+
label="design-system.cancel"
|
|
61
|
+
/>
|
|
73
62
|
|
|
74
63
|
<Button
|
|
75
64
|
variant="cta"
|
|
76
65
|
id="registerButton"
|
|
77
66
|
data-action={InviteUser.id}
|
|
78
67
|
type="submit"
|
|
79
|
-
|
|
80
|
-
|
|
68
|
+
label="workspace.invite.submit"
|
|
69
|
+
onClick={sendInvite}
|
|
70
|
+
/>
|
|
81
71
|
</View>
|
|
82
72
|
</View>
|
|
83
73
|
</form>
|
|
@@ -86,11 +76,11 @@ export const Invite = () => {
|
|
|
86
76
|
|
|
87
77
|
<Switch.Case match={[FlowStage.Success]}>
|
|
88
78
|
<Guide
|
|
89
|
-
title=
|
|
79
|
+
title="workspace.invite.successTitle"
|
|
90
80
|
titleVariant="primary"
|
|
91
81
|
>
|
|
92
82
|
<Text>
|
|
93
|
-
|
|
83
|
+
<Text as="span" text="workspace.invite.successBody" />
|
|
94
84
|
<br/>
|
|
95
85
|
✌️
|
|
96
86
|
</Text>
|
|
@@ -98,9 +88,7 @@ export const Invite = () => {
|
|
|
98
88
|
</Switch.Case>
|
|
99
89
|
|
|
100
90
|
<Switch.Case match={[FlowStage.Error]}>
|
|
101
|
-
<Text
|
|
102
|
-
{t('workspace.invite.error')}
|
|
103
|
-
</Text>
|
|
91
|
+
<Text text="workspace.invite.error" />
|
|
104
92
|
</Switch.Case>
|
|
105
93
|
|
|
106
94
|
</Switch>
|
package/src/Invite.page.jsx
CHANGED
|
@@ -13,7 +13,7 @@ export const metadata = {
|
|
|
13
13
|
export const InvitePage = () => {
|
|
14
14
|
return (
|
|
15
15
|
<View layout="off-center-m" style={{ height: '100%' }}>
|
|
16
|
-
<View
|
|
16
|
+
<View data-region="content" surface="primary" roundness="m" inset="m">
|
|
17
17
|
<Invite />
|
|
18
18
|
</View>
|
|
19
19
|
</View>
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import React, { useEffect } from 'react'
|
|
2
|
+
import { AsyncStatus, cacheKey, useReadCacheStore } from '@ossy/sdk-react'
|
|
3
|
+
import { metadata as GetSchemas } from './get-schemas.action.js'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Seed the GetSchemas read cache from SSR props so schema lookups work on first paint.
|
|
7
|
+
*/
|
|
8
|
+
export function SchemasReadBootstrap ({ schemas }) {
|
|
9
|
+
const store = useReadCacheStore()
|
|
10
|
+
|
|
11
|
+
useEffect(() => {
|
|
12
|
+
if (!Array.isArray(schemas) || schemas.length === 0) return
|
|
13
|
+
const key = cacheKey(GetSchemas)
|
|
14
|
+
const entry = store.getEntry(key)
|
|
15
|
+
if (entry.status === AsyncStatus.Success) return
|
|
16
|
+
store.setEntry(key, { status: AsyncStatus.Success, data: schemas, error: null })
|
|
17
|
+
}, [schemas, store])
|
|
18
|
+
|
|
19
|
+
return null
|
|
20
|
+
}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import React from 'react'
|
|
2
2
|
import { ListWorkspaces } from '@ossy/workspaces'
|
|
3
3
|
import { useSdk, AsyncStatus } from '@ossy/sdk-react'
|
|
4
|
-
import {
|
|
4
|
+
import { Text, Switch, Button, View, Guide, Icon, useLocale } from '@ossy/design-system'
|
|
5
5
|
import { useRouter } from '@ossy/router-react'
|
|
6
6
|
import { AuthenticationGuard } from './AuthenticationGuard.jsx'
|
|
7
|
-
import {
|
|
7
|
+
import { selectWorkspace } from './selectWorkspace.js'
|
|
8
8
|
|
|
9
9
|
const PageFlow = {
|
|
10
10
|
Error: AsyncStatus.Error,
|
|
@@ -24,10 +24,7 @@ export const SelectWorkspacePage = () => {
|
|
|
24
24
|
: asyncStatus
|
|
25
25
|
|
|
26
26
|
const setWorkspace = workspaceId => {
|
|
27
|
-
|
|
28
|
-
if (!res.ok) return
|
|
29
|
-
window.location.assign(router.getHref('@home/home'))
|
|
30
|
-
})
|
|
27
|
+
selectWorkspace(workspaceId, router.getHref('@home/home'))
|
|
31
28
|
}
|
|
32
29
|
|
|
33
30
|
return (
|
|
@@ -43,9 +40,9 @@ export const SelectWorkspacePage = () => {
|
|
|
43
40
|
alignItems: 'center',
|
|
44
41
|
marginBottom: 'var(--space-m)',
|
|
45
42
|
}}>
|
|
46
|
-
<
|
|
43
|
+
<Text variant="heading-primary" as="h1">
|
|
47
44
|
{t('workspaces.select.title')}
|
|
48
|
-
</
|
|
45
|
+
</Text>
|
|
49
46
|
<Button
|
|
50
47
|
prefix="math-plus"
|
|
51
48
|
variant="cta"
|
|
@@ -62,7 +59,7 @@ export const SelectWorkspacePage = () => {
|
|
|
62
59
|
<Switch.Case match={[PageFlow.Error]}>
|
|
63
60
|
<View layout="off-center" style={{ height: '100%' }}>
|
|
64
61
|
<Guide
|
|
65
|
-
|
|
62
|
+
data-region="content"
|
|
66
63
|
style={{ width: '100vw', maxWidth: '500px' }}
|
|
67
64
|
title={t('workspaces.select.errorTitle')}
|
|
68
65
|
text={t('workspaces.select.errorText')}
|
|
@@ -77,7 +74,7 @@ export const SelectWorkspacePage = () => {
|
|
|
77
74
|
|
|
78
75
|
<Switch.Case match={[PageFlow.Loading]}>
|
|
79
76
|
<Icon
|
|
80
|
-
|
|
77
|
+
data-region="content"
|
|
81
78
|
name="spinner"
|
|
82
79
|
style={{ width: '64px', height: '64px' }}
|
|
83
80
|
className="rotate"
|
|
@@ -119,7 +116,7 @@ export const SelectWorkspacePage = () => {
|
|
|
119
116
|
<Switch.Case match={[PageFlow.NoWorkspacesCreated]}>
|
|
120
117
|
<View layout="vertical-top" style={{ flexGrow: '1' }}>
|
|
121
118
|
<Guide
|
|
122
|
-
|
|
119
|
+
data-region="content"
|
|
123
120
|
style={{ padding: 'var(--space-xl) var(--space-m)', width: '100vw', maxWidth: '500px' }}
|
|
124
121
|
title={`🎉 ${t('workspaces.select.emptyTitle')} 🎉`}
|
|
125
122
|
text={t('workspaces.select.emptyText')}
|
package/src/Users.jsx
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import React from 'react'
|
|
2
2
|
import { GetUsers } from '@ossy/workspaces'
|
|
3
3
|
import { useSdk, AsyncStatus } from '@ossy/sdk-react'
|
|
4
|
-
import { Text, View, Icon,
|
|
4
|
+
import { Text, View, Icon, Button, ContentHeader, useLocale } from '@ossy/design-system'
|
|
5
5
|
import { useRouter } from '@ossy/router-react'
|
|
6
6
|
|
|
7
7
|
export const Users = () => {
|
|
@@ -14,15 +14,15 @@ export const Users = () => {
|
|
|
14
14
|
|
|
15
15
|
return (
|
|
16
16
|
<View gap="m">
|
|
17
|
-
<
|
|
18
|
-
|
|
19
|
-
|
|
17
|
+
<ContentHeader
|
|
18
|
+
title="workspace.users.title"
|
|
19
|
+
description="workspace.users.description"
|
|
20
|
+
far={(
|
|
20
21
|
<Button variant="cta" href={router.getHref('@workspace/invitations/add')} prefix="user-add">
|
|
21
22
|
{t('workspace.users.invite')}
|
|
22
23
|
</Button>
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
</View>
|
|
24
|
+
)}
|
|
25
|
+
/>
|
|
26
26
|
<View gap="xs">
|
|
27
27
|
{users.map(user => (
|
|
28
28
|
<View layout="row" roundness="s" alignItems="center" inset="m" gap="m" selectable key={user.id}>
|
package/src/Users.page.jsx
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React from 'react'
|
|
2
|
-
import {
|
|
2
|
+
import { Page } from '@ossy/design-system'
|
|
3
3
|
import { Users } from './Users.jsx'
|
|
4
4
|
|
|
5
5
|
export const metadata = {
|
|
@@ -12,9 +12,9 @@ export const metadata = {
|
|
|
12
12
|
|
|
13
13
|
export const UsersPage = () => {
|
|
14
14
|
return (
|
|
15
|
-
<
|
|
15
|
+
<Page>
|
|
16
16
|
<Users />
|
|
17
|
-
</
|
|
17
|
+
</Page>
|
|
18
18
|
)
|
|
19
19
|
}
|
|
20
20
|
|
|
@@ -13,7 +13,7 @@ export const metadata = {
|
|
|
13
13
|
export const WorkspaceSettingsPage = () => {
|
|
14
14
|
return (
|
|
15
15
|
<View layout="off-center-m" style={{ height: '100%' }}>
|
|
16
|
-
<View
|
|
16
|
+
<View data-region="content" surface="primary" roundness="m" inset="m">
|
|
17
17
|
<GeneralSettings />
|
|
18
18
|
</View>
|
|
19
19
|
</View>
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export const metadata = { id: 'workspaces/accept-invitation', access: 'public' }
|
|
1
|
+
export const metadata = { id: '@ossy/workspaces/actions/accept-invitation', access: 'public' }
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ActionService,
|
|
3
|
+
mergeUserAppSettingsCookie,
|
|
4
|
+
setAuthCookie,
|
|
5
|
+
} from '@ossy/platform'
|
|
6
|
+
|
|
7
|
+
export const metadata = {
|
|
8
|
+
id: 'users.accept-invitation',
|
|
9
|
+
path: '/api/v0/users/accept-invitation',
|
|
10
|
+
action: '@ossy/workspaces/actions/accept-invitation',
|
|
11
|
+
method: 'GET',
|
|
12
|
+
query: ['token', 'workspaceId', 'redirect'],
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export default async function handle (req, res) {
|
|
16
|
+
if (req.method !== 'GET') {
|
|
17
|
+
res.setHeader('Allow', 'GET')
|
|
18
|
+
res.status(405).json({ error: 'Method Not Allowed' })
|
|
19
|
+
return
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
try {
|
|
23
|
+
const { authToken, workspaceId } = await ActionService.invoke('@ossy/workspaces/actions/accept-invitation', {
|
|
24
|
+
payload: {
|
|
25
|
+
token: req.query.token,
|
|
26
|
+
workspaceId: req.query.workspaceId,
|
|
27
|
+
},
|
|
28
|
+
req,
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
setAuthCookie(res, authToken)
|
|
32
|
+
if (workspaceId) {
|
|
33
|
+
mergeUserAppSettingsCookie(req, res, { workspaceId })
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const redirect = req.query.redirect
|
|
37
|
+
if (redirect && typeof redirect === 'string') {
|
|
38
|
+
res.redirect(302, redirect)
|
|
39
|
+
return
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
res.status(200).json('')
|
|
43
|
+
} catch (err) {
|
|
44
|
+
res.status(err.status || 401).json('')
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
import jwt from 'jsonwebtoken'
|
|
2
2
|
import { Aggregate } from '@ossy/event-store'
|
|
3
|
+
import { UserSchema } from '@ossy/users/server'
|
|
3
4
|
import { Workspace, WorkspacesEvents } from '@ossy/workspaces/server'
|
|
4
5
|
import { User, UsersEvents } from '@ossy/users/server'
|
|
5
6
|
import { Token, TokenEvents } from '@ossy/tokens/server'
|
|
6
7
|
import { ConfigService } from '@ossy/platform'
|
|
7
8
|
import { createLogger } from '@ossy/observability'
|
|
8
9
|
|
|
9
|
-
export const metadata = { id: 'workspaces/accept-invitation' }
|
|
10
|
+
export const metadata = { id: '@ossy/workspaces/tasks/accept-invitation' }
|
|
10
11
|
|
|
11
|
-
const log = createLogger('workspaces/accept-invitation')
|
|
12
|
+
const log = createLogger('@ossy/workspaces/tasks/accept-invitation')
|
|
12
13
|
|
|
13
14
|
function verifyToken(token) {
|
|
14
15
|
return new Promise((resolve, reject) => {
|
|
@@ -32,7 +33,7 @@ export async function run({ payload, req }) {
|
|
|
32
33
|
|
|
33
34
|
if (!email) throw Object.assign(new Error('No email found in token payload'), { status: 401 })
|
|
34
35
|
|
|
35
|
-
const existingUser = await Aggregate.Collection.findOne({ type:
|
|
36
|
+
const existingUser = await Aggregate.Collection.findOne({ type: UserSchema.user, 'state.email': email }).then(agg => agg?.state)
|
|
36
37
|
|
|
37
38
|
if (existingUser && workspace.users.includes(existingUser.id)) {
|
|
38
39
|
throw Object.assign(new Error('User already in workspace'), { status: 400 })
|
|
@@ -60,7 +61,7 @@ export async function run({ payload, req }) {
|
|
|
60
61
|
Aggregate.Of(Workspace, resolvedWorkspaceId).then(Aggregate.Add(userInvitationAccepted)),
|
|
61
62
|
])
|
|
62
63
|
} else {
|
|
63
|
-
const signUpEvent = UsersEvents.
|
|
64
|
+
const signUpEvent = UsersEvents.Created({ email })
|
|
64
65
|
userId = signUpEvent.createdBy
|
|
65
66
|
const signInExpiresIn = ConfigService.TokenValidity
|
|
66
67
|
signInExpiresAt = Date.now() + signInExpiresIn * 1000
|
|
@@ -80,5 +81,5 @@ export async function run({ payload, req }) {
|
|
|
80
81
|
])
|
|
81
82
|
}
|
|
82
83
|
|
|
83
|
-
return { authToken: signInToken, authExpiresAt: signInExpiresAt }
|
|
84
|
+
return { authToken: signInToken, authExpiresAt: signInExpiresAt, workspaceId: resolvedWorkspaceId }
|
|
84
85
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export const metadata = { id: 'workspaces/create-api-token', access: 'workspace' }
|
|
1
|
+
export const metadata = { id: '@ossy/workspaces/actions/create-api-token', access: 'workspace' }
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Aggregate } from '@ossy/event-store'
|
|
2
2
|
import { Workspace, WorkspacesEvents } from '@ossy/workspaces/server'
|
|
3
3
|
|
|
4
|
-
export const metadata = { id: 'workspaces/create-api-token' }
|
|
4
|
+
export const metadata = { id: '@ossy/workspaces/tasks/create-api-token' }
|
|
5
5
|
|
|
6
6
|
export async function run({ payload, req }) {
|
|
7
7
|
const createdBy = payload?.userId ?? req?.userId
|
package/src/create.action.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const metadata = { id: 'workspaces/create', access: 'authenticated' }
|
|
1
|
+
export const metadata = { id: '@ossy/workspaces/actions/create', access: 'authenticated' }
|