@ossy/workspaces 1.17.1 → 1.17.3

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.
Files changed (64) hide show
  1. package/package.json +8 -4
  2. package/src/CreateWorkspacePage.jsx +7 -16
  3. package/src/Definition.js +1 -0
  4. package/src/GeneralSettings.jsx +77 -19
  5. package/src/Invitations.jsx +7 -7
  6. package/src/Invite.jsx +13 -25
  7. package/src/SchemasReadBootstrap.jsx +20 -0
  8. package/src/SelectWorkspacePage.jsx +5 -8
  9. package/src/Users.jsx +7 -7
  10. package/src/Users.page.jsx +3 -3
  11. package/src/accept-invitation.action.js +1 -1
  12. package/src/accept-invitation.api.js +46 -0
  13. package/src/accept-invitation.task.js +6 -5
  14. package/src/create-api-token.action.js +1 -1
  15. package/src/create-api-token.task.js +1 -1
  16. package/src/create.action.js +1 -1
  17. package/src/create.task.js +1 -1
  18. package/src/disable-service.action.js +1 -1
  19. package/src/disable-service.task.js +3 -15
  20. package/src/en.translations.json +36 -30
  21. package/src/enable-service.action.js +1 -1
  22. package/src/enable-service.task.js +3 -15
  23. package/src/entitlements.js +199 -0
  24. package/src/entitlements.spec.js +88 -0
  25. package/src/get-api-tokens.action.js +1 -1
  26. package/src/get-api-tokens.task.js +1 -1
  27. package/src/get-invitations.action.js +1 -1
  28. package/src/get-invitations.task.js +1 -1
  29. package/src/get-schemas.action.js +1 -0
  30. package/src/get-schemas.task.js +14 -0
  31. package/src/get-users.action.js +1 -1
  32. package/src/get-users.task.js +1 -1
  33. package/src/get.action.js +1 -1
  34. package/src/get.task.js +1 -1
  35. package/src/import-schemas.action.js +1 -0
  36. package/src/import-schemas.task.js +26 -0
  37. package/src/index.js +3 -2
  38. package/src/invite-user.action.js +1 -1
  39. package/src/invite-user.task.js +4 -3
  40. package/src/list.action.js +1 -1
  41. package/src/list.task.js +1 -1
  42. package/src/merge-workspace-schemas.js +13 -0
  43. package/src/merge-workspace-schemas.spec.js +16 -0
  44. package/src/remove-member.action.js +1 -1
  45. package/src/remove-member.task.js +1 -1
  46. package/src/schema-ids.js +3 -0
  47. package/src/select-workspace.api.js +50 -0
  48. package/src/selectWorkspace.js +11 -0
  49. package/src/service-keys.js +1 -0
  50. package/src/stories/Users.stories.jsx +35 -0
  51. package/src/stories/decorators.jsx +59 -0
  52. package/src/sv.translations.json +36 -30
  53. package/src/sync-workspace-membership.task.js +7 -6
  54. package/src/workspace-invitation.email.jsx +1 -1
  55. package/src/workspace-users.component.jsx +3 -3
  56. package/src/workspace.aggregate.js +35 -47
  57. package/src/workspace.schema.js +9 -0
  58. package/src/workspaces.events.js +61 -155
  59. package/src/{workspaces.spec.js → workspaces.integration.spec.js} +40 -38
  60. package/src/workspaces.queries.js +27 -12
  61. package/src/get-resource-templates.action.js +0 -1
  62. package/src/get-resource-templates.task.js +0 -18
  63. package/src/import-resource-templates.action.js +0 -1
  64. package/src/import-resource-templates.task.js +0 -26
@@ -0,0 +1,50 @@
1
+ import {
2
+ ActionService,
3
+ mergeUserAppSettingsCookie,
4
+ } from '@ossy/platform'
5
+
6
+ export const metadata = {
7
+ id: 'users.select-workspace',
8
+ path: '/api/v0/users/select-workspace',
9
+ method: 'GET',
10
+ query: ['workspaceId', 'redirect'],
11
+ }
12
+
13
+ export default async function handle (req, res) {
14
+ if (req.method !== 'GET') {
15
+ res.setHeader('Allow', 'GET')
16
+ res.status(405).json({ error: 'Method Not Allowed' })
17
+ return
18
+ }
19
+
20
+ if (!req.userId || req.userId === 'anonymous') {
21
+ res.status(401).json({ error: 'Unauthorized' })
22
+ return
23
+ }
24
+
25
+ const workspaceId = req.query.workspaceId
26
+ if (!workspaceId || typeof workspaceId !== 'string') {
27
+ res.status(400).json({ message: 'No workspaceId provided' })
28
+ return
29
+ }
30
+
31
+ try {
32
+ const workspaces = await ActionService.invoke('@ossy/workspaces/actions/list', { req })
33
+ if (!workspaces.some((w) => w.id === workspaceId)) {
34
+ res.status(403).json({ message: 'Forbidden' })
35
+ return
36
+ }
37
+
38
+ mergeUserAppSettingsCookie(req, res, { workspaceId })
39
+
40
+ const redirect = req.query.redirect
41
+ if (redirect && typeof redirect === 'string') {
42
+ res.redirect(302, redirect)
43
+ return
44
+ }
45
+
46
+ res.status(200).json('')
47
+ } catch (err) {
48
+ res.status(err.status || 500).json({ message: err.message || 'Internal Server Error' })
49
+ }
50
+ }
@@ -0,0 +1,11 @@
1
+ export function selectWorkspaceHref (workspaceId, redirect) {
2
+ const params = new URLSearchParams({ workspaceId: String(workspaceId) })
3
+ if (redirect != null && redirect !== '') {
4
+ params.set('redirect', String(redirect))
5
+ }
6
+ return `/api/v0/users/select-workspace?${params}`
7
+ }
8
+
9
+ export function selectWorkspace (workspaceId, redirect) {
10
+ window.location.assign(selectWorkspaceHref(workspaceId, redirect))
11
+ }
@@ -0,0 +1 @@
1
+ export { assertEnableableService } from './entitlements.js'
@@ -0,0 +1,35 @@
1
+ import React from 'react'
2
+ import { View } from '@ossy/design-system'
3
+ import { Users } from '../Users.jsx'
4
+ import { withMockRouter, withMockSdk } from './decorators.jsx'
5
+
6
+ export default {
7
+ title: 'Workspaces/Pages/Users',
8
+ component: Users,
9
+ decorators: [withMockRouter, withMockSdk],
10
+ parameters: {
11
+ layout: 'padded',
12
+ docs: {
13
+ story: {
14
+ inline: true,
15
+ height: 'auto',
16
+ },
17
+ },
18
+ },
19
+ }
20
+
21
+ export const Default = () => (
22
+ <View style={{ maxWidth: 720, width: '100%' }}>
23
+ <Users />
24
+ </View>
25
+ )
26
+
27
+ export const Empty = () => (
28
+ <View style={{ maxWidth: 720, width: '100%' }}>
29
+ <Users />
30
+ </View>
31
+ )
32
+
33
+ Empty.parameters = {
34
+ mockSdk: { users: [] },
35
+ }
@@ -0,0 +1,59 @@
1
+ import {
2
+ createMockRouterDecorator,
3
+ createMockSdkDecorator,
4
+ } from '../../../../.storybook/decorators/index.jsx'
5
+
6
+ export const sampleWorkspaceUsers = [
7
+ { id: 'user-1', firstName: 'Anna', lastName: 'Svensson', email: 'anna@example.com', type: 'User' },
8
+ { id: 'user-2', firstName: 'Erik', lastName: 'Lind', email: 'erik@example.com', type: 'User' },
9
+ { id: 'bot-1', firstName: 'Deploy', lastName: 'Bot', email: 'bot@example.com', type: 'Bot' },
10
+ ]
11
+
12
+ export const sampleWorkspaces = [
13
+ { id: 'ws-demo-1', name: 'Demo Workspace' },
14
+ { id: 'ws-demo-2', name: 'Acme Corp' },
15
+ ]
16
+
17
+ export const sampleCurrentUser = {
18
+ id: 'user-1',
19
+ firstName: 'Anna',
20
+ lastName: 'Svensson',
21
+ email: 'anna@example.com',
22
+ }
23
+
24
+ function createWorkspaceHandlers (overrides = {}) {
25
+ const users = 'users' in overrides ? overrides.users : sampleWorkspaceUsers
26
+ const workspaces = 'workspaces' in overrides ? overrides.workspaces : sampleWorkspaces
27
+ const currentUser = 'currentUser' in overrides ? overrides.currentUser : sampleCurrentUser
28
+
29
+ return {
30
+ '@ossy/workspaces/actions/get-users': async () => users,
31
+ '@ossy/workspaces/actions/list': async () => workspaces,
32
+ '@ossy/authentication/actions/get-current-user': async () => currentUser,
33
+ }
34
+ }
35
+
36
+ const defaultMockRouter = {
37
+ href: 'http://localhost/en/workspaces',
38
+ pages: [],
39
+ language: 'en',
40
+ defaultLanguage: 'en',
41
+ supportedLanguages: ['en', 'sv'],
42
+ params: {},
43
+ searchParams: {},
44
+ navigate: () => {},
45
+ getHref: (target) => {
46
+ const id = typeof target === 'string' ? target : target?.id
47
+ if (id === '@workspace/invitations/add') return '/en/invitations/add'
48
+ if (id === '@profile/workspaces/create') return '/en/profile/workspaces/create'
49
+ if (id === '@home/home') return '/en/home'
50
+ if (id === '@storage/home' && target?.params?.workspaceId) {
51
+ return `/en/storage/${target.params.workspaceId}`
52
+ }
53
+ return `/en/${String(id ?? '').replace(/\//g, '-')}`
54
+ },
55
+ back: () => {},
56
+ }
57
+
58
+ export const withMockSdk = createMockSdkDecorator(createWorkspaceHandlers)
59
+ export const withMockRouter = createMockRouterDecorator(defaultMockRouter)
@@ -8,41 +8,41 @@
8
8
  "workspaces.select.title": "Mina workspaces",
9
9
  "workspaces.select.newWorkspace": "Nytt workspace",
10
10
  "workspaces.select.loading": "Laddar...",
11
- "workspaces.select.description": "Workspaces är separata miljöer där du kan hantera resurser, mallar, tjänster och fakturering.",
11
+ "workspaces.select.description": "Workspaces är separata miljöer där du kan hantera resurser, scheman, tjänster och fakturering.",
12
12
  "workspaces.select.errorTitle": "Vi kunde inte ladda dina workspaces",
13
13
  "workspaces.select.errorText": "Något gick fel när dina workspaces skulle laddas. Kanske har din session gått ut, eller så har vi klantat till det. Prova att börja om från startsidan.",
14
14
  "workspaces.select.errorAction": "Gå till startsidan",
15
15
  "workspaces.select.emptyTitle": "Nu sätter vi igång",
16
- "workspaces.select.emptyText": "För att komma igång behöver vi skapa ett workspace. Ett workspace kapslar in resurser, resursmallar, faktureringsinformation och vilka användare som har tillgång.",
16
+ "workspaces.select.emptyText": "För att komma igång behöver vi skapa ett workspace. Ett workspace kapslar in resurser, scheman, faktureringsinformation och vilka användare som har tillgång.",
17
17
  "workspaces.select.emptyAction": "Skapa ett workspace",
18
- "workspaces/create.label": "Skapa workspace",
19
- "workspaces/create.description": "Skapa ett nytt workspace för den inloggade användaren",
20
- "workspaces/accept-invitation.label": "Acceptera inbjudan",
21
- "workspaces/accept-invitation.description": "Acceptera en workspace-inbjudan",
22
- "workspaces/create-api-token.label": "Skapa workspace API-nyckel",
23
- "workspaces/create-api-token.description": "Skapa en API-nyckel kopplad till workspace",
24
- "workspaces/disable-service.label": "Inaktivera tjänst",
25
- "workspaces/disable-service.description": "Inaktivera en tjänst för workspace",
26
- "workspaces/enable-service.label": "Aktivera tjänst",
27
- "workspaces/enable-service.description": "Aktivera en tjänst för workspace",
28
- "workspaces/get-api-tokens.label": "Hämta workspace API-nycklar",
29
- "workspaces/get-api-tokens.description": "Lista API-nycklar för workspace",
30
- "workspaces/get-invitations.label": "Hämta inbjudningar",
31
- "workspaces/get-invitations.description": "Lista väntande workspace-inbjudningar",
32
- "workspaces/get-resource-templates.label": "Hämta resursmallar",
33
- "workspaces/get-resource-templates.description": "Lista resursmallar tillgängliga för workspace",
34
- "workspaces/get-users.label": "Hämta workspace-användare",
35
- "workspaces/get-users.description": "Lista medlemmar i workspace",
36
- "workspaces/get.label": "Hämta workspace",
37
- "workspaces/get.description": "Ladda ett workspace via id",
38
- "workspaces/import-resource-templates.label": "Importera resursmallar",
39
- "workspaces/import-resource-templates.description": "Importera resursmallar till workspace",
40
- "workspaces/invite-user.label": "Bjud in användare",
41
- "workspaces/invite-user.description": "Skicka en workspace-inbjudan till en användare",
42
- "workspaces/remove-member.label": "Ta bort medlem",
43
- "workspaces/remove-member.description": "Ta bort en användare från workspace",
44
- "workspaces/list.label": "Lista workspaces",
45
- "workspaces/list.description": "Lista workspaces för den inloggade användaren",
18
+ "@ossy/workspaces/actions/create.label": "Skapa workspace",
19
+ "@ossy/workspaces/actions/create.description": "Skapa ett nytt workspace för den inloggade användaren",
20
+ "@ossy/workspaces/actions/accept-invitation.label": "Acceptera inbjudan",
21
+ "@ossy/workspaces/actions/accept-invitation.description": "Acceptera en workspace-inbjudan",
22
+ "@ossy/workspaces/actions/create-api-token.label": "Skapa workspace API-nyckel",
23
+ "@ossy/workspaces/actions/create-api-token.description": "Skapa en API-nyckel kopplad till workspace",
24
+ "@ossy/workspaces/actions/disable-service.label": "Inaktivera tjänst",
25
+ "@ossy/workspaces/actions/disable-service.description": "Inaktivera en tjänst för workspace",
26
+ "@ossy/workspaces/actions/enable-service.label": "Aktivera tjänst",
27
+ "@ossy/workspaces/actions/enable-service.description": "Aktivera en tjänst för workspace",
28
+ "@ossy/workspaces/actions/get-api-tokens.label": "Hämta workspace API-nycklar",
29
+ "@ossy/workspaces/actions/get-api-tokens.description": "Lista API-nycklar för workspace",
30
+ "@ossy/workspaces/actions/get-invitations.label": "Hämta inbjudningar",
31
+ "@ossy/workspaces/actions/get-invitations.description": "Lista väntande workspace-inbjudningar",
32
+ "@ossy/workspaces/actions/get-schemas.label": "Hämta scheman",
33
+ "@ossy/workspaces/actions/get-schemas.description": "Lista scheman tillgängliga för workspace",
34
+ "@ossy/workspaces/actions/get-users.label": "Hämta workspace-användare",
35
+ "@ossy/workspaces/actions/get-users.description": "Lista medlemmar i workspace",
36
+ "@ossy/workspaces/actions/get.label": "Hämta workspace",
37
+ "@ossy/workspaces/actions/get.description": "Ladda ett workspace via id",
38
+ "@ossy/workspaces/actions/import-schemas.label": "Importera scheman",
39
+ "@ossy/workspaces/actions/import-schemas.description": "Importera scheman till workspace",
40
+ "@ossy/workspaces/actions/invite-user.label": "Bjud in användare",
41
+ "@ossy/workspaces/actions/invite-user.description": "Skicka en workspace-inbjudan till en användare",
42
+ "@ossy/workspaces/actions/remove-member.label": "Ta bort medlem",
43
+ "@ossy/workspaces/actions/remove-member.description": "Ta bort en användare från workspace",
44
+ "@ossy/workspaces/actions/list.label": "Lista workspaces",
45
+ "@ossy/workspaces/actions/list.description": "Lista workspaces för den inloggade användaren",
46
46
  "workspace/settings.documentTitle": "Workspace-inställningar",
47
47
  "workspace.settings.title": "Workspace-inställningar",
48
48
  "workspace/users.documentTitle": "Användare",
@@ -54,10 +54,16 @@
54
54
  "workspace/invitations/add.documentTitle": "Bjud in användare",
55
55
  "workspace.invitations.title": "Inbjudningar",
56
56
  "workspace.settings.detailsTitle": "Workspace-detaljer",
57
+ "workspace.settings.detailsDescription": "Hantera workspace-namn, tjänster och metadata.",
57
58
  "workspace.settings.edit": "Redigera",
58
59
  "workspace.settings.name": "Namn",
59
60
  "workspace.settings.created": "Skapad",
60
61
  "workspace.settings.services": "Tjänster",
62
+ "workspace.settings.service.booking": "Bokning",
63
+ "workspace.settings.service.on": "på",
64
+ "workspace.settings.service.off": "av",
65
+ "workspace.settings.service.enable": "Aktivera",
66
+ "workspace.settings.service.disable": "Inaktivera",
61
67
  "workspace.settings.loading": "Laddar...",
62
68
  "workspace.invitations.description": "Bjud in användare till ditt workspace för att samarbeta.",
63
69
  "workspace.invitations.invite": "Bjud in användare",
@@ -1,20 +1,21 @@
1
1
  import { Aggregate } from '@ossy/event-store'
2
2
  import { User, UsersEvents } from '@ossy/users/server'
3
3
  import { JoinWorkspace, LeaveWorkspace } from '@ossy/users'
4
+ import { WorkspaceSchema } from './schema-ids.js'
4
5
 
5
6
  export const metadata = {
6
- id: 'sync-workspace-membership',
7
+ id: '@ossy/workspaces/tasks/sync-workspace-membership',
7
8
  triggers: [
8
- { aggregateType: 'Workspace', event: 'Created' },
9
- { aggregateType: 'Workspace', event: 'UserInvitationAccepted' },
10
- { aggregateType: 'Workspace', event: 'UserRemoved' },
9
+ { type: WorkspaceSchema.workspace, event: 'Created' },
10
+ { type: WorkspaceSchema.workspace, event: 'UserInvitationAccepted' },
11
+ { type: WorkspaceSchema.workspace, event: 'UserRemoved' },
11
12
  ],
12
13
  }
13
14
 
14
15
  export async function run ({ event, sdk }) {
15
- const workspaceId = event.aggregateId
16
+ const workspaceId = event.resourceId
16
17
 
17
- if (event.type === 'UserRemoved') {
18
+ if (event.event === 'UserRemoved') {
18
19
  const userId = event.payload.userId
19
20
 
20
21
  if (sdk) {
@@ -1,6 +1,6 @@
1
1
  import { EmailLayout, EmailButton, EmailText } from '@ossy/email'
2
2
 
3
- export const id = 'workspaces/invitation'
3
+ export const id = '@ossy/workspaces/emails/invitation'
4
4
  export const subject = 'You have been invited'
5
5
 
6
6
  export default function WorkspaceInvitationEmail({ workspace, invitedBy, verificationToken, baseUrl = 'https://app.ossy.se', theme }) {
@@ -1,5 +1,5 @@
1
1
  import React from 'react'
2
- import { View } from '@ossy/design-system'
2
+ import { Page } from '@ossy/design-system'
3
3
  import { Users } from './Users.jsx'
4
4
 
5
5
  export const metadata = {
@@ -7,9 +7,9 @@ export const metadata = {
7
7
  }
8
8
 
9
9
  export const WorkspaceUsersContent = () => (
10
- <View gap="m" surface="primary" style={{ padding: 'var(--space-m) var(--space-l)', height: '100%', overflowY: 'auto' }}>
10
+ <Page>
11
11
  <Users />
12
- </View>
12
+ </Page>
13
13
  )
14
14
 
15
15
  export default WorkspaceUsersContent
@@ -1,50 +1,42 @@
1
+ import { normalizeWorkspaceServices } from './entitlements.js'
2
+ import { WorkspaceSchema } from './schema-ids.js'
3
+
1
4
  export class Workspace {
2
5
 
6
+ static kind = 'entity'
7
+ static SchemaId = WorkspaceSchema.workspace
3
8
  static AggregateType = 'Workspace'
4
9
 
5
- static View(events, state = {}) {
6
- return events
7
- .reduce((workspace, event) => {
8
-
9
- switch (event.type) {
10
+ static View (events, state = {}) {
11
+ const folded = events.reduce((workspace, event) => {
12
+ switch (event.event) {
10
13
 
11
14
  case 'Created':
12
15
  return {
13
16
  ...workspace,
14
- id: event.aggregateId,
17
+ id: event.resourceId,
18
+ type: event.type,
15
19
  name: event.payload.name,
16
20
  createdBy: event.createdBy,
17
21
  created: event.created,
18
22
  apiTokens: [],
19
23
  users: [event.createdBy],
20
24
  invitations: [],
21
- resourceTemplates: [],
22
- services: {
23
- // '@ossy/visual-content-classifiers': true,
24
- '@ossy/tasks/resize-common-web': true,
25
- '@ossy/apps': true,
26
- '@ossy/domains': true,
27
- }
25
+ schemas: [],
26
+ services: {},
28
27
  }
29
28
 
30
- case 'ResourceTemplatesImported':
29
+ case 'SchemasImported':
31
30
  return {
32
31
  ...workspace,
33
- resourceTemplates: [
34
- ...(event.payload.templates || []),
35
- ]
32
+ schemas: [...(event.payload.schemas || [])],
36
33
  }
37
34
 
38
35
  case 'UserInvited': {
39
36
  const oneWeek = 1000 * 60 * 60 * 24 * 7
40
37
  const expired = event.payload.expiresAt < Date.now()
41
- // show expired workspace invitations for 7 days
42
38
  const showInvitation = (event.payload.expiresAt + oneWeek) > Date.now()
43
-
44
- if (!showInvitation) {
45
- return workspace
46
- }
47
-
39
+ if (!showInvitation) return workspace
48
40
  return {
49
41
  ...workspace,
50
42
  invitations: [
@@ -53,42 +45,35 @@ export class Workspace {
53
45
  email: event.payload.email,
54
46
  invitedAt: event.created,
55
47
  expiresAt: event.payload.expiresAt,
56
- expired: expired
57
- }
58
- ]
48
+ expired,
49
+ },
50
+ ],
59
51
  }
60
52
  }
61
53
 
62
- case 'UserInvitationAccepted': {
54
+ case 'UserInvitationAccepted':
63
55
  return {
64
56
  ...workspace,
65
- users: [
66
- ...workspace.users,
67
- event.createdBy
68
- ],
69
- invitations: workspace.invitations.filter(invitation => invitation.email !== event.payload.email)
57
+ users: [...workspace.users, event.createdBy],
58
+ invitations: workspace.invitations.filter(invitation => invitation.email !== event.payload.email),
70
59
  }
71
- }
72
60
 
73
61
  case 'ApiTokenCreated':
74
62
  return {
75
63
  ...workspace,
76
64
  apiTokens: [
77
- ...(apiTokens || []),
78
- {
79
- id: event.id,
80
- description: event.payload.description
81
- }
82
- ]
65
+ ...(workspace.apiTokens || []),
66
+ { id: event.id, description: event.payload.description },
67
+ ],
83
68
  }
84
-
69
+
85
70
  case 'ServiceEnabled':
86
71
  return {
87
72
  ...workspace,
88
73
  services: {
89
74
  ...(workspace.services || {}),
90
- [event.payload.service]: true
91
- }
75
+ [event.payload.service]: { enabled: true },
76
+ },
92
77
  }
93
78
 
94
79
  case 'ServiceDisabled':
@@ -96,23 +81,26 @@ export class Workspace {
96
81
  ...workspace,
97
82
  services: {
98
83
  ...(workspace.services || {}),
99
- [event.payload.service]: false
100
- }
84
+ [event.payload.service]: { enabled: false },
85
+ },
101
86
  }
102
87
 
103
88
  case 'UserRemoved':
104
89
  return {
105
90
  ...workspace,
106
- users: (workspace.users || []).filter(id => id !== event.payload.userId)
91
+ users: (workspace.users || []).filter(id => id !== event.payload.userId),
107
92
  }
108
93
 
109
94
  default:
110
95
  return workspace
111
-
112
96
  }
113
97
  }, state)
114
- }
115
98
 
99
+ return {
100
+ ...folded,
101
+ services: normalizeWorkspaceServices(folded.services),
102
+ }
103
+ }
116
104
  }
117
105
 
118
106
  export { Workspace as Aggregate }
@@ -0,0 +1,9 @@
1
+ export default {
2
+ id: '@ossy/workspaces/schema/workspace',
3
+ name: 'Workspace',
4
+ categoryName: 'Workspaces',
5
+ icon: 'building',
6
+ fields: [
7
+ { name: 'name', type: 'text' },
8
+ ],
9
+ }