@ossy/resources 3.10.0 → 3.11.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.
Files changed (86) hide show
  1. package/package.json +4 -4
  2. package/src/CreateDirectory.jsx +47 -53
  3. package/src/CreateDocument.jsx +1 -0
  4. package/src/GenericResourceForm.jsx +49 -31
  5. package/src/ResourceDialogMove.jsx +99 -50
  6. package/src/ResourceGrid.jsx +36 -84
  7. package/src/ResourceList.jsx +10 -1
  8. package/src/ResourcePanel.jsx +96 -17
  9. package/src/ResourceShareDialog.jsx +50 -12
  10. package/src/Upload.jsx +25 -8
  11. package/src/batch-delete.flow.js +81 -0
  12. package/src/batch-download.flow.js +69 -0
  13. package/src/cancel-upload.action.js +4 -0
  14. package/src/cancel-upload.flow.js +40 -0
  15. package/src/close-share.action.js +4 -0
  16. package/src/confirm-upload.action.js +4 -0
  17. package/src/copy-share-link.action.js +4 -0
  18. package/src/create-directory.action.js +5 -0
  19. package/src/create-directory.flow.js +30 -0
  20. package/src/create-directory.form.js +5 -0
  21. package/src/create-directory.schema.js +9 -0
  22. package/src/create-document.flow.js +36 -0
  23. package/src/create-document.form.js +5 -0
  24. package/src/create-document.schema.js +11 -0
  25. package/src/create.action.js +5 -1
  26. package/src/delete-directory.flow.js +39 -0
  27. package/src/delete-file.flow.js +32 -0
  28. package/src/done-selecting.action.js +5 -0
  29. package/src/download-file.flow.js +31 -0
  30. package/src/download-resource.action.js +4 -0
  31. package/src/download-selected.action.js +4 -0
  32. package/src/en.translations.json +18 -0
  33. package/src/finish-upload.action.js +4 -0
  34. package/src/index.js +42 -0
  35. package/src/list.task.js +17 -6
  36. package/src/list.task.spec.js +81 -0
  37. package/src/make-private-resource.flow.js +72 -0
  38. package/src/make-private.action.js +4 -0
  39. package/src/make-public.action.js +4 -0
  40. package/src/move-file.flow.js +78 -0
  41. package/src/move.form.js +5 -0
  42. package/src/move.schema.js +9 -0
  43. package/src/open-delete-directory.action.js +7 -0
  44. package/src/open-move.action.js +4 -0
  45. package/src/open-remove-selected.action.js +5 -0
  46. package/src/open-rename.action.js +4 -0
  47. package/src/open-share.action.js +4 -0
  48. package/src/open-sort.action.js +5 -0
  49. package/src/open-view.action.js +5 -0
  50. package/src/rename-file.flow.js +43 -0
  51. package/src/rename.form.js +5 -0
  52. package/src/rename.schema.js +9 -0
  53. package/src/resource-list-query.js +182 -0
  54. package/src/resource-list-query.spec.js +75 -0
  55. package/src/resource-selection.js +25 -0
  56. package/src/resource-selection.spec.js +40 -0
  57. package/src/resource.helpers.js +8 -1
  58. package/src/resources-list-options.js +169 -0
  59. package/src/resources-list-options.spec.js +141 -0
  60. package/src/resources.action-media.js +6 -0
  61. package/src/resources.queries.js +91 -6
  62. package/src/search-payload.js +10 -0
  63. package/src/search-payload.spec.js +20 -0
  64. package/src/search.task.js +2 -1
  65. package/src/share-resource.flow.js +59 -0
  66. package/src/sort-ascending.action.js +5 -0
  67. package/src/sort-by-created.action.js +5 -0
  68. package/src/sort-by-last-updated.action.js +5 -0
  69. package/src/sort-by-name.action.js +5 -0
  70. package/src/sort-by-size.action.js +5 -0
  71. package/src/sort-by-type.action.js +5 -0
  72. package/src/sort-descending.action.js +5 -0
  73. package/src/sort-storage-created.flow.js +87 -0
  74. package/src/sort-storage-last-updated.flow.js +119 -0
  75. package/src/sort-storage-name-asc.flow.js +34 -0
  76. package/src/sort-storage-size-desc.flow.js +34 -0
  77. package/src/sort-storage-size.flow.js +87 -0
  78. package/src/sort-storage-type.flow.js +83 -0
  79. package/src/sort-storage.flow.js +86 -0
  80. package/src/sv.translations.json +18 -0
  81. package/src/toggle-selection-mode.action.js +4 -0
  82. package/src/upload-file.flow.js +44 -0
  83. package/src/useResourceList.js +94 -0
  84. package/src/view-grid.action.js +5 -0
  85. package/src/view-list.action.js +5 -0
  86. package/src/view-storage.flow.js +49 -0
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@ossy/resources",
3
3
  "description": "Resource domain — aggregate and events for the Ossy resource model",
4
- "version": "3.10.0",
4
+ "version": "3.11.0",
5
5
  "private": false,
6
6
  "type": "module",
7
7
  "main": "./src/index.js",
@@ -23,9 +23,9 @@
23
23
  "src": "./src"
24
24
  },
25
25
  "dependencies": {
26
- "@ossy/event-store": "^3.8.0",
26
+ "@ossy/event-store": "^3.11.0",
27
27
  "@ossy/fold": "^3.4.0",
28
- "@ossy/platform": "^3.9.0",
28
+ "@ossy/platform": "^3.11.0",
29
29
  "@ossy/schema": "^3.8.0"
30
30
  },
31
31
  "peerDependencies": {
@@ -49,5 +49,5 @@
49
49
  "/src",
50
50
  "README.md"
51
51
  ],
52
- "gitHead": "198084eb98871876396ae625d043dd0f03aa0325"
52
+ "gitHead": "53ff8456920e09151c9d88df4a6c3ee958d811eb"
53
53
  }
@@ -1,91 +1,85 @@
1
- import React, { useState, useRef } from 'react'
1
+ import React, { useMemo, useState } from 'react'
2
2
  import { createDirectory } from './resource.helpers.js'
3
3
  import { useSdk } from '@ossy/sdk-react'
4
- import { Input, Button, View, useInputValue, Text, useLocale } from '@ossy/design-system'
4
+ import { Alert, Button, FieldFactory, Form, View, Text, useLocale } from '@ossy/design-system'
5
5
  import { useRouter } from '@ossy/router-react'
6
+ import { Schema, contentValidator } from '@ossy/schema'
7
+ import { metadata as CreateDirectoryAction } from './create-directory.action.js'
8
+ import { metadata as CreateDirectoryForm } from './create-directory.form.js'
9
+ import createDirectoryTemplate from './create-directory.schema.js'
6
10
 
7
11
  export const CreateDirectory = () => {
8
12
  const { t } = useLocale()
9
13
  const router = useRouter()
10
14
  const sdk = useSdk()
11
- const [directoryName, setDirectoryName] = useInputValue('')
12
15
  const [error, setError] = useState()
13
- const formRef = useRef()
14
16
  const directoryLocation = router.searchParams.location || '/'
15
17
 
16
- const onCreateDirectory = event => {
17
- event.preventDefault()
18
- setError()
18
+ const validate = useMemo(
19
+ () => contentValidator(Schema.of({ schemas: [createDirectoryTemplate] }), createDirectoryTemplate),
20
+ [],
21
+ )
19
22
 
23
+ const onCreateDirectory = (data) => {
24
+ setError()
25
+ const directoryName = String(data?.name ?? '').trim()
20
26
 
21
- if (!directoryName || directoryName.trim() === '') {
22
- return setError(t('resources.createDirectory.errorEmpty'))
27
+ if (!directoryName) {
28
+ setError(t('resources.createDirectory.errorEmpty'))
29
+ return
23
30
  }
24
31
 
25
- if (directoryName?.startsWith?.('@ossy') || directoryName?.startsWith?.('/@ossy')) {
32
+ if (directoryName.startsWith('@ossy') || directoryName.startsWith('/@ossy')) {
26
33
  setError(t('resources.createDirectory.errorOssyPrefix'))
27
34
  return
28
35
  }
29
36
 
30
- formRef.current.reportValidity() && createDirectory(sdk, {
37
+ createDirectory(sdk, {
31
38
  name: directoryName,
32
- location: directoryLocation
39
+ location: directoryLocation,
33
40
  })
34
41
  .then(() => {
35
- setDirectoryName()
36
- router.navigate(`@storage/home?location=${directoryLocation}`)
42
+ router.navigate(`@storage/home?location=${encodeURIComponent(directoryLocation)}`)
37
43
  })
38
- .catch(err => {
39
- setError(err.message)
44
+ .catch((err) => {
45
+ setError(err?.message || t('resources.form.createFailed'))
40
46
  })
41
47
  }
42
48
 
43
49
  return (
44
- <View gap="s" style={{ height: '100%' }}>
45
-
46
- <Text variant="heading-primary" style={{ marginBottom: 'var(--space-m)' }} as="h1" text="resources.createDirectory.title" />
47
-
48
- <Text style={{ marginBottom: 'var(--space-m)' }}>
49
- {t('resources.createDirectory.description', { location: directoryLocation })}
50
- </Text>
51
-
52
- <form
53
- ref={formRef}
50
+ <View gap="s" style={{ height: '100%' }} data-flow-stage="create-directory">
51
+ <Form
52
+ id={CreateDirectoryForm.id}
53
+ schemaId={CreateDirectoryForm.schemaId}
54
+ fields={createDirectoryTemplate.fields}
55
+ defaultData={{}}
54
56
  onSubmit={onCreateDirectory}
55
- style={{ width: '100%', flexGrow: '1', display: 'flex', flexDirection: 'column', gap: 'var(--space-m)', justifyContent: 'space-between' }}
57
+ validate={validate}
58
+ gap="m"
59
+ style={{ width: '100%', flexGrow: 1, display: 'flex', flexDirection: 'column', justifyContent: 'space-between' }}
56
60
  >
61
+ <View gap="m">
62
+ <Text variant="heading-primary" as="h1" text="resources.createDirectory.title" />
63
+ <Text>
64
+ {t('resources.createDirectory.description', { location: directoryLocation })}
65
+ </Text>
66
+ <FieldFactory />
67
+ {error ? <Alert variant="danger">{error}</Alert> : null}
68
+ </View>
57
69
 
58
- <Input
59
- id="path"
60
- type="text"
61
- placeholder={t('resources.createDirectory.namePlaceholder')}
62
- value={directoryName}
63
- onChange={setDirectoryName}
64
- style={{ width: '100%', marginBottom: 'var(--space-l)' }}
65
- />
66
-
67
- <div>{ error }</div>
68
-
69
-
70
- <View layout="row" style={{ gap: 'var(--space-s)', justifyContent: 'flex-end' }} >
71
-
70
+ <View layout="row" gap="s" justifyContent="flex-end">
72
71
  <Button
73
- id="cancel"
72
+ variant="link"
74
73
  onClick={() => router.navigate('@back')}
75
- >{t('design-system.cancel')}
76
- </Button>
77
-
74
+ label="design-system.cancel"
75
+ />
78
76
  <Button
79
- id="createDirectory"
77
+ {...CreateDirectoryAction}
80
78
  variant="cta"
81
- onClick={onCreateDirectory}
82
- >{t('resources.createDirectory.submit')}
83
- </Button>
84
-
79
+ type="submit"
80
+ />
85
81
  </View>
86
- </form>
87
-
82
+ </Form>
88
83
  </View>
89
84
  )
90
85
  }
91
-
@@ -77,6 +77,7 @@ export const CreateDocument = () => {
77
77
  key={schema.id}
78
78
  variant="neutral"
79
79
  prefix={schema.icon || 'add'}
80
+ data-schema-id={schema.id}
80
81
  onClick={() => selectSchema(schema.id)}
81
82
  >
82
83
  {schema.name || schema.id}
@@ -1,21 +1,24 @@
1
1
  import React, { useMemo, useState } from 'react'
2
2
  import { useSchema } from './useSchemas.js'
3
3
  import { useSchemaEngine } from './useSchemaEngine.js'
4
- import { validateContent } from '@ossy/schema'
4
+ import { Schema, contentValidator, validateContent } from '@ossy/schema'
5
5
  import { resolveFormComponentChain } from '@ossy/schema/resolve'
6
6
  import {
7
7
  Slot,
8
8
  View,
9
9
  Text,
10
- InputTitle,
11
10
  Button,
12
11
  Alert,
13
12
  Fields,
13
+ Form,
14
+ FieldFactory,
14
15
  applyFieldChange,
15
- useInputValue,
16
16
  useLocale,
17
17
  } from '@ossy/design-system'
18
18
  import { SchemaFormDefault } from './SchemaFormDefault.jsx'
19
+ import { metadata as CreateResource } from './create.action.js'
20
+ import { metadata as CreateDocumentForm } from './create-document.form.js'
21
+ import createDocumentTemplate from './create-document.schema.js'
19
22
 
20
23
  /** CMS create-resource form — name + schema fields (resources-specific tertiary fallback). */
21
24
  function ResourceCreateFormFallback ({
@@ -29,13 +32,17 @@ function ResourceCreateFormFallback ({
29
32
  const { t } = useLocale()
30
33
  const template = useSchema(schemaId)
31
34
  const engine = useSchemaEngine()
32
- const [name, setName] = useInputValue(initialName)
33
35
  const [content, setContent] = useState(initialContent)
34
36
  const [error, setError] = useState()
35
37
 
36
- const handleSubmit = (event) => {
37
- event?.preventDefault?.()
38
- const title = name?.trim() || (typeof content?.name === 'string' ? content.name.trim() : '')
38
+ const validate = useMemo(
39
+ () => contentValidator(Schema.of({ schemas: [createDocumentTemplate] }), createDocumentTemplate),
40
+ [],
41
+ )
42
+
43
+ const handleSubmit = (data) => {
44
+ const title = String(data?.name ?? '').trim()
45
+ || (typeof content?.name === 'string' ? content.name.trim() : '')
39
46
  if (!title) {
40
47
  setError(t('resources.form.nameRequired'))
41
48
  return
@@ -58,31 +65,42 @@ function ResourceCreateFormFallback ({
58
65
  }
59
66
  }
60
67
 
68
+ const defaultSubmitLabel = template?.name
69
+ ? `${t('design-system.add')} ${template.name}`
70
+ : t('resources.form.createResource')
71
+
61
72
  return (
62
- <View as="form" stack gap="m" onSubmit={handleSubmit}>
63
- <InputTitle
64
- id="resource-name"
65
- type="text"
66
- placeholder={t('resources.form.untitled')}
67
- value={name}
68
- onChange={setName}
69
- />
70
- <Fields
71
- schemaId={schemaId}
72
- data={content}
73
- onChange={(event) => {
74
- setError(undefined)
75
- setContent((prev) => applyFieldChange(prev, event))
76
- }}
77
- fields={template?.fields || []}
78
- />
79
- {error && <Alert variant="danger">{error}</Alert>}
80
- <View layout="row" gap="s" justifyContent="flex-end">
81
- {onCancel && <Button variant="link" onClick={onCancel} label="design-system.cancel" />}
82
- <Button type="submit" variant="cta">
83
- {submitLabel || (template?.name ? `${t('design-system.add')} ${template.name}` : t('resources.form.createResource'))}
84
- </Button>
85
- </View>
73
+ <View gap="s" data-flow-stage="create-document" data-schema-id={schemaId}>
74
+ <Form
75
+ id={CreateDocumentForm.id}
76
+ schemaId={CreateDocumentForm.schemaId}
77
+ fields={createDocumentTemplate.fields}
78
+ defaultData={{ name: initialName }}
79
+ onSubmit={handleSubmit}
80
+ validate={validate}
81
+ gap="m"
82
+ >
83
+ <FieldFactory />
84
+ <Fields
85
+ schemaId={schemaId}
86
+ data={content}
87
+ onChange={(event) => {
88
+ setError(undefined)
89
+ setContent((prev) => applyFieldChange(prev, event))
90
+ }}
91
+ fields={template?.fields || []}
92
+ />
93
+ {error && <Alert variant="danger">{error}</Alert>}
94
+ <View layout="row" gap="s" justifyContent="flex-end">
95
+ {onCancel && <Button variant="link" onClick={onCancel} label="design-system.cancel" />}
96
+ <Button
97
+ {...CreateResource}
98
+ type="submit"
99
+ variant="cta"
100
+ label={submitLabel || defaultSubmitLabel}
101
+ />
102
+ </View>
103
+ </Form>
86
104
  </View>
87
105
  )
88
106
  }
@@ -1,65 +1,114 @@
1
- import React from 'react'
1
+ import React, { useEffect, useState } from 'react'
2
2
  import { moveResource } from './resource.helpers.js'
3
3
  import { useSdk } from '@ossy/sdk-react'
4
- import { Button, View, Input, Text, useInputValue } from '@ossy/design-system'
4
+ import {
5
+ Alert,
6
+ Button,
7
+ FieldFactory,
8
+ Form,
9
+ Overlay,
10
+ Text,
11
+ View,
12
+ useLocale,
13
+ } from '@ossy/design-system'
14
+ import { metadata as GetResource } from './get.action.js'
15
+ import { metadata as UpdateResourceLocation } from './update-location.action.js'
16
+ import { metadata as MoveForm } from './move.form.js'
17
+ import moveTemplate from './move.schema.js'
5
18
 
19
+ /**
20
+ * Move a resource to a new location.
21
+ *
22
+ * @param {{
23
+ * resourceId?: string | null,
24
+ * isVisible?: boolean,
25
+ * onClose?: () => void,
26
+ * }} props
27
+ */
6
28
  export const ResourceDialogMove = ({
7
- resourceId,
29
+ resourceId,
30
+ isVisible = true,
8
31
  onClose = () => {},
9
32
  }) => {
10
33
  const sdk = useSdk()
11
- const [location, setLocation] = useInputValue('')
12
-
13
- const onMove = () => {
14
- moveResource(sdk, resourceId, location)
15
- .then(() => {
16
- onClose()
17
- })
18
- .catch(error => {
19
- console.error('Failed to move resource:', error)
20
- // Handle error (e.g., show a notification)
21
- })
22
- }
34
+ const { t } = useLocale()
35
+ const [error, setError] = useState()
36
+ const [busy, setBusy] = useState(false)
37
+ const { data: resource = {} } = sdk.read(
38
+ GetResource,
39
+ { resourceId },
40
+ { enabled: Boolean(resourceId) && isVisible },
41
+ )
23
42
 
24
- return (
25
- <View
26
- layout="off-center-s" style={{ height: '100%' }}
27
- >
43
+ useEffect(() => {
44
+ if (!isVisible) {
45
+ setError(undefined)
46
+ setBusy(false)
47
+ }
48
+ }, [isVisible])
28
49
 
29
- <View data-region="content">
30
- <View surface="primary" roundness="s">
31
- <View surface="primary" inset="l" roundness="s" gap="m">
50
+ const onMove = (data) => {
51
+ const target = String(data?.target ?? '').trim()
52
+ if (!resourceId || busy) return
53
+ if (!target) {
54
+ setError(t('resources.move.errorEmpty'))
55
+ return
56
+ }
32
57
 
58
+ setError(undefined)
59
+ setBusy(true)
60
+ moveResource(sdk, resourceId, target, resource?.location)
61
+ .then(() => onClose())
62
+ .catch((err) => {
63
+ console.error('Failed to move resource:', err)
64
+ setError(err?.message || t('resources.move.errorFailed'))
65
+ })
66
+ .finally(() => setBusy(false))
67
+ }
33
68
 
34
- <View>
35
- <Text variant="heading-tertiary" as="h3">Move Resource</Text>
36
- </View>
69
+ if (!isVisible) return null
37
70
 
38
- <View>
39
- <Text style={{ fontWeight: 'bold' }}>
40
- New location:
41
- </Text>
42
- <Input
43
- value={location}
44
- onChange={setLocation}
45
- placeholder="/path/to/target/resource"
46
- />
71
+ return (
72
+ <Overlay isVisible={true} onClose={onClose}>
73
+ <View
74
+ layout="off-center-s"
75
+ style={{ height: '100%' }}
76
+ data-flow-stage="move-resource"
77
+ >
78
+ <View data-region="content">
79
+ <View surface="primary" roundness="s">
80
+ <View surface="primary" inset="l" roundness="s" gap="m">
81
+ <Text variant="heading-tertiary" as="h3" text="resources.move.title" />
82
+ <Form
83
+ id={MoveForm.id}
84
+ schemaId={MoveForm.schemaId}
85
+ fields={moveTemplate.fields}
86
+ defaultData={{}}
87
+ onSubmit={onMove}
88
+ gap="m"
89
+ >
90
+ <FieldFactory />
91
+ {error ? <Alert variant="danger">{error}</Alert> : null}
92
+ <View layout="row" gap="s" justifyContent="flex-end">
93
+ <Button
94
+ type="button"
95
+ variant="command"
96
+ onClick={onClose}
97
+ label="design-system.cancel"
98
+ disabled={busy}
99
+ />
100
+ <Button
101
+ {...UpdateResourceLocation}
102
+ variant="cta"
103
+ type="submit"
104
+ disabled={busy}
105
+ />
106
+ </View>
107
+ </Form>
108
+ </View>
109
+ </View>
47
110
  </View>
48
-
49
- <View>
50
-
51
- <Button onClick={onClose} style={{ marginTop: 'var(--space-m)' }}>
52
- Close
53
- </Button>
54
-
55
- <Button variant="cta" onClick={onMove}>
56
- Move
57
- </Button>
58
- </View>
59
-
60
- </View>
61
- </View>
62
111
  </View>
63
- </View>
112
+ </Overlay>
64
113
  )
65
- }
114
+ }
@@ -4,6 +4,7 @@ import { useSchemas } from './useSchemas.js'
4
4
  import {
5
5
  Dropdown,
6
6
  DropZone,
7
+ GridView,
7
8
  Icon,
8
9
  Image,
9
10
  View,
@@ -15,8 +16,6 @@ import { formatBytes } from './utils/format-bytes.js'
15
16
  import { ResourceSelectionCheckbox } from './ResourceSelectionCheckbox.jsx'
16
17
  import { resourceAccessIcon, isShareableResource } from './resource-share.js'
17
18
 
18
- const TILE_WIDTH = 160
19
-
20
19
  function useSchemaMap() {
21
20
  const templates = useSchemas()
22
21
  return useMemo(
@@ -40,13 +39,7 @@ export const ResourceGrid = ({
40
39
  }, [selectedIds])
41
40
 
42
41
  return (
43
- <View
44
- layout="row-wrap"
45
- gap="m"
46
- inset="m"
47
- alignItems="stretch"
48
- style={{ alignContent: 'flex-start' }}
49
- >
42
+ <GridView>
50
43
  {inlineFolder && <InlineFolderTile {...inlineFolder} />}
51
44
  {resources.map(resource => (
52
45
  <ResourceGridItem
@@ -59,7 +52,7 @@ export const ResourceGrid = ({
59
52
  selected={selectedSet.has(resource.id) || Boolean(resource.selected)}
60
53
  />
61
54
  ))}
62
- </View>
55
+ </GridView>
63
56
  )
64
57
  }
65
58
 
@@ -90,20 +83,10 @@ function InlineFolderTile({ name, onChange, onSave, onCancel }) {
90
83
  }
91
84
 
92
85
  return (
93
- <View
94
- surface="primary"
95
- roundness="m"
96
- inset="m"
97
- gap="s"
98
- style={{
99
- width: TILE_WIDTH,
100
- border: '1px dashed var(--separator-primary)',
101
- boxSizing: 'border-box',
102
- }}
86
+ <GridView.Item
87
+ leading={<Icon size="m" name="folder" style={{ fill: 'hsl(0, 0%, 60%)' }} />}
88
+ style={{ borderStyle: 'dashed' }}
103
89
  >
104
- <View layout="row" alignItems="center" justifyContent="center" style={{ height: 96 }}>
105
- <Icon size="m" name="folder" style={{ fill: 'hsl(0, 0%, 60%)' }} />
106
- </View>
107
90
  <input
108
91
  ref={inputRef}
109
92
  value={name}
@@ -124,7 +107,7 @@ function InlineFolderTile({ name, onChange, onSave, onCancel }) {
124
107
  boxSizing: 'border-box',
125
108
  }}
126
109
  />
127
- </View>
110
+ </GridView.Item>
128
111
  )
129
112
  }
130
113
 
@@ -177,32 +160,8 @@ const ResourceGridItem = memo(function ResourceGridItem({
177
160
  Container = DropZone.Dragable
178
161
  }
179
162
 
180
- const tile = (
181
- <View
182
- as={href ? 'a' : 'div'}
183
- href={href}
184
- onClick={handleClick}
185
- onContextMenu={handleContextMenu}
186
- selectable
187
- data-selected={selected || undefined}
188
- surface="primary"
189
- roundness="m"
190
- inset="m"
191
- gap="s"
192
- data-resource-id={resource.id}
193
- data-resource-access={resource.access || 'restricted'}
194
- style={{
195
- width: TILE_WIDTH,
196
- textDecoration: 'none',
197
- color: 'inherit',
198
- cursor: 'pointer',
199
- border: selected
200
- ? '1px solid var(--foreground-active, var(--separator-primary))'
201
- : '1px solid var(--separator-primary)',
202
- boxSizing: 'border-box',
203
- position: 'relative',
204
- }}
205
- >
163
+ const overlay = (
164
+ <>
206
165
  {showSelectionControls && (
207
166
  <View
208
167
  style={{
@@ -236,41 +195,34 @@ const ResourceGridItem = memo(function ResourceGridItem({
236
195
  />
237
196
  </View>
238
197
  )}
239
- {actions ? (
240
- <View
241
- style={{
242
- position: 'absolute',
243
- top: 'var(--space-xs, 4px)',
244
- right: 'var(--space-xs, 4px)',
245
- zIndex: 1,
246
- }}
247
- onClick={event => event.stopPropagation()}
248
- >
249
- <RowActions
250
- actions={actions}
251
- open={menuOpen}
252
- onOpenChange={handleMenuOpenChange}
253
- position={menuPosition}
254
- onTriggerClick={handleActionsTriggerClick}
255
- />
256
- </View>
198
+ </>
199
+ )
200
+
201
+ const tile = (
202
+ <GridView.Item
203
+ href={href}
204
+ onClick={handleClick}
205
+ onContextMenu={handleContextMenu}
206
+ selectable
207
+ selected={selected}
208
+ overlay={overlay}
209
+ leading={<ResourceThumb type={type} content={content} templateMap={templateMap} />}
210
+ meta={<Size content={content} />}
211
+ trailing={actions ? (
212
+ <RowActions
213
+ actions={actions}
214
+ open={menuOpen}
215
+ onOpenChange={handleMenuOpenChange}
216
+ position={menuPosition}
217
+ onTriggerClick={handleActionsTriggerClick}
218
+ />
257
219
  ) : null}
258
- <View layout="row" alignItems="center" justifyContent="center" style={{ height: 96 }}>
259
- <ResourceThumb type={type} content={content} templateMap={templateMap} />
260
- </View>
261
- <Text
262
- variant="small"
263
- style={{
264
- fontWeight: 'bold',
265
- overflow: 'hidden',
266
- textOverflow: 'ellipsis',
267
- whiteSpace: 'nowrap',
268
- }}
269
- >
270
- {typeof name === 'string' ? name : ''}
271
- </Text>
272
- <Size content={content} />
273
- </View>
220
+ data-resource-id={resource.id}
221
+ data-resource-name={typeof resource.name === 'string' ? resource.name : undefined}
222
+ data-resource-access={resource.access || 'restricted'}
223
+ >
224
+ {typeof name === 'string' ? name : ''}
225
+ </GridView.Item>
274
226
  )
275
227
 
276
228
  return (
@@ -203,6 +203,7 @@ const ResourceListItem = memo(function ResourceListItem({
203
203
  />
204
204
  )}
205
205
  data-resource-id={resource.id}
206
+ data-resource-name={typeof name === 'string' ? name : undefined}
206
207
  data-resource-access={resource.access || 'restricted'}
207
208
  >
208
209
  {typeof name === 'string' ? name : ''}
@@ -255,7 +256,15 @@ function RowActions({ actions, open, onOpenChange, position, onTriggerClick }) {
255
256
 
256
257
  return (
257
258
  <Dropdown
258
- trigger={<Button prefix="more-vertical-alt" variant="command" onClick={onTriggerClick} />}
259
+ trigger={(
260
+ <Button
261
+ prefix="more-vertical-alt"
262
+ variant="command"
263
+ onClick={onTriggerClick}
264
+ data-resource-actions
265
+ aria-label="Resource actions"
266
+ />
267
+ )}
259
268
  open={open}
260
269
  onOpenChange={onOpenChange}
261
270
  position={position}