Compare commits

...
Author SHA1 Message Date
StandardNotes CI 5f96281948 chore(release): publish
- @standardnotes/[email protected]
 - @standardnotes/[email protected].9
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].3
 - @standardnotes/[email protected].1
2023-01-06 16:02:46 +00:00
Aman Harwara 2e79fc412c fix: Fixed issue where the importer would not correctly parse Google Keep JSON notes 2023-01-06 20:57:18 +05:30
StandardNotes CI 5bdbfe9fc7 chore(release): publish
- @standardnotes/[email protected].4
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].0
 - @standardnotes/[email protected].2
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].8
 - @standardnotes/[email protected].0
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].2
 - @standardnotes/[email protected].2
 - @standardnotes/[email protected].2
 - @standardnotes/[email protected].0
2023-01-06 11:59:51 +00:00
Aman Harwara dd8ccdeadc feat: Added per-tag preference to use table layout and removed "Files Table View" from Labs 2023-01-06 17:00:19 +05:30
StandardNotes CI eea97362f8 chore(release): publish
- @standardnotes/[email protected]
 - @standardnotes/[email protected].7
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].1
 - @standardnotes/[email protected].1
2023-01-05 20:59:57 +00:00
Aman Harwara 01294868c9 Merge branch 'main' of github.com:standardnotes/app 2023-01-06 02:02:16 +05:30
Aman Harwara 77ec9ca335 refactor: files table view -> content table view 2023-01-06 02:02:07 +05:30
StandardNotes CI 3e6c752967 chore(release): publish
- @standardnotes/[email protected]
 - @standardnotes/[email protected].6
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].0
 - @standardnotes/[email protected].0
2023-01-05 18:43:40 +00:00
Aman Harwara ba017d42f5 feat: Added keyboard navigation to Files table view (#2131) 2023-01-05 23:45:10 +05:30
StandardNotes CI bd3df71dd4 chore(release): publish
- @standardnotes/[email protected]
 - @standardnotes/[email protected].5
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].0
2023-01-05 10:28:19 +00:00
Aman Harwara 22c6e2c56c feat: Added linked items container & autocomplete input to file preview modal 2023-01-05 15:32:18 +05:30
58 changed files with 854 additions and 245 deletions
+4
View File
@@ -3,6 +3,10 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.21.4](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-06)
**Note:** Version bump only for package @standardnotes/api
## [1.21.3](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-04)
**Note:** Version bump only for package @standardnotes/api
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/api",
"version": "1.21.3",
"version": "1.21.4",
"engines": {
"node": ">=16.0.0 <17.0.0"
},
+20
View File
@@ -3,6 +3,26 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [3.104.73](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-06)
**Note:** Version bump only for package @standardnotes/desktop
## [3.104.72](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-06)
**Note:** Version bump only for package @standardnotes/desktop
## [3.104.71](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-05)
**Note:** Version bump only for package @standardnotes/desktop
## [3.104.70](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-05)
**Note:** Version bump only for package @standardnotes/desktop
## [3.104.69](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-05)
**Note:** Version bump only for package @standardnotes/desktop
## [3.104.68](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-04)
**Note:** Version bump only for package @standardnotes/desktop
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@standardnotes/desktop",
"main": "./app/dist/index.js",
"version": "3.104.68",
"version": "3.104.73",
"license": "AGPL-3.0-or-later",
"author": "Standard Notes.",
"private": true,
+4
View File
@@ -3,6 +3,10 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.19.35](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-06)
**Note:** Version bump only for package @standardnotes/encryption
## [1.19.34](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-04)
**Note:** Version bump only for package @standardnotes/encryption
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/encryption",
"version": "1.19.34",
"version": "1.19.35",
"engines": {
"node": ">=16.0.0 <17.0.0"
},
+6
View File
@@ -3,6 +3,12 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [1.58.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-06)
### Features
* Added per-tag preference to use table layout and removed "Files Table View" from Labs ([dd8ccde](https://github.com/standardnotes/app/commit/dd8ccdeadc6c2fd7f7ccd66f2dc4b0081616fb2b))
## [1.57.1](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-29)
**Note:** Version bump only for package @standardnotes/features
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/features",
"version": "1.57.1",
"version": "1.58.0",
"engines": {
"node": ">=16.0.0 <17.0.0"
},
@@ -38,8 +38,6 @@ export enum FeatureIdentifier {
TaskEditor = 'org.standardnotes.simple-task-editor',
TokenVaultEditor = 'org.standardnotes.token-vault',
FilesTableView = 'org.standardnotes.files-table-view',
DeprecatedBoldEditor = 'org.standardnotes.bold-editor',
DeprecatedMarkdownBasicEditor = 'org.standardnotes.simple-markdown-editor',
DeprecatedMarkdownMathEditor = 'org.standardnotes.fancy-markdown-editor',
@@ -53,4 +51,4 @@ export enum FeatureIdentifier {
*/
export const LegacyFileSafeIdentifier = 'org.standardnotes.legacy.file-safe'
export const ExperimentalFeatures = [FeatureIdentifier.FilesTableView]
export const ExperimentalFeatures = []
@@ -1,18 +1,5 @@
import { RoleName, SubscriptionName } from '@standardnotes/common'
import { FeatureDescription } from '../Feature/FeatureDescription'
import { FeatureIdentifier } from '../Feature/FeatureIdentifier'
import { PermissionName } from '../Permission/PermissionName'
export function experimentalFeatures(): FeatureDescription[] {
const filesTableView: FeatureDescription = {
identifier: FeatureIdentifier.FilesTableView,
name: 'Files Table View',
description:
'Replaces the current Files view with a table view, with name, size, and date sort options. Requires reload to take effect.',
availableInSubscriptions: [SubscriptionName.PlusPlan, SubscriptionName.ProPlan],
availableInRoles: [RoleName.PlusUser, RoleName.ProUser],
permission_name: PermissionName.FilesTableView,
}
return [filesTableView]
return []
}
@@ -40,5 +40,4 @@ export enum PermissionName {
TwoFactorAuth = 'server:two-factor-auth',
SubscriptionSharing = 'server:subscription-sharing',
SuperEditor = 'editor:super-editor',
FilesTableView = 'app:files-table-view',
}
+4
View File
@@ -3,6 +3,10 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.28.2](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-06)
**Note:** Version bump only for package @standardnotes/filepicker
## [1.28.1](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-04)
**Note:** Version bump only for package @standardnotes/filepicker
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/filepicker",
"version": "1.28.1",
"version": "1.28.2",
"engines": {
"node": ">=16.0.0 <17.0.0"
},
+4
View File
@@ -3,6 +3,10 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.14.12](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-06)
**Note:** Version bump only for package @standardnotes/files
## [1.14.11](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-04)
**Note:** Version bump only for package @standardnotes/files
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/files",
"version": "1.14.11",
"version": "1.14.12",
"engines": {
"node": ">=16.0.0 <17.0.0"
},
+20
View File
@@ -3,6 +3,26 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [3.50.9](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-06)
**Note:** Version bump only for package @standardnotes/mobile
## [3.50.8](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-06)
**Note:** Version bump only for package @standardnotes/mobile
## [3.50.7](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-05)
**Note:** Version bump only for package @standardnotes/mobile
## [3.50.6](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-05)
**Note:** Version bump only for package @standardnotes/mobile
## [3.50.5](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-05)
**Note:** Version bump only for package @standardnotes/mobile
## [3.50.4](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-04)
**Note:** Version bump only for package @standardnotes/mobile
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/mobile",
"version": "3.50.4",
"version": "3.50.9",
"author": "Standard Notes.",
"private": true,
"license": "AGPL-3.0-or-later",
+6
View File
@@ -3,6 +3,12 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [1.42.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-06)
### Features
* Added per-tag preference to use table layout and removed "Files Table View" from Labs ([dd8ccde](https://github.com/standardnotes/app/commit/dd8ccdeadc6c2fd7f7ccd66f2dc4b0081616fb2b))
## [1.41.1](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-04)
**Note:** Version bump only for package @standardnotes/models
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/models",
"version": "1.41.1",
"version": "1.42.0",
"engines": {
"node": ">=16.0.0 <17.0.0"
},
@@ -18,4 +18,5 @@ export interface TagPreferences {
editorIdentifier?: EditorIdentifier
entryMode?: 'normal' | 'daily'
panelWidth?: number
useTableView?: boolean
}
+20
View File
@@ -3,6 +3,26 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.4.96](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-06)
**Note:** Version bump only for package @standardnotes/releases
## [1.4.95](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-06)
**Note:** Version bump only for package @standardnotes/releases
## [1.4.94](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-05)
**Note:** Version bump only for package @standardnotes/releases
## [1.4.93](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-05)
**Note:** Version bump only for package @standardnotes/releases
## [1.4.92](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-05)
**Note:** Version bump only for package @standardnotes/releases
## [1.4.91](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-04)
**Note:** Version bump only for package @standardnotes/releases
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/releases",
"version": "1.4.91",
"version": "1.4.96",
"license": "AGPL-3.0-or-later",
"main": "dist/releases.json",
"types": "dist/index.d.ts",
+4
View File
@@ -3,6 +3,10 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.12.13](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-06)
**Note:** Version bump only for package @standardnotes/responses
## [1.12.12](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-29)
**Note:** Version bump only for package @standardnotes/responses
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/responses",
"version": "1.12.12",
"version": "1.12.13",
"engines": {
"node": ">=16.0.0 <17.0.0"
},
+4
View File
@@ -3,6 +3,10 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.52.2](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-06)
**Note:** Version bump only for package @standardnotes/services
## [1.52.1](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-04)
**Note:** Version bump only for package @standardnotes/services
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/services",
"version": "1.52.1",
"version": "1.52.2",
"engines": {
"node": ">=16.0.0 <17.0.0"
},
+4
View File
@@ -3,6 +3,10 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [2.158.2](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-06)
**Note:** Version bump only for package @standardnotes/snjs
## [2.158.1](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-04)
**Note:** Version bump only for package @standardnotes/snjs
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/snjs",
"version": "2.158.1",
"version": "2.158.2",
"engines": {
"node": ">=16.0.0 <17.0.0"
},
+20
View File
@@ -3,6 +3,26 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.23.3](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-06)
### Bug Fixes
* Fixed issue where the importer would not correctly parse Google Keep JSON notes ([2e79fc4](https://github.com/standardnotes/app/commit/2e79fc412c8b57b175381c4f59593a03906d89a1))
## [1.23.2](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-06)
**Note:** Version bump only for package @standardnotes/ui-services
## [1.23.1](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-05)
**Note:** Version bump only for package @standardnotes/ui-services
# [1.23.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-05)
### Features
* Added keyboard navigation to Files table view ([#2131](https://github.com/standardnotes/app/issues/2131)) ([ba017d4](https://github.com/standardnotes/app/commit/ba017d42f5145a3f9fb4a4610d7582bd8571b29c))
## [1.22.4](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-04)
**Note:** Version bump only for package @standardnotes/ui-services
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/ui-services",
"version": "1.22.4",
"version": "1.23.3",
"engines": {
"node": ">=16.0.0 <17.0.0"
},
@@ -98,13 +98,13 @@ export class GoogleKeepConverter {
static isValidGoogleKeepJson(json: any): boolean {
return (
json.title &&
json.textContent &&
json.userEditedTimestampUsec &&
typeof json.title === 'string' &&
typeof json.textContent === 'string' &&
typeof json.userEditedTimestampUsec === 'number' &&
typeof json.isArchived === 'boolean' &&
typeof json.isTrashed === 'boolean' &&
typeof json.isPinned === 'boolean' &&
json.color
typeof json.color === 'string'
)
}
@@ -10,4 +10,6 @@ export enum KeyboardKey {
Home = 'Home',
End = 'End',
Space = ' ',
PageUp = 'PageUp',
PageDown = 'PageDown',
}
+26
View File
@@ -3,6 +3,32 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [3.136.1](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-06)
**Note:** Version bump only for package @standardnotes/web
# [3.136.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-06)
### Features
* Added per-tag preference to use table layout and removed "Files Table View" from Labs ([dd8ccde](https://github.com/standardnotes/app/commit/dd8ccdeadc6c2fd7f7ccd66f2dc4b0081616fb2b))
## [3.135.1](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-05)
**Note:** Version bump only for package @standardnotes/web
# [3.135.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-05)
### Features
* Added keyboard navigation to Files table view ([#2131](https://github.com/standardnotes/app/issues/2131)) ([ba017d4](https://github.com/standardnotes/app/commit/ba017d42f5145a3f9fb4a4610d7582bd8571b29c))
# [3.134.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-05)
### Features
* Added linked items container & autocomplete input to file preview modal ([22c6e2c](https://github.com/standardnotes/app/commit/22c6e2c56c656cdbbc9788e5e9dc9112b22a9127))
# [3.133.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-04)
### Features
+64
View File
@@ -1,5 +1,69 @@
{
"versions": [
{
"version": "3.136.1",
"title": "[3.136.1](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-06)",
"date": null,
"body": "**Note:** Version bump only for package @standardnotes/web",
"parsed": {
"_": [
"Note: Version bump only for package @standardnotes/web"
]
}
},
{
"version": "3.136.0",
"title": "[3.136.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-06)",
"date": null,
"body": "### Features\n\n* Added per-tag preference to use table layout and removed \"Files Table View\" from Labs ([dd8ccde](https://github.com/standardnotes/app/commit/dd8ccdeadc6c2fd7f7ccd66f2dc4b0081616fb2b))",
"parsed": {
"_": [
"Added per-tag preference to use table layout and removed \"Files Table View\" from Labs (dd8ccde)"
],
"Features": [
"Added per-tag preference to use table layout and removed \"Files Table View\" from Labs (dd8ccde)"
]
}
},
{
"version": "3.135.1",
"title": "[3.135.1](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-05)",
"date": null,
"body": "**Note:** Version bump only for package @standardnotes/web",
"parsed": {
"_": [
"Note: Version bump only for package @standardnotes/web"
]
}
},
{
"version": "3.135.0",
"title": "[3.135.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-05)",
"date": null,
"body": "### Features\n\n* Added keyboard navigation to Files table view ([#2131](https://github.com/standardnotes/app/issues/2131)) ([ba017d4](https://github.com/standardnotes/app/commit/ba017d42f5145a3f9fb4a4610d7582bd8571b29c))",
"parsed": {
"_": [
"Added keyboard navigation to Files table view (#2131) (ba017d4)"
],
"Features": [
"Added keyboard navigation to Files table view (#2131) (ba017d4)"
]
}
},
{
"version": "3.134.0",
"title": "[3.134.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-05)",
"date": null,
"body": "### Features\n\n* Added linked items container & autocomplete input to file preview modal ([22c6e2c](https://github.com/standardnotes/app/commit/22c6e2c56c656cdbbc9788e5e9dc9112b22a9127))",
"parsed": {
"_": [
"Added linked items container & autocomplete input to file preview modal (22c6e2c)"
],
"Features": [
"Added linked items container & autocomplete input to file preview modal (22c6e2c)"
]
}
},
{
"version": "3.133.0",
"title": "[3.133.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-04)",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/web",
"version": "3.133.0",
"version": "3.136.1",
"license": "AGPL-3.0-or-later",
"main": "dist/app.js",
"author": "Standard Notes.",
@@ -12,7 +12,7 @@ import { NavigationController } from '@/Controllers/Navigation/NavigationControl
import { NotesController } from '@/Controllers/NotesController/NotesController'
import { ElementIds } from '@/Constants/ElementIDs'
import { classNames } from '@standardnotes/utils'
import { ContentType, FeatureIdentifier, SNTag } from '@standardnotes/snjs'
import { ContentType, SNTag } from '@standardnotes/snjs'
type Props = {
application: WebApplication
@@ -97,8 +97,6 @@ const ContentList: FunctionComponent<Props> = ({
const hasNotes = items.some((item) => item.content_type === ContentType.Note)
const isFilesTableViewEnabled = application.features.isExperimentalFeatureEnabled(FeatureIdentifier.FilesTableView)
return (
<div
className={classNames(
@@ -112,13 +110,7 @@ const ContentList: FunctionComponent<Props> = ({
onKeyDown={onKeyDown}
tabIndex={FOCUSABLE_BUT_NOT_TABBABLE}
>
{items.map((item, index) => {
const previousIndex = index - 1
const previousItem = previousIndex >= 0 ? items[previousIndex] : undefined
const nextIndex = index + 1
const nextItem = nextIndex < items.length ? items[nextIndex] : undefined
{items.map((item) => {
return (
<ContentListItem
key={item.uuid}
@@ -134,8 +126,6 @@ const ContentList: FunctionComponent<Props> = ({
onSelect={selectItem}
tags={getTagsForItem(item)}
notesController={notesController}
isPreviousItemTiled={previousItem?.content_type === ContentType.File && !isFilesTableViewEnabled}
isNextItemTiled={nextItem?.content_type === ContentType.File && !isFilesTableViewEnabled}
/>
)
})}
@@ -1,25 +1,16 @@
import { ContentType, FeatureIdentifier, FileItem, SNNote } from '@standardnotes/snjs'
import { ContentType, FileItem, SNNote } from '@standardnotes/snjs'
import React, { FunctionComponent } from 'react'
import FileListItem from './FileListItem'
import FileListItemCard from './FileListItemCard'
import NoteListItem from './NoteListItem'
import { AbstractListItemProps, doListItemPropsMeritRerender } from './Types/AbstractListItemProps'
import { ListableContentItem } from './Types/ListableContentItem'
const ContentListItem: FunctionComponent<AbstractListItemProps<ListableContentItem>> = (props) => {
const isFilesTableViewEnabled = props.application.features.isExperimentalFeatureEnabled(
FeatureIdentifier.FilesTableView,
)
switch (props.item.content_type) {
case ContentType.Note:
return <NoteListItem {...props} item={props.item as SNNote} />
case ContentType.File: {
if (isFilesTableViewEnabled) {
return <FileListItem {...props} item={props.item as FileItem} />
}
return <FileListItemCard {...props} item={props.item as FileItem} />
return <FileListItem {...props} item={props.item as FileItem} />
}
default:
return null
@@ -9,7 +9,7 @@ import {
} from '@standardnotes/ui-services'
import { WebApplication } from '@/Application/Application'
import { PANEL_NAME_NOTES } from '@/Constants/Constants'
import { FeatureIdentifier, FileItem, PrefKey, SystemViewId, WebAppEvent } from '@standardnotes/snjs'
import { FileItem, PrefKey, WebAppEvent } from '@standardnotes/snjs'
import { observer } from 'mobx-react-lite'
import { forwardRef, useCallback, useEffect, useMemo } from 'react'
import ContentList from '@/Components/ContentListView/ContentList'
@@ -36,9 +36,11 @@ import { FeatureName } from '@/Controllers/FeatureName'
import { PanelResizedData } from '@/Types/PanelResizedData'
import { useForwardedRef } from '@/Hooks/useForwardedRef'
import FloatingAddButton from './FloatingAddButton'
import FilesTableView from '../FilesTableView/FilesTableView'
import ContentTableView from '../ContentTableView/ContentTableView'
import { FeaturesController } from '@/Controllers/FeaturesController'
import { MutuallyExclusiveMediaQueryBreakpoints, useMediaQuery } from '@/Hooks/useMediaQuery'
import { HistoryModalController } from '@/Controllers/NoteHistory/HistoryModalController'
import { PaneController } from '@/Controllers/PaneController/PaneController'
type Props = {
accountMenuController: AccountMenuController
@@ -52,6 +54,8 @@ type Props = {
searchOptionsController: SearchOptionsController
linkingController: LinkingController
featuresController: FeaturesController
historyModalController: HistoryModalController
paneController: PaneController
className?: string
id: string
children?: React.ReactNode
@@ -72,6 +76,8 @@ const ContentListView = forwardRef<HTMLDivElement, Props>(
searchOptionsController,
linkingController,
featuresController,
historyModalController,
paneController,
className,
id,
children,
@@ -91,6 +97,7 @@ const ContentListView = forwardRef<HTMLDivElement, Props>(
renderedItems,
items,
isCurrentNoteTemplate,
isTableViewEnabled,
} = itemListController
const innerRef = useForwardedRef(ref)
@@ -182,9 +189,7 @@ const ContentListView = forwardRef<HTMLDivElement, Props>(
}, [isFilesSmartView, filesController, createNewNote, toggleAppPane, application])
const isMobileScreen = useMediaQuery(MutuallyExclusiveMediaQueryBreakpoints.sm)
const isFilesTableViewEnabled = application.features.isExperimentalFeatureEnabled(FeatureIdentifier.FilesTableView)
const shouldShowFilesTableView =
isFilesTableViewEnabled && !isMobileScreen && selectedTag?.uuid === SystemViewId.Files
const shouldUseTableView = (isFilesSmartView || isTableViewEnabled) && !isMobileScreen
useEffect(() => {
const searchBarElement = document.getElementById(ElementIds.SearchBar)
@@ -208,7 +213,7 @@ const ContentListView = forwardRef<HTMLDivElement, Props>(
if (searchBarElement === document.activeElement) {
searchBarElement?.blur()
}
if (shouldShowFilesTableView) {
if (shouldUseTableView) {
return
}
selectNextItem()
@@ -218,7 +223,7 @@ const ContentListView = forwardRef<HTMLDivElement, Props>(
command: PREVIOUS_LIST_ITEM_KEYBOARD_COMMAND,
element: document.body,
onKeyDown: () => {
if (shouldShowFilesTableView) {
if (shouldUseTableView) {
return
}
selectPreviousItem()
@@ -262,7 +267,7 @@ const ContentListView = forwardRef<HTMLDivElement, Props>(
selectNextItem,
selectPreviousItem,
selectionController,
shouldShowFilesTableView,
shouldUseTableView,
])
const shortcutForCreate = useMemo(
@@ -316,18 +321,19 @@ const ContentListView = forwardRef<HTMLDivElement, Props>(
addButtonLabel={addButtonLabel}
addNewItem={addNewItem}
isFilesSmartView={isFilesSmartView}
isFilesTableViewEnabled={isFilesTableViewEnabled}
isTableViewEnabled={isTableViewEnabled}
optionsSubtitle={optionsSubtitle}
selectedTag={selectedTag}
filesController={filesController}
itemListController={itemListController}
paneController={paneController}
/>
)}
{(!shouldShowFilesTableView || isMobileScreen) && (
{(!shouldUseTableView || isMobileScreen) && (
<SearchBar
itemListController={itemListController}
searchOptionsController={searchOptionsController}
hideOptions={shouldShowFilesTableView}
hideOptions={shouldUseTableView}
/>
)}
<NoAccountWarning
@@ -352,13 +358,16 @@ const ContentListView = forwardRef<HTMLDivElement, Props>(
<p className="empty-items-list opacity-50">Loading...</p>
) : null}
{!dailyMode && renderedItems.length ? (
shouldShowFilesTableView ? (
<FilesTableView
shouldUseTableView ? (
<ContentTableView
items={items}
application={application}
filesController={filesController}
featuresController={featuresController}
linkingController={linkingController}
navigationController={navigationController}
notesController={notesController}
historyModalController={historyModalController}
/>
) : (
<ContentList
@@ -13,6 +13,7 @@ import AddItemMenuButton from './AddItemMenuButton'
import { FilesController } from '@/Controllers/FilesController'
import SearchButton from './SearchButton'
import { ItemListController } from '@/Controllers/ItemList/ItemListController'
import { PaneController } from '@/Controllers/PaneController/PaneController'
type Props = {
application: WebApplication
@@ -21,11 +22,12 @@ type Props = {
addButtonLabel: string
addNewItem: () => void
isFilesSmartView: boolean
isFilesTableViewEnabled: boolean
isTableViewEnabled: boolean
optionsSubtitle?: string
selectedTag: AnyTag
filesController: FilesController
itemListController: ItemListController
paneController: PaneController
}
const ContentListHeader = ({
@@ -35,11 +37,12 @@ const ContentListHeader = ({
addButtonLabel,
addNewItem,
isFilesSmartView,
isFilesTableViewEnabled,
isTableViewEnabled,
optionsSubtitle,
selectedTag,
filesController,
itemListController,
paneController,
}: Props) => {
const displayOptionsContainerRef = useRef<HTMLDivElement>(null)
const displayOptionsButtonRef = useRef<HTMLButtonElement>(null)
@@ -79,19 +82,13 @@ const ContentListHeader = ({
isFilesSmartView={isFilesSmartView}
isOpen={showDisplayOptionsMenu}
selectedTag={selectedTag}
paneController={paneController}
/>
</Popover>
</div>
</div>
)
}, [
showDisplayOptionsMenu,
toggleDisplayOptionsMenu,
displayOptionsButtonRef,
application,
isFilesSmartView,
selectedTag,
])
}, [showDisplayOptionsMenu, toggleDisplayOptionsMenu, application, isFilesSmartView, selectedTag, paneController])
const AddButton = useMemo(() => {
return (
@@ -106,12 +103,12 @@ const ContentListHeader = ({
}, [addButtonLabel, addNewItem, filesController, isDailyEntry, isFilesSmartView])
const SearchBarButton = useMemo(() => {
if (!isFilesSmartView || !isFilesTableViewEnabled || isMobileScreen) {
if (!isTableViewEnabled || isMobileScreen) {
return null
}
return <SearchButton itemListController={itemListController} />
}, [isFilesSmartView, isFilesTableViewEnabled, isMobileScreen, itemListController])
}, [isTableViewEnabled, isMobileScreen, itemListController])
const FolderName = useMemo(() => {
return (
@@ -1,7 +1,6 @@
import {
CollectionSort,
CollectionSortProperty,
FeatureIdentifier,
IconType,
isSmartView,
isSystemView,
@@ -25,6 +24,8 @@ import NoSubscriptionBanner from '@/Components/NoSubscriptionBanner/NoSubscripti
import MenuRadioButtonItem from '@/Components/Menu/MenuRadioButtonItem'
import MenuSwitchButtonItem from '@/Components/Menu/MenuSwitchButtonItem'
import { Pill } from '@/Components/Preferences/PreferencesComponents/Content'
import { MutuallyExclusiveMediaQueryBreakpoints, useMediaQuery } from '@/Hooks/useMediaQuery'
import { PaneLayout } from '@/Controllers/PaneController/PaneLayout'
const DailyEntryModeEnabled = true
@@ -33,6 +34,7 @@ const DisplayOptionsMenu: FunctionComponent<DisplayOptionsMenuProps> = ({
isOpen,
isFilesSmartView,
selectedTag,
paneController,
}) => {
const isSystemTag = isSmartView(selectedTag) && isSystemView(selectedTag)
const selectedTagPreferences = isSystemTag
@@ -217,6 +219,14 @@ const DisplayOptionsMenu: FunctionComponent<DisplayOptionsMenuProps> = ({
void changePreferences({ entryMode: isDailyEntry ? 'normal' : 'daily' })
}, [isDailyEntry, changePreferences])
const toggleTableView = useCallback(() => {
const useTableView = !preferences.useTableView
void changePreferences({ useTableView })
if (useTableView) {
paneController.setPaneLayout(PaneLayout.TableView)
}
}, [preferences.useTableView, changePreferences, paneController])
const TabButton: FunctionComponent<{
label: string
mode: PreferenceMode
@@ -250,8 +260,9 @@ const DisplayOptionsMenu: FunctionComponent<DisplayOptionsMenuProps> = ({
)
}
const isFilesTableViewEnabled = application.features.isExperimentalFeatureEnabled(FeatureIdentifier.FilesTableView)
const shouldHideNonApplicableOptions = isFilesTableViewEnabled && selectedTag?.uuid === SystemViewId.Files
const isMobileScreen = useMediaQuery(MutuallyExclusiveMediaQueryBreakpoints.sm)
const isTableViewEnabled = Boolean(isFilesSmartView || preferences.useTableView)
const shouldHideNonApplicableOptions = isTableViewEnabled && !isMobileScreen
return (
<Menu className="text-sm" a11yLabel="Notes list options menu" isOpen={isOpen}>
@@ -410,7 +421,7 @@ const DisplayOptionsMenu: FunctionComponent<DisplayOptionsMenuProps> = ({
</>
)}
{currentMode === 'tag' && !isSystemTag && DailyEntryModeEnabled && (
{currentMode === 'tag' && !isSystemTag && DailyEntryModeEnabled && !isTableViewEnabled && (
<>
<MenuItemSeparator />
<MenuSwitchButtonItem
@@ -432,6 +443,28 @@ const DisplayOptionsMenu: FunctionComponent<DisplayOptionsMenuProps> = ({
</>
)}
{currentMode === 'tag' && !isSystemTag && !isDailyEntry && (
<>
<MenuItemSeparator />
<MenuSwitchButtonItem
disabled={controlsDisabled}
className="py-1 hover:bg-contrast focus:bg-info-backdrop"
checked={isTableViewEnabled}
onChange={toggleTableView}
>
<div className="flex flex-col pr-5">
<div className="flex flex-row items-center">
<div className="text-base font-semibold uppercase text-text lg:text-xs">Table view</div>
<Pill className="py-0 px-1.5" style="success">
Labs
</Pill>
</div>
<div className="mt-1">Display the notes & files in the current tag in a table layout</div>
</div>
</MenuSwitchButtonItem>
</>
)}
{!shouldHideNonApplicableOptions && (!isSystemTag || currentMode === 'global') && (
<>
<MenuItemSeparator />
@@ -1,5 +1,6 @@
import { WebApplication } from '@/Application/Application'
import { AnyTag } from '@/Controllers/Navigation/AnyTagType'
import { PaneController } from '@/Controllers/PaneController/PaneController'
export type DisplayOptionsMenuPositionProps = {
top: number
@@ -11,4 +12,5 @@ export type DisplayOptionsMenuProps = {
selectedTag: AnyTag
isOpen: boolean
isFilesSmartView: boolean
paneController: PaneController
}
@@ -5,7 +5,6 @@ import { formatDateForContextMenu } from '@/Utils/DateUtils'
import { getIconForFileType } from '@/Utils/Items/Icons/getIconForFileType'
import { formatSizeToReadableString } from '@standardnotes/filepicker'
import {
ContentType,
FileItem,
SortableItem,
PrefKey,
@@ -13,6 +12,8 @@ import {
naturalSort,
FileBackupRecord,
SystemViewId,
DecryptedItemInterface,
SNNote,
} from '@standardnotes/snjs'
import { useState, useEffect, useCallback, useMemo, useRef } from 'react'
import { FileItemActionType } from '../AttachedFilesPopover/PopoverFileItemAction'
@@ -32,21 +33,42 @@ import { FeaturesController } from '@/Controllers/FeaturesController'
import { MutuallyExclusiveMediaQueryBreakpoints, useMediaQuery } from '@/Hooks/useMediaQuery'
import { useApplication } from '../ApplicationProvider'
import { NavigationController } from '@/Controllers/Navigation/NavigationController'
import { getIconAndTintForNoteType } from '@/Utils/Items/Icons/getIconAndTintForNoteType'
import NotesOptions from '../NotesOptions/NotesOptions'
import { NotesController } from '@/Controllers/NotesController/NotesController'
import { HistoryModalController } from '@/Controllers/NoteHistory/HistoryModalController'
const ContextMenuCell = ({
files,
items,
filesController,
navigationController,
linkingController,
notesController,
historyModalController,
}: {
files: FileItem[]
items: DecryptedItemInterface[]
filesController: FilesController
navigationController: NavigationController
linkingController: LinkingController
notesController: NotesController
historyModalController: HistoryModalController
}) => {
const application = useApplication()
const [contextMenuVisible, setContextMenuVisible] = useState(false)
const anchorElementRef = useRef<HTMLButtonElement>(null)
const allItemsAreNotes = useMemo(() => {
return items.every((item) => item instanceof SNNote)
}, [items])
const allItemsAreFiles = useMemo(() => {
return items.every((item) => item instanceof FileItem)
}, [items])
if (!allItemsAreNotes && !allItemsAreFiles) {
return null
}
return (
<>
<button
@@ -71,30 +93,45 @@ const ContextMenuCell = ({
className="py-2"
>
<Menu a11yLabel="File context menu" isOpen={contextMenuVisible}>
<FileMenuOptions
linkingController={linkingController}
navigationController={navigationController}
closeMenu={() => {
setContextMenuVisible(false)
}}
filesController={filesController}
shouldShowRenameOption={false}
shouldShowAttachOption={false}
selectedFiles={files}
/>
{allItemsAreFiles && (
<FileMenuOptions
linkingController={linkingController}
navigationController={navigationController}
closeMenu={() => {
setContextMenuVisible(false)
}}
filesController={filesController}
shouldShowRenameOption={false}
shouldShowAttachOption={false}
selectedFiles={items as FileItem[]}
/>
)}
{allItemsAreNotes && (
<NotesOptions
notes={items as SNNote[]}
application={application}
navigationController={navigationController}
notesController={notesController}
linkingController={linkingController}
historyModalController={historyModalController}
closeMenu={() => {
setContextMenuVisible(false)
}}
/>
)}
</Menu>
</Popover>
</>
)
}
const FileLinksCell = ({
file,
const ItemLinksCell = ({
item,
filesController,
linkingController,
featuresController,
}: {
file: FileItem
item: DecryptedItemInterface
filesController: FilesController
linkingController: LinkingController
featuresController: FeaturesController
@@ -130,25 +167,38 @@ const FileLinksCell = ({
filesController={filesController}
featuresController={featuresController}
isOpen={contextMenuVisible}
item={file}
item={item}
/>
</Popover>
</>
)
}
const FileNameCell = ({ file }: { file: FileItem }) => {
const ItemNameCell = ({ item }: { item: DecryptedItemInterface }) => {
const application = useApplication()
const [backupInfo, setBackupInfo] = useState<FileBackupRecord | undefined>(undefined)
const isItemFile = item instanceof FileItem
const noteType =
item instanceof SNNote
? item.noteType || application.componentManager.editorForNote(item)?.package_info.note_type
: undefined
const [noteIcon, noteIconTint] = getIconAndTintForNoteType(noteType)
useEffect(() => {
void application.fileBackups?.getFileBackupInfo(file).then(setBackupInfo)
}, [application, file])
if (isItemFile) {
void application.fileBackups?.getFileBackupInfo(item).then(setBackupInfo)
}
}, [application, isItemFile, item])
return (
<div className="flex items-center gap-3 whitespace-normal">
<span className="relative">
{getFileIconComponent(getIconForFileType(file.mimeType), 'w-6 h-6 flex-shrink-0')}
{isItemFile ? (
getFileIconComponent(getIconForFileType(item.mimeType), 'w-6 h-6 flex-shrink-0')
) : (
<Icon type={noteIcon} className={`text-accessory-tint-${noteIconTint}`} />
)}
{backupInfo && (
<div
className="absolute bottom-1 right-1 translate-x-1/2 translate-y-1/2 rounded-full bg-default text-success"
@@ -158,8 +208,8 @@ const FileNameCell = ({ file }: { file: FileItem }) => {
</div>
)}
</span>
<span className="overflow-hidden text-ellipsis whitespace-nowrap text-sm font-medium">{file.title}</span>
{file.protected && (
<span className="overflow-hidden text-ellipsis whitespace-nowrap text-sm font-medium">{item.title}</span>
{item.protected && (
<span className="flex items-center" title="File is protected">
<Icon ariaLabel="File is protected" type="lock-filled" className="h-3.5 w-3.5 text-passive-1" size="custom" />
</span>
@@ -170,22 +220,26 @@ const FileNameCell = ({ file }: { file: FileItem }) => {
type Props = {
application: WebApplication
items: DecryptedItemInterface[]
filesController: FilesController
featuresController: FeaturesController
linkingController: LinkingController
navigationController: NavigationController
notesController: NotesController
historyModalController: HistoryModalController
}
const FilesTableView = ({
const ContentTableView = ({
application,
items,
filesController,
featuresController,
linkingController,
navigationController,
notesController,
historyModalController,
}: Props) => {
const files = application.items
.getDisplayableNotesAndFiles()
.filter((item) => item.content_type === ContentType.File) as FileItem[]
const listHasFiles = items.some((item) => item instanceof FileItem)
const getSortByPreference = useCallback(() => {
const globalPrefValue = application.getPreference(PrefKey.SortNotesBy, PrefDefaults[PrefKey.SortNotesBy])
@@ -231,48 +285,48 @@ const FilesTableView = ({
[application],
)
const [contextMenuFile, setContextMenuFile] = useState<FileItem | undefined>(undefined)
const [contextMenuItem, setContextMenuItem] = useState<DecryptedItemInterface | undefined>(undefined)
const [contextMenuPosition, setContextMenuPosition] = useState<{ x: number; y: number } | undefined>(undefined)
const isSmallBreakpoint = useMediaQuery(MutuallyExclusiveMediaQueryBreakpoints.sm)
const isMediumBreakpoint = useMediaQuery(MutuallyExclusiveMediaQueryBreakpoints.md)
const isLargeBreakpoint = useMediaQuery(MutuallyExclusiveMediaQueryBreakpoints.lg)
const columnDefs: TableColumn<FileItem>[] = useMemo(
const columnDefs: TableColumn<DecryptedItemInterface>[] = useMemo(
() => [
{
name: 'Name',
sortBy: 'title',
cell: (file) => <FileNameCell file={file} />,
cell: (item) => <ItemNameCell item={item} />,
},
{
name: 'Upload date',
sortBy: 'created_at',
cell: (file) => {
return formatDateForContextMenu(file.created_at)
cell: (item) => {
return formatDateForContextMenu(item.created_at)
},
hidden: isSmallBreakpoint,
},
{
name: 'Size',
sortBy: 'decryptedSize',
cell: (file) => {
return formatSizeToReadableString(file.decryptedSize)
cell: (item) => {
return item instanceof FileItem ? formatSizeToReadableString(item.decryptedSize) : null
},
hidden: isSmallBreakpoint,
hidden: isSmallBreakpoint || !listHasFiles,
},
{
name: 'Attached to',
hidden: isSmallBreakpoint || isMediumBreakpoint || isLargeBreakpoint,
cell: (file) => {
cell: (item) => {
const links = [
...naturalSort(application.items.referencesForItem(file), 'title').map((item) =>
...naturalSort(application.items.referencesForItem(item), 'title').map((item) =>
createLinkFromItem(item, 'linked'),
),
...naturalSort(application.items.itemsReferencingItem(file), 'title').map((item) =>
...naturalSort(application.items.itemsReferencingItem(item), 'title').map((item) =>
createLinkFromItem(item, 'linked-by'),
),
...application.items.getSortedTagsForItem(file).map((item) => createLinkFromItem(item, 'linked')),
...application.items.getSortedTagsForItem(item).map((item) => createLinkFromItem(item, 'linked')),
]
if (!links.length) {
@@ -286,7 +340,7 @@ const FilesTableView = ({
link={links[0]}
key={links[0].id}
unlinkItem={async (itemToUnlink) => {
void application.items.unlinkItems(file, itemToUnlink)
void application.items.unlinkItems(item, itemToUnlink)
}}
isBidirectional={false}
/>
@@ -296,13 +350,13 @@ const FilesTableView = ({
},
},
],
[application.items, isLargeBreakpoint, isMediumBreakpoint, isSmallBreakpoint],
[application.items, isLargeBreakpoint, isMediumBreakpoint, isSmallBreakpoint, listHasFiles],
)
const getRowId = useCallback((file: FileItem) => file.uuid, [])
const getRowId = useCallback((item: DecryptedItemInterface) => item.uuid, [])
const table = useTable({
data: files,
data: items,
sortBy,
sortReversed,
onSortChange,
@@ -310,80 +364,103 @@ const FilesTableView = ({
columns: columnDefs,
enableRowSelection: true,
enableMultipleRowSelection: true,
onRowDoubleClick(file) {
void filesController.handleFileAction({
type: FileItemActionType.PreviewFile,
payload: {
file,
otherFiles: files,
},
})
onRowActivate(item) {
if (item instanceof FileItem) {
void filesController.handleFileAction({
type: FileItemActionType.PreviewFile,
payload: {
file: item,
otherFiles: items.filter((i) => i instanceof FileItem) as FileItem[],
},
})
}
},
onRowContextMenu(x, y, file) {
setContextMenuPosition({ x, y })
setContextMenuFile(file)
setContextMenuItem(file)
},
rowActions: (file) => {
rowActions: (item) => {
return (
<div className="flex items-center gap-2">
<FileLinksCell
file={file}
<ItemLinksCell
item={item}
filesController={filesController}
featuresController={featuresController}
linkingController={linkingController}
/>
<ContextMenuCell
files={[file]}
items={[item]}
filesController={filesController}
linkingController={linkingController}
navigationController={navigationController}
notesController={notesController}
historyModalController={historyModalController}
/>
</div>
)
},
selectionActions: (fileIds) => (
selectionActions: (itemIds) => (
<ContextMenuCell
files={files.filter((file) => fileIds.includes(file.uuid))}
items={items.filter((item) => itemIds.includes(item.uuid))}
filesController={filesController}
linkingController={linkingController}
navigationController={navigationController}
notesController={notesController}
historyModalController={historyModalController}
/>
),
showSelectionActions: true,
})
const closeContextMenu = () => {
setContextMenuPosition(undefined)
setContextMenuItem(undefined)
}
return (
<>
<Table table={table} />
{contextMenuPosition && contextMenuFile && (
{contextMenuPosition && contextMenuItem && (
<Popover
open={true}
anchorPoint={contextMenuPosition}
togglePopover={() => {
setContextMenuPosition(undefined)
setContextMenuFile(undefined)
setContextMenuItem(undefined)
}}
side="bottom"
align="start"
className="py-2"
>
<Menu a11yLabel="File context menu" isOpen={true}>
<FileMenuOptions
closeMenu={() => {
setContextMenuPosition(undefined)
setContextMenuFile(undefined)
}}
filesController={filesController}
shouldShowRenameOption={false}
shouldShowAttachOption={false}
selectedFiles={[contextMenuFile]}
linkingController={linkingController}
navigationController={navigationController}
/>
</Menu>
{contextMenuItem instanceof FileItem && (
<Menu a11yLabel="File context menu" isOpen={true}>
<FileMenuOptions
closeMenu={closeContextMenu}
filesController={filesController}
shouldShowRenameOption={false}
shouldShowAttachOption={false}
selectedFiles={[contextMenuItem]}
linkingController={linkingController}
navigationController={navigationController}
/>
</Menu>
)}
{contextMenuItem instanceof SNNote && (
<Menu className="select-none" a11yLabel="Note context menu" isOpen={true}>
<NotesOptions
notes={[contextMenuItem]}
application={application}
navigationController={navigationController}
notesController={notesController}
linkingController={linkingController}
historyModalController={historyModalController}
closeMenu={closeContextMenu}
/>
</Menu>
)}
</Popover>
)}
</>
)
}
export default FilesTableView
export default ContentTableView
@@ -13,6 +13,7 @@ import { getIconForFileType } from '@/Utils/Items/Icons/getIconForFileType'
import FileMenuOptions from '../FileContextMenu/FileMenuOptions'
import Menu from '../Menu/Menu'
import Popover from '../Popover/Popover'
import LinkedItemBubblesContainer from '../LinkedItems/LinkedItemBubblesContainer'
type Props = {
application: WebApplication
@@ -26,6 +27,7 @@ const FilePreviewModal: FunctionComponent<Props> = observer(({ application, view
return null
}
const [showLinkedBubblesContainer, setShowLinkedBubblesContainer] = useState(false)
const [showOptionsMenu, setShowOptionsMenu] = useState(false)
const [showFileInfoPanel, setShowFileInfoPanel] = useState(false)
const menuButtonRef = useRef<HTMLButtonElement>(null)
@@ -96,6 +98,12 @@ const FilePreviewModal: FunctionComponent<Props> = observer(({ application, view
<span className="ml-3 font-medium">{currentFile.name}</span>
</div>
<div className="flex items-center">
<button
className="mr-4 flex cursor-pointer rounded border border-solid border-border bg-transparent p-1.5 hover:bg-contrast"
onClick={() => setShowLinkedBubblesContainer((show) => !show)}
>
<Icon type="link" className="text-neutral" />
</button>
<button
className="mr-4 flex cursor-pointer rounded border border-solid border-border bg-transparent p-1.5 hover:bg-contrast"
onClick={() => setShowOptionsMenu((show) => !show)}
@@ -144,6 +152,14 @@ const FilePreviewModal: FunctionComponent<Props> = observer(({ application, view
</button>
</div>
</div>
{showLinkedBubblesContainer && (
<div className="-mt-1 border-b border-border py-1.5 px-3.5">
<LinkedItemBubblesContainer
linkingController={viewControllerManager.linkingController}
item={currentFile}
/>
</div>
)}
<div className="flex min-h-0 flex-grow">
<div className="relative flex max-w-full flex-grow items-center justify-center">
<FilePreview file={currentFile} application={application} key={currentFile.uuid} />
@@ -117,7 +117,7 @@ const FileViewWithoutProtection = ({ application, viewControllerManager, file }:
/>
</div>
</div>
<LinkedItemBubblesContainer linkingController={viewControllerManager.linkingController} />
<LinkedItemBubblesContainer item={file} linkingController={viewControllerManager.linkingController} />
</div>
</div>
<div className="flex min-h-0 flex-grow flex-col">
@@ -6,7 +6,7 @@ import { useCallback, useEffect, useMemo, useState } from 'react'
import { useResponsiveAppPane } from '../Panes/ResponsivePaneProvider'
import { ElementIds } from '@/Constants/ElementIDs'
import { classNames } from '@standardnotes/utils'
import { ContentType } from '@standardnotes/snjs'
import { ContentType, DecryptedItemInterface } from '@standardnotes/snjs'
import { LinkableItem } from '@/Utils/Items/Search/LinkableItem'
import { ItemLink } from '@/Utils/Items/Search/ItemLink'
import { FOCUS_TAGS_INPUT_COMMAND, keyboardStringForShortcut } from '@standardnotes/ui-services'
@@ -15,17 +15,18 @@ import { useItemLinks } from '@/Hooks/useItemLinks'
type Props = {
linkingController: LinkingController
item: DecryptedItemInterface
}
const LinkedItemBubblesContainer = ({ linkingController }: Props) => {
const LinkedItemBubblesContainer = ({ item, linkingController }: Props) => {
const { toggleAppPane } = useResponsiveAppPane()
const commandService = useCommandService()
const { activeItem, unlinkItemFromSelectedItem: unlinkItem, activateItem } = linkingController
const { unlinkItemFromSelectedItem: unlinkItem, activateItem } = linkingController
const { notesLinkedToItem, filesLinkedToItem, tagsLinkedToItem, notesLinkingToItem, filesLinkingToItem } =
useItemLinks(activeItem)
useItemLinks(item)
const allItemsLinkedToItem: ItemLink[] = useMemo(
() => new Array<ItemLink>().concat(notesLinkedToItem, filesLinkedToItem, tagsLinkedToItem),
@@ -97,10 +98,6 @@ const LinkedItemBubblesContainer = ({ linkingController }: Props) => {
)
}
if (!activeItem) {
return null
}
return (
<div
className={classNames(
@@ -130,7 +127,7 @@ const LinkedItemBubblesContainer = ({ linkingController }: Props) => {
focusPreviousItem={focusPreviousItem}
setFocusedId={setFocusedId}
hoverLabel={`Focus input to add a link (${shortcut})`}
item={activeItem}
item={item}
/>
</div>
)
@@ -905,7 +905,10 @@ class NoteView extends AbstractComponent<NoteViewProps, State> {
</div>
)}
</div>
<LinkedItemBubblesContainer linkingController={this.viewControllerManager.linkingController} />
<LinkedItemBubblesContainer
item={this.note}
linkingController={this.viewControllerManager.linkingController}
/>
</div>
)}
@@ -47,6 +47,7 @@ const NotesContextMenu = ({
>
<Menu className="select-none" a11yLabel="Note context menu" isOpen={contextMenuOpen}>
<NotesOptions
notes={notesController.selectedNotes}
application={application}
navigationController={navigationController}
notesController={notesController}
@@ -39,6 +39,7 @@ const iconClassWarning = `text-warning mr-2 ${iconSize}`
const iconClassSuccess = `text-success mr-2 ${iconSize}`
const NotesOptions = ({
notes,
application,
navigationController,
notesController,
@@ -61,7 +62,6 @@ const NotesOptions = ({
return notesMatchingAttribute.length > notesNotMatchingAttribute.length
}
const notes = notesController.selectedNotes
const hidePreviews = toggleOn((note) => note.hidePreview)
const locked = toggleOn((note) => note.locked)
const protect = toggleOn((note) => note.protected)
@@ -55,6 +55,7 @@ const NotesOptionsPanel = ({
>
<Menu a11yLabel="Note options menu" isOpen={isOpen}>
<NotesOptions
notes={notesController.selectedNotes}
application={application}
navigationController={navigationController}
notesController={notesController}
@@ -3,8 +3,10 @@ import { HistoryModalController } from '@/Controllers/NoteHistory/HistoryModalCo
import { NavigationController } from '@/Controllers/Navigation/NavigationController'
import { NotesController } from '@/Controllers/NotesController/NotesController'
import { LinkingController } from '@/Controllers/LinkingController'
import { SNNote } from '@standardnotes/snjs'
export type NotesOptionsProps = {
notes: SNNote[]
application: WebApplication
navigationController: NavigationController
notesController: NotesController
@@ -304,6 +304,8 @@ const PanesSystemComponent = () => {
searchOptionsController={viewControllerManager.searchOptionsController}
linkingController={viewControllerManager.linkingController}
featuresController={viewControllerManager.featuresController}
historyModalController={viewControllerManager.historyModalController}
paneController={viewControllerManager.paneController}
>
{showPanelResizers && listRef && (
<PanelResizer
@@ -35,12 +35,15 @@ export type TableHeader = {
}
export type Table<Data> = {
id: string
headers: TableHeader[]
rows: TableRow<Data>[]
rowCount: number
colCount: number
handleRowClick: (id: string) => MouseEventHandler<HTMLTableRowElement>
handleRowDoubleClick: (id: string) => MouseEventHandler<HTMLTableRowElement>
selectRow: (id: string) => void
multiSelectRow: (id: string) => void
rangeSelectUpToRow: (id: string) => void
handleActivateRow: (id: string) => void
handleRowContextMenu: (id: string) => MouseEventHandler<HTMLTableRowElement>
canSelectRows: boolean
canSelectMultipleRows: boolean
@@ -1,5 +1,7 @@
import { classNames } from '@standardnotes/snjs'
import { useCallback, useState } from 'react'
import { KeyboardKey } from '@standardnotes/ui-services'
import { useCallback, useState, useRef } from 'react'
import { useApplication } from '../ApplicationProvider'
import Icon from '../Icon/Icon'
import { Table, TableRow } from './CommonTypes'
@@ -9,22 +11,25 @@ function TableRow<Data>({
canSelectRows,
handleRowClick,
handleRowContextMenu,
handleRowDoubleClick,
handleActivateRow,
}: {
row: TableRow<Data>
index: number
canSelectRows: Table<Data>['canSelectRows']
handleRowClick: Table<Data>['handleRowClick']
handleRowClick: (event: React.MouseEvent<HTMLDivElement, MouseEvent>, id: string) => void
handleRowContextMenu: Table<Data>['handleRowContextMenu']
handleRowDoubleClick: Table<Data>['handleRowDoubleClick']
handleActivateRow: Table<Data>['handleActivateRow']
}) {
const [isHovered, setIsHovered] = useState(false)
const [isFocused, setIsFocused] = useState(false)
const isHoveredOrFocused = isHovered || isFocused
const visibleCells = row.cells.filter((cell) => !cell.hidden)
return (
<div
role="row"
id={row.id}
aria-rowindex={rowIndex + 2}
{...(canSelectRows ? { 'aria-selected': row.isSelected } : {})}
className="group relative contents"
@@ -34,9 +39,17 @@ function TableRow<Data>({
onMouseLeave={() => {
setIsHovered(false)
}}
onClick={handleRowClick(row.id)}
onDoubleClick={handleRowDoubleClick(row.id)}
onClick={(event) => handleRowClick(event, row.id)}
onDoubleClick={() => handleActivateRow(row.id)}
onContextMenu={handleRowContextMenu(row.id)}
onFocus={() => {
setIsFocused(true)
}}
onBlur={(event) => {
if (!event.relatedTarget?.closest(`[id="${row.id}"]`)) {
setIsFocused(false)
}
}}
>
{visibleCells.map((cell, index, array) => {
return (
@@ -46,25 +59,27 @@ function TableRow<Data>({
aria-colindex={cell.colIndex + 1}
key={index}
className={classNames(
'relative flex items-center overflow-hidden border-b border-border py-3 px-3',
'relative flex items-center overflow-hidden border-b border-border py-3 px-3 focus:border-info',
row.isSelected && 'bg-info-backdrop',
canSelectRows && 'cursor-pointer',
canSelectRows && isHovered && 'bg-contrast',
canSelectRows && isHoveredOrFocused && 'bg-contrast',
)}
tabIndex={-1}
>
{cell.render}
{row.rowActions && index === array.length - 1 && (
<div
className={classNames(
'absolute right-0 top-0 flex h-full items-center p-2',
row.isSelected ? '' : isHovered ? '' : 'invisible',
row.isSelected ? '' : isHoveredOrFocused ? '' : 'invisible',
isFocused && 'visible',
)}
>
<div className="z-[1]">{row.rowActions}</div>
<div
className={classNames(
'absolute top-0 right-0 z-0 h-full w-full backdrop-blur-[2px]',
row.isSelected ? '' : isHovered ? '' : 'invisible',
row.isSelected ? '' : isHoveredOrFocused ? '' : 'invisible',
)}
/>
</div>
@@ -82,6 +97,8 @@ const PageSize = Math.ceil(document.documentElement.clientHeight / MinTableRowHe
const PageScrollThreshold = 200
function Table<Data>({ table }: { table: Table<Data> }) {
const application = useApplication()
const [rowsToDisplay, setRowsToDisplay] = useState<number>(PageSize)
const paginate = useCallback(() => {
setRowsToDisplay((cellsToDisplay) => cellsToDisplay + PageSize)
@@ -98,13 +115,16 @@ function Table<Data>({ table }: { table: Table<Data> }) {
)
const {
id,
headers,
rows,
colCount,
rowCount,
handleRowClick,
selectRow,
multiSelectRow,
rangeSelectUpToRow,
handleRowContextMenu,
handleRowDoubleClick,
handleActivateRow,
selectedRows,
selectionActions,
canSelectRows,
@@ -112,10 +132,236 @@ function Table<Data>({ table }: { table: Table<Data> }) {
showSelectionActions,
} = table
const focusedRowIndex = useRef<number>(0)
const focusedCellIndex = useRef<number>(0)
const onFocus: React.FocusEventHandler = useCallback((event) => {
const target = event.target as HTMLElement
const row = target.closest('[role="row"]') as HTMLElement
const cell = target.closest('[role="gridcell"],[role="columnheader"]') as HTMLElement
if (row) {
focusedRowIndex.current = parseInt(row.getAttribute('aria-rowindex') || '0')
}
if (cell) {
focusedCellIndex.current = parseInt(cell.getAttribute('aria-colindex') || '0')
}
}, [])
const onBlur: React.FocusEventHandler = useCallback((event) => {
const activeElement = document.activeElement as HTMLElement
if (activeElement.closest('[role="grid"]') !== event.target) {
focusedRowIndex.current = 0
focusedCellIndex.current = 0
}
}, [])
const onKeyDown: React.KeyboardEventHandler = useCallback(
(event) => {
const gridElement = event.currentTarget
const allRenderedRows = gridElement.querySelectorAll<HTMLElement>('[role="row"]')
const currentRow = Array.from(allRenderedRows).find(
(row) => row.getAttribute('aria-rowindex') === focusedRowIndex.current.toString(),
)
const allFocusableCells = Array.from(currentRow ? currentRow.querySelectorAll<HTMLElement>('[tabindex]') : [])
const allRenderedColumnsLength = headers.length
const focusCell = (rowIndex: number, colIndex: number) => {
const row = gridElement.querySelector(`[role="row"][aria-rowindex="${rowIndex}"]`)
if (!row) {
return
}
const cell = row.querySelector<HTMLElement>(`[aria-colindex="${colIndex}"]`)
if (cell) {
cell.focus()
}
}
switch (event.key) {
case KeyboardKey.Up:
event.preventDefault()
if (focusedRowIndex.current > 1) {
const previousRow = focusedRowIndex.current - 1
focusCell(previousRow, focusedCellIndex.current)
}
break
case KeyboardKey.Down:
event.preventDefault()
if (focusedRowIndex.current <= rowCount) {
const nextRow = focusedRowIndex.current + 1
focusCell(nextRow, focusedCellIndex.current)
}
break
case KeyboardKey.Left: {
event.preventDefault()
if (!allFocusableCells) {
return
}
const currentCellIndex = allFocusableCells.findIndex(
(cell) => parseInt(cell.getAttribute('aria-colindex') || '0') === focusedCellIndex.current,
)
if (currentCellIndex === 0) {
return
}
const previousCell = allFocusableCells[currentCellIndex - 1]
if (!previousCell) {
return
}
previousCell.focus()
break
}
case KeyboardKey.Right: {
event.preventDefault()
if (!allFocusableCells) {
return
}
const currentCellIndex = allFocusableCells.findIndex(
(cell) => parseInt(cell.getAttribute('aria-colindex') || '0') === focusedCellIndex.current,
)
if (currentCellIndex === allFocusableCells.length - 1) {
return
}
const nextCell = allFocusableCells[currentCellIndex + 1]
if (!nextCell) {
return
}
nextCell.focus()
break
}
case KeyboardKey.Home:
event.preventDefault()
if (event.ctrlKey) {
focusCell(1, 1)
} else {
if (!allFocusableCells) {
return
}
const firstFocusableCell = allFocusableCells[0]
if (!firstFocusableCell) {
return
}
const firstCellIndex = parseInt(firstFocusableCell.getAttribute('aria-colindex') || '0')
if (firstCellIndex > 0) {
focusCell(focusedRowIndex.current, firstCellIndex)
}
}
break
case KeyboardKey.End: {
event.preventDefault()
if (event.ctrlKey) {
focusCell(allRenderedRows.length, allRenderedColumnsLength || colCount)
return
}
if (!allFocusableCells) {
return
}
const lastFocusableCell = allFocusableCells[allFocusableCells.length - 1]
if (!lastFocusableCell) {
return
}
const lastCellIndex = parseInt(lastFocusableCell.getAttribute('aria-colindex') || '0')
if (lastCellIndex > 0) {
focusCell(focusedRowIndex.current, lastCellIndex)
}
break
}
case KeyboardKey.PageUp: {
event.preventDefault()
const previousRow = focusedRowIndex.current - 5
if (previousRow > 0) {
focusCell(previousRow, focusedCellIndex.current)
} else {
focusCell(1, focusedCellIndex.current)
}
break
}
case KeyboardKey.PageDown: {
event.preventDefault()
const nextRow = focusedRowIndex.current + 5
if (nextRow <= allRenderedRows.length) {
focusCell(nextRow, focusedCellIndex.current)
} else {
focusCell(allRenderedRows.length, focusedCellIndex.current)
}
break
}
case KeyboardKey.Enter: {
const target = event.target as HTMLElement
const closestColumnHeader = target.closest<HTMLElement>('[role="columnheader"]')
if (closestColumnHeader && closestColumnHeader.getAttribute('data-can-sort')) {
event.preventDefault()
closestColumnHeader.click()
return
}
const currentRowId = currentRow?.id
if (currentRowId) {
event.preventDefault()
handleActivateRow(currentRowId)
}
break
}
case KeyboardKey.Space: {
const target = event.target as HTMLElement
const currentRowId = currentRow?.id
if (!currentRowId) {
return
}
if (target.getAttribute('role') !== 'gridcell') {
return
}
event.preventDefault()
const isCmdOrCtrlPressed = application.keyboardService.isMac ? event.metaKey : event.ctrlKey
if (isCmdOrCtrlPressed && canSelectMultipleRows) {
multiSelectRow(currentRowId)
} else if (event.shiftKey && canSelectMultipleRows) {
rangeSelectUpToRow(currentRowId)
} else {
selectRow(currentRowId)
}
break
}
}
},
[
application.keyboardService.isMac,
canSelectMultipleRows,
colCount,
handleActivateRow,
headers.length,
multiSelectRow,
rangeSelectUpToRow,
rowCount,
selectRow,
],
)
const handleRowClick = useCallback(
(event: React.MouseEvent, rowId: string) => {
if (!canSelectRows) {
return
}
const isCmdOrCtrlPressed = application.keyboardService.isMac ? event.metaKey : event.ctrlKey
if (isCmdOrCtrlPressed && canSelectMultipleRows) {
multiSelectRow(rowId)
} else if (event.shiftKey && canSelectMultipleRows) {
rangeSelectUpToRow(rowId)
} else {
selectRow(rowId)
}
},
[
application.keyboardService.isMac,
canSelectMultipleRows,
canSelectRows,
multiSelectRow,
rangeSelectUpToRow,
selectRow,
],
)
return (
<div className="block min-h-0 overflow-auto" onScroll={onScroll}>
{showSelectionActions && selectedRows.length >= 2 && (
<div className="flex items-center justify-between border-b border-border px-3 py-2">
<div className="sticky top-0 z-[2] flex items-center justify-between border-b border-border bg-default px-3 py-2">
<span className="text-info-0 text-sm font-medium">{selectedRows.length} selected</span>
{selectedRows.length > 0 && selectionActions}
</div>
@@ -126,6 +372,10 @@ function Table<Data>({ table }: { table: Table<Data> }) {
aria-colcount={colCount}
aria-rowcount={rowCount}
aria-multiselectable={canSelectMultipleRows}
onFocus={onFocus}
onBlur={onBlur}
onKeyDown={onKeyDown}
id={`table-${id}`}
>
<div role="row" aria-rowindex={1} className="contents">
{headers
@@ -139,13 +389,16 @@ function Table<Data>({ table }: { table: Table<Data> }) {
aria-sort={header.isSorting ? (header.sortReversed ? 'descending' : 'ascending') : 'none'}
className={classNames(
'border-b border-border px-3 pt-3 pb-2 text-left text-sm font-medium text-passive-0',
header.sortBy && 'cursor-pointer hover:bg-info-backdrop hover:underline',
header.sortBy &&
'cursor-pointer hover:bg-info-backdrop hover:underline focus:border-info focus:bg-info-backdrop',
)}
style={{
gridColumn: index + 1,
}}
onClick={header.onSortChange}
key={index.toString()}
data-can-sort={header.sortBy ? true : undefined}
{...(header.sortBy && { tabIndex: index === 0 ? 0 : -1 })}
>
<div className="flex items-center gap-1">
{header.name}
@@ -170,7 +423,7 @@ function Table<Data>({ table }: { table: Table<Data> }) {
canSelectRows={canSelectRows}
handleRowClick={handleRowClick}
handleRowContextMenu={handleRowContextMenu}
handleRowDoubleClick={handleRowDoubleClick}
handleActivateRow={handleActivateRow}
/>
))}
</div>
@@ -1,5 +1,5 @@
import { MouseEventHandler, ReactNode, useCallback, useEffect, useMemo, useState } from 'react'
import { useApplication } from '../ApplicationProvider'
import { UuidGenerator } from '@standardnotes/snjs'
import { MouseEventHandler, ReactNode, useCallback, useEffect, useMemo, useState, useRef } from 'react'
import { Table, TableColumn, TableHeader, TableRow, TableSortBy } from './CommonTypes'
type TableSortOptions =
@@ -34,7 +34,7 @@ type TableSelectionOptions =
type TableRowOptions<Data> = {
getRowId?: (data: Data) => string
onRowDoubleClick?: (data: Data) => void
onRowActivate?: (data: Data) => void
onRowContextMenu?: (x: number, y: number, data: Data) => void
rowActions?: (data: Data) => ReactNode
}
@@ -57,14 +57,14 @@ export function useTable<Data>({
enableMultipleRowSelection,
selectedRowIds,
onRowSelectionChange,
onRowDoubleClick,
onRowActivate,
onRowContextMenu,
rowActions,
selectionActions,
showSelectionActions,
}: UseTableOptions<Data>): Table<Data> {
const application = useApplication()
const [selectedRows, setSelectedRows] = useState<string[]>(selectedRowIds || [])
const id = useRef(UuidGenerator.GenerateUuid())
useEffect(() => {
if (selectedRowIds) {
@@ -122,45 +122,55 @@ export function useTable<Data>({
[columns, data, enableRowSelection, getRowId, rowActions, selectedRows],
)
const handleRowClick = useCallback(
const selectRow = useCallback(
(id: string) => {
const handler: MouseEventHandler<HTMLTableRowElement> = (event) => {
if (!enableRowSelection) {
return
}
const isCmdOrCtrlPressed = application.keyboardService.isMac ? event.metaKey : event.ctrlKey
if (isCmdOrCtrlPressed && enableMultipleRowSelection) {
setSelectedRows((prev) => (prev.includes(id) ? prev.filter((rowId) => rowId !== id) : [...prev, id]))
} else if (event.shiftKey && enableMultipleRowSelection) {
const lastSelectedIndex = rows.findIndex((row) => row.id === selectedRows[selectedRows.length - 1])
const currentIndex = rows.findIndex((row) => row.id === id)
const start = Math.min(lastSelectedIndex, currentIndex)
const end = Math.max(lastSelectedIndex, currentIndex)
const newSelectedRows = rows.slice(start, end + 1).map((row) => row.id)
setSelectedRows(newSelectedRows)
} else {
setSelectedRows([id])
}
if (!enableRowSelection) {
return
}
return handler
setSelectedRows([id])
},
[application.keyboardService.isMac, enableMultipleRowSelection, enableRowSelection, rows, selectedRows],
[enableRowSelection],
)
const handleRowDoubleClick = useCallback(
const multiSelectRow = useCallback(
(id: string) => {
const handler: MouseEventHandler<HTMLTableRowElement> = () => {
if (!onRowDoubleClick) {
return
}
const rowData = rows.find((row) => row.id === id)?.rowData
if (rowData) {
onRowDoubleClick(rowData)
}
if (!enableRowSelection || !enableMultipleRowSelection) {
return
}
return handler
setSelectedRows((prev) => (prev.includes(id) ? prev.filter((rowId) => rowId !== id) : [...prev, id]))
},
[onRowDoubleClick, rows],
[enableMultipleRowSelection, enableRowSelection],
)
const rangeSelectUpToRow = useCallback(
(id: string) => {
if (!enableRowSelection || !enableMultipleRowSelection) {
return
}
const lastSelectedIndex = rows.findIndex((row) => row.id === selectedRows[selectedRows.length - 1])
const currentIndex = rows.findIndex((row) => row.id === id)
const start = Math.min(lastSelectedIndex, currentIndex)
const end = Math.max(lastSelectedIndex, currentIndex)
const newSelectedRows = rows.slice(start, end + 1).map((row) => row.id)
setSelectedRows(newSelectedRows)
},
[enableMultipleRowSelection, enableRowSelection, rows, selectedRows],
)
const handleActivateRow = useCallback(
(id: string) => {
if (!onRowActivate) {
return
}
const rowData = rows.find((row) => row.id === id)?.rowData
if (rowData) {
onRowActivate(rowData)
}
},
[onRowActivate, rows],
)
const handleRowContextMenu = useCallback(
@@ -186,12 +196,15 @@ export function useTable<Data>({
const table: Table<Data> = useMemo(
() => ({
id: id.current,
headers,
rows,
colCount,
rowCount,
handleRowClick,
handleRowDoubleClick,
selectRow,
multiSelectRow,
rangeSelectUpToRow,
handleActivateRow,
handleRowContextMenu,
selectedRows,
canSelectRows: enableRowSelection || false,
@@ -200,16 +213,18 @@ export function useTable<Data>({
showSelectionActions: showSelectionActions || false,
}),
[
colCount,
enableMultipleRowSelection,
enableRowSelection,
handleRowClick,
handleRowContextMenu,
handleRowDoubleClick,
headers,
rowCount,
rows,
colCount,
rowCount,
selectRow,
multiSelectRow,
rangeSelectUpToRow,
handleActivateRow,
handleRowContextMenu,
selectedRows,
enableRowSelection,
enableMultipleRowSelection,
selectionActions,
showSelectionActions,
],
@@ -1,5 +1,5 @@
import { ListableContentItem } from '@/Components/ContentListView/Types/ListableContentItem'
import { destroyAllObjectProperties } from '@/Utils'
import { destroyAllObjectProperties, isMobileScreen } from '@/Utils'
import {
ApplicationEvent,
CollectionSort,
@@ -71,6 +71,7 @@ export class ItemListController extends AbstractViewController implements Intern
hideNotePreview: false,
hideEditorIcon: false,
}
isTableViewEnabled = false
private reloadItemsPromise?: Promise<unknown>
override deinit() {
@@ -451,6 +452,10 @@ export class ItemListController extends AbstractViewController implements Intern
this.selectionController.deselectItem(activeItem)
if (this.shouldSelectFirstItem(itemsReloadSource)) {
if (this.isTableViewEnabled && !isMobileScreen()) {
return
}
log(LoggingDomain.Selection, 'Selecting next item after closing active one')
this.selectionController.selectNextItem({ userTriggered: false })
}
@@ -513,6 +518,8 @@ export class ItemListController extends AbstractViewController implements Intern
? this.application.getPreference(PrefKey.SystemViewPreferences)?.[selectedTag.uuid as SystemViewId]
: selectedTag?.preferences
this.isTableViewEnabled = Boolean(selectedTagPreferences?.useTableView)
const currentSortBy = this.displayOptions.sortBy
let sortBy =
selectedTagPreferences?.sortBy ||
@@ -729,6 +736,10 @@ export class ItemListController extends AbstractViewController implements Intern
selectFirstItem = async () => {
const item = this.getFirstNonProtectedItem()
if (this.isTableViewEnabled && !isMobileScreen()) {
return
}
if (item) {
log(LoggingDomain.Selection, 'Selecting first item', item.uuid)
@@ -14,6 +14,7 @@ import {
InternalEventPublishStrategy,
VectorIconNameOrEmoji,
isTag,
PrefKey,
} from '@standardnotes/snjs'
import { action, computed, makeAutoObservable, makeObservable, observable, reaction, runInAction } from 'mobx'
import { WebApplication } from '../../Application/Application'
@@ -268,6 +269,14 @@ export class NavigationController
return tag.uuid === SystemViewId.Files
}
tagUsesTableView(tag: AnyTag): boolean {
const isSystemView = tag instanceof SmartView && Object.values(SystemViewId).includes(tag.uuid as SystemViewId)
const useTableView = isSystemView
? this.application.getPreference(PrefKey.SystemViewPreferences)?.[tag.uuid as SystemViewId]
: tag?.preferences
return Boolean(useTableView)
}
public isInAnySystemView(): boolean {
return (
this.selected instanceof SmartView && Object.values(SystemViewId).includes(this.selected.uuid as SystemViewId)
@@ -466,8 +475,8 @@ export class NavigationController
.catch(console.error)
}
if (tag && this.isTagFilesView(tag)) {
this.application.paneController.setPaneLayout(PaneLayout.FilesView)
if (tag && (this.isTagFilesView(tag) || this.tagUsesTableView(tag))) {
this.application.paneController.setPaneLayout(PaneLayout.TableView)
} else if (userTriggered) {
this.application.paneController.setPaneLayout(PaneLayout.ItemSelection)
}
@@ -1,6 +1,6 @@
export enum PaneLayout {
TagSelection = 'tag-selection',
ItemSelection = 'item-selection',
FilesView = 'files-view',
TableView = 'files-view',
Editing = 'editing',
}
@@ -11,20 +11,20 @@ export function panesForLayout(layout: PaneLayout, application: WebApplication):
} else if (
layout === PaneLayout.ItemSelection ||
layout === PaneLayout.Editing ||
layout === PaneLayout.FilesView
layout === PaneLayout.TableView
) {
return [AppPaneId.Items, AppPaneId.Editor]
}
} else if (screen.isMobile) {
if (layout === PaneLayout.TagSelection) {
return [AppPaneId.Navigation]
} else if (layout === PaneLayout.ItemSelection || layout === PaneLayout.FilesView) {
} else if (layout === PaneLayout.ItemSelection || layout === PaneLayout.TableView) {
return [AppPaneId.Navigation, AppPaneId.Items]
} else if (layout === PaneLayout.Editing) {
return [AppPaneId.Navigation, AppPaneId.Items, AppPaneId.Editor]
}
} else {
if (layout === PaneLayout.FilesView) {
if (layout === PaneLayout.TableView) {
return [AppPaneId.Navigation, AppPaneId.Items]
} else {
return [AppPaneId.Navigation, AppPaneId.Items, AppPaneId.Editor]