Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c41807e78f | ||
|
|
06fe7d60e2 |
No files matched your search
@@ -180,24 +180,17 @@ jobs:
|
||||
- name: Create macOS dmg
|
||||
run: |
|
||||
brew install create-dmg
|
||||
i=0
|
||||
until [[ -e "${{ env.MACOS_APP_RELEASE_PATH }}/${{ env.MACOS_DMG_NAME }}.dmg" ]]; do
|
||||
create-dmg \
|
||||
--volname ${{ env.MACOS_DMG_NAME }} \
|
||||
--hide-extension "AppFlowy.app" \
|
||||
--background frontend/scripts/dmg_assets/AppFlowyInstallerBackground.jpg \
|
||||
--window-size 600 450 \
|
||||
--icon-size 94 \
|
||||
--icon "AppFlowy.app" 141 249 \
|
||||
--app-drop-link 458 249 \
|
||||
"${{ env.MACOS_APP_RELEASE_PATH }}/${{ env.MACOS_DMG_NAME }}.dmg" \
|
||||
"${{ env.MACOS_APP_RELEASE_PATH }}/AppFlowy.app" || true
|
||||
if [[ $i -eq 10 ]]; then
|
||||
echo 'Error: create-dmg did not succeed even after 10 tries.'
|
||||
exit 1
|
||||
fi
|
||||
i=$((i+1))
|
||||
done
|
||||
create-dmg \
|
||||
--volname ${{ env.MACOS_DMG_NAME }} \
|
||||
--hide-extension "AppFlowy.app" \
|
||||
--background frontend/scripts/dmg_assets/AppFlowyInstallerBackground.jpg \
|
||||
--window-size 600 450 \
|
||||
--icon-size 94 \
|
||||
--icon "AppFlowy.app" 141 249 \
|
||||
--app-drop-link 458 249 \
|
||||
"${{ env.MACOS_APP_RELEASE_PATH }}/${{ env.MACOS_DMG_NAME }}.dmg" \
|
||||
"${{ env.MACOS_APP_RELEASE_PATH }}/AppFlowy.app"
|
||||
|
||||
- name: Notarize AppFlowy
|
||||
run: |
|
||||
xcrun notarytool submit ${{ env.MACOS_APP_RELEASE_PATH }}/${{ env.MACOS_DMG_NAME }}.dmg --apple-id ${{ secrets.MACOS_NOTARY_USER }} --team-id ${{ secrets.MACOS_TEAM_ID }} --password ${{ secrets.MACOS_NOTARY_PWD }} -v -f "json" --wait
|
||||
@@ -240,7 +233,7 @@ jobs:
|
||||
job:
|
||||
- {
|
||||
targets: "aarch64-apple-darwin,x86_64-apple-darwin",
|
||||
os: macos-14,
|
||||
os: macos-latest,
|
||||
extra-build-args: "",
|
||||
}
|
||||
steps:
|
||||
|
||||
+3
-320
@@ -1,321 +1,4 @@
|
||||
# Release Notes
|
||||
## Version 0.11.4 – 09/03/2026
|
||||
### Desktop
|
||||
#### New Features
|
||||
- Notification System: In-app notification center to track mentions, reminders, and activity updates
|
||||
- Revamped Slash Menu: Redesigned `/` command menu with improved layout and discoverability
|
||||
- Editor Block Spacing: Optimize spacing between blocks in the document editor
|
||||
|
||||
#### Bug Fixes
|
||||
- Fixed row document issues
|
||||
- Fixed bulk delete not working correctly
|
||||
- Fixed connection check causing unnecessary errors
|
||||
|
||||
### Mobile
|
||||
#### New Features
|
||||
- Notification System: In-app notification center to track mentions, reminders, and activity updates
|
||||
- Revamped Slash Menu: Redesigned `/` command menu with improved layout and discoverability
|
||||
- Editor Block Spacing: Optimize spacing between blocks in the document editor
|
||||
|
||||
#### Bug Fixes
|
||||
- Fixed mobile block selection disappearing
|
||||
- Fixed select-all behavior with Gboard on Android
|
||||
- Fixed duplicate column names when creating board groups
|
||||
|
||||
## Version 0.11.3 – 25/02/2026
|
||||
### Desktop
|
||||
#### Bug Fixes
|
||||
- Fixed the space dropdown menu not displaying correctly
|
||||
- Fixed relation filter not applying correctly in advanced filter scenarios
|
||||
- Fixed the disconnect banner not refreshing properly
|
||||
|
||||
### Mobile
|
||||
#### Bug Fixes
|
||||
- Fixed losing focus after typing the first character in a text cell
|
||||
- Fixed FCM blocking the main app on startup
|
||||
|
||||
## Version 0.11.2 – 14/02/2026
|
||||
### Desktop
|
||||
#### New Features
|
||||
- New Database View: Feed view
|
||||
- AI Transcript & Meeting Blocks: Automatic speaker name guessing, referenced AI summaries, and meeting detection from Google Meet and Slack
|
||||
- Google Drive Block: Embed and select Google Drive files via link or connected account
|
||||
|
||||
#### Bug Fixes
|
||||
- File Block: Display upload progress during file uploads
|
||||
- Image Upload: Improved upload indicator and instant image caching
|
||||
- Unsplash Cover Image: Improved preview and keyboard navigation
|
||||
|
||||
### Mobile
|
||||
#### New Features
|
||||
- New Database View: Feed view
|
||||
- Google Drive Block: Embed and select Google Drive files via link or connected account
|
||||
|
||||
#### Bug Fixes
|
||||
- File Block: Display upload progress during file uploads
|
||||
- Image Upload: Improved upload indicator and instant image caching
|
||||
|
||||
## Version 0.11.1 – 28/01/2026
|
||||
### Desktop
|
||||
#### New Features
|
||||
- New Database Views: Gallery, List, and Chart views
|
||||
- Grouping in a Grid (Table) view: Group by allows you to organize database entries into collapsible sections based on property values (e.g., Status, Priority)
|
||||
- Multi-row actions: Allows for powerful multi-row (bulk) actions within databases to edit, delete, or duplicate all at once
|
||||
- Database advanced filters: Useful when combining AND logic and OR logic in your filter
|
||||
- Number property new formats: Comma separated and percent
|
||||
- Export as PDF: Export a single page as .PDF
|
||||
- Database Cover Images: Added support for cover images to enhance visual representation
|
||||
- AI Transcript & Meeting Notes: Use the mention person type for task assignees in the action items of the AI summary
|
||||
|
||||
#### Bug Fixes
|
||||
- Enhanced playback experience for video blocks
|
||||
- Resolved issue with inline mention styles not matching block text styles
|
||||
|
||||
### Mobile
|
||||
#### New Features
|
||||
- New Database Views: Gallery, List, and Chart views
|
||||
|
||||
#### Bug Fixes
|
||||
- Improved playback experience for video blocks
|
||||
- Fixed issue with inline mention styles not matching block text styles
|
||||
|
||||
## Version 0.11.0 – 12/01/2026
|
||||
### Desktop
|
||||
#### Bug Fixes
|
||||
- Fixed row detail page button style not following theme changes
|
||||
- Fixed target limit not being removed when creating two-way relation field
|
||||
- Improved speaker selector menu UI style in the transcript tab of AI meeting block
|
||||
|
||||
## Version 0.10.9 – 09/01/2026
|
||||
### Desktop
|
||||
#### New Features
|
||||
- Database Rollup: A powerful property used to retrieve and aggregate information from items in a related database
|
||||
- Database search: Search names of database pages, as well as property values (such as a status of "In progress") and content inside database pages
|
||||
- AI Transcript with auto speaker identification: Upload an audio file to generate transcriptions with speaker labels. You can assign or change speaker names
|
||||
- AI Meeting Notes with auto speaker identification: Real-time transcription during meetings with automatic speaker label detection after recording stops. You can assign or change speaker names
|
||||
- Mermaid diagram support: Add diagrams directly in Code blocks with split, preview, and code display modes, plus an expandable view
|
||||
- Top nav bar breadcrumb revamp: Refreshed design with improved layout
|
||||
- Document and database cover reposition: Reposition cover images for the best display
|
||||
|
||||
#### Bug Fixes
|
||||
- Fixed various database issues to improve stability and performance
|
||||
|
||||
### Mobile
|
||||
#### New Features
|
||||
- AI Transcript with auto speaker identification: Upload an audio file to generate transcriptions with speaker labels. You can assign or change speaker names
|
||||
- Mermaid diagram support: Add diagrams directly in Code blocks with split, preview, and code display modes, plus an expandable view
|
||||
|
||||
#### Bug Fixes
|
||||
- Fixed various database issues to improve stability and performance
|
||||
|
||||
## Version 0.10.8 – 22/12/2025
|
||||
### Desktop
|
||||
#### Bug Fixes
|
||||
- Grid sort order preserved after edits — Editing a row no longer moves it to the bottom of a sorted Grid
|
||||
- Delete banner for databases — Grid, Board, and Calendar now show restore/delete banner when deleted
|
||||
- Timestamp field conversion — Converting Created time or Last modified to Text/Date no longer loses values
|
||||
- Unicode string sorting — Sorting now handles accented characters and special symbols correctly
|
||||
- Kanban filter fix — Filters now apply correctly to items added after view creation
|
||||
|
||||
### Mobile
|
||||
#### Bug Fixes
|
||||
- Grid sort order preserved after edits — Editing a row no longer moves it to the bottom of a sorted Grid
|
||||
- Timestamp field conversion — Converting Created time or Last modified to Text/Date no longer loses values
|
||||
- Unicode string sorting — Sorting now handles accented characters and special symbols correctly
|
||||
- Kanban filter fix — Filters now apply correctly to items added after view creation
|
||||
|
||||
## Version 0.10.7 – 19/12/2025
|
||||
### Desktop
|
||||
#### New Features
|
||||
- AI Meeting Notes Block: Supports real-time transcription on macOS and Windows. AI will turn everything into a clean, share-ready summary
|
||||
- Database Two-way Relations: Automatically sync connected data between databases, ensuring that information added in one place instantly appears in the other
|
||||
- Video Block: Upload or embed video files, then view and interact with them directly inside your document
|
||||
- Audio Block: Upload or embed audio files, then view and interact with them directly inside your document
|
||||
- Callout Block: Supports removing the callout icon
|
||||
- Table of Contents Block: Supports collapsing/uncollapsing the content
|
||||
|
||||
#### Bug Fixes
|
||||
- Fixed various issues related to database views, including sorting, filtering, counting, syncing, and layouts
|
||||
- Fixed various issues related to the linked views of databases
|
||||
- Fixed database issues related to date formats, file name display, view tabs, horizontal scroll, and Relation property performance
|
||||
|
||||
### Mobile
|
||||
#### New Features
|
||||
- Video Block: Upload or embed video files, then view and interact with them directly inside your document
|
||||
- Audio Block: Upload or embed audio files, then view and interact with them directly inside your document
|
||||
- Callout Block: Supports removing the callout icon
|
||||
- Table of Contents Block: Supports collapsing/uncollapsing the content
|
||||
|
||||
#### Bug Fixes
|
||||
- Fixed various issues related to database views, including sorting, filtering, counting, syncing, and layouts
|
||||
- Fixed various issues related to the linked views of databases
|
||||
- Fixed database issues related to date formats, file name display, view tabs, horizontal scroll, and Relation property performance
|
||||
|
||||
## Version 0.10.6 – 05/12/2025
|
||||
### Desktop
|
||||
#### Bug Fixes
|
||||
- Fixed issues related to the duplicated databases
|
||||
- Fixed an issue where the Chinese (Simplified) and Chinese (Traditional) language options were not being saved properly in the summary language of the AI meeting block
|
||||
|
||||
### Mobile
|
||||
#### Bug Fixes
|
||||
- Fixed issues related to the duplicated databases
|
||||
|
||||
## Version 0.10.5 – 02/12/2025
|
||||
### Desktop
|
||||
#### New Features
|
||||
- AI Transcript Block: Turn audio and video recordings into ready-to-share notes with AI Transcribe. Easily use our rich-text editing features to refine both the AI transcription and summary. Perfect for work, school, or personal projects
|
||||
- PDF Block: Upload or embed PDF files, then view and interact with them directly inside your document
|
||||
- Improved Image Block: Supported new features such as captions, embedded external or page links, saving images, and replacing images with more polished visuals and interactions
|
||||
- Self-hosted: Enabled custom namespaces, fixed issues with inviting members via link, and removed Pro plan banners that were intended for the managed cloud version
|
||||
|
||||
### Mobile
|
||||
#### New Features
|
||||
- PDF Block: Upload or embed PDF files, then view and interact with them directly inside your document
|
||||
- Improved Image Block: Supported new features such as captions, embedded external or page links, saving images, and replacing images—with more polished visuals and interactions
|
||||
- Self-hosted: Fixed issues with inviting members via link, and removed Pro plan banners that were intended for the managed cloud version
|
||||
|
||||
## Version 0.10.4 – 14/11/2025
|
||||
### Desktop
|
||||
#### New Features
|
||||
- Revamped the Math Equation block with an easier-to-use UX and new features: multiline equations, more environment variables, inline math equation markdown support, and Ask AI
|
||||
- Revamped the Toggle List block with an improved UI and bug fixes
|
||||
- Revamped the right-click Context Menu with a more polished UI and new features: Paste as plain text and Ask AI
|
||||
|
||||
#### Bug Fixes
|
||||
- Fixed issues related to the Math Equation and Toggle List blocks
|
||||
- Fixed issues related to the right-click Context Menu
|
||||
|
||||
### Mobile
|
||||
#### New Features
|
||||
- Revamped the Math Equation and Toggle List blocks with improved UIs and new features
|
||||
|
||||
#### Bug Fixes
|
||||
- Fixed issues related to the Math Equation and Toggle List blocks
|
||||
|
||||
## Version 0.10.3 – 31/10/2025
|
||||
### Desktop
|
||||
#### New Features
|
||||
- Revamped the Code block with new features: pin language, caption, Ask AI, and Turn Text into Code
|
||||
- Revamped the Callout block with an improved UI and quick styles
|
||||
- Revamped the Outline block with a more polished UI and custom background colors
|
||||
- Notification Settings: enable or disable mobile push notifications
|
||||
|
||||
#### Bug Fixes
|
||||
- Fixed issues related to the Code and Callout blocks
|
||||
|
||||
### Mobile
|
||||
#### New Features
|
||||
- Supported mobile push notifications: you'll receive a push message when you're mentioned, invited to a page, or when page permissions change
|
||||
- Revamped the Code, Callout, and Outline blocks with improved UIs and new features
|
||||
|
||||
## Version 0.10.2 – 24/10/2025
|
||||
### Desktop
|
||||
#### Bug Fixes
|
||||
- Fixed inability to import Notion zip file
|
||||
|
||||
## Version 0.10.1 – 16/10/2025
|
||||
### Desktop
|
||||
#### Bug Fixes
|
||||
- Fixed potential login token expired issue
|
||||
- Fixed inability to create Vault workspace on self-hosted servers with commercial plans
|
||||
- Fixed switching page rapidly caused the page flash
|
||||
|
||||
### Mobile
|
||||
#### Bug Fixes
|
||||
- Fixed potential login token expired issue
|
||||
|
||||
## Version 0.10.0 – 29/09/2025
|
||||
### Desktop
|
||||
#### New Features
|
||||
- Person, a new database property, lets you assign a row to one or more collaborators
|
||||
|
||||
### Mobile
|
||||
#### New Features
|
||||
- Person, a new database property, lets you assign a row to one or more collaborators
|
||||
- iOS Share to AppFlowy: quickly save a web page with optional notes to a target AppFlowy page or database
|
||||
- Android Share to AppFlowy: quickly save a web page with optional notes to a target AppFlowy page or database
|
||||
- A new Android widget: Quick Page Access
|
||||
|
||||
## Version 0.9.9 – 11/09/2025
|
||||
### Desktop
|
||||
#### New Features
|
||||
- Workspace user profile: customize your avatar, about me, and profile card banner
|
||||
|
||||
### Mobile
|
||||
#### New Features
|
||||
- Workspace user profile: customize your avatar, about me, and profile card banner
|
||||
- Two new Android widgets: Recent Pages and Favorites
|
||||
- A new iOS widget: Quick Page Access
|
||||
- iOS Share to AppFlowy: quickly save browser page links with optional notes to a target AppFlowy page
|
||||
|
||||
## Version 0.9.8 – 28/08/2025
|
||||
### Desktop
|
||||
#### New Features
|
||||
- Bulk add collaborators with Can view / Can edit permissions via the Share menu
|
||||
- Enable setting the start of the week to Monday to control default calendar layouts in the calendar view and date picker
|
||||
- Sync date and time formats, language, and week start day across devices under the same user account
|
||||
- Offer 10 premium Select-option colors to Pro Plan users
|
||||
- Support managing pending members via Settings → Members
|
||||
|
||||
### Mobile
|
||||
#### New Features
|
||||
- Support iOS widgets for quick access to recent and favorite pages
|
||||
- Offer 10 premium Select option colors to Pro Plan users
|
||||
- Support managing pending members via Settings → Members
|
||||
|
||||
## Version 0.9.7 – 13/08/2025
|
||||
### Desktop
|
||||
#### New Features
|
||||
- Mention or assign persons in documents via '@' or '/'
|
||||
- Mentioned persons get notified about the mention via email
|
||||
- More colors are available for database Select options and page covers
|
||||
- Back up your AppFlowy workspace: Export your workspace as a ZIP file and import it back at any time
|
||||
- GPT-5 is now available in AppFlowy
|
||||
|
||||
### Mobile
|
||||
#### New Features
|
||||
- Mention or assign persons in documents via '@' or '/'
|
||||
- Mentioned persons get notified about the mention via email
|
||||
- More colors are available for database Select options and page cover
|
||||
|
||||
#### Bug Fixes
|
||||
- Fixed login page UI overflow issues on small screen devices
|
||||
|
||||
## Version 0.9.6 – 05/08/2025
|
||||
|
||||
### Bug Fixes
|
||||
- Fixed some syncing issues
|
||||
- Improved the Share menu
|
||||
- Fixed some UI issues related to database filters and sorts
|
||||
|
||||
## Version 0.9.5 – 17/07/2025
|
||||
### Desktop
|
||||
#### New Features
|
||||
- Vault Workspace: A new workspace type, private and offline. AI runs locally with no data transfer. Supports switching embedding models, chatting with files (PDF, Markdown, TXT). Includes RAG search with AI-generated overviews.
|
||||
- Revamped color pickers in documents: Expanded palette with support for custom colors
|
||||
|
||||
### Mobile
|
||||
#### New Features
|
||||
- iOS In‑App Sign‑In: Sign in directly within the iOS app
|
||||
- New colors: Improved text and background color options
|
||||
|
||||
Bug Fixes
|
||||
- Add a network connection indicator
|
||||
- Fix sync bugs and issues with WebSocket connections.
|
||||
|
||||
## Version 0.9.4 – 02/07/2025
|
||||
### Desktop
|
||||
#### New Features
|
||||
- Private page sharing: Add members to private pages with Can View or Can Edit permissions
|
||||
- Guest editor collaboration: Invite non-members (guest editors) to collaborate in real-time on your pages
|
||||
- Shared with me: Browse all pages shared with you under the new Shared with me section
|
||||
- New syncing protocol: Optimized for faster, more reliable multi-user and multi-device data sync
|
||||
### Mobile
|
||||
#### New Features
|
||||
- Shared page collaboration: View and edit pages that have been shared with you on iOS and Android
|
||||
- New syncing protocol: Optimized for faster, more reliable multi-user and multi-device data sync
|
||||
|
||||
## Version 0.9.3 - 28/05/2025
|
||||
### Desktop
|
||||
#### New Features
|
||||
@@ -340,8 +23,8 @@ Bug Fixes
|
||||
## Version 0.9.2 - 14/05/2025
|
||||
### Desktop
|
||||
#### New Features
|
||||
- Supported AI Overview in Search to answer user queries based on their entire workspace
|
||||
- Revamped the Search panel in the desktop app
|
||||
- Supported AI Overview in Search to answer user queries based on their entire workspace
|
||||
- Revamped the Search panel in the desktop app
|
||||
- Enabled loading custom prompts from an AppFlowy database page
|
||||
#### Bug Fixes
|
||||
- Improved inserting emojis using the colon (:)
|
||||
@@ -552,7 +235,7 @@ Bug Fixes
|
||||
- Fixed an error when opening files in the database in local mode
|
||||
- Fixed arrow up/down navigation not working for selecting a language in Code Block
|
||||
- Fixed an issue where deleting multiple blocks using the drag button on the document page didn’t work
|
||||
|
||||
|
||||
## Version 0.7.7 - 09/12/2024
|
||||
### Bug Fixes
|
||||
- Fixed sidebar menu resize regression
|
||||
|
||||
@@ -48,7 +48,7 @@ AppFlowy is the AI workspace where you achieve more without losing control of yo
|
||||
- [App Store](https://apps.apple.com/app/appflowy/id6457261352): iPhone
|
||||
- [Play Store](https://play.google.com/store/apps/details?id=io.appflowy.appflowy): Android 10 or above; ARMv7 is
|
||||
not supported
|
||||
- [Self-hosting AppFlowy](https://appflowy.com/docs/Step-by-step-Self-Hosting-Guide---From-Zero-to-Production)
|
||||
- [Self-hosting AppFlowy](https://appflowy.com/docs/self-host-appflowy-overview)
|
||||
- [Source](https://docs.appflowy.io/docs/documentation/appflowy/from-source)
|
||||
|
||||
## Built With
|
||||
|
||||
@@ -26,7 +26,7 @@ CARGO_MAKE_EXTEND_WORKSPACE_MAKEFILE = true
|
||||
CARGO_MAKE_CRATE_FS_NAME = "dart_ffi"
|
||||
CARGO_MAKE_CRATE_NAME = "dart-ffi"
|
||||
LIB_NAME = "dart_ffi"
|
||||
APPFLOWY_VERSION = "0.11.4"
|
||||
APPFLOWY_VERSION = "0.9.3"
|
||||
FLUTTER_DESKTOP_FEATURES = "dart"
|
||||
PRODUCT_NAME = "AppFlowy"
|
||||
MACOSX_DEPLOYMENT_TARGET = "11.0"
|
||||
|
||||
@@ -32,7 +32,7 @@ if (keystorePropertiesFile.exists()) {
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdkVersion 35
|
||||
compileSdkVersion 34
|
||||
ndkVersion "24.0.8215888"
|
||||
|
||||
compileOptions {
|
||||
|
||||
@@ -6,7 +6,7 @@ buildscript {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:8.1.0'
|
||||
classpath 'com.android.tools.build:gradle:7.4.2'
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.1-all.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-bin.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"{""data"":""Juan"",""last_modified"":1726063423,""created_at"":1726063423,""field_type"":0}","{""created_at"":1726122510,""reminder_id"":"""",""include_time"":false,""is_range"":true,""last_modified"":1726122515,""data"":""1725604115"",""end_timestamp"":""1725776915"",""field_type"":2}","{""field_type"":1,""created_at"":1726063677,""last_modified"":1726063677,""data"":""93100""}","{""field_type"":1,""data"":""4.86"",""created_at"":1726062597,""last_modified"":1726062597}",,"{""last_modified"":1726062377,""field_type"":5,""data"":""Yes"",""created_at"":1726062377}","{""last_modified"":1726064412,""field_type"":7,""data"":""{\""options\"":[{\""id\"":\""tTDq\"",\""name\"":\""complete onboarding\"",\""color\"":\""Purple\""},{\""id\"":\""E8Ds\"",\""name\"":\""contact support\"",\""color\"":\""Purple\""},{\""id\"":\""RoGN\"",\""name\"":\""get started\"",\""color\"":\""Purple\""}],\""selected_option_ids\"":[\""tTDq\"",\""E8Ds\""]}"",""created_at"":1726064396}",,"{""created_at"":1726065278,""field_type"":3,""data"":""qnja"",""last_modified"":1726065278}","{""data"":""R9I7,yORP,1i4f"",""field_type"":4,""created_at"":1726105126,""last_modified"":1726105127}","{""data"":""1726122515"",""field_type"":8}","{""data"":""1726060541"",""field_type"":9}"
|
||||
"{""data"":""Alex"",""created_at"":1726063432,""last_modified"":1726063432,""field_type"":0}","{""reminder_id"":"""",""data"":""1725292800"",""include_time"":true,""last_modified"":1726122448,""created_at"":1726122422,""is_range"":true,""end_timestamp"":""1725551940"",""field_type"":2}","{""field_type"":1,""last_modified"":1726063683,""created_at"":1726063683,""data"":""3560""}","{""created_at"":1726062561,""data"":""1.96"",""last_modified"":1726062561,""field_type"":1}","{""last_modified"":1726063952,""created_at"":1726063931,""data"":""al3x1343@protonmail.com"",""field_type"":6}","{""last_modified"":1726062375,""field_type"":5,""created_at"":1726062375,""data"":""Yes""}","{""data"":""{\""options\"":[{\""id\"":\""qNyr\"",\""name\"":\""finish reading book\"",\""color\"":\""Purple\""}],\""selected_option_ids\"":[]}"",""created_at"":1726064616,""last_modified"":1726064616,""field_type"":7}",,"{""data"":""qnja"",""created_at"":1726065272,""last_modified"":1726065272,""field_type"":3}","{""created_at"":1726105180,""last_modified"":1726105180,""field_type"":4,""data"":""R9I7,1i4f""}","{""field_type"":8,""data"":""1726122448""}","{""field_type"":9,""data"":""1726060541""}"
|
||||
"{""last_modified"":1726063478,""created_at"":1726063436,""field_type"":0,""data"":""Alexander""}",,"{""field_type"":1,""last_modified"":1726063691,""created_at"":1726063691,""data"":""2073""}","{""field_type"":1,""data"":""0.5"",""last_modified"":1726062577,""created_at"":1726062577}","{""last_modified"":1726063991,""field_type"":6,""created_at"":1726063991,""data"":""alexandernotthedra@gmail.com""}","{""field_type"":5,""last_modified"":1726062378,""created_at"":1726062377,""data"":""No""}",,,"{""created_at"":1726065291,""data"":""GSf_"",""last_modified"":1726065291,""field_type"":3}","{""last_modified"":1726105142,""created_at"":1726105133,""data"":""SEUo"",""field_type"":4}","{""field_type"":8,""data"":""1726105142""}","{""field_type"":9,""data"":""1726060542""}"
|
||||
"{""field_type"":0,""created_at"":1726063454,""last_modified"":1726063454,""data"":""George""}","{""created_at"":1726122467,""end_timestamp"":""1726468070"",""include_time"":false,""is_range"":true,""reminder_id"":"""",""field_type"":2,""data"":""1726295270"",""last_modified"":1726122470}",,,"{""field_type"":6,""data"":""george.aq@appflowy.io"",""last_modified"":1726064104,""created_at"":1726064016}","{""last_modified"":1726062376,""created_at"":1726062376,""field_type"":5,""data"":""Yes""}","{""data"":""{\""options\"":[{\""id\"":\""s_dQ\"",\""name\"":\""bug triage\"",\""color\"":\""Purple\""},{\""id\"":\""-Zfo\"",\""name\"":\""fix bugs\"",\""color\"":\""Purple\""},{\""id\"":\""wsDN\"",\""name\"":\""attend meetings\"",\""color\"":\""Purple\""}],\""selected_option_ids\"":[\""s_dQ\"",\""-Zfo\""]}"",""last_modified"":1726064468,""created_at"":1726064424,""field_type"":7}","{""data"":[""{\""id\"":\""8a77f84d-64e9-4e67-b902-fa23980459ec\"",\""name\"":\""BQdTmxpRI6f.png\"",\""url\"":\""https://samplelib.com/lib/preview/png/sample-blue-200x200.png\"",\""upload_type\"":\""NetworkMedia\"",\""file_type\"":\""Image\""}""],""field_type"":14,""created_at"":1726122956,""last_modified"":1726122956}","{""field_type"":3,""data"":""qnja"",""created_at"":1726065313,""last_modified"":1726065313}","{""data"":""R9I7,yORP"",""field_type"":4,""last_modified"":1726105198,""created_at"":1726105187}","{""data"":""1726122956"",""field_type"":8}","{""data"":""1726060543"",""field_type"":9}"
|
||||
"{""field_type"":0,""created_at"":1726063454,""last_modified"":1726063454,""data"":""George""}","{""created_at"":1726122467,""end_timestamp"":""1726468070"",""include_time"":false,""is_range"":true,""reminder_id"":"""",""field_type"":2,""data"":""1726295270"",""last_modified"":1726122470}",,,"{""field_type"":6,""data"":""george.aq@appflowy.io"",""last_modified"":1726064104,""created_at"":1726064016}","{""last_modified"":1726062376,""created_at"":1726062376,""field_type"":5,""data"":""Yes""}","{""data"":""{\""options\"":[{\""id\"":\""s_dQ\"",\""name\"":\""bug triage\"",\""color\"":\""Purple\""},{\""id\"":\""-Zfo\"",\""name\"":\""fix bugs\"",\""color\"":\""Purple\""},{\""id\"":\""wsDN\"",\""name\"":\""attend meetings\"",\""color\"":\""Purple\""}],\""selected_option_ids\"":[\""s_dQ\"",\""-Zfo\""]}"",""last_modified"":1726064468,""created_at"":1726064424,""field_type"":7}","{""data"":[""{\""id\"":\""8a77f84d-64e9-4e67-b902-fa23980459ec\"",\""name\"":\""BQdTmxpRI6f.png\"",\""url\"":\""https://file-examples.com/wp-content/storage/2017/10/file_example_PNG_500kB.png\"",\""upload_type\"":\""NetworkMedia\"",\""file_type\"":\""Image\""}""],""field_type"":14,""created_at"":1726122956,""last_modified"":1726122956}","{""field_type"":3,""data"":""qnja"",""created_at"":1726065313,""last_modified"":1726065313}","{""data"":""R9I7,yORP"",""field_type"":4,""last_modified"":1726105198,""created_at"":1726105187}","{""data"":""1726122956"",""field_type"":8}","{""data"":""1726060543"",""field_type"":9}"
|
||||
"{""field_type"":0,""last_modified"":1726063467,""data"":""Joanna"",""created_at"":1726063467}","{""include_time"":false,""end_timestamp"":""1727072893"",""is_range"":true,""last_modified"":1726122493,""created_at"":1726122483,""data"":""1726554493"",""field_type"":2,""reminder_id"":""""}","{""last_modified"":1726065463,""data"":""16470"",""field_type"":1,""created_at"":1726065463}","{""created_at"":1726062626,""field_type"":1,""last_modified"":1726062626,""data"":""-5.36""}","{""last_modified"":1726064069,""data"":""joannastrawberry29+hello@gmail.com"",""created_at"":1726064069,""field_type"":6}",,"{""field_type"":7,""created_at"":1726064444,""last_modified"":1726064460,""data"":""{\""options\"":[{\""id\"":\""ZxJz\"",\""name\"":\""post on Twitter\"",\""color\"":\""Purple\""},{\""id\"":\""upwi\"",\""name\"":\""watch Youtube videos\"",\""color\"":\""Purple\""}],\""selected_option_ids\"":[\""upwi\""]}""}",,"{""created_at"":1726065317,""last_modified"":1726065317,""field_type"":3,""data"":""qnja""}","{""field_type"":4,""last_modified"":1726105173,""data"":""uRAO,yORP"",""created_at"":1726105170}","{""data"":""1726122493"",""field_type"":8}","{""data"":""1726060545"",""field_type"":9}"
|
||||
"{""last_modified"":1726063457,""created_at"":1726063457,""data"":""George"",""field_type"":0}","{""include_time"":true,""reminder_id"":"""",""field_type"":2,""is_range"":true,""created_at"":1726122521,""end_timestamp"":""1725829200"",""data"":""1725822900"",""last_modified"":1726122535}","{""last_modified"":1726065493,""field_type"":1,""data"":""9500"",""created_at"":1726065493}","{""last_modified"":1726062680,""created_at"":1726062680,""field_type"":1,""data"":""1.7""}","{""data"":""plgeorgebball@gmail.com"",""field_type"":6,""last_modified"":1726064087,""created_at"":1726064036}",,"{""last_modified"":1726064513,""data"":""{\""options\"":[{\""id\"":\""zy0x\"",\""name\"":\""game vs celtics\"",\""color\"":\""Purple\""},{\""id\"":\""WJsv\"",\""name\"":\""training\"",\""color\"":\""Purple\""},{\""id\"":\""w-f8\"",\""name\"":\""game vs spurs\"",\""color\"":\""Purple\""},{\""id\"":\""p1VQ\"",\""name\"":\""game vs knicks\"",\""color\"":\""Purple\""},{\""id\"":\""VjUA\"",\""name\"":\""recovery\"",\""color\"":\""Purple\""},{\""id\"":\""sQ8X\"",\""name\"":\""don't get injured\"",\""color\"":\""Purple\""}],\""selected_option_ids\"":[]}"",""created_at"":1726064486,""field_type"":7}",,"{""field_type"":3,""last_modified"":1726065310,""data"":""qnja"",""created_at"":1726065310}","{""created_at"":1726105205,""field_type"":4,""last_modified"":1726105249,""data"":""R9I7,1i4f,yORP,SEUo""}","{""data"":""1726122535"",""field_type"":8}","{""field_type"":9,""data"":""1726060546""}"
|
||||
"{""data"":""Judy"",""created_at"":1726063475,""field_type"":0,""last_modified"":1726063487}","{""end_timestamp"":"""",""reminder_id"":"""",""data"":""1726640950"",""field_type"":2,""include_time"":false,""created_at"":1726122550,""last_modified"":1726122550,""is_range"":false}",,,"{""created_at"":1726063882,""field_type"":6,""last_modified"":1726064000,""data"":""judysmithjr@outlook.com""}","{""last_modified"":1726062712,""field_type"":5,""data"":""Yes"",""created_at"":1726062712}","{""created_at"":1726064549,""field_type"":7,""data"":""{\""options\"":[{\""id\"":\""j8cC\"",\""name\"":\""finish training\"",\""color\"":\""Purple\""},{\""id\"":\""SmSk\"",\""name\"":\""brainwash\"",\""color\"":\""Purple\""},{\""id\"":\""mnf5\"",\""name\"":\""welcome to ba sing se\"",\""color\"":\""Purple\""},{\""id\"":\""hcrj\"",\""name\"":\""don't mess up\"",\""color\"":\""Purple\""}],\""selected_option_ids\"":[\""j8cC\"",\""SmSk\"",\""mnf5\"",\""hcrj\""]}"",""last_modified"":1726064591}",,,"{""field_type"":4,""last_modified"":1726105152,""created_at"":1726105152,""data"":""R9I7""}","{""field_type"":8,""data"":""1726122550""}","{""field_type"":9,""data"":""1726060549""}"
|
||||
+6
-6
@@ -1,10 +1,10 @@
|
||||
import 'package:appflowy/env/cloud_env.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/workspace/application/view/view_ext.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/dialog_v2.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pbenum.dart';
|
||||
import 'package:appflowy_ui/appflowy_ui.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/text_input.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
@@ -38,12 +38,12 @@ void main() {
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byType(AFTextFieldDialog), findsOneWidget);
|
||||
expect(find.byType(NavigatorTextFieldDialog), findsOneWidget);
|
||||
|
||||
final textField = tester.widget<AFTextField>(
|
||||
final textField = tester.widget<FlowyFormTextInput>(
|
||||
find.descendant(
|
||||
of: find.byType(AFTextFieldDialog),
|
||||
matching: find.byType(AFTextField),
|
||||
of: find.byType(NavigatorTextFieldDialog),
|
||||
matching: find.byType(FlowyFormTextInput),
|
||||
),
|
||||
);
|
||||
|
||||
|
||||
+5
-5
@@ -34,7 +34,7 @@ void main() {
|
||||
var workspaceIcon = tester.widget<WorkspaceIcon>(
|
||||
find.byType(WorkspaceIcon),
|
||||
);
|
||||
expect(workspaceIcon.workspaceIcon, '');
|
||||
expect(workspaceIcon.workspace.icon, '');
|
||||
|
||||
await tester.openWorkspaceMenu();
|
||||
await tester.changeWorkspaceIcon(icon);
|
||||
@@ -47,8 +47,8 @@ void main() {
|
||||
workspaceIcon = tester.widget<WorkspaceIcon>(
|
||||
find.byType(WorkspaceIcon),
|
||||
);
|
||||
expect(workspaceIcon.workspaceIcon, icon);
|
||||
expect(workspaceIcon.workspaceName, name);
|
||||
expect(workspaceIcon.workspace.icon, icon);
|
||||
expect(find.findTextInFlowyText(name), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('verify the result again after relaunching', (tester) async {
|
||||
@@ -69,7 +69,7 @@ void main() {
|
||||
final workspaceIcon = tester.widget<WorkspaceIcon>(
|
||||
find.byType(WorkspaceIcon),
|
||||
);
|
||||
expect(workspaceIcon.workspaceIcon, icon);
|
||||
expect(workspaceIcon.workspaceName, name);
|
||||
expect(workspaceIcon.workspace.icon, icon);
|
||||
expect(workspaceIcon.workspace.name, name);
|
||||
});
|
||||
}
|
||||
+12
-7
@@ -36,20 +36,23 @@ void main() {
|
||||
final loading = find.byType(Loading);
|
||||
await tester.pumpUntilNotFound(loading);
|
||||
|
||||
Finder success;
|
||||
|
||||
final Finder items = find.byType(WorkspaceMenuItem);
|
||||
|
||||
// delete the newly created workspace
|
||||
await tester.openCollaborativeWorkspaceMenu();
|
||||
await tester.pumpUntilFound(items);
|
||||
|
||||
final items = find.byType(WorkspaceMenuItem);
|
||||
expect(items, findsNWidgets(2));
|
||||
|
||||
final lastWorkspace = items.last;
|
||||
expect(
|
||||
tester.widget<WorkspaceMenuItem>(lastWorkspace).workspace.name,
|
||||
tester.widget<WorkspaceMenuItem>(items.last).workspace.name,
|
||||
name,
|
||||
);
|
||||
|
||||
final secondWorkspace = find.byType(WorkspaceMenuItem).last;
|
||||
await tester.hoverOnWidget(
|
||||
lastWorkspace,
|
||||
secondWorkspace,
|
||||
onHover: () async {
|
||||
// click the more button
|
||||
final moreButton = find.byType(WorkspaceMoreActionList);
|
||||
@@ -65,8 +68,10 @@ void main() {
|
||||
expect(confirm, findsOneWidget);
|
||||
await tester.tapButton(find.text(LocaleKeys.button_ok.tr()));
|
||||
// delete success
|
||||
final success = find.text(LocaleKeys.workspace_createSuccess.tr());
|
||||
success = find.text(LocaleKeys.workspace_createSuccess.tr());
|
||||
await tester.pumpUntilFound(success);
|
||||
expect(success, findsOneWidget);
|
||||
await tester.pumpUntilNotFound(success);
|
||||
},
|
||||
);
|
||||
});
|
||||
@@ -95,7 +100,7 @@ void main() {
|
||||
|
||||
// expect to see the member count
|
||||
final memberCount = find.text('1 member');
|
||||
expect(memberCount, findsAny);
|
||||
expect(memberCount, findsNWidgets(2));
|
||||
});
|
||||
|
||||
testWidgets('workspace menu popover behavior test', (tester) async {
|
||||
|
||||
+1
-1
@@ -7,7 +7,7 @@ import '../../shared/util.dart';
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
group('create and delete the document:', () {
|
||||
group('create and delete the document', () {
|
||||
testWidgets('create a new document when launching app in first time',
|
||||
(tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
|
||||
+2
-3
@@ -7,7 +7,6 @@ import 'package:appflowy/shared/icon_emoji_picker/recent_icons.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/view/view_action_type.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:appflowy_ui/appflowy_ui.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
@@ -522,8 +521,8 @@ extension _SubPageTestHelper on WidgetTester {
|
||||
await hoverOnPageName(currentName, onHover: () async => pumpAndSettle());
|
||||
await rightClickOnPageName(currentName);
|
||||
await tapButtonWithName(ViewMoreActionType.rename.name);
|
||||
await enterText(find.byType(AFTextField), newName);
|
||||
await tapButton(find.text(LocaleKeys.button_confirm.tr()));
|
||||
await enterText(find.byType(TextFormField), newName);
|
||||
await tapOKButton();
|
||||
await pumpAndSettle();
|
||||
}
|
||||
}
|
||||
@@ -56,9 +56,6 @@ void main() {
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
currentScaleFactor += 0.1;
|
||||
currentScaleFactor = double.parse(
|
||||
currentScaleFactor.toStringAsFixed(2),
|
||||
);
|
||||
|
||||
final scaleFactor = await windowSizeManager.getScaleFactor();
|
||||
expect(currentScaleFactor, appflowyScaleFactor);
|
||||
|
||||
@@ -38,13 +38,11 @@ import 'package:appflowy/workspace/presentation/notifications/widgets/notificati
|
||||
import 'package:appflowy/workspace/presentation/notifications/widgets/notification_tab_bar.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/shared/settings_body.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/widgets/settings_menu.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/dialog_v2.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/more_view_actions/more_view_actions.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/more_view_actions/widgets/common_view_action.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/view_title_bar.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
import 'package:appflowy_ui/appflowy_ui.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/widget/buttons/primary_button.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
@@ -266,8 +264,8 @@ extension CommonOperations on WidgetTester {
|
||||
/// Rename the page.
|
||||
Future<void> renamePage(String name) async {
|
||||
await tapRenamePageButton();
|
||||
await enterText(find.byType(AFTextField), name);
|
||||
await tapButton(find.text(LocaleKeys.button_confirm.tr()));
|
||||
await enterText(find.byType(TextFormField), name);
|
||||
await tapOKButton();
|
||||
}
|
||||
|
||||
Future<void> tapTrashButton() async {
|
||||
@@ -361,7 +359,7 @@ extension CommonOperations on WidgetTester {
|
||||
);
|
||||
final showRenameDialog = settingsOrFailure ?? false;
|
||||
if (showRenameDialog) {
|
||||
await tapButton(find.text(LocaleKeys.button_confirm.tr()));
|
||||
await tapOKButton();
|
||||
}
|
||||
await pumpAndSettle();
|
||||
|
||||
@@ -738,7 +736,8 @@ extension CommonOperations on WidgetTester {
|
||||
final workspace = find.byType(SidebarWorkspace);
|
||||
expect(workspace, findsOneWidget);
|
||||
|
||||
await tapButton(workspace, milliseconds: 5000);
|
||||
await tapButton(workspace, pumpAndSettle: false);
|
||||
await pump(const Duration(seconds: 5));
|
||||
}
|
||||
|
||||
Future<void> createCollaborativeWorkspace(String name) async {
|
||||
@@ -753,20 +752,22 @@ extension CommonOperations on WidgetTester {
|
||||
// click the create button
|
||||
final createButton = find.byKey(createWorkspaceButtonKey);
|
||||
expect(createButton, findsOneWidget);
|
||||
await tapButton(createButton);
|
||||
await tapButton(createButton, pumpAndSettle: false);
|
||||
await pump(const Duration(seconds: 5));
|
||||
|
||||
// see the create workspace dialog
|
||||
final createWorkspaceDialog = find.byType(CreateWorkspaceDialog);
|
||||
expect(createWorkspaceDialog, findsOneWidget);
|
||||
|
||||
// input the workspace name
|
||||
final workspaceNameInput = find.descendant(
|
||||
of: find.byType(AFTextFieldDialog),
|
||||
of: createWorkspaceDialog,
|
||||
matching: find.byType(TextField),
|
||||
);
|
||||
await enterText(workspaceNameInput, name);
|
||||
await pumpAndSettle();
|
||||
|
||||
await tapButton(
|
||||
find.text(LocaleKeys.button_confirm.tr()),
|
||||
milliseconds: 2000,
|
||||
);
|
||||
await tapButtonWithName(LocaleKeys.button_ok.tr(), pumpAndSettle: false);
|
||||
await pump(const Duration(seconds: 5));
|
||||
}
|
||||
|
||||
// For mobile platform to launch the app in anonymous mode
|
||||
|
||||
@@ -76,16 +76,15 @@ import 'package:appflowy/util/field_type_extension.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/clear_date_button.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/date_type_option_button.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/reminder_selector.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/dialog_v2.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/toggle/toggle.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
import 'package:appflowy_board/appflowy_board.dart';
|
||||
import 'package:appflowy_ui/appflowy_ui.dart';
|
||||
import 'package:calendar_view/calendar_view.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/text_input.dart';
|
||||
import 'package:flowy_infra_ui/widget/buttons/primary_button.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
@@ -1647,13 +1646,18 @@ extension AppFlowyDatabaseTest on WidgetTester {
|
||||
|
||||
await enterText(
|
||||
find.descendant(
|
||||
of: find.byType(AFTextFieldDialog),
|
||||
matching: find.byType(AFTextField),
|
||||
of: find.byType(FlowyFormTextInput),
|
||||
matching: find.byType(TextFormField),
|
||||
),
|
||||
name,
|
||||
);
|
||||
|
||||
await tapButton(find.text(LocaleKeys.button_confirm.tr()));
|
||||
final field = find.byWidgetPredicate(
|
||||
(widget) =>
|
||||
widget is PrimaryTextButton &&
|
||||
widget.label == LocaleKeys.button_ok.tr(),
|
||||
);
|
||||
await tapButton(field);
|
||||
}
|
||||
|
||||
Future<void> deleteDatebaseView(Finder linkedView) async {
|
||||
|
||||
@@ -98,8 +98,10 @@ extension Expectation on WidgetTester {
|
||||
|
||||
/// Expect to the markdown file export success dialog.
|
||||
void expectToExportSuccess() {
|
||||
final exportSuccess = find.text(
|
||||
LocaleKeys.settings_files_exportFileSuccess.tr(),
|
||||
final exportSuccess = find.byWidgetPredicate(
|
||||
(widget) =>
|
||||
widget is FlowyText &&
|
||||
widget.text == LocaleKeys.settings_files_exportFileSuccess.tr(),
|
||||
);
|
||||
expect(exportSuccess, findsOneWidget);
|
||||
}
|
||||
|
||||
@@ -4,9 +4,8 @@ import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sid
|
||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_icon.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_menu.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/sidebar_workspace.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/dialog_v2.dart';
|
||||
import 'package:appflowy_ui/appflowy_ui.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import 'util.dart';
|
||||
@@ -32,24 +31,26 @@ extension AppFlowyWorkspace on WidgetTester {
|
||||
}
|
||||
|
||||
Future<void> changeWorkspaceName(String name) async {
|
||||
final menuItem = find.byType(WorkspaceMenuItem);
|
||||
expect(menuItem, findsOneWidget);
|
||||
final moreButton = find.descendant(
|
||||
of: find.byType(WorkspaceMenuItem),
|
||||
matching: find.byType(WorkspaceMoreActionList),
|
||||
);
|
||||
expect(moreButton, findsOneWidget);
|
||||
await hoverOnWidget(
|
||||
menuItem,
|
||||
moreButton,
|
||||
onHover: () async {
|
||||
await tapButton(
|
||||
find.descendant(
|
||||
of: menuItem,
|
||||
matching: find.byType(WorkspaceMoreActionList),
|
||||
),
|
||||
);
|
||||
await tapButton(find.text(LocaleKeys.button_rename.tr()));
|
||||
final input = find.descendant(
|
||||
of: find.byType(AFTextFieldDialog),
|
||||
matching: find.byType(AFTextField),
|
||||
await tapButton(moreButton);
|
||||
// wait for the menu to open
|
||||
final renameButton = find.findTextInFlowyText(
|
||||
LocaleKeys.button_rename.tr(),
|
||||
);
|
||||
await pumpUntilFound(renameButton);
|
||||
expect(renameButton, findsOneWidget);
|
||||
await tapButton(renameButton);
|
||||
final input = find.byType(TextFormField);
|
||||
expect(input, findsOneWidget);
|
||||
await enterText(input, name);
|
||||
await tapButton(find.text(LocaleKeys.button_confirm.tr()));
|
||||
await tapButton(find.text(LocaleKeys.button_ok.tr()));
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,9 +1,9 @@
|
||||
import 'package:appflowy/ai/ai.dart';
|
||||
import 'package:appflowy/ai/service/ai_prompt_database_selector_cubit.dart';
|
||||
import 'package:appflowy/features/workspace/logic/workspace_bloc.dart';
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/workspace/application/sidebar/space/space_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/view/view_ext.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/dialog_v2.dart';
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import 'package:appflowy/ai/ai.dart';
|
||||
import 'package:appflowy/features/workspace/logic/workspace_bloc.dart';
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/workspace/application/user/prelude.dart';
|
||||
|
||||
+2
-65
@@ -1,14 +1,13 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:appflowy/ai/service/view_selector_cubit.dart';
|
||||
import 'package:appflowy/features/workspace/logic/workspace_bloc.dart';
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/mobile/presentation/base/flowy_search_text_field.dart';
|
||||
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
|
||||
import 'package:appflowy/ai/service/view_selector_cubit.dart';
|
||||
import 'package:appflowy/plugins/base/drag_handler.dart';
|
||||
import 'package:appflowy/plugins/document/application/document_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/sidebar/space/space_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/view/view_ext.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';
|
||||
@@ -100,26 +99,6 @@ class _PromptInputMobileSelectSourcesButtonState
|
||||
color: Theme.of(context).iconTheme.color,
|
||||
size: const Size.square(20.0),
|
||||
),
|
||||
const HSpace(2.0),
|
||||
ValueListenableBuilder(
|
||||
valueListenable: widget.selectedSourcesNotifier,
|
||||
builder: (context, selectedSourceIds, _) {
|
||||
final documentId =
|
||||
context.read<DocumentBloc?>()?.documentId;
|
||||
final label = documentId != null &&
|
||||
selectedSourceIds.length == 1 &&
|
||||
selectedSourceIds[0] == documentId
|
||||
? LocaleKeys.chat_currentPage.tr()
|
||||
: selectedSourceIds.length.toString();
|
||||
return FlowyText(
|
||||
label,
|
||||
fontSize: 14,
|
||||
figmaLineHeight: 20,
|
||||
color: Theme.of(context).hintColor,
|
||||
);
|
||||
},
|
||||
),
|
||||
const HSpace(2.0),
|
||||
FlowySvg(
|
||||
FlowySvgs.ai_source_drop_down_s,
|
||||
color: Theme.of(context).hintColor,
|
||||
@@ -221,48 +200,6 @@ class _MobileSelectSourcesSheetBody extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
),
|
||||
BlocBuilder<ViewSelectorCubit, ViewSelectorState>(
|
||||
builder: (context, state) {
|
||||
return SliverList(
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
childCount: state.selectedSources.length,
|
||||
(context, index) {
|
||||
final source = state.selectedSources.elementAt(index);
|
||||
return ViewSelectorTreeItem(
|
||||
key: ValueKey(
|
||||
'selected_select_sources_tree_item_${source.view.id}',
|
||||
),
|
||||
viewSelectorItem: source,
|
||||
level: 0,
|
||||
isDescendentOfSpace: source.view.isSpace,
|
||||
isSelectedSection: true,
|
||||
onSelected: (item) {
|
||||
context
|
||||
.read<ViewSelectorCubit>()
|
||||
.toggleSelectedStatus(item, true);
|
||||
},
|
||||
height: 40.0,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
BlocBuilder<ViewSelectorCubit, ViewSelectorState>(
|
||||
builder: (context, state) {
|
||||
if (state.selectedSources.isNotEmpty &&
|
||||
state.visibleSources.isNotEmpty) {
|
||||
return SliverToBoxAdapter(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
child: AFDivider(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return const SliverToBoxAdapter();
|
||||
},
|
||||
),
|
||||
BlocBuilder<ViewSelectorCubit, ViewSelectorState>(
|
||||
builder: (context, state) {
|
||||
return SliverList(
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import 'package:appflowy/features/workspace/logic/workspace_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/sidebar/space/space_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/user/prelude.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
@@ -120,9 +120,4 @@ class KVKeys {
|
||||
///
|
||||
/// The value is a json list of id
|
||||
static const String compactModeIds = 'compactModeIds';
|
||||
|
||||
/// v0.9.4: has the user clicked the upgrade to pro button
|
||||
/// The value is a boolean string
|
||||
static const String hasClickedUpgradeToProButton =
|
||||
'hasClickedUpgradeToProButton';
|
||||
}
|
||||
-33
@@ -1,33 +0,0 @@
|
||||
import 'package:appflowy/features/share_tab/data/models/models.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
|
||||
import 'package:appflowy_result/appflowy_result.dart';
|
||||
|
||||
/// Abstract repository for managing view lock status.
|
||||
///
|
||||
/// For example, we're using rust events now, but we can still use the http api
|
||||
/// for the future.
|
||||
abstract class PageAccessLevelRepository {
|
||||
/// Gets the current view from the backend.
|
||||
Future<FlowyResult<ViewPB, FlowyError>> getView(String pageId);
|
||||
|
||||
/// Locks the view.
|
||||
Future<FlowyResult<void, FlowyError>> lockView(String pageId);
|
||||
|
||||
/// Unlocks the view.
|
||||
Future<FlowyResult<void, FlowyError>> unlockView(String pageId);
|
||||
|
||||
/// Gets the access level of the current user.
|
||||
Future<FlowyResult<ShareAccessLevel, FlowyError>> getAccessLevel(
|
||||
String pageId,
|
||||
);
|
||||
|
||||
/// Gets the section type of the shared section.
|
||||
Future<FlowyResult<SharedSectionType, FlowyError>> getSectionType(
|
||||
String pageId,
|
||||
);
|
||||
|
||||
/// Get current workspace
|
||||
Future<FlowyResult<UserWorkspacePB, FlowyError>> getCurrentWorkspace();
|
||||
}
|
||||
-206
@@ -1,206 +0,0 @@
|
||||
import 'package:appflowy/features/page_access_level/data/repositories/page_access_level_repository.dart';
|
||||
import 'package:appflowy/features/share_tab/data/models/models.dart';
|
||||
import 'package:appflowy/features/util/extensions.dart';
|
||||
import 'package:appflowy/user/application/user_service.dart';
|
||||
import 'package:appflowy/workspace/application/view/view_service.dart';
|
||||
import 'package:appflowy_backend/dispatch/dispatch.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-error/code.pbenum.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'
|
||||
hide AFRolePB;
|
||||
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
|
||||
import 'package:appflowy_result/appflowy_result.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
|
||||
class RustPageAccessLevelRepositoryImpl implements PageAccessLevelRepository {
|
||||
@override
|
||||
Future<FlowyResult<ViewPB, FlowyError>> getView(String pageId) async {
|
||||
final result = await ViewBackendService.getView(pageId);
|
||||
return result.fold(
|
||||
(view) {
|
||||
Log.debug('get view(${view.id}) success');
|
||||
return FlowyResult.success(view);
|
||||
},
|
||||
(error) {
|
||||
Log.error('failed to get view, error: $error');
|
||||
return FlowyResult.failure(error);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<FlowyResult<void, FlowyError>> lockView(String pageId) async {
|
||||
final result = await ViewBackendService.lockView(pageId);
|
||||
return result.fold(
|
||||
(_) {
|
||||
Log.debug('lock view($pageId) success');
|
||||
return FlowyResult.success(null);
|
||||
},
|
||||
(error) {
|
||||
Log.error('failed to lock view, error: $error');
|
||||
return FlowyResult.failure(error);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<FlowyResult<void, FlowyError>> unlockView(String pageId) async {
|
||||
final result = await ViewBackendService.unlockView(pageId);
|
||||
return result.fold(
|
||||
(_) {
|
||||
Log.debug('unlock view($pageId) success');
|
||||
return FlowyResult.success(null);
|
||||
},
|
||||
(error) {
|
||||
Log.error('failed to unlock view, error: $error');
|
||||
return FlowyResult.failure(error);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// 1. local users have full access
|
||||
/// 2. local workspace users have full access
|
||||
/// 3. page creator has full access
|
||||
/// 4. owner and members in public page have full access
|
||||
/// 5. check the shared users list
|
||||
@override
|
||||
Future<FlowyResult<ShareAccessLevel, FlowyError>> getAccessLevel(
|
||||
String pageId,
|
||||
) async {
|
||||
final userResult = await UserBackendService.getCurrentUserProfile();
|
||||
final user = userResult.fold(
|
||||
(s) => s,
|
||||
(_) => null,
|
||||
);
|
||||
|
||||
if (user == null) {
|
||||
return FlowyResult.failure(
|
||||
FlowyError(
|
||||
code: ErrorCode.Internal,
|
||||
msg: 'User not found',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (user.userAuthType == AuthTypePB.Local) {
|
||||
// Local user can always have full access.
|
||||
return FlowyResult.success(ShareAccessLevel.fullAccess);
|
||||
}
|
||||
|
||||
if (user.workspaceType == WorkspaceTypePB.LocalW) {
|
||||
// Local workspace can always have full access.
|
||||
return FlowyResult.success(ShareAccessLevel.fullAccess);
|
||||
}
|
||||
|
||||
// If the user is the creator of the page, they can always have full access.
|
||||
final viewResult = await getView(pageId);
|
||||
final view = viewResult.fold(
|
||||
(s) => s,
|
||||
(_) => null,
|
||||
);
|
||||
if (view?.createdBy == user.id) {
|
||||
return FlowyResult.success(ShareAccessLevel.fullAccess);
|
||||
}
|
||||
|
||||
// If the page is public, the user can always have full access.
|
||||
final workspaceResult = await getCurrentWorkspace();
|
||||
final workspace = workspaceResult.fold(
|
||||
(s) => s,
|
||||
(_) => null,
|
||||
);
|
||||
if (workspace == null) {
|
||||
return FlowyResult.failure(
|
||||
FlowyError(
|
||||
code: ErrorCode.Internal,
|
||||
msg: 'Current workspace not found',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final sectionTypeResult = await getSectionType(pageId);
|
||||
final sectionType = sectionTypeResult.fold(
|
||||
(s) => s,
|
||||
(_) => null,
|
||||
);
|
||||
if (sectionType == SharedSectionType.public &&
|
||||
workspace.role != AFRolePB.Guest) {
|
||||
return FlowyResult.success(ShareAccessLevel.fullAccess);
|
||||
}
|
||||
|
||||
final email = user.email;
|
||||
|
||||
final request = GetSharedUsersPayloadPB(
|
||||
viewId: pageId,
|
||||
);
|
||||
final result = await FolderEventGetSharedUsers(request).send();
|
||||
return result.fold(
|
||||
(success) {
|
||||
final accessLevel = success.items
|
||||
.firstWhereOrNull(
|
||||
(item) => item.email == email,
|
||||
)
|
||||
?.accessLevel
|
||||
.shareAccessLevel ??
|
||||
ShareAccessLevel.readOnly;
|
||||
|
||||
Log.debug('current user access level: $accessLevel, in page: $pageId');
|
||||
|
||||
return FlowyResult.success(accessLevel);
|
||||
},
|
||||
(failure) {
|
||||
Log.error(
|
||||
'failed to get user access level: $failure, in page: $pageId',
|
||||
);
|
||||
|
||||
// return the read access level if the user is not found
|
||||
return FlowyResult.success(ShareAccessLevel.readOnly);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<FlowyResult<SharedSectionType, FlowyError>> getSectionType(
|
||||
String pageId,
|
||||
) async {
|
||||
final request = ViewIdPB(value: pageId);
|
||||
final result = await FolderEventGetSharedViewSection(request).send();
|
||||
return result.fold(
|
||||
(success) {
|
||||
final sectionType = success.section.sharedSectionType;
|
||||
Log.debug('shared section type: $sectionType, in page: $pageId');
|
||||
return FlowyResult.success(sectionType);
|
||||
},
|
||||
(failure) {
|
||||
Log.error(
|
||||
'failed to get shared section type: $failure, in page: $pageId',
|
||||
);
|
||||
|
||||
return FlowyResult.failure(failure);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<FlowyResult<UserWorkspacePB, FlowyError>> getCurrentWorkspace() async {
|
||||
final result = await UserBackendService.getCurrentWorkspace();
|
||||
final currentWorkspaceId = result.fold(
|
||||
(s) => s.id,
|
||||
(_) => null,
|
||||
);
|
||||
|
||||
if (currentWorkspaceId == null) {
|
||||
return FlowyResult.failure(
|
||||
FlowyError(
|
||||
code: ErrorCode.Internal,
|
||||
msg: 'Current workspace not found',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final workspaceResult = await UserBackendService.getWorkspaceById(
|
||||
currentWorkspaceId,
|
||||
);
|
||||
return workspaceResult;
|
||||
}
|
||||
}
|
||||
-163
@@ -1,163 +0,0 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:appflowy/features/page_access_level/data/repositories/page_access_level_repository.dart';
|
||||
import 'package:appflowy/features/page_access_level/data/repositories/rust_page_access_level_repository_impl.dart';
|
||||
import 'package:appflowy/features/page_access_level/logic/page_access_level_event.dart';
|
||||
import 'package:appflowy/features/page_access_level/logic/page_access_level_state.dart';
|
||||
import 'package:appflowy/features/share_tab/data/models/models.dart';
|
||||
import 'package:appflowy/shared/feature_flags.dart';
|
||||
import 'package:appflowy/workspace/application/view/view_listener.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
import 'package:bloc/bloc.dart';
|
||||
import 'package:protobuf/protobuf.dart';
|
||||
|
||||
export 'page_access_level_event.dart';
|
||||
export 'page_access_level_state.dart';
|
||||
|
||||
class PageAccessLevelBloc
|
||||
extends Bloc<PageAccessLevelEvent, PageAccessLevelState> {
|
||||
PageAccessLevelBloc({
|
||||
required this.view,
|
||||
this.ignorePageAccessLevel = false,
|
||||
PageAccessLevelRepository? repository,
|
||||
}) : repository = repository ?? RustPageAccessLevelRepositoryImpl(),
|
||||
listener = ViewListener(viewId: view.id),
|
||||
super(PageAccessLevelState.initial(view)) {
|
||||
on<PageAccessLevelInitialEvent>(_onInitial);
|
||||
on<PageAccessLevelLockEvent>(_onLock);
|
||||
on<PageAccessLevelUnlockEvent>(_onUnlock);
|
||||
on<PageAccessLevelUpdateLockStatusEvent>(_onUpdateLockStatus);
|
||||
on<PageAccessLevelUpdateSectionTypeEvent>(_onUpdateSectionType);
|
||||
}
|
||||
|
||||
final ViewPB view;
|
||||
|
||||
// The repository to manage view lock status.
|
||||
// If you need to test this bloc, you can add your own repository implementation.
|
||||
final PageAccessLevelRepository repository;
|
||||
|
||||
// Used to listen for view updates.
|
||||
late final ViewListener listener;
|
||||
|
||||
// should ignore the page access level
|
||||
// in the row details page, we don't need to check the page access level
|
||||
final bool ignorePageAccessLevel;
|
||||
|
||||
@override
|
||||
Future<void> close() async {
|
||||
await listener.stop();
|
||||
return super.close();
|
||||
}
|
||||
|
||||
Future<void> _onInitial(
|
||||
PageAccessLevelInitialEvent event,
|
||||
Emitter<PageAccessLevelState> emit,
|
||||
) async {
|
||||
// lock status
|
||||
listener.start(
|
||||
onViewUpdated: (view) async {
|
||||
add(PageAccessLevelEvent.updateLockStatus(view.isLocked));
|
||||
},
|
||||
);
|
||||
|
||||
// section type
|
||||
final sectionTypeResult = await repository.getSectionType(view.id);
|
||||
final sectionType = sectionTypeResult.fold(
|
||||
(sectionType) => sectionType,
|
||||
(_) => SharedSectionType.public,
|
||||
);
|
||||
|
||||
if (!FeatureFlag.sharedSection.isOn || ignorePageAccessLevel) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
view: view,
|
||||
isLocked: view.isLocked,
|
||||
isLoadingLockStatus: false,
|
||||
accessLevel: ShareAccessLevel.fullAccess,
|
||||
sectionType: sectionType,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
final result = await repository.getView(view.id);
|
||||
final accessLevel = await repository.getAccessLevel(view.id);
|
||||
final latestView = result.fold(
|
||||
(view) => view,
|
||||
(_) => view,
|
||||
);
|
||||
emit(
|
||||
state.copyWith(
|
||||
view: latestView,
|
||||
isLocked: latestView.isLocked,
|
||||
isLoadingLockStatus: false,
|
||||
accessLevel: accessLevel.fold(
|
||||
(accessLevel) => accessLevel,
|
||||
(_) => ShareAccessLevel.readOnly,
|
||||
),
|
||||
sectionType: sectionType,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _onLock(
|
||||
PageAccessLevelLockEvent event,
|
||||
Emitter<PageAccessLevelState> emit,
|
||||
) async {
|
||||
final result = await repository.lockView(view.id);
|
||||
final isLocked = result.fold(
|
||||
(_) => true,
|
||||
(_) => false,
|
||||
);
|
||||
add(
|
||||
PageAccessLevelEvent.updateLockStatus(
|
||||
isLocked,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _onUnlock(
|
||||
PageAccessLevelUnlockEvent event,
|
||||
Emitter<PageAccessLevelState> emit,
|
||||
) async {
|
||||
final result = await repository.unlockView(view.id);
|
||||
final isLocked = result.fold(
|
||||
(_) => false,
|
||||
(_) => true,
|
||||
);
|
||||
add(
|
||||
PageAccessLevelEvent.updateLockStatus(
|
||||
isLocked,
|
||||
lockCounter: state.lockCounter + 1,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _onUpdateLockStatus(
|
||||
PageAccessLevelUpdateLockStatusEvent event,
|
||||
Emitter<PageAccessLevelState> emit,
|
||||
) {
|
||||
state.view.freeze();
|
||||
final updatedView = state.view.rebuild(
|
||||
(update) => update.isLocked = event.isLocked,
|
||||
);
|
||||
emit(
|
||||
state.copyWith(
|
||||
view: updatedView,
|
||||
isLocked: event.isLocked,
|
||||
lockCounter: event.lockCounter ?? state.lockCounter,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _onUpdateSectionType(
|
||||
PageAccessLevelUpdateSectionTypeEvent event,
|
||||
Emitter<PageAccessLevelState> emit,
|
||||
) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
sectionType: event.sectionType,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
-55
@@ -1,55 +0,0 @@
|
||||
import 'package:appflowy/features/share_tab/data/models/models.dart';
|
||||
|
||||
/// Base class for all PageAccessLevel events
|
||||
sealed class PageAccessLevelEvent {
|
||||
const PageAccessLevelEvent();
|
||||
|
||||
/// Initialize the view lock status, it will create a view listener to listen for view updates.
|
||||
/// Also, it will fetch the current view lock status from the repository.
|
||||
const factory PageAccessLevelEvent.initial() = PageAccessLevelInitialEvent;
|
||||
|
||||
/// Lock the view.
|
||||
const factory PageAccessLevelEvent.lock() = PageAccessLevelLockEvent;
|
||||
|
||||
/// Unlock the view.
|
||||
const factory PageAccessLevelEvent.unlock() = PageAccessLevelUnlockEvent;
|
||||
|
||||
/// Update the lock status in the state.
|
||||
const factory PageAccessLevelEvent.updateLockStatus(
|
||||
bool isLocked, {
|
||||
int? lockCounter,
|
||||
}) = PageAccessLevelUpdateLockStatusEvent;
|
||||
|
||||
/// Update the section type in the state.
|
||||
const factory PageAccessLevelEvent.updateSectionType(
|
||||
SharedSectionType sectionType,
|
||||
) = PageAccessLevelUpdateSectionTypeEvent;
|
||||
}
|
||||
|
||||
class PageAccessLevelInitialEvent extends PageAccessLevelEvent {
|
||||
const PageAccessLevelInitialEvent();
|
||||
}
|
||||
|
||||
class PageAccessLevelLockEvent extends PageAccessLevelEvent {
|
||||
const PageAccessLevelLockEvent();
|
||||
}
|
||||
|
||||
class PageAccessLevelUnlockEvent extends PageAccessLevelEvent {
|
||||
const PageAccessLevelUnlockEvent();
|
||||
}
|
||||
|
||||
class PageAccessLevelUpdateLockStatusEvent extends PageAccessLevelEvent {
|
||||
const PageAccessLevelUpdateLockStatusEvent(
|
||||
this.isLocked, {
|
||||
this.lockCounter,
|
||||
});
|
||||
|
||||
final bool isLocked;
|
||||
final int? lockCounter;
|
||||
}
|
||||
|
||||
class PageAccessLevelUpdateSectionTypeEvent extends PageAccessLevelEvent {
|
||||
const PageAccessLevelUpdateSectionTypeEvent(this.sectionType);
|
||||
|
||||
final SharedSectionType sectionType;
|
||||
}
|
||||
-97
@@ -1,97 +0,0 @@
|
||||
import 'package:appflowy/features/share_tab/data/models/models.dart';
|
||||
import 'package:appflowy/shared/feature_flags.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
|
||||
class PageAccessLevelState {
|
||||
factory PageAccessLevelState.initial(ViewPB view) => PageAccessLevelState(
|
||||
view: view,
|
||||
isLocked: false,
|
||||
lockCounter: 0,
|
||||
sectionType: SharedSectionType.public,
|
||||
accessLevel: ShareAccessLevel.readOnly,
|
||||
);
|
||||
|
||||
const PageAccessLevelState({
|
||||
required this.view,
|
||||
required this.isLocked,
|
||||
required this.lockCounter,
|
||||
required this.accessLevel,
|
||||
required this.sectionType,
|
||||
this.myRole,
|
||||
this.isLoadingLockStatus = true,
|
||||
});
|
||||
|
||||
final ViewPB view;
|
||||
final bool isLocked;
|
||||
final int lockCounter;
|
||||
final bool isLoadingLockStatus;
|
||||
final ShareAccessLevel accessLevel;
|
||||
final SharedSectionType sectionType;
|
||||
final ShareRole? myRole;
|
||||
|
||||
bool get isPublic => sectionType == SharedSectionType.public;
|
||||
bool get isPrivate => sectionType == SharedSectionType.private;
|
||||
bool get isShared => sectionType == SharedSectionType.shared;
|
||||
bool get shouldHideSpace => myRole == ShareRole.guest;
|
||||
|
||||
bool get isEditable {
|
||||
if (!FeatureFlag.sharedSection.isOn) {
|
||||
return !isLocked;
|
||||
}
|
||||
|
||||
return accessLevel != ShareAccessLevel.readOnly && !isLocked;
|
||||
}
|
||||
|
||||
bool get isReadOnly => accessLevel == ShareAccessLevel.readOnly;
|
||||
|
||||
PageAccessLevelState copyWith({
|
||||
ViewPB? view,
|
||||
bool? isLocked,
|
||||
int? lockCounter,
|
||||
bool? isLoadingLockStatus,
|
||||
ShareAccessLevel? accessLevel,
|
||||
SharedSectionType? sectionType,
|
||||
ShareRole? myRole,
|
||||
}) {
|
||||
return PageAccessLevelState(
|
||||
view: view ?? this.view,
|
||||
isLocked: isLocked ?? this.isLocked,
|
||||
lockCounter: lockCounter ?? this.lockCounter,
|
||||
isLoadingLockStatus: isLoadingLockStatus ?? this.isLoadingLockStatus,
|
||||
accessLevel: accessLevel ?? this.accessLevel,
|
||||
sectionType: sectionType ?? this.sectionType,
|
||||
myRole: myRole ?? this.myRole,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (identical(this, other)) return true;
|
||||
return other is PageAccessLevelState &&
|
||||
other.view == view &&
|
||||
other.isLocked == isLocked &&
|
||||
other.lockCounter == lockCounter &&
|
||||
other.isLoadingLockStatus == isLoadingLockStatus &&
|
||||
other.accessLevel == accessLevel &&
|
||||
other.sectionType == sectionType &&
|
||||
other.myRole == myRole;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
return Object.hash(
|
||||
view,
|
||||
isLocked,
|
||||
lockCounter,
|
||||
isLoadingLockStatus,
|
||||
accessLevel,
|
||||
sectionType,
|
||||
myRole,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'PageAccessLevelState(view: $view, isLocked: $isLocked, lockCounter: $lockCounter, isLoadingLockStatus: $isLoadingLockStatus, accessLevel: $accessLevel, sectionType: $sectionType, myRole: $myRole)';
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
class UserDataLocation extends Equatable {
|
||||
const UserDataLocation({
|
||||
required this.path,
|
||||
required this.isCustom,
|
||||
});
|
||||
|
||||
final String path;
|
||||
final bool isCustom;
|
||||
|
||||
@override
|
||||
List<Object?> get props => [path, isCustom];
|
||||
}
|
||||
-61
@@ -1,61 +0,0 @@
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy/startup/tasks/rust_sdk.dart';
|
||||
import 'package:appflowy/user/application/user_settings_service.dart';
|
||||
import 'package:appflowy/workspace/application/settings/prelude.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-error/protobuf.dart';
|
||||
import 'package:appflowy_result/appflowy_result.dart';
|
||||
|
||||
import '../models/user_data_location.dart';
|
||||
import 'settings_repository.dart';
|
||||
|
||||
class RustSettingsRepositoryImpl implements SettingsRepository {
|
||||
const RustSettingsRepositoryImpl();
|
||||
|
||||
final _userBackendService = const UserSettingsBackendService();
|
||||
|
||||
@override
|
||||
Future<FlowyResult<UserDataLocation, FlowyError>>
|
||||
getUserDataLocation() async {
|
||||
final defaultDirectory = (await appFlowyApplicationDataDirectory()).path;
|
||||
final result = await _userBackendService.getUserSetting();
|
||||
|
||||
return result.map(
|
||||
(settings) {
|
||||
final userDirectory = settings.userFolder;
|
||||
return UserDataLocation(
|
||||
path: userDirectory,
|
||||
isCustom: userDirectory.contains(defaultDirectory),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<FlowyResult<UserDataLocation, FlowyError>>
|
||||
resetUserDataLocation() async {
|
||||
final directory = await appFlowyApplicationDataDirectory();
|
||||
await getIt<ApplicationDataStorage>().setPath(directory.path);
|
||||
|
||||
return FlowyResult.success(
|
||||
UserDataLocation(
|
||||
path: directory.path,
|
||||
isCustom: false,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<FlowyResult<UserDataLocation, FlowyError>> setCustomLocation(
|
||||
String path,
|
||||
) async {
|
||||
final defaultDirectory = (await appFlowyApplicationDataDirectory()).path;
|
||||
await getIt<ApplicationDataStorage>().setCustomPath(path);
|
||||
|
||||
return FlowyResult.success(
|
||||
UserDataLocation(
|
||||
path: path,
|
||||
isCustom: path.contains(defaultDirectory),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
-14
@@ -1,14 +0,0 @@
|
||||
import 'package:appflowy_backend/protobuf/flowy-error/protobuf.dart';
|
||||
import 'package:appflowy_result/appflowy_result.dart';
|
||||
|
||||
import '../models/user_data_location.dart';
|
||||
|
||||
abstract class SettingsRepository {
|
||||
Future<FlowyResult<UserDataLocation, FlowyError>> getUserDataLocation();
|
||||
|
||||
Future<FlowyResult<UserDataLocation, FlowyError>> resetUserDataLocation();
|
||||
|
||||
Future<FlowyResult<UserDataLocation, FlowyError>> setCustomLocation(
|
||||
String path,
|
||||
);
|
||||
}
|
||||
@@ -1,76 +0,0 @@
|
||||
import 'package:appflowy_result/appflowy_result.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import '../data/repositories/settings_repository.dart';
|
||||
import 'data_location_event.dart';
|
||||
import 'data_location_state.dart';
|
||||
|
||||
class DataLocationBloc extends Bloc<DataLocationEvent, DataLocationState> {
|
||||
DataLocationBloc({
|
||||
required SettingsRepository repository,
|
||||
}) : _repository = repository,
|
||||
super(DataLocationState.initial()) {
|
||||
on<DataLocationInitial>(_onStarted);
|
||||
on<DataLocationResetToDefault>(_onResetToDefault);
|
||||
on<DataLocationSetCustomPath>(_onSetCustomPath);
|
||||
on<DataLocationClearState>(_onClearState);
|
||||
}
|
||||
|
||||
final SettingsRepository _repository;
|
||||
|
||||
Future<void> _onStarted(
|
||||
DataLocationInitial event,
|
||||
Emitter<DataLocationState> emit,
|
||||
) async {
|
||||
final userDataLocation =
|
||||
await _repository.getUserDataLocation().toNullable();
|
||||
|
||||
emit(
|
||||
DataLocationState(
|
||||
userDataLocation: userDataLocation,
|
||||
didResetToDefault: false,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _onResetToDefault(
|
||||
DataLocationResetToDefault event,
|
||||
Emitter<DataLocationState> emit,
|
||||
) async {
|
||||
final defaultLocation =
|
||||
await _repository.resetUserDataLocation().toNullable();
|
||||
|
||||
emit(
|
||||
DataLocationState(
|
||||
userDataLocation: defaultLocation,
|
||||
didResetToDefault: true,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _onClearState(
|
||||
DataLocationClearState event,
|
||||
Emitter<DataLocationState> emit,
|
||||
) async {
|
||||
emit(
|
||||
state.copyWith(
|
||||
didResetToDefault: false,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _onSetCustomPath(
|
||||
DataLocationSetCustomPath event,
|
||||
Emitter<DataLocationState> emit,
|
||||
) async {
|
||||
final userDataLocation =
|
||||
await _repository.setCustomLocation(event.path).toNullable();
|
||||
|
||||
emit(
|
||||
state.copyWith(
|
||||
userDataLocation: userDataLocation,
|
||||
didResetToDefault: false,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
sealed class DataLocationEvent {
|
||||
const DataLocationEvent();
|
||||
|
||||
factory DataLocationEvent.initial() = DataLocationInitial;
|
||||
|
||||
factory DataLocationEvent.resetToDefault() = DataLocationResetToDefault;
|
||||
|
||||
factory DataLocationEvent.setCustomPath(String path) =
|
||||
DataLocationSetCustomPath;
|
||||
|
||||
factory DataLocationEvent.clearState() = DataLocationClearState;
|
||||
}
|
||||
|
||||
class DataLocationInitial extends DataLocationEvent {}
|
||||
|
||||
class DataLocationResetToDefault extends DataLocationEvent {}
|
||||
|
||||
class DataLocationSetCustomPath extends DataLocationEvent {
|
||||
const DataLocationSetCustomPath(this.path);
|
||||
|
||||
final String path;
|
||||
}
|
||||
|
||||
class DataLocationClearState extends DataLocationEvent {}
|
||||
@@ -1,29 +0,0 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
import '../data/models/user_data_location.dart';
|
||||
|
||||
class DataLocationState extends Equatable {
|
||||
const DataLocationState({
|
||||
required this.userDataLocation,
|
||||
required this.didResetToDefault,
|
||||
});
|
||||
|
||||
factory DataLocationState.initial() =>
|
||||
const DataLocationState(userDataLocation: null, didResetToDefault: false);
|
||||
|
||||
final UserDataLocation? userDataLocation;
|
||||
final bool didResetToDefault;
|
||||
|
||||
@override
|
||||
List<Object?> get props => [userDataLocation, didResetToDefault];
|
||||
|
||||
DataLocationState copyWith({
|
||||
UserDataLocation? userDataLocation,
|
||||
bool? didResetToDefault,
|
||||
}) {
|
||||
return DataLocationState(
|
||||
userDataLocation: userDataLocation ?? this.userDataLocation,
|
||||
didResetToDefault: didResetToDefault ?? this.didResetToDefault,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
export 'logic/data_location_bloc.dart';
|
||||
export 'logic/data_location_event.dart';
|
||||
export 'logic/data_location_state.dart';
|
||||
export 'data/models/user_data_location.dart';
|
||||
export 'data/repositories/settings_repository.dart';
|
||||
export 'data/repositories/rust_settings_repository_impl.dart';
|
||||
@@ -2,4 +2,3 @@ export 'share_access_level.dart';
|
||||
export 'share_popover_group_id.dart';
|
||||
export 'share_role.dart';
|
||||
export 'shared_user.dart';
|
||||
export 'share_section_type.dart';
|
||||
+1
-28
@@ -1,4 +1,3 @@
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
|
||||
@@ -16,7 +15,7 @@ enum ShareAccessLevel {
|
||||
/// Full access (edit, share, remove, etc.) and can add new users.
|
||||
fullAccess;
|
||||
|
||||
String get title {
|
||||
String get i18n {
|
||||
switch (this) {
|
||||
case ShareAccessLevel.readOnly:
|
||||
return LocaleKeys.shareTab_accessLevel_view.tr();
|
||||
@@ -28,30 +27,4 @@ enum ShareAccessLevel {
|
||||
return LocaleKeys.shareTab_accessLevel_fullAccess.tr();
|
||||
}
|
||||
}
|
||||
|
||||
String get subtitle {
|
||||
switch (this) {
|
||||
case ShareAccessLevel.readOnly:
|
||||
return LocaleKeys.shareTab_cantMakeChanges.tr();
|
||||
case ShareAccessLevel.readAndComment:
|
||||
return LocaleKeys.shareTab_canMakeAnyChanges.tr();
|
||||
case ShareAccessLevel.readAndWrite:
|
||||
return LocaleKeys.shareTab_canMakeAnyChanges.tr();
|
||||
case ShareAccessLevel.fullAccess:
|
||||
return LocaleKeys.shareTab_canMakeAnyChanges.tr();
|
||||
}
|
||||
}
|
||||
|
||||
FlowySvgData get icon {
|
||||
switch (this) {
|
||||
case ShareAccessLevel.readOnly:
|
||||
return FlowySvgs.access_level_view_m;
|
||||
case ShareAccessLevel.readAndComment:
|
||||
return FlowySvgs.access_level_edit_m;
|
||||
case ShareAccessLevel.readAndWrite:
|
||||
return FlowySvgs.access_level_edit_m;
|
||||
case ShareAccessLevel.fullAccess:
|
||||
return FlowySvgs.access_level_edit_m;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
/// The type of the shared section.
|
||||
///
|
||||
/// - public: the shared section is public, anyone in the workspace can view/edit it.
|
||||
/// - shared: the shared section is shared, anyone in the shared section can view/edit it.
|
||||
/// - private: the shared section is private, only the users in the shared section can view/edit it.
|
||||
enum SharedSectionType {
|
||||
unknown,
|
||||
public,
|
||||
shared,
|
||||
private;
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
class SharedGroup {
|
||||
SharedGroup({
|
||||
required this.id,
|
||||
required this.name,
|
||||
required this.icon,
|
||||
});
|
||||
|
||||
final String id;
|
||||
|
||||
final String name;
|
||||
|
||||
final String icon;
|
||||
|
||||
SharedGroup copyWith({
|
||||
String? id,
|
||||
String? name,
|
||||
String? icon,
|
||||
}) {
|
||||
return SharedGroup(
|
||||
id: id ?? this.id,
|
||||
name: name ?? this.name,
|
||||
icon: icon ?? this.icon,
|
||||
);
|
||||
}
|
||||
}
|
||||
+10
-49
@@ -2,25 +2,21 @@ import 'dart:math';
|
||||
|
||||
import 'package:appflowy/features/share_tab/data/models/models.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';
|
||||
import 'package:appflowy_result/appflowy_result.dart';
|
||||
|
||||
import 'share_with_user_repository.dart';
|
||||
|
||||
// Move this file to test folder
|
||||
class LocalShareWithUserRepositoryImpl extends ShareWithUserRepository {
|
||||
LocalShareWithUserRepositoryImpl();
|
||||
class LocalShareWithUserRepository extends ShareWithUserRepository {
|
||||
LocalShareWithUserRepository();
|
||||
|
||||
final SharedUsers _sharedUsers = [
|
||||
// current user has full access
|
||||
SharedUser(
|
||||
email: 'lucas.xu@appflowy.io',
|
||||
name: 'Lucas Xu - Long long long long long name',
|
||||
name: 'Lucas Xu',
|
||||
accessLevel: ShareAccessLevel.readOnly,
|
||||
role: ShareRole.guest,
|
||||
avatarUrl: 'https://avatar.iran.liara.run/public',
|
||||
),
|
||||
// member user has read and write access
|
||||
SharedUser(
|
||||
email: 'vivian@appflowy.io',
|
||||
name: 'Vivian Wang',
|
||||
@@ -28,31 +24,27 @@ class LocalShareWithUserRepositoryImpl extends ShareWithUserRepository {
|
||||
role: ShareRole.member,
|
||||
avatarUrl: 'https://avatar.iran.liara.run/public/girl',
|
||||
),
|
||||
// member user has read access
|
||||
SharedUser(
|
||||
email: 'shuheng@appflowy.io',
|
||||
name: 'Shuheng',
|
||||
accessLevel: ShareAccessLevel.readOnly,
|
||||
role: ShareRole.member,
|
||||
accessLevel: ShareAccessLevel.fullAccess,
|
||||
role: ShareRole.owner,
|
||||
avatarUrl: 'https://avatar.iran.liara.run/public/boy',
|
||||
),
|
||||
// guest user has read access
|
||||
SharedUser(
|
||||
email: 'guest_user_1@appflowy.io',
|
||||
name: 'Read Only Guest',
|
||||
name: 'Guest User 1',
|
||||
accessLevel: ShareAccessLevel.readOnly,
|
||||
role: ShareRole.guest,
|
||||
avatarUrl: 'https://avatar.iran.liara.run/public/boy/10',
|
||||
),
|
||||
// guest user has read and write access
|
||||
SharedUser(
|
||||
email: 'guest_user_2@appflowy.io',
|
||||
name: 'Read And Write Guest',
|
||||
accessLevel: ShareAccessLevel.readAndWrite,
|
||||
role: ShareRole.guest,
|
||||
name: 'Guest User 2',
|
||||
accessLevel: ShareAccessLevel.readOnly,
|
||||
role: ShareRole.owner,
|
||||
avatarUrl: 'https://avatar.iran.liara.run/public/boy/11',
|
||||
),
|
||||
// Others
|
||||
SharedUser(
|
||||
email: 'member_user_1@appflowy.io',
|
||||
name: 'Member User 1',
|
||||
@@ -153,7 +145,7 @@ class LocalShareWithUserRepositoryImpl extends ShareWithUserRepository {
|
||||
|
||||
@override
|
||||
Future<FlowyResult<void, FlowyError>> changeRole({
|
||||
required String workspaceId,
|
||||
required String pageId,
|
||||
required String email,
|
||||
required ShareRole role,
|
||||
}) async {
|
||||
@@ -164,35 +156,4 @@ class LocalShareWithUserRepositoryImpl extends ShareWithUserRepository {
|
||||
|
||||
return FlowySuccess(null);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<FlowyResult<UserProfilePB, FlowyError>> getCurrentUserProfile() async {
|
||||
// Simulate fetching current user profile
|
||||
return FlowySuccess(
|
||||
UserProfilePB()
|
||||
..email = 'lucas.xu@appflowy.io'
|
||||
..name = 'Lucas Xu',
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<FlowyResult<SharedSectionType, FlowyError>> getCurrentPageSectionType({
|
||||
required String pageId,
|
||||
}) async {
|
||||
return FlowySuccess(SharedSectionType.private);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> getUpgradeToProButtonClicked({
|
||||
required String workspaceId,
|
||||
}) async {
|
||||
return false;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> setUpgradeToProButtonClicked({
|
||||
required String workspaceId,
|
||||
}) async {
|
||||
return;
|
||||
}
|
||||
}
|
||||
+13
-78
@@ -1,23 +1,16 @@
|
||||
import 'package:appflowy/core/config/kv.dart';
|
||||
import 'package:appflowy/core/config/kv_keys.dart';
|
||||
import 'package:appflowy/features/share_tab/data/models/models.dart';
|
||||
import 'package:appflowy/features/util/extensions.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy/workspace/application/sidebar/space/space_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/view/view_ext.dart';
|
||||
import 'package:appflowy_backend/dispatch/dispatch.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-user/workspace.pb.dart';
|
||||
import 'package:appflowy_result/appflowy_result.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
|
||||
import 'share_with_user_repository.dart';
|
||||
|
||||
class RustShareWithUserRepositoryImpl extends ShareWithUserRepository {
|
||||
RustShareWithUserRepositoryImpl();
|
||||
class RustShareWithUserRepository extends ShareWithUserRepository {
|
||||
RustShareWithUserRepository();
|
||||
|
||||
@override
|
||||
Future<FlowyResult<SharedUsers, FlowyError>> getSharedUsersInPage({
|
||||
@@ -30,12 +23,12 @@ class RustShareWithUserRepositoryImpl extends ShareWithUserRepository {
|
||||
|
||||
return result.fold(
|
||||
(success) {
|
||||
Log.debug('get shared users success: $success');
|
||||
Log.info('get shared users: $success');
|
||||
|
||||
return FlowySuccess(success.sharedUsers);
|
||||
},
|
||||
(failure) {
|
||||
Log.error('get shared users failed: $failure');
|
||||
Log.error('getUsersInSharedPage: $failure');
|
||||
|
||||
return FlowyFailure(failure);
|
||||
},
|
||||
@@ -55,12 +48,12 @@ class RustShareWithUserRepositoryImpl extends ShareWithUserRepository {
|
||||
|
||||
return result.fold(
|
||||
(success) {
|
||||
Log.debug('remove users($emails) from shared page($pageId)');
|
||||
Log.info('remove users($emails) from shared page($pageId)');
|
||||
|
||||
return FlowySuccess(success);
|
||||
},
|
||||
(failure) {
|
||||
Log.error('remove users($emails) from shared page($pageId): $failure');
|
||||
Log.error('removeUserFromPage: $failure');
|
||||
|
||||
return FlowyFailure(failure);
|
||||
},
|
||||
@@ -77,22 +70,19 @@ class RustShareWithUserRepositoryImpl extends ShareWithUserRepository {
|
||||
viewId: pageId,
|
||||
emails: emails,
|
||||
accessLevel: accessLevel.accessLevel,
|
||||
autoConfirm: true,
|
||||
);
|
||||
final result = await FolderEventSharePageWithUser(request).send();
|
||||
|
||||
return result.fold(
|
||||
(success) {
|
||||
Log.debug(
|
||||
Log.info(
|
||||
'share page($pageId) with users($emails) with access level($accessLevel)',
|
||||
);
|
||||
|
||||
return FlowySuccess(success);
|
||||
},
|
||||
(failure) {
|
||||
Log.error(
|
||||
'share page($pageId) with users($emails) with access level($accessLevel): $failure',
|
||||
);
|
||||
Log.error('sharePageWithUser: $failure');
|
||||
|
||||
return FlowyFailure(failure);
|
||||
},
|
||||
@@ -103,89 +93,34 @@ class RustShareWithUserRepositoryImpl extends ShareWithUserRepository {
|
||||
Future<FlowyResult<SharedUsers, FlowyError>> getAvailableSharedUsers({
|
||||
required String pageId,
|
||||
}) async {
|
||||
// TODO: Implement this
|
||||
return FlowySuccess([]);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<FlowyResult<void, FlowyError>> changeRole({
|
||||
required String workspaceId,
|
||||
required String pageId,
|
||||
required String email,
|
||||
required ShareRole role,
|
||||
}) async {
|
||||
final request = UpdateWorkspaceMemberPB(
|
||||
workspaceId: workspaceId,
|
||||
workspaceId: pageId,
|
||||
email: email,
|
||||
role: role.userRole,
|
||||
);
|
||||
final result = await UserEventUpdateWorkspaceMember(request).send();
|
||||
return result.fold(
|
||||
(success) {
|
||||
Log.debug(
|
||||
'change role($role) for user($email) in workspaceId($workspaceId)',
|
||||
);
|
||||
Log.info('change role($role) for user($email) in page($pageId)');
|
||||
return FlowySuccess(success);
|
||||
},
|
||||
(failure) {
|
||||
Log.error(
|
||||
'failed to change role($role) for user($email) in workspaceId($workspaceId)',
|
||||
'failed to change role($role) for user($email) in page($pageId)',
|
||||
failure,
|
||||
);
|
||||
return FlowyFailure(failure);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<FlowyResult<UserProfilePB, FlowyError>> getCurrentUserProfile() async {
|
||||
final result = await UserEventGetUserProfile().send();
|
||||
return result;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<FlowyResult<SharedSectionType, FlowyError>> getCurrentPageSectionType({
|
||||
required String pageId,
|
||||
}) async {
|
||||
final request = ViewIdPB.create()..value = pageId;
|
||||
final result = await FolderEventGetViewAncestors(request).send();
|
||||
final ancestors = result.fold(
|
||||
(s) => s.items,
|
||||
(f) => <ViewPB>[],
|
||||
);
|
||||
final space = ancestors.firstWhereOrNull((e) => e.isSpace);
|
||||
|
||||
if (space == null) {
|
||||
return FlowySuccess(SharedSectionType.unknown);
|
||||
}
|
||||
|
||||
final sectionType = switch (space.spacePermission) {
|
||||
SpacePermission.publicToAll => SharedSectionType.public,
|
||||
SpacePermission.private => SharedSectionType.private,
|
||||
};
|
||||
|
||||
return FlowySuccess(sectionType);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> getUpgradeToProButtonClicked({
|
||||
required String workspaceId,
|
||||
}) async {
|
||||
final result = await getIt<KeyValueStorage>().getWithFormat(
|
||||
'${KVKeys.hasClickedUpgradeToProButton}_$workspaceId',
|
||||
(value) => bool.parse(value),
|
||||
);
|
||||
if (result == null) {
|
||||
return false;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> setUpgradeToProButtonClicked({
|
||||
required String workspaceId,
|
||||
}) async {
|
||||
await getIt<KeyValueStorage>().set(
|
||||
'${KVKeys.hasClickedUpgradeToProButton}_$workspaceId',
|
||||
'true',
|
||||
);
|
||||
}
|
||||
}
|
||||
+1
-20
@@ -1,6 +1,5 @@
|
||||
import 'package:appflowy/features/share_tab/data/models/models.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';
|
||||
import 'package:appflowy_result/appflowy_result.dart';
|
||||
|
||||
/// Abstract repository for sharing with users.
|
||||
@@ -35,26 +34,8 @@ abstract class ShareWithUserRepository {
|
||||
|
||||
/// Change the role of a user in a shared page.
|
||||
Future<FlowyResult<void, FlowyError>> changeRole({
|
||||
required String workspaceId,
|
||||
required String pageId,
|
||||
required String email,
|
||||
required ShareRole role,
|
||||
});
|
||||
|
||||
/// Get current user profile.
|
||||
Future<FlowyResult<UserProfilePB, FlowyError>> getCurrentUserProfile();
|
||||
|
||||
/// Get current page is in public section or private section.
|
||||
Future<FlowyResult<SharedSectionType, FlowyError>> getCurrentPageSectionType({
|
||||
required String pageId,
|
||||
});
|
||||
|
||||
/// Get the upgrade to pro button has been clicked.
|
||||
Future<bool> getUpgradeToProButtonClicked({
|
||||
required String workspaceId,
|
||||
});
|
||||
|
||||
/// Set the upgrade to pro button has been clicked.
|
||||
Future<void> setUpgradeToProButtonClicked({
|
||||
required String workspaceId,
|
||||
});
|
||||
}
|
||||
@@ -1,154 +0,0 @@
|
||||
import 'package:appflowy/features/share_tab/data/models/models.dart';
|
||||
|
||||
sealed class ShareTabEvent {
|
||||
const ShareTabEvent();
|
||||
|
||||
// Factory functions for creating events
|
||||
factory ShareTabEvent.initialize() => const ShareTabEventInitialize();
|
||||
|
||||
factory ShareTabEvent.loadSharedUsers() =>
|
||||
const ShareTabEventLoadSharedUsers();
|
||||
|
||||
factory ShareTabEvent.inviteUsers({
|
||||
required List<String> emails,
|
||||
required ShareAccessLevel accessLevel,
|
||||
}) =>
|
||||
ShareTabEventInviteUsers(emails: emails, accessLevel: accessLevel);
|
||||
|
||||
factory ShareTabEvent.removeUsers({
|
||||
required List<String> emails,
|
||||
}) =>
|
||||
ShareTabEventRemoveUsers(emails: emails);
|
||||
|
||||
factory ShareTabEvent.updateUserAccessLevel({
|
||||
required String email,
|
||||
required ShareAccessLevel accessLevel,
|
||||
}) =>
|
||||
ShareTabEventUpdateUserAccessLevel(
|
||||
email: email,
|
||||
accessLevel: accessLevel,
|
||||
);
|
||||
|
||||
factory ShareTabEvent.updateGeneralAccessLevel({
|
||||
required ShareAccessLevel accessLevel,
|
||||
}) =>
|
||||
ShareTabEventUpdateGeneralAccessLevel(accessLevel: accessLevel);
|
||||
|
||||
factory ShareTabEvent.copyShareLink({
|
||||
required String link,
|
||||
}) =>
|
||||
ShareTabEventCopyShareLink(link: link);
|
||||
|
||||
factory ShareTabEvent.searchAvailableUsers({
|
||||
required String query,
|
||||
}) =>
|
||||
ShareTabEventSearchAvailableUsers(query: query);
|
||||
|
||||
factory ShareTabEvent.convertToMember({
|
||||
required String email,
|
||||
}) =>
|
||||
ShareTabEventConvertToMember(email: email);
|
||||
|
||||
factory ShareTabEvent.clearState() => const ShareTabEventClearState();
|
||||
|
||||
factory ShareTabEvent.updateSharedUsers({
|
||||
required SharedUsers users,
|
||||
}) =>
|
||||
ShareTabEventUpdateSharedUsers(users: users);
|
||||
|
||||
factory ShareTabEvent.upgradeToProClicked() =>
|
||||
const ShareTabEventUpgradeToProClicked();
|
||||
}
|
||||
|
||||
/// Initializes the share tab bloc.
|
||||
class ShareTabEventInitialize extends ShareTabEvent {
|
||||
const ShareTabEventInitialize();
|
||||
}
|
||||
|
||||
/// Loads the shared users for the current page.
|
||||
class ShareTabEventLoadSharedUsers extends ShareTabEvent {
|
||||
const ShareTabEventLoadSharedUsers();
|
||||
}
|
||||
|
||||
/// Invites users to the page with specified access level.
|
||||
class ShareTabEventInviteUsers extends ShareTabEvent {
|
||||
const ShareTabEventInviteUsers({
|
||||
required this.emails,
|
||||
required this.accessLevel,
|
||||
});
|
||||
|
||||
final List<String> emails;
|
||||
final ShareAccessLevel accessLevel;
|
||||
}
|
||||
|
||||
/// Removes users from the shared page.
|
||||
class ShareTabEventRemoveUsers extends ShareTabEvent {
|
||||
const ShareTabEventRemoveUsers({
|
||||
required this.emails,
|
||||
});
|
||||
|
||||
final List<String> emails;
|
||||
}
|
||||
|
||||
/// Updates the access level for a specific user.
|
||||
class ShareTabEventUpdateUserAccessLevel extends ShareTabEvent {
|
||||
const ShareTabEventUpdateUserAccessLevel({
|
||||
required this.email,
|
||||
required this.accessLevel,
|
||||
});
|
||||
|
||||
final String email;
|
||||
final ShareAccessLevel accessLevel;
|
||||
}
|
||||
|
||||
/// Updates the general access level for all users.
|
||||
class ShareTabEventUpdateGeneralAccessLevel extends ShareTabEvent {
|
||||
const ShareTabEventUpdateGeneralAccessLevel({
|
||||
required this.accessLevel,
|
||||
});
|
||||
|
||||
final ShareAccessLevel accessLevel;
|
||||
}
|
||||
|
||||
/// Copies the share link to the clipboard.
|
||||
class ShareTabEventCopyShareLink extends ShareTabEvent {
|
||||
const ShareTabEventCopyShareLink({
|
||||
required this.link,
|
||||
});
|
||||
|
||||
final String link;
|
||||
}
|
||||
|
||||
/// Searches for available users by name or email.
|
||||
class ShareTabEventSearchAvailableUsers extends ShareTabEvent {
|
||||
const ShareTabEventSearchAvailableUsers({
|
||||
required this.query,
|
||||
});
|
||||
|
||||
final String query;
|
||||
}
|
||||
|
||||
/// Converts a user into a member.
|
||||
class ShareTabEventConvertToMember extends ShareTabEvent {
|
||||
const ShareTabEventConvertToMember({
|
||||
required this.email,
|
||||
});
|
||||
|
||||
final String email;
|
||||
}
|
||||
|
||||
class ShareTabEventClearState extends ShareTabEvent {
|
||||
const ShareTabEventClearState();
|
||||
}
|
||||
|
||||
class ShareTabEventUpdateSharedUsers extends ShareTabEvent {
|
||||
const ShareTabEventUpdateSharedUsers({
|
||||
required this.users,
|
||||
});
|
||||
|
||||
final SharedUsers users;
|
||||
}
|
||||
|
||||
class ShareTabEventUpgradeToProClicked extends ShareTabEvent {
|
||||
const ShareTabEventUpgradeToProClicked();
|
||||
}
|
||||
@@ -1,126 +0,0 @@
|
||||
import 'package:appflowy/features/share_tab/data/models/models.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
|
||||
import 'package:appflowy_result/appflowy_result.dart';
|
||||
|
||||
class ShareTabState {
|
||||
factory ShareTabState.initial() => const ShareTabState();
|
||||
|
||||
const ShareTabState({
|
||||
this.currentUser,
|
||||
this.users = const [],
|
||||
this.availableUsers = const [],
|
||||
this.isLoading = false,
|
||||
this.errorMessage = '',
|
||||
this.shareLink = '',
|
||||
this.generalAccessRole,
|
||||
this.linkCopied = false,
|
||||
this.sectionType = SharedSectionType.private,
|
||||
this.initialResult,
|
||||
this.shareResult,
|
||||
this.removeResult,
|
||||
this.updateAccessLevelResult,
|
||||
this.turnIntoMemberResult,
|
||||
this.hasClickedUpgradeToPro = false,
|
||||
});
|
||||
|
||||
final UserProfilePB? currentUser;
|
||||
final SharedUsers users;
|
||||
final SharedUsers availableUsers;
|
||||
final bool isLoading;
|
||||
final String errorMessage;
|
||||
final String shareLink;
|
||||
final ShareAccessLevel? generalAccessRole;
|
||||
final bool linkCopied;
|
||||
final SharedSectionType sectionType;
|
||||
final FlowyResult<void, FlowyError>? initialResult;
|
||||
final FlowyResult<void, FlowyError>? shareResult;
|
||||
final FlowyResult<void, FlowyError>? removeResult;
|
||||
final FlowyResult<void, FlowyError>? updateAccessLevelResult;
|
||||
final FlowyResult<void, FlowyError>? turnIntoMemberResult;
|
||||
final bool hasClickedUpgradeToPro;
|
||||
|
||||
ShareTabState copyWith({
|
||||
UserProfilePB? currentUser,
|
||||
SharedUsers? users,
|
||||
SharedUsers? availableUsers,
|
||||
bool? isLoading,
|
||||
String? errorMessage,
|
||||
String? shareLink,
|
||||
ShareAccessLevel? generalAccessRole,
|
||||
bool? linkCopied,
|
||||
SharedSectionType? sectionType,
|
||||
FlowyResult<void, FlowyError>? initialResult,
|
||||
FlowyResult<void, FlowyError>? shareResult,
|
||||
FlowyResult<void, FlowyError>? removeResult,
|
||||
FlowyResult<void, FlowyError>? updateAccessLevelResult,
|
||||
FlowyResult<void, FlowyError>? turnIntoMemberResult,
|
||||
bool? hasClickedUpgradeToPro,
|
||||
}) {
|
||||
return ShareTabState(
|
||||
currentUser: currentUser ?? this.currentUser,
|
||||
users: users ?? this.users,
|
||||
availableUsers: availableUsers ?? this.availableUsers,
|
||||
isLoading: isLoading ?? this.isLoading,
|
||||
errorMessage: errorMessage ?? this.errorMessage,
|
||||
shareLink: shareLink ?? this.shareLink,
|
||||
generalAccessRole: generalAccessRole ?? this.generalAccessRole,
|
||||
linkCopied: linkCopied ?? this.linkCopied,
|
||||
sectionType: sectionType ?? this.sectionType,
|
||||
initialResult: initialResult,
|
||||
shareResult: shareResult,
|
||||
removeResult: removeResult,
|
||||
updateAccessLevelResult: updateAccessLevelResult,
|
||||
turnIntoMemberResult: turnIntoMemberResult,
|
||||
hasClickedUpgradeToPro:
|
||||
hasClickedUpgradeToPro ?? this.hasClickedUpgradeToPro,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (identical(this, other)) return true;
|
||||
return other is ShareTabState &&
|
||||
other.currentUser == currentUser &&
|
||||
other.users == users &&
|
||||
other.availableUsers == availableUsers &&
|
||||
other.isLoading == isLoading &&
|
||||
other.errorMessage == errorMessage &&
|
||||
other.shareLink == shareLink &&
|
||||
other.generalAccessRole == generalAccessRole &&
|
||||
other.linkCopied == linkCopied &&
|
||||
other.sectionType == sectionType &&
|
||||
other.initialResult == initialResult &&
|
||||
other.shareResult == shareResult &&
|
||||
other.removeResult == removeResult &&
|
||||
other.updateAccessLevelResult == updateAccessLevelResult &&
|
||||
other.turnIntoMemberResult == turnIntoMemberResult &&
|
||||
other.hasClickedUpgradeToPro == hasClickedUpgradeToPro;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
return Object.hash(
|
||||
currentUser,
|
||||
users,
|
||||
availableUsers,
|
||||
isLoading,
|
||||
errorMessage,
|
||||
shareLink,
|
||||
generalAccessRole,
|
||||
linkCopied,
|
||||
sectionType,
|
||||
initialResult,
|
||||
shareResult,
|
||||
removeResult,
|
||||
updateAccessLevelResult,
|
||||
turnIntoMemberResult,
|
||||
hasClickedUpgradeToPro,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'ShareTabState(currentUser: $currentUser, users: $users, availableUsers: $availableUsers, isLoading: $isLoading, errorMessage: $errorMessage, shareLink: $shareLink, generalAccessRole: $generalAccessRole, shareSectionType: $SharedSectionType, linkCopied: $linkCopied, initialResult: $initialResult, shareResult: $shareResult, removeResult: $removeResult, updateAccessLevelResult: $updateAccessLevelResult, turnIntoMemberResult: $turnIntoMemberResult, hasClickedUpgradeToPro: $hasClickedUpgradeToPro)';
|
||||
}
|
||||
}
|
||||
+128
-146
@@ -1,111 +1,72 @@
|
||||
import 'package:appflowy/core/notification/folder_notification.dart';
|
||||
import 'package:appflowy/features/share_tab/data/models/models.dart';
|
||||
import 'package:appflowy/features/share_tab/data/repositories/share_with_user_repository.dart';
|
||||
import 'package:appflowy/features/share_tab/logic/share_tab_event.dart';
|
||||
import 'package:appflowy/features/share_tab/logic/share_tab_state.dart';
|
||||
import 'package:appflowy/features/util/extensions.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart';
|
||||
import 'package:appflowy/plugins/shared/share/constants.dart';
|
||||
import 'package:appflowy/shared/feature_flags.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';
|
||||
import 'package:appflowy/user/application/user_service.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
|
||||
import 'package:appflowy_result/appflowy_result.dart';
|
||||
import 'package:bloc/bloc.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
export 'share_tab_event.dart';
|
||||
export 'share_tab_state.dart';
|
||||
part 'share_with_user_bloc.freezed.dart';
|
||||
|
||||
class ShareTabBloc extends Bloc<ShareTabEvent, ShareTabState> {
|
||||
ShareTabBloc({
|
||||
class ShareWithUserBloc extends Bloc<ShareWithUserEvent, ShareWithUserState> {
|
||||
ShareWithUserBloc({
|
||||
required this.repository,
|
||||
required this.pageId,
|
||||
required this.workspaceId,
|
||||
}) : super(ShareTabState.initial()) {
|
||||
on<ShareTabEventInitialize>(_onInitial);
|
||||
on<ShareTabEventLoadSharedUsers>(_onGetSharedUsers);
|
||||
on<ShareTabEventInviteUsers>(_onShare);
|
||||
on<ShareTabEventRemoveUsers>(_onRemove);
|
||||
on<ShareTabEventUpdateUserAccessLevel>(_onUpdateAccessLevel);
|
||||
on<ShareTabEventUpdateGeneralAccessLevel>(_onUpdateGeneralAccess);
|
||||
on<ShareTabEventCopyShareLink>(_onCopyLink);
|
||||
on<ShareTabEventSearchAvailableUsers>(_onSearchAvailableUsers);
|
||||
on<ShareTabEventConvertToMember>(_onTurnIntoMember);
|
||||
on<ShareTabEventClearState>(_onClearState);
|
||||
on<ShareTabEventUpdateSharedUsers>(_onUpdateSharedUsers);
|
||||
on<ShareTabEventUpgradeToProClicked>(_onUpgradeToProClicked);
|
||||
}) : super(ShareWithUserState.initial()) {
|
||||
on<Initial>(_onInitial);
|
||||
on<GetSharedUsers>(_onGetSharedUsers);
|
||||
on<ShareWithUser>(_onShare);
|
||||
on<RemoveUser>(_onRemove);
|
||||
on<UpdateAccessLevel>(_onUpdateAccessLevel);
|
||||
on<UpdateGeneralAccess>(_onUpdateGeneralAccess);
|
||||
on<CopyLink>(_onCopyLink);
|
||||
on<SearchAvailableUsers>(_onSearchAvailableUsers);
|
||||
on<TurnIntoMember>(_onTurnIntoMember);
|
||||
}
|
||||
|
||||
final ShareWithUserRepository repository;
|
||||
final String workspaceId;
|
||||
final String pageId;
|
||||
|
||||
// Used to listen for shared view updates.
|
||||
FolderNotificationListener? _folderNotificationListener;
|
||||
|
||||
@override
|
||||
Future<void> close() async {
|
||||
await _folderNotificationListener?.stop();
|
||||
await super.close();
|
||||
}
|
||||
|
||||
Future<void> _onInitial(
|
||||
ShareTabEventInitialize event,
|
||||
Emitter<ShareTabState> emit,
|
||||
Initial event,
|
||||
Emitter<ShareWithUserState> emit,
|
||||
) async {
|
||||
if (!FeatureFlag.sharedSection.isOn) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
errorMessage: 'Sharing is currently disabled.',
|
||||
users: [],
|
||||
isLoading: false,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
_initFolderNotificationListener();
|
||||
|
||||
final result = await repository.getCurrentUserProfile();
|
||||
final result = await UserBackendService.getCurrentUserProfile();
|
||||
final currentUser = result.fold(
|
||||
(user) => user,
|
||||
(error) => null,
|
||||
);
|
||||
|
||||
final sectionTypeResult = await repository.getCurrentPageSectionType(
|
||||
pageId: pageId,
|
||||
);
|
||||
final sectionType = sectionTypeResult.fold(
|
||||
(type) => type,
|
||||
(error) => SharedSectionType.unknown,
|
||||
);
|
||||
|
||||
final shareLink = ShareConstants.buildShareUrl(
|
||||
workspaceId: workspaceId,
|
||||
viewId: pageId,
|
||||
);
|
||||
|
||||
final users = await _getSharedUsers();
|
||||
|
||||
final hasClickedUpgradeToPro =
|
||||
await repository.getUpgradeToProButtonClicked(
|
||||
workspaceId: workspaceId,
|
||||
);
|
||||
final users = await _getLatestSharedUsersOrCurrentUsers();
|
||||
|
||||
emit(
|
||||
state.copyWith(
|
||||
currentUser: currentUser,
|
||||
shareLink: shareLink,
|
||||
users: users,
|
||||
sectionType: sectionType,
|
||||
hasClickedUpgradeToPro: hasClickedUpgradeToPro,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _onGetSharedUsers(
|
||||
ShareTabEventLoadSharedUsers event,
|
||||
Emitter<ShareTabState> emit,
|
||||
GetSharedUsers event,
|
||||
Emitter<ShareWithUserState> emit,
|
||||
) async {
|
||||
if (!FeatureFlag.sharedSection.isOn) {
|
||||
return;
|
||||
@@ -114,6 +75,11 @@ class ShareTabBloc extends Bloc<ShareTabEvent, ShareTabState> {
|
||||
emit(
|
||||
state.copyWith(
|
||||
errorMessage: '',
|
||||
initialResult: null,
|
||||
shareResult: null,
|
||||
removeResult: null,
|
||||
updateAccessLevelResult: null,
|
||||
turnIntoMemberResult: null,
|
||||
),
|
||||
);
|
||||
|
||||
@@ -138,12 +104,16 @@ class ShareTabBloc extends Bloc<ShareTabEvent, ShareTabState> {
|
||||
}
|
||||
|
||||
Future<void> _onShare(
|
||||
ShareTabEventInviteUsers event,
|
||||
Emitter<ShareTabState> emit,
|
||||
ShareWithUser event,
|
||||
Emitter<ShareWithUserState> emit,
|
||||
) async {
|
||||
emit(
|
||||
state.copyWith(
|
||||
errorMessage: '',
|
||||
shareResult: null,
|
||||
turnIntoMemberResult: null,
|
||||
removeResult: null,
|
||||
updateAccessLevelResult: null,
|
||||
),
|
||||
);
|
||||
|
||||
@@ -155,7 +125,7 @@ class ShareTabBloc extends Bloc<ShareTabEvent, ShareTabState> {
|
||||
|
||||
await result.fold(
|
||||
(_) async {
|
||||
final users = await _getSharedUsers();
|
||||
final users = await _getLatestSharedUsersOrCurrentUsers();
|
||||
|
||||
emit(
|
||||
state.copyWith(
|
||||
@@ -176,12 +146,16 @@ class ShareTabBloc extends Bloc<ShareTabEvent, ShareTabState> {
|
||||
}
|
||||
|
||||
Future<void> _onRemove(
|
||||
ShareTabEventRemoveUsers event,
|
||||
Emitter<ShareTabState> emit,
|
||||
RemoveUser event,
|
||||
Emitter<ShareWithUserState> emit,
|
||||
) async {
|
||||
emit(
|
||||
state.copyWith(
|
||||
errorMessage: '',
|
||||
removeResult: null,
|
||||
shareResult: null,
|
||||
updateAccessLevelResult: null,
|
||||
turnIntoMemberResult: null,
|
||||
),
|
||||
);
|
||||
|
||||
@@ -192,7 +166,7 @@ class ShareTabBloc extends Bloc<ShareTabEvent, ShareTabState> {
|
||||
|
||||
await result.fold(
|
||||
(_) async {
|
||||
final users = await _getSharedUsers();
|
||||
final users = await _getLatestSharedUsersOrCurrentUsers();
|
||||
emit(
|
||||
state.copyWith(
|
||||
removeResult: FlowySuccess(null),
|
||||
@@ -212,11 +186,13 @@ class ShareTabBloc extends Bloc<ShareTabEvent, ShareTabState> {
|
||||
}
|
||||
|
||||
Future<void> _onUpdateAccessLevel(
|
||||
ShareTabEventUpdateUserAccessLevel event,
|
||||
Emitter<ShareTabState> emit,
|
||||
UpdateAccessLevel event,
|
||||
Emitter<ShareWithUserState> emit,
|
||||
) async {
|
||||
emit(
|
||||
state.copyWith(),
|
||||
state.copyWith(
|
||||
updateAccessLevelResult: null,
|
||||
),
|
||||
);
|
||||
|
||||
final result = await repository.sharePageWithUser(
|
||||
@@ -227,7 +203,7 @@ class ShareTabBloc extends Bloc<ShareTabEvent, ShareTabState> {
|
||||
|
||||
await result.fold(
|
||||
(_) async {
|
||||
final users = await _getSharedUsers();
|
||||
final users = await _getLatestSharedUsersOrCurrentUsers();
|
||||
emit(
|
||||
state.copyWith(
|
||||
updateAccessLevelResult: FlowySuccess(null),
|
||||
@@ -247,8 +223,8 @@ class ShareTabBloc extends Bloc<ShareTabEvent, ShareTabState> {
|
||||
}
|
||||
|
||||
void _onUpdateGeneralAccess(
|
||||
ShareTabEventUpdateGeneralAccessLevel event,
|
||||
Emitter<ShareTabState> emit,
|
||||
UpdateGeneralAccess event,
|
||||
Emitter<ShareWithUserState> emit,
|
||||
) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
@@ -257,10 +233,7 @@ class ShareTabBloc extends Bloc<ShareTabEvent, ShareTabState> {
|
||||
);
|
||||
}
|
||||
|
||||
void _onCopyLink(
|
||||
ShareTabEventCopyShareLink event,
|
||||
Emitter<ShareTabState> emit,
|
||||
) {
|
||||
void _onCopyLink(CopyLink event, Emitter<ShareWithUserState> emit) {
|
||||
getIt<ClipboardService>().setData(
|
||||
ClipboardServiceData(
|
||||
plainText: event.link,
|
||||
@@ -275,8 +248,8 @@ class ShareTabBloc extends Bloc<ShareTabEvent, ShareTabState> {
|
||||
}
|
||||
|
||||
Future<void> _onSearchAvailableUsers(
|
||||
ShareTabEventSearchAvailableUsers event,
|
||||
Emitter<ShareTabState> emit,
|
||||
SearchAvailableUsers event,
|
||||
Emitter<ShareWithUserState> emit,
|
||||
) async {
|
||||
emit(
|
||||
state.copyWith(
|
||||
@@ -310,24 +283,28 @@ class ShareTabBloc extends Bloc<ShareTabEvent, ShareTabState> {
|
||||
}
|
||||
|
||||
Future<void> _onTurnIntoMember(
|
||||
ShareTabEventConvertToMember event,
|
||||
Emitter<ShareTabState> emit,
|
||||
TurnIntoMember event,
|
||||
Emitter<ShareWithUserState> emit,
|
||||
) async {
|
||||
emit(
|
||||
state.copyWith(
|
||||
turnIntoMemberResult: null,
|
||||
errorMessage: '',
|
||||
removeResult: null,
|
||||
shareResult: null,
|
||||
updateAccessLevelResult: null,
|
||||
),
|
||||
);
|
||||
|
||||
final result = await repository.changeRole(
|
||||
workspaceId: workspaceId,
|
||||
pageId: pageId,
|
||||
email: event.email,
|
||||
role: ShareRole.member,
|
||||
);
|
||||
|
||||
await result.fold(
|
||||
(_) async {
|
||||
final users = await _getSharedUsers();
|
||||
final users = await _getLatestSharedUsersOrCurrentUsers();
|
||||
emit(
|
||||
state.copyWith(
|
||||
turnIntoMemberResult: FlowySuccess(null),
|
||||
@@ -346,7 +323,7 @@ class ShareTabBloc extends Bloc<ShareTabEvent, ShareTabState> {
|
||||
);
|
||||
}
|
||||
|
||||
Future<SharedUsers> _getSharedUsers() async {
|
||||
Future<SharedUsers> _getLatestSharedUsersOrCurrentUsers() async {
|
||||
final shareResult = await repository.getSharedUsersInPage(
|
||||
pageId: pageId,
|
||||
);
|
||||
@@ -355,66 +332,71 @@ class ShareTabBloc extends Bloc<ShareTabEvent, ShareTabState> {
|
||||
(error) => state.users,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void _onClearState(
|
||||
ShareTabEventClearState event,
|
||||
Emitter<ShareTabState> emit,
|
||||
) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
errorMessage: '',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _onUpdateSharedUsers(
|
||||
ShareTabEventUpdateSharedUsers event,
|
||||
Emitter<ShareTabState> emit,
|
||||
) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
users: event.users,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _onUpgradeToProClicked(
|
||||
ShareTabEventUpgradeToProClicked event,
|
||||
Emitter<ShareTabState> emit,
|
||||
) async {
|
||||
await repository.setUpgradeToProButtonClicked(
|
||||
workspaceId: workspaceId,
|
||||
);
|
||||
emit(
|
||||
state.copyWith(
|
||||
hasClickedUpgradeToPro: true,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _initFolderNotificationListener() {
|
||||
_folderNotificationListener = FolderNotificationListener(
|
||||
objectId: pageId,
|
||||
handler: (notification, result) {
|
||||
if (notification == FolderNotification.DidUpdateSharedUsers) {
|
||||
final response = result.fold(
|
||||
(payload) {
|
||||
final repeatedSharedUsers =
|
||||
RepeatedSharedUserPB.fromBuffer(payload);
|
||||
return repeatedSharedUsers;
|
||||
},
|
||||
(error) => null,
|
||||
);
|
||||
Log.debug('update shared users: $response');
|
||||
if (response != null) {
|
||||
add(
|
||||
ShareTabEvent.updateSharedUsers(
|
||||
users: response.sharedUsers.reversed.toList(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
@freezed
|
||||
class ShareWithUserEvent with _$ShareWithUserEvent {
|
||||
/// Initializes the bloc.
|
||||
const factory ShareWithUserEvent.init() = Initial;
|
||||
|
||||
/// Loads the shared users for the page.
|
||||
const factory ShareWithUserEvent.getSharedUsers() = GetSharedUsers;
|
||||
|
||||
/// Invites the users to the page.
|
||||
const factory ShareWithUserEvent.share({
|
||||
required List<String> emails,
|
||||
required ShareAccessLevel accessLevel,
|
||||
}) = ShareWithUser;
|
||||
|
||||
/// Removes the users from the page.
|
||||
const factory ShareWithUserEvent.remove({
|
||||
required List<String> emails,
|
||||
}) = RemoveUser;
|
||||
|
||||
/// Updates the access level of the user.
|
||||
const factory ShareWithUserEvent.updateAccessLevel({
|
||||
required String email,
|
||||
required ShareAccessLevel accessLevel,
|
||||
}) = UpdateAccessLevel;
|
||||
|
||||
/// Updates the general access role for all users.
|
||||
const factory ShareWithUserEvent.updateGeneralAccess({
|
||||
required ShareAccessLevel accessLevel,
|
||||
}) = UpdateGeneralAccess;
|
||||
|
||||
/// Copies the link to the clipboard.
|
||||
const factory ShareWithUserEvent.copyLink({
|
||||
required String link,
|
||||
}) = CopyLink;
|
||||
|
||||
/// Searches available users by name or email.
|
||||
const factory ShareWithUserEvent.searchAvailableUsers({
|
||||
required String query,
|
||||
}) = SearchAvailableUsers;
|
||||
|
||||
/// Turns the user into a member.
|
||||
const factory ShareWithUserEvent.turnIntoMember({
|
||||
required String email,
|
||||
}) = TurnIntoMember;
|
||||
}
|
||||
|
||||
@freezed
|
||||
class ShareWithUserState with _$ShareWithUserState {
|
||||
const factory ShareWithUserState({
|
||||
@Default(null) UserProfilePB? currentUser,
|
||||
@Default([]) SharedUsers users,
|
||||
@Default([]) SharedUsers availableUsers,
|
||||
@Default(false) bool isLoading,
|
||||
@Default('') String errorMessage,
|
||||
@Default('') String shareLink,
|
||||
ShareAccessLevel? generalAccessRole,
|
||||
@Default(false) bool linkCopied,
|
||||
@Default(null) FlowyResult<void, FlowyError>? initialResult,
|
||||
@Default(null) FlowyResult<void, FlowyError>? shareResult,
|
||||
@Default(null) FlowyResult<void, FlowyError>? removeResult,
|
||||
@Default(null) FlowyResult<void, FlowyError>? updateAccessLevelResult,
|
||||
@Default(null) FlowyResult<void, FlowyError>? turnIntoMemberResult,
|
||||
}) = _ShareWithUserState;
|
||||
|
||||
factory ShareWithUserState.initial() => const ShareWithUserState();
|
||||
}
|
||||
@@ -1,18 +1,10 @@
|
||||
import 'package:appflowy/features/share_tab/data/models/models.dart';
|
||||
import 'package:appflowy/features/share_tab/data/models/shared_group.dart';
|
||||
import 'package:appflowy/features/share_tab/logic/share_tab_bloc.dart';
|
||||
import 'package:appflowy/features/share_tab/data/models/share_access_level.dart';
|
||||
import 'package:appflowy/features/share_tab/logic/share_with_user_bloc.dart';
|
||||
import 'package:appflowy/features/share_tab/presentation/widgets/copy_link_widget.dart';
|
||||
import 'package:appflowy/features/share_tab/presentation/widgets/general_access_section.dart';
|
||||
import 'package:appflowy/features/share_tab/presentation/widgets/people_with_access_section.dart';
|
||||
import 'package:appflowy/features/share_tab/presentation/widgets/share_with_user_widget.dart';
|
||||
import 'package:appflowy/features/share_tab/presentation/widgets/upgrade_to_pro_widget.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/shared_widget.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-error/code.pbenum.dart';
|
||||
import 'package:appflowy_ui/appflowy_ui.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
@@ -22,41 +14,21 @@ class ShareTab extends StatefulWidget {
|
||||
super.key,
|
||||
required this.workspaceId,
|
||||
required this.pageId,
|
||||
required this.workspaceName,
|
||||
required this.workspaceIcon,
|
||||
required this.isInProPlan,
|
||||
required this.onUpgradeToPro,
|
||||
});
|
||||
|
||||
final String workspaceId;
|
||||
final String pageId;
|
||||
|
||||
// these 2 values should be provided by the share tab bloc
|
||||
final String workspaceName;
|
||||
final String workspaceIcon;
|
||||
|
||||
final bool isInProPlan;
|
||||
final VoidCallback onUpgradeToPro;
|
||||
|
||||
@override
|
||||
State<ShareTab> createState() => _ShareTabState();
|
||||
}
|
||||
|
||||
class _ShareTabState extends State<ShareTab> {
|
||||
final TextEditingController controller = TextEditingController();
|
||||
late final ShareTabBloc shareTabBloc;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
shareTabBloc = context.read<ShareTabBloc>();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
controller.dispose();
|
||||
shareTabBloc.add(ShareTabEvent.clearState());
|
||||
|
||||
super.dispose();
|
||||
}
|
||||
@@ -65,7 +37,7 @@ class _ShareTabState extends State<ShareTab> {
|
||||
Widget build(BuildContext context) {
|
||||
final theme = AppFlowyTheme.of(context);
|
||||
|
||||
return BlocConsumer<ShareTabBloc, ShareTabState>(
|
||||
return BlocConsumer<ShareWithUserBloc, ShareWithUserState>(
|
||||
listener: (context, state) {
|
||||
_onListenShareWithUserState(context, state);
|
||||
},
|
||||
@@ -74,25 +46,14 @@ class _ShareTabState extends State<ShareTab> {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
final currentUser = state.currentUser;
|
||||
final accessLevel = state.users
|
||||
.firstWhereOrNull(
|
||||
(user) => user.email == currentUser?.email,
|
||||
)
|
||||
?.accessLevel;
|
||||
final isFullAccess = accessLevel == ShareAccessLevel.fullAccess;
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// share page with user by email
|
||||
// only user with full access can invite others
|
||||
|
||||
VSpace(theme.spacing.l),
|
||||
ShareWithUserWidget(
|
||||
controller: controller,
|
||||
disabled: !isFullAccess,
|
||||
onInvite: (emails) => _onSharePageWithUser(
|
||||
context,
|
||||
emails: emails,
|
||||
@@ -100,22 +61,11 @@ class _ShareTabState extends State<ShareTab> {
|
||||
),
|
||||
),
|
||||
|
||||
if (!widget.isInProPlan && !state.hasClickedUpgradeToPro) ...[
|
||||
UpgradeToProWidget(
|
||||
onClose: () {
|
||||
context.read<ShareTabBloc>().add(
|
||||
ShareTabEvent.upgradeToProClicked(),
|
||||
);
|
||||
},
|
||||
onUpgrade: widget.onUpgradeToPro,
|
||||
),
|
||||
],
|
||||
|
||||
// shared users
|
||||
|
||||
if (state.users.isNotEmpty) ...[
|
||||
VSpace(theme.spacing.l),
|
||||
PeopleWithAccessSection(
|
||||
isInPublicPage: state.sectionType == SharedSectionType.public,
|
||||
currentUserEmail: state.currentUser?.email ?? '',
|
||||
users: state.users,
|
||||
callbacks: _buildPeopleWithAccessSectionCallbacks(context),
|
||||
@@ -123,18 +73,13 @@ class _ShareTabState extends State<ShareTab> {
|
||||
],
|
||||
|
||||
// general access
|
||||
if (state.sectionType == SharedSectionType.public) ...[
|
||||
VSpace(theme.spacing.m),
|
||||
GeneralAccessSection(
|
||||
group: SharedGroup(
|
||||
id: widget.workspaceId,
|
||||
name: widget.workspaceName,
|
||||
icon: widget.workspaceIcon,
|
||||
),
|
||||
),
|
||||
],
|
||||
// enable it when the backend support general access features.
|
||||
// VSpace(theme.spacing.m),
|
||||
// GeneralAccessSection(),
|
||||
|
||||
// copy link
|
||||
VSpace(theme.spacing.l),
|
||||
const AFDivider(),
|
||||
VSpace(theme.spacing.xl),
|
||||
CopyLinkWidget(shareLink: state.shareLink),
|
||||
VSpace(theme.spacing.m),
|
||||
@@ -149,8 +94,8 @@ class _ShareTabState extends State<ShareTab> {
|
||||
required List<String> emails,
|
||||
required ShareAccessLevel accessLevel,
|
||||
}) {
|
||||
context.read<ShareTabBloc>().add(
|
||||
ShareTabEvent.inviteUsers(emails: emails, accessLevel: accessLevel),
|
||||
context.read<ShareWithUserBloc>().add(
|
||||
ShareWithUserEvent.share(emails: emails, accessLevel: accessLevel),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -159,52 +104,24 @@ class _ShareTabState extends State<ShareTab> {
|
||||
) {
|
||||
return PeopleWithAccessSectionCallbacks(
|
||||
onSelectAccessLevel: (user, accessLevel) {
|
||||
context.read<ShareTabBloc>().add(
|
||||
ShareTabEvent.updateUserAccessLevel(
|
||||
email: user.email,
|
||||
accessLevel: accessLevel,
|
||||
),
|
||||
);
|
||||
// do nothing. the event doesn't support in the backend yet
|
||||
},
|
||||
onTurnIntoMember: (user) {
|
||||
context.read<ShareTabBloc>().add(
|
||||
ShareTabEvent.convertToMember(email: user.email),
|
||||
context.read<ShareWithUserBloc>().add(
|
||||
ShareWithUserEvent.turnIntoMember(email: user.email),
|
||||
);
|
||||
},
|
||||
onRemoveAccess: (user) {
|
||||
// show a dialog to confirm the action when removing self access
|
||||
final theme = AppFlowyTheme.of(context);
|
||||
final shareTabBloc = context.read<ShareTabBloc>();
|
||||
final removingSelf =
|
||||
user.email == shareTabBloc.state.currentUser?.email;
|
||||
if (removingSelf) {
|
||||
showConfirmDialog(
|
||||
context: context,
|
||||
title: 'Remove your own access',
|
||||
titleStyle: theme.textStyle.body.standard(
|
||||
color: theme.textColorScheme.primary,
|
||||
),
|
||||
description: '',
|
||||
style: ConfirmPopupStyle.cancelAndOk,
|
||||
confirmLabel: 'Remove',
|
||||
onConfirm: (_) {
|
||||
shareTabBloc.add(
|
||||
ShareTabEvent.removeUsers(emails: [user.email]),
|
||||
);
|
||||
},
|
||||
);
|
||||
} else {
|
||||
shareTabBloc.add(
|
||||
ShareTabEvent.removeUsers(emails: [user.email]),
|
||||
);
|
||||
}
|
||||
context.read<ShareWithUserBloc>().add(
|
||||
ShareWithUserEvent.remove(emails: [user.email]),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void _onListenShareWithUserState(
|
||||
BuildContext context,
|
||||
ShareTabState state,
|
||||
ShareWithUserState state,
|
||||
) {
|
||||
final shareResult = state.shareResult;
|
||||
if (shareResult != null) {
|
||||
@@ -213,25 +130,12 @@ class _ShareTabState extends State<ShareTab> {
|
||||
controller.clear();
|
||||
|
||||
showToastNotification(
|
||||
message: LocaleKeys.shareTab_invitationSent.tr(),
|
||||
message: 'Invitation sent',
|
||||
);
|
||||
}, (error) {
|
||||
String message;
|
||||
switch (error.code) {
|
||||
case ErrorCode.InvalidGuest:
|
||||
message = LocaleKeys.shareTab_emailAlreadyInList.tr();
|
||||
break;
|
||||
case ErrorCode.FreePlanGuestLimitExceeded:
|
||||
message = LocaleKeys.shareTab_upgradeToProToInviteGuests.tr();
|
||||
break;
|
||||
case ErrorCode.PaidPlanGuestLimitExceeded:
|
||||
message = LocaleKeys.shareTab_maxGuestsReached.tr();
|
||||
break;
|
||||
default:
|
||||
message = error.msg;
|
||||
}
|
||||
// TODO: handle the limiation error
|
||||
showToastNotification(
|
||||
message: message,
|
||||
message: error.msg,
|
||||
type: ToastificationType.error,
|
||||
);
|
||||
});
|
||||
@@ -241,7 +145,7 @@ class _ShareTabState extends State<ShareTab> {
|
||||
if (removeResult != null) {
|
||||
removeResult.fold((success) {
|
||||
showToastNotification(
|
||||
message: LocaleKeys.shareTab_removedGuestSuccessfully.tr(),
|
||||
message: 'Removed guest successfully',
|
||||
);
|
||||
}, (error) {
|
||||
showToastNotification(
|
||||
@@ -255,21 +159,7 @@ class _ShareTabState extends State<ShareTab> {
|
||||
if (updateAccessLevelResult != null) {
|
||||
updateAccessLevelResult.fold((success) {
|
||||
showToastNotification(
|
||||
message: LocaleKeys.shareTab_updatedAccessLevelSuccessfully.tr(),
|
||||
);
|
||||
}, (error) {
|
||||
showToastNotification(
|
||||
message: error.msg,
|
||||
type: ToastificationType.error,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
final turnIntoMemberResult = state.turnIntoMemberResult;
|
||||
if (turnIntoMemberResult != null) {
|
||||
turnIntoMemberResult.fold((success) {
|
||||
showToastNotification(
|
||||
message: LocaleKeys.shareTab_turnedIntoMemberSuccessfully.tr(),
|
||||
message: 'Updated access level successfully',
|
||||
);
|
||||
}, (error) {
|
||||
showToastNotification(
|
||||
|
||||
+45
-40
@@ -43,11 +43,6 @@ class AccessLevelListCallbacks {
|
||||
}
|
||||
}
|
||||
|
||||
enum AdditionalUserManagementOptions {
|
||||
turnIntoMember,
|
||||
removeAccess,
|
||||
}
|
||||
|
||||
/// A widget that displays a list of access levels for sharing.
|
||||
///
|
||||
/// This widget is used in a popover to allow users to select different access levels
|
||||
@@ -55,74 +50,84 @@ enum AdditionalUserManagementOptions {
|
||||
class AccessLevelListWidget extends StatelessWidget {
|
||||
const AccessLevelListWidget({
|
||||
super.key,
|
||||
this.enableAccessLevelSelection = false,
|
||||
required this.selectedAccessLevel,
|
||||
required this.callbacks,
|
||||
required this.supportedAccessLevels,
|
||||
required this.additionalUserManagementOptions,
|
||||
});
|
||||
|
||||
/// Enable access level selection
|
||||
final bool enableAccessLevelSelection;
|
||||
|
||||
/// The currently selected access level
|
||||
final ShareAccessLevel selectedAccessLevel;
|
||||
|
||||
/// Callbacks
|
||||
final AccessLevelListCallbacks callbacks;
|
||||
|
||||
/// Supported access levels
|
||||
final List<ShareAccessLevel> supportedAccessLevels;
|
||||
|
||||
/// Additional user management options
|
||||
final List<AdditionalUserManagementOptions> additionalUserManagementOptions;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = AppFlowyTheme.of(context);
|
||||
return AFMenu(
|
||||
width: supportedAccessLevels.isNotEmpty ? 260 : 160,
|
||||
width: enableAccessLevelSelection ? 240 : 160,
|
||||
children: [
|
||||
// Display all available access level options
|
||||
if (supportedAccessLevels.isNotEmpty) ...[
|
||||
...supportedAccessLevels.map(
|
||||
(accessLevel) => _buildAccessLevelItem(
|
||||
context,
|
||||
accessLevel: accessLevel,
|
||||
onTap: () => callbacks.onSelectAccessLevel(accessLevel),
|
||||
),
|
||||
if (enableAccessLevelSelection) ...[
|
||||
_buildAccessLevelItem(
|
||||
context,
|
||||
accessLevel: ShareAccessLevel.fullAccess,
|
||||
onTap: () =>
|
||||
callbacks.onSelectAccessLevel(ShareAccessLevel.fullAccess),
|
||||
),
|
||||
_buildAccessLevelItem(
|
||||
context,
|
||||
accessLevel: ShareAccessLevel.readAndWrite,
|
||||
onTap: () =>
|
||||
callbacks.onSelectAccessLevel(ShareAccessLevel.readAndWrite),
|
||||
),
|
||||
_buildAccessLevelItem(
|
||||
context,
|
||||
accessLevel: ShareAccessLevel.readAndComment,
|
||||
onTap: () =>
|
||||
callbacks.onSelectAccessLevel(ShareAccessLevel.readAndComment),
|
||||
),
|
||||
_buildAccessLevelItem(
|
||||
context,
|
||||
accessLevel: ShareAccessLevel.readOnly,
|
||||
onTap: () =>
|
||||
callbacks.onSelectAccessLevel(ShareAccessLevel.readOnly),
|
||||
),
|
||||
AFDivider(spacing: theme.spacing.m),
|
||||
],
|
||||
|
||||
// Additional user management options
|
||||
if (additionalUserManagementOptions
|
||||
.contains(AdditionalUserManagementOptions.turnIntoMember))
|
||||
AFTextMenuItem(
|
||||
title: LocaleKeys.shareTab_turnIntoMember.tr(),
|
||||
onTap: callbacks.onTurnIntoMember,
|
||||
),
|
||||
if (additionalUserManagementOptions
|
||||
.contains(AdditionalUserManagementOptions.removeAccess))
|
||||
AFTextMenuItem(
|
||||
title: LocaleKeys.shareTab_removeAccess.tr(),
|
||||
titleColor: theme.textColorScheme.error,
|
||||
onTap: callbacks.onRemoveAccess,
|
||||
),
|
||||
AFTextMenuItem(
|
||||
title: LocaleKeys.shareTab_turnIntoMember.tr(),
|
||||
onTap: callbacks.onTurnIntoMember,
|
||||
),
|
||||
AFTextMenuItem(
|
||||
title: LocaleKeys.shareTab_removeAccess.tr(),
|
||||
titleColor: theme.textColorScheme.error,
|
||||
onTap: callbacks.onRemoveAccess,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// Builds an individual access level menu item
|
||||
///
|
||||
/// @param context The build context
|
||||
/// @param accessLevel The access level to display
|
||||
/// @param onTap Callback when this item is tapped
|
||||
/// @return A menu item widget for the specified access level
|
||||
Widget _buildAccessLevelItem(
|
||||
BuildContext context, {
|
||||
required ShareAccessLevel accessLevel,
|
||||
required VoidCallback onTap,
|
||||
}) {
|
||||
return AFTextMenuItem(
|
||||
title: accessLevel.title,
|
||||
subtitle: accessLevel.subtitle,
|
||||
|
||||
title: accessLevel.i18n,
|
||||
showSelectedBackground: false,
|
||||
selected: selectedAccessLevel == accessLevel,
|
||||
leading: FlowySvg(
|
||||
accessLevel.icon,
|
||||
),
|
||||
// Show a checkmark icon for the currently selected access level
|
||||
trailing: selectedAccessLevel == accessLevel
|
||||
? FlowySvg(
|
||||
|
||||
+28
-58
@@ -1,7 +1,6 @@
|
||||
import 'package:appflowy/features/share_tab/logic/share_tab_bloc.dart';
|
||||
import 'package:appflowy/features/share_tab/logic/share_with_user_bloc.dart';
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
|
||||
import 'package:appflowy_ui/appflowy_ui.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
@@ -20,66 +19,37 @@ class CopyLinkWidget extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = AppFlowyTheme.of(context);
|
||||
return Container(
|
||||
padding: EdgeInsets.symmetric(
|
||||
vertical: theme.spacing.m,
|
||||
horizontal: theme.spacing.l,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.surfaceContainerColorScheme.layer01,
|
||||
borderRadius: BorderRadius.circular(theme.spacing.m),
|
||||
border: Border.all(
|
||||
color: theme.borderColorScheme.primary,
|
||||
return Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: AFTextField(
|
||||
initialText: shareLink,
|
||||
size: AFTextFieldSize.m,
|
||||
readOnly: true,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
FlowySvg(
|
||||
HSpace(theme.spacing.s),
|
||||
AFOutlinedIconTextButton.normal(
|
||||
text: LocaleKeys.shareTab_copyLink.tr(),
|
||||
size: AFButtonSize.l,
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: theme.spacing.l,
|
||||
vertical: theme.spacing.s,
|
||||
),
|
||||
iconBuilder: (context, isHovering, disabled) => FlowySvg(
|
||||
FlowySvgs.toolbar_link_m,
|
||||
),
|
||||
HSpace(theme.spacing.m),
|
||||
Expanded(
|
||||
child: Text(
|
||||
LocaleKeys.shareTab_peopleAboveCanAccessWithTheLink.tr(),
|
||||
style: theme.textStyle.caption.standard(
|
||||
color: theme.textColorScheme.primary,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
AFOutlinedTextButton.normal(
|
||||
text: LocaleKeys.shareTab_copyLink.tr(),
|
||||
size: AFButtonSize.l,
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: theme.spacing.l,
|
||||
vertical: theme.spacing.s,
|
||||
),
|
||||
backgroundColor: (context, isHovering, disabled) {
|
||||
final theme = AppFlowyTheme.of(context);
|
||||
if (disabled) {
|
||||
return theme.fillColorScheme.content;
|
||||
}
|
||||
if (isHovering) {
|
||||
return theme.fillColorScheme.contentHover;
|
||||
}
|
||||
return theme.surfaceColorScheme.layer02;
|
||||
},
|
||||
onTap: () {
|
||||
context.read<ShareTabBloc>().add(
|
||||
ShareTabEvent.copyShareLink(link: shareLink),
|
||||
);
|
||||
onTap: () {
|
||||
context.read<ShareWithUserBloc>().add(
|
||||
ShareWithUserEvent.copyLink(link: shareLink),
|
||||
);
|
||||
|
||||
if (FlowyRunner.currentMode.isUnitTest) {
|
||||
return;
|
||||
}
|
||||
|
||||
showToastNotification(
|
||||
message: LocaleKeys.shareTab_copiedLinkToClipboard.tr(),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
showToastNotification(
|
||||
message: LocaleKeys.shareTab_copiedLinkToClipboard.tr(),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
+1
-12
@@ -10,8 +10,6 @@ class EditAccessLevelWidget extends StatefulWidget {
|
||||
super.key,
|
||||
required this.callbacks,
|
||||
required this.selectedAccessLevel,
|
||||
required this.supportedAccessLevels,
|
||||
required this.additionalUserManagementOptions,
|
||||
this.disabled = false,
|
||||
});
|
||||
|
||||
@@ -24,12 +22,6 @@ class EditAccessLevelWidget extends StatefulWidget {
|
||||
/// Whether the widget is disabled
|
||||
final bool disabled;
|
||||
|
||||
/// Supported access levels
|
||||
final List<ShareAccessLevel> supportedAccessLevels;
|
||||
|
||||
/// Additional user management options
|
||||
final List<AdditionalUserManagementOptions> additionalUserManagementOptions;
|
||||
|
||||
@override
|
||||
State<EditAccessLevelWidget> createState() => _EditAccessLevelWidgetState();
|
||||
}
|
||||
@@ -55,9 +47,6 @@ class _EditAccessLevelWidgetState extends State<EditAccessLevelWidget> {
|
||||
popover: (_) {
|
||||
return AccessLevelListWidget(
|
||||
selectedAccessLevel: widget.selectedAccessLevel,
|
||||
supportedAccessLevels: widget.supportedAccessLevels,
|
||||
additionalUserManagementOptions:
|
||||
widget.additionalUserManagementOptions,
|
||||
callbacks: widget.callbacks.copyWith(
|
||||
onSelectAccessLevel: (accessLevel) {
|
||||
widget.callbacks.onSelectAccessLevel(accessLevel);
|
||||
@@ -85,7 +74,7 @@ class _EditAccessLevelWidgetState extends State<EditAccessLevelWidget> {
|
||||
return Row(
|
||||
children: [
|
||||
Text(
|
||||
widget.selectedAccessLevel.title,
|
||||
widget.selectedAccessLevel.i18n,
|
||||
style: theme.textStyle.body.standard(
|
||||
color: disabled
|
||||
? theme.textColorScheme.secondary
|
||||
|
||||
+4
-15
@@ -1,31 +1,20 @@
|
||||
import 'package:appflowy/features/share_tab/data/models/shared_group.dart';
|
||||
import 'package:appflowy/features/share_tab/presentation/widgets/shared_group_widget.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy_ui/appflowy_ui.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class GeneralAccessSection extends StatelessWidget {
|
||||
const GeneralAccessSection({
|
||||
super.key,
|
||||
required this.group,
|
||||
});
|
||||
|
||||
final SharedGroup group;
|
||||
const GeneralAccessSection({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = AppFlowyTheme.of(context);
|
||||
return AFMenuSection(
|
||||
title: LocaleKeys.shareTab_generalAccess.tr(),
|
||||
title: 'General access',
|
||||
padding: EdgeInsets.symmetric(
|
||||
vertical: theme.spacing.xs,
|
||||
horizontal: theme.spacing.m,
|
||||
vertical: theme.spacing.s,
|
||||
),
|
||||
children: [
|
||||
SharedGroupWidget(
|
||||
group: group,
|
||||
),
|
||||
SharedGroupWidget(),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy_ui/appflowy_ui.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class GuestTag extends StatelessWidget {
|
||||
const GuestTag({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = AppFlowyTheme.of(context);
|
||||
return Container(
|
||||
padding: EdgeInsets.only(
|
||||
left: theme.spacing.m,
|
||||
right: theme.spacing.m,
|
||||
bottom: 2,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.fillColorScheme.warningLight,
|
||||
borderRadius: BorderRadius.circular(theme.spacing.s),
|
||||
),
|
||||
child: Text(
|
||||
LocaleKeys.shareTab_guest.tr(),
|
||||
style: theme.textStyle.caption
|
||||
.standard(
|
||||
color: theme.textColorScheme.warning,
|
||||
)
|
||||
.copyWith(
|
||||
height: 16.0 / 12.0,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
-4
@@ -36,13 +36,11 @@ class PeopleWithAccessSection extends StatelessWidget {
|
||||
super.key,
|
||||
required this.currentUserEmail,
|
||||
required this.users,
|
||||
required this.isInPublicPage,
|
||||
this.callbacks,
|
||||
});
|
||||
|
||||
final String currentUserEmail;
|
||||
final SharedUsers users;
|
||||
final bool isInPublicPage;
|
||||
final PeopleWithAccessSectionCallbacks? callbacks;
|
||||
|
||||
@override
|
||||
@@ -59,7 +57,6 @@ class PeopleWithAccessSection extends StatelessWidget {
|
||||
),
|
||||
padding: EdgeInsets.symmetric(
|
||||
vertical: theme.spacing.xs,
|
||||
horizontal: theme.spacing.m,
|
||||
),
|
||||
children: users.map((user) {
|
||||
if (currentUser == null) {
|
||||
@@ -69,7 +66,6 @@ class PeopleWithAccessSection extends StatelessWidget {
|
||||
return SharedUserWidget(
|
||||
user: user,
|
||||
currentUser: currentUser,
|
||||
isInPublicPage: isInPublicPage,
|
||||
callbacks: AccessLevelListCallbacks(
|
||||
onRemoveAccess: () => callbacks?.onRemoveAccess.call(user),
|
||||
onTurnIntoMember: () => callbacks?.onTurnIntoMember.call(user),
|
||||
|
||||
+2
-19
@@ -1,7 +1,6 @@
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy_ui/appflowy_ui.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/widget/flowy_tooltip.dart';
|
||||
import 'package:flowy_infra_ui/widget/spacing.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:string_validator/string_validator.dart';
|
||||
@@ -11,14 +10,10 @@ class ShareWithUserWidget extends StatefulWidget {
|
||||
super.key,
|
||||
required this.onInvite,
|
||||
this.controller,
|
||||
this.disabled = false,
|
||||
this.tooltip,
|
||||
});
|
||||
|
||||
final TextEditingController? controller;
|
||||
final void Function(List<String> emails) onInvite;
|
||||
final bool disabled;
|
||||
final String? tooltip;
|
||||
final TextEditingController? controller;
|
||||
|
||||
@override
|
||||
State<ShareWithUserWidget> createState() => _ShareWithUserWidgetState();
|
||||
@@ -49,7 +44,7 @@ class _ShareWithUserWidgetState extends State<ShareWithUserWidget> {
|
||||
Widget build(BuildContext context) {
|
||||
final theme = AppFlowyTheme.of(context);
|
||||
|
||||
final Widget child = Row(
|
||||
return Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: AFTextField(
|
||||
@@ -68,18 +63,6 @@ class _ShareWithUserWidgetState extends State<ShareWithUserWidget> {
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
if (widget.disabled) {
|
||||
return FlowyTooltip(
|
||||
message:
|
||||
widget.tooltip ?? LocaleKeys.shareTab_onlyFullAccessCanInvite.tr(),
|
||||
child: IgnorePointer(
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return child;
|
||||
}
|
||||
|
||||
void _onTextChanged() {
|
||||
|
||||
+18
-34
@@ -1,20 +1,15 @@
|
||||
import 'package:appflowy/features/share_tab/data/models/share_access_level.dart';
|
||||
import 'package:appflowy/features/share_tab/data/models/shared_group.dart';
|
||||
import 'package:appflowy/features/share_tab/presentation/widgets/access_level_list_widget.dart';
|
||||
import 'package:appflowy/features/share_tab/presentation/widgets/edit_access_level_widget.dart';
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_icon.dart';
|
||||
import 'package:appflowy_ui/appflowy_ui.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/widget/spacing.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class SharedGroupWidget extends StatelessWidget {
|
||||
const SharedGroupWidget({
|
||||
super.key,
|
||||
required this.group,
|
||||
});
|
||||
|
||||
final SharedGroup group;
|
||||
const SharedGroupWidget({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -23,28 +18,23 @@ class SharedGroupWidget extends StatelessWidget {
|
||||
return AFMenuItem(
|
||||
padding: EdgeInsets.symmetric(
|
||||
vertical: theme.spacing.s,
|
||||
horizontal: theme.spacing.m,
|
||||
),
|
||||
leading: _buildLeading(context),
|
||||
title: _buildTitle(context),
|
||||
subtitle: _buildSubtitle(context),
|
||||
trailing: _buildTrailing(context),
|
||||
onTap: () {},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildLeading(BuildContext context) {
|
||||
return WorkspaceIcon(
|
||||
isEditable: false,
|
||||
workspaceIcon: group.icon,
|
||||
workspaceName: group.name,
|
||||
iconSize: 32.0,
|
||||
emojiSize: 24.0,
|
||||
fontSize: 16.0,
|
||||
onSelected: (r) {},
|
||||
borderRadius: 8.0,
|
||||
showBorder: false,
|
||||
figmaLineHeight: 24.0,
|
||||
return AFAvatar(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(3.0),
|
||||
child: FlowySvg(
|
||||
FlowySvgs.app_logo_s, // replace it with group avatar
|
||||
blendMode: null,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -54,22 +44,18 @@ class SharedGroupWidget extends StatelessWidget {
|
||||
children: [
|
||||
Flexible(
|
||||
child: Text(
|
||||
LocaleKeys.shareTab_anyoneAtWorkspace.tr(
|
||||
namedArgs: {
|
||||
'workspace': group.name,
|
||||
},
|
||||
),
|
||||
LocaleKeys.shareTab_anyoneAtWorkspace.tr(),
|
||||
style: theme.textStyle.body.standard(
|
||||
color: theme.textColorScheme.primary,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
// HSpace(theme.spacing.xs),
|
||||
// FlowySvg(
|
||||
// FlowySvgs.arrow_down_s,
|
||||
// color: theme.textColorScheme.secondary,
|
||||
// ),
|
||||
HSpace(theme.spacing.xs),
|
||||
FlowySvg(
|
||||
FlowySvgs.arrow_down_s,
|
||||
color: theme.textColorScheme.secondary,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
@@ -88,10 +74,8 @@ class SharedGroupWidget extends StatelessWidget {
|
||||
Widget _buildTrailing(BuildContext context) {
|
||||
return EditAccessLevelWidget(
|
||||
disabled: true,
|
||||
supportedAccessLevels: ShareAccessLevel.values,
|
||||
selectedAccessLevel: ShareAccessLevel.readAndWrite,
|
||||
selectedAccessLevel: ShareAccessLevel.fullAccess,
|
||||
callbacks: AccessLevelListCallbacks.none(),
|
||||
additionalUserManagementOptions: [],
|
||||
);
|
||||
}
|
||||
}
|
||||
+26
-85
@@ -1,8 +1,6 @@
|
||||
import 'package:appflowy/features/share_tab/data/models/models.dart';
|
||||
import 'package:appflowy/features/share_tab/presentation/widgets/access_level_list_widget.dart';
|
||||
import 'package:appflowy/features/share_tab/presentation/widgets/edit_access_level_widget.dart';
|
||||
import 'package:appflowy/features/share_tab/presentation/widgets/guest_tag.dart';
|
||||
import 'package:appflowy/features/share_tab/presentation/widgets/turn_into_member_widget.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy_ui/appflowy_ui.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
@@ -15,14 +13,12 @@ class SharedUserWidget extends StatelessWidget {
|
||||
super.key,
|
||||
required this.user,
|
||||
required this.currentUser,
|
||||
required this.isInPublicPage,
|
||||
this.callbacks,
|
||||
});
|
||||
|
||||
final SharedUser user;
|
||||
final SharedUser currentUser;
|
||||
final AccessLevelListCallbacks? callbacks;
|
||||
final bool isInPublicPage;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -31,7 +27,6 @@ class SharedUserWidget extends StatelessWidget {
|
||||
return AFMenuItem(
|
||||
padding: EdgeInsets.symmetric(
|
||||
vertical: theme.spacing.s,
|
||||
horizontal: theme.spacing.m,
|
||||
),
|
||||
leading: AFAvatar(
|
||||
name: user.name,
|
||||
@@ -40,9 +35,6 @@ class SharedUserWidget extends StatelessWidget {
|
||||
title: _buildTitle(context),
|
||||
subtitle: _buildSubtitle(context),
|
||||
trailing: _buildTrailing(context),
|
||||
onTap: () {
|
||||
// callbacks?.onSelectAccessLevel.call(user, user.accessLevel);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -53,7 +45,6 @@ class SharedUserWidget extends StatelessWidget {
|
||||
final isCurrentUser = user.email == currentUser.email;
|
||||
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Flexible(
|
||||
child: Text(
|
||||
@@ -67,20 +58,22 @@ class SharedUserWidget extends StatelessWidget {
|
||||
// if the user is the current user, show '(You)'
|
||||
if (isCurrentUser) ...[
|
||||
HSpace(theme.spacing.xs),
|
||||
Flexible(
|
||||
child: Text(
|
||||
LocaleKeys.shareTab_you.tr(),
|
||||
style: theme.textStyle.caption.standard(
|
||||
color: theme.textColorScheme.secondary,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
Text(
|
||||
LocaleKeys.shareTab_you.tr(),
|
||||
style: theme.textStyle.body.standard(
|
||||
color: theme.textColorScheme.secondary,
|
||||
),
|
||||
),
|
||||
],
|
||||
// if the user is a guest, show 'Guest'
|
||||
if (user.role == ShareRole.guest) ...[
|
||||
HSpace(theme.spacing.m),
|
||||
const GuestTag(),
|
||||
HSpace(theme.spacing.xs),
|
||||
Text(
|
||||
LocaleKeys.shareTab_guest.tr(),
|
||||
style: theme.textStyle.body.standard(
|
||||
color: theme.textColorScheme.warning,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
@@ -98,74 +91,22 @@ class SharedUserWidget extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTrailing(BuildContext context) {
|
||||
Widget _buildTrailing(
|
||||
BuildContext context,
|
||||
) {
|
||||
final isCurrentUser = user.email == currentUser.email;
|
||||
final theme = AppFlowyTheme.of(context);
|
||||
final currentAccessLevel = currentUser.accessLevel;
|
||||
|
||||
Widget disabledAccessButton() => AFGhostTextButton.disabled(
|
||||
text: user.accessLevel.title,
|
||||
textStyle: theme.textStyle.body.standard(
|
||||
color: theme.textColorScheme.secondary,
|
||||
),
|
||||
);
|
||||
|
||||
Widget editAccessWidget(List<ShareAccessLevel> supported) =>
|
||||
EditAccessLevelWidget(
|
||||
selectedAccessLevel: user.accessLevel,
|
||||
supportedAccessLevels: supported,
|
||||
additionalUserManagementOptions: [
|
||||
AdditionalUserManagementOptions.removeAccess,
|
||||
],
|
||||
callbacks: callbacks ?? AccessLevelListCallbacks.none(),
|
||||
);
|
||||
|
||||
// In public page, member/owner permissions are fixed
|
||||
if (isInPublicPage &&
|
||||
(user.role == ShareRole.member || user.role == ShareRole.owner)) {
|
||||
return disabledAccessButton();
|
||||
}
|
||||
|
||||
// Full access user can turn a guest into a member
|
||||
if (user.role == ShareRole.guest &&
|
||||
currentAccessLevel == ShareAccessLevel.fullAccess) {
|
||||
return Row(
|
||||
children: [
|
||||
TurnIntoMemberWidget(
|
||||
onTap: () => callbacks?.onTurnIntoMember.call(),
|
||||
),
|
||||
editAccessWidget([
|
||||
ShareAccessLevel.readOnly,
|
||||
ShareAccessLevel.readAndWrite,
|
||||
]),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
// Self-management
|
||||
if (isCurrentUser) {
|
||||
if (currentAccessLevel == ShareAccessLevel.readOnly ||
|
||||
currentAccessLevel == ShareAccessLevel.readAndWrite) {
|
||||
// Can only remove self
|
||||
return editAccessWidget([]);
|
||||
} else if (currentAccessLevel == ShareAccessLevel.fullAccess) {
|
||||
// Full access user cannot change own access
|
||||
return disabledAccessButton();
|
||||
}
|
||||
}
|
||||
|
||||
// Managing others
|
||||
if (currentAccessLevel == ShareAccessLevel.readOnly ||
|
||||
currentAccessLevel == ShareAccessLevel.readAndWrite) {
|
||||
// Cannot change others' access
|
||||
return disabledAccessButton();
|
||||
} else {
|
||||
// Full access user can manage others
|
||||
final supportedAccessLevels = [
|
||||
ShareAccessLevel.readOnly,
|
||||
ShareAccessLevel.readAndWrite,
|
||||
];
|
||||
return editAccessWidget(supportedAccessLevels);
|
||||
}
|
||||
// The current guest user can't edit the access level of the other user
|
||||
return isCurrentUser || currentUser.role == ShareRole.guest
|
||||
? AFGhostTextButton.disabled(
|
||||
text: user.accessLevel.i18n,
|
||||
textStyle: theme.textStyle.body.standard(
|
||||
color: theme.textColorScheme.secondary,
|
||||
),
|
||||
)
|
||||
: EditAccessLevelWidget(
|
||||
selectedAccessLevel: user.accessLevel,
|
||||
callbacks: callbacks ?? AccessLevelListCallbacks.none(),
|
||||
);
|
||||
}
|
||||
}
|
||||
-30
@@ -1,30 +0,0 @@
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy_ui/appflowy_ui.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/widget/flowy_tooltip.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class TurnIntoMemberWidget extends StatelessWidget {
|
||||
const TurnIntoMemberWidget({
|
||||
super.key,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
final VoidCallback onTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = AppFlowyTheme.of(context);
|
||||
return FlowyTooltip(
|
||||
message: LocaleKeys.shareTab_turnIntoMember.tr(),
|
||||
child: AFGhostButton.normal(
|
||||
onTap: onTap,
|
||||
padding: EdgeInsets.all(theme.spacing.s),
|
||||
builder: (context, isHovering, disabled) {
|
||||
return FlowySvg(FlowySvgs.turn_into_member_m);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
-88
@@ -1,88 +0,0 @@
|
||||
import 'package:appflowy/features/share_tab/logic/share_tab_bloc.dart';
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy_ui/appflowy_ui.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
class UpgradeToProWidget extends StatelessWidget {
|
||||
const UpgradeToProWidget({
|
||||
super.key,
|
||||
required this.onUpgrade,
|
||||
required this.onClose,
|
||||
});
|
||||
|
||||
final VoidCallback onClose;
|
||||
final VoidCallback onUpgrade;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = AppFlowyTheme.of(context);
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Color(0x129327ff),
|
||||
borderRadius: BorderRadius.circular(theme.borderRadius.m),
|
||||
),
|
||||
padding: EdgeInsets.symmetric(
|
||||
vertical: theme.spacing.m,
|
||||
horizontal: theme.spacing.l,
|
||||
),
|
||||
margin: EdgeInsets.only(
|
||||
top: theme.spacing.l,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
FlowySvg(
|
||||
FlowySvgs.upgrade_pro_crown_m,
|
||||
blendMode: null,
|
||||
),
|
||||
HSpace(
|
||||
theme.spacing.m,
|
||||
),
|
||||
RichText(
|
||||
text: TextSpan(
|
||||
children: [
|
||||
TextSpan(
|
||||
text: LocaleKeys.shareTab_upgrade.tr(),
|
||||
style: theme.textStyle.caption.standard().copyWith(
|
||||
color: theme.textColorScheme.featured,
|
||||
decoration: TextDecoration.underline,
|
||||
),
|
||||
recognizer: TapGestureRecognizer()
|
||||
..onTap = () {
|
||||
onUpgrade();
|
||||
},
|
||||
mouseCursor: SystemMouseCursors.click,
|
||||
),
|
||||
TextSpan(
|
||||
text: LocaleKeys.shareTab_toProPlanToInviteGuests.tr(),
|
||||
style: theme.textStyle.caption.standard().copyWith(
|
||||
color: theme.textColorScheme.featured,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
AFGhostButton.normal(
|
||||
size: AFButtonSize.s,
|
||||
padding: EdgeInsets.all(theme.spacing.xs),
|
||||
onTap: () {
|
||||
context
|
||||
.read<ShareTabBloc>()
|
||||
.add(ShareTabEvent.upgradeToProClicked());
|
||||
onClose();
|
||||
},
|
||||
builder: (context, isHovering, disabled) => FlowySvg(
|
||||
FlowySvgs.upgrade_to_pro_close_m,
|
||||
size: const Size.square(20),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
+2
-8
@@ -1,12 +1,11 @@
|
||||
import 'package:appflowy/features/share_tab/data/models/share_access_level.dart';
|
||||
import 'package:appflowy/features/shared_section/data/repositories/shared_pages_repository.dart';
|
||||
import 'package:appflowy/features/shared_section/data/share_pages_repository.dart';
|
||||
import 'package:appflowy/features/shared_section/models/shared_page.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
import 'package:appflowy_result/appflowy_result.dart';
|
||||
|
||||
// Move this file to test folder
|
||||
class LocalSharedPagesRepositoryImpl implements SharedPagesRepository {
|
||||
class LocalSharePagesRepository implements SharePagesRepository {
|
||||
@override
|
||||
Future<FlowyResult<SharedPages, FlowyError>> getSharedPages() async {
|
||||
final pages = [
|
||||
@@ -31,9 +30,4 @@ class LocalSharedPagesRepositoryImpl implements SharedPagesRepository {
|
||||
];
|
||||
return FlowyResult.success(pages);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<FlowyResult<void, FlowyError>> leaveSharedPage(String pageId) async {
|
||||
return FlowyResult.success(null);
|
||||
}
|
||||
}
|
||||
-63
@@ -1,63 +0,0 @@
|
||||
import 'package:appflowy/features/shared_section/data/repositories/shared_pages_repository.dart';
|
||||
import 'package:appflowy/features/shared_section/models/shared_page.dart';
|
||||
import 'package:appflowy/features/util/extensions.dart';
|
||||
import 'package:appflowy_backend/dispatch/dispatch.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';
|
||||
import 'package:appflowy_result/appflowy_result.dart';
|
||||
|
||||
class RustSharePagesRepositoryImpl implements SharedPagesRepository {
|
||||
@override
|
||||
Future<FlowyResult<SharedPages, FlowyError>> getSharedPages() async {
|
||||
final result = await FolderEventGetSharedViews().send();
|
||||
return result.fold(
|
||||
(success) {
|
||||
final sharedPages = success.sharedPages;
|
||||
|
||||
Log.debug('get shared pages success, len: ${sharedPages.length}');
|
||||
|
||||
return FlowyResult.success(sharedPages);
|
||||
},
|
||||
(error) {
|
||||
Log.error('failed to get shared pages, error: $error');
|
||||
|
||||
return FlowyResult.failure(error);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<FlowyResult<void, FlowyError>> leaveSharedPage(String pageId) async {
|
||||
final user = await UserEventGetUserProfile().send();
|
||||
final userEmail = user.fold(
|
||||
(success) => success.email,
|
||||
(error) => null,
|
||||
);
|
||||
|
||||
if (userEmail == null) {
|
||||
return FlowyResult.failure(FlowyError(msg: 'User email is null'));
|
||||
}
|
||||
|
||||
final request = RemoveUserFromSharedPagePayloadPB(
|
||||
viewId: pageId,
|
||||
emails: [userEmail],
|
||||
);
|
||||
final result = await FolderEventRemoveUserFromSharedPage(request).send();
|
||||
|
||||
return result.fold(
|
||||
(success) {
|
||||
Log.debug('remove user($userEmail) from shared page($pageId)');
|
||||
|
||||
return FlowySuccess(success);
|
||||
},
|
||||
(failure) {
|
||||
Log.error(
|
||||
'remove user($userEmail) from shared page($pageId): $failure',
|
||||
);
|
||||
|
||||
return FlowyFailure(failure);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
import 'package:appflowy/features/shared_section/data/share_pages_repository.dart';
|
||||
import 'package:appflowy/features/shared_section/models/shared_page.dart';
|
||||
import 'package:appflowy/features/util/extensions.dart';
|
||||
import 'package:appflowy_backend/dispatch/dispatch.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
||||
import 'package:appflowy_result/appflowy_result.dart';
|
||||
|
||||
class RustSharePagesRepository implements SharePagesRepository {
|
||||
@override
|
||||
Future<FlowyResult<SharedPages, FlowyError>> getSharedPages() async {
|
||||
final result = await FolderEventGetSharedViews().send();
|
||||
return result.fold(
|
||||
(success) {
|
||||
final sharedPages = success.sharedPages;
|
||||
|
||||
Log.info('get shared pages success, len: ${sharedPages.length}');
|
||||
|
||||
return FlowyResult.success(sharedPages);
|
||||
},
|
||||
(error) {
|
||||
Log.error('failed to get shared pages, error: $error');
|
||||
|
||||
return FlowyResult.failure(error);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
+1
-4
@@ -6,10 +6,7 @@ import 'package:appflowy_result/appflowy_result.dart';
|
||||
///
|
||||
/// For example, we're using rust events now, but we can still use the http api
|
||||
/// for the future.
|
||||
abstract class SharedPagesRepository {
|
||||
abstract class SharePagesRepository {
|
||||
/// Gets the list of users and their roles for a shared page.
|
||||
Future<FlowyResult<SharedPages, FlowyError>> getSharedPages();
|
||||
|
||||
/// Removes a shared page from the repository.
|
||||
Future<FlowyResult<void, FlowyError>> leaveSharedPage(String pageId);
|
||||
}
|
||||
+39
-48
@@ -1,17 +1,15 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:appflowy/core/notification/folder_notification.dart';
|
||||
import 'package:appflowy/features/shared_section/data/repositories/shared_pages_repository.dart';
|
||||
import 'package:appflowy/features/shared_section/logic/shared_section_event.dart';
|
||||
import 'package:appflowy/features/shared_section/logic/shared_section_state.dart';
|
||||
import 'package:appflowy/features/shared_section/data/share_pages_repository.dart';
|
||||
import 'package:appflowy/features/shared_section/models/shared_page.dart';
|
||||
import 'package:appflowy/features/util/extensions.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';
|
||||
import 'package:bloc/bloc.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
export 'shared_section_event.dart';
|
||||
export 'shared_section_state.dart';
|
||||
part 'shared_section_bloc.freezed.dart';
|
||||
|
||||
class SharedSectionBloc extends Bloc<SharedSectionEvent, SharedSectionState> {
|
||||
SharedSectionBloc({
|
||||
@@ -20,18 +18,16 @@ class SharedSectionBloc extends Bloc<SharedSectionEvent, SharedSectionState> {
|
||||
this.enablePolling = false,
|
||||
this.pollingIntervalSeconds = 30,
|
||||
}) : super(SharedSectionState.initial()) {
|
||||
on<SharedSectionInitEvent>(_onInit);
|
||||
on<SharedSectionRefreshEvent>(_onRefresh);
|
||||
on<SharedSectionUpdateSharedPagesEvent>(_onUpdateSharedPages);
|
||||
on<SharedSectionToggleExpandedEvent>(_onToggleExpanded);
|
||||
on<SharedSectionLeaveSharedPageEvent>(_onLeaveSharedPage);
|
||||
on<_Init>(_onInit);
|
||||
on<_Refresh>(_onRefresh);
|
||||
on<_UpdateSharedPages>(_onUpdateSharedPages);
|
||||
}
|
||||
|
||||
final String workspaceId;
|
||||
|
||||
// The repository to fetch the shared views.
|
||||
// If you need to test this bloc, you can add your own repository implementation.
|
||||
final SharedPagesRepository repository;
|
||||
final SharePagesRepository repository;
|
||||
|
||||
// Used to listen for shared view updates.
|
||||
late final FolderNotificationListener _folderNotificationListener;
|
||||
@@ -53,7 +49,7 @@ class SharedSectionBloc extends Bloc<SharedSectionEvent, SharedSectionState> {
|
||||
}
|
||||
|
||||
Future<void> _onInit(
|
||||
SharedSectionInitEvent event,
|
||||
_Init event,
|
||||
Emitter<SharedSectionState> emit,
|
||||
) async {
|
||||
_initFolderNotificationListener();
|
||||
@@ -87,7 +83,7 @@ class SharedSectionBloc extends Bloc<SharedSectionEvent, SharedSectionState> {
|
||||
}
|
||||
|
||||
Future<void> _onRefresh(
|
||||
SharedSectionRefreshEvent event,
|
||||
_Refresh event,
|
||||
Emitter<SharedSectionState> emit,
|
||||
) async {
|
||||
final result = await repository.getSharedPages();
|
||||
@@ -111,7 +107,7 @@ class SharedSectionBloc extends Bloc<SharedSectionEvent, SharedSectionState> {
|
||||
}
|
||||
|
||||
void _onUpdateSharedPages(
|
||||
SharedSectionUpdateSharedPagesEvent event,
|
||||
_UpdateSharedPages event,
|
||||
Emitter<SharedSectionState> emit,
|
||||
) {
|
||||
emit(
|
||||
@@ -121,17 +117,6 @@ class SharedSectionBloc extends Bloc<SharedSectionEvent, SharedSectionState> {
|
||||
);
|
||||
}
|
||||
|
||||
void _onToggleExpanded(
|
||||
SharedSectionToggleExpandedEvent event,
|
||||
Emitter<SharedSectionState> emit,
|
||||
) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
isExpanded: !state.isExpanded,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _initFolderNotificationListener() {
|
||||
_folderNotificationListener = FolderNotificationListener(
|
||||
objectId: workspaceId,
|
||||
@@ -157,28 +142,6 @@ class SharedSectionBloc extends Bloc<SharedSectionEvent, SharedSectionState> {
|
||||
);
|
||||
}
|
||||
|
||||
void _onLeaveSharedPage(
|
||||
SharedSectionLeaveSharedPageEvent event,
|
||||
Emitter<SharedSectionState> emit,
|
||||
) async {
|
||||
final result = await repository.leaveSharedPage(event.pageId);
|
||||
result.fold(
|
||||
(success) {
|
||||
add(
|
||||
SharedSectionEvent.updateSharedPages(
|
||||
sharedPages: state.sharedPages
|
||||
..removeWhere(
|
||||
(page) => page.view.id == event.pageId,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
(error) {
|
||||
emit(state.copyWith(errorMessage: error.msg));
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void _startPollingIfNeeded() {
|
||||
_pollingTimer?.cancel();
|
||||
if (enablePolling && pollingIntervalSeconds > 0) {
|
||||
@@ -193,3 +156,31 @@ class SharedSectionBloc extends Bloc<SharedSectionEvent, SharedSectionState> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@freezed
|
||||
class SharedSectionEvent with _$SharedSectionEvent {
|
||||
/// Initialize, it will create a folder notification listener to listen for shared view updates.
|
||||
/// Also, it will fetch the shared pages from the repository.
|
||||
const factory SharedSectionEvent.init() = _Init;
|
||||
|
||||
/// Refresh, it will re-fetch the shared pages from the repository.
|
||||
const factory SharedSectionEvent.refresh() = _Refresh;
|
||||
|
||||
/// Update the shared pages in the state.
|
||||
const factory SharedSectionEvent.updateSharedPages({
|
||||
required SharedPages sharedPages,
|
||||
}) = _UpdateSharedPages;
|
||||
}
|
||||
|
||||
@freezed
|
||||
class SharedSectionState with _$SharedSectionState {
|
||||
const factory SharedSectionState({
|
||||
@Default([]) SharedPages sharedPages,
|
||||
@Default(false) bool isLoading,
|
||||
@Default('') String errorMessage,
|
||||
}) = _SharedSectionState;
|
||||
|
||||
const SharedSectionState._();
|
||||
|
||||
factory SharedSectionState.initial() => const SharedSectionState();
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
import 'package:appflowy/features/shared_section/models/shared_page.dart';
|
||||
|
||||
/// Base class for all SharedSection events
|
||||
sealed class SharedSectionEvent {
|
||||
const SharedSectionEvent();
|
||||
|
||||
/// Initialize, it will create a folder notification listener to listen for shared view updates.
|
||||
/// Also, it will fetch the shared pages from the repository.
|
||||
const factory SharedSectionEvent.init() = SharedSectionInitEvent;
|
||||
|
||||
/// Refresh, it will re-fetch the shared pages from the repository.
|
||||
const factory SharedSectionEvent.refresh() = SharedSectionRefreshEvent;
|
||||
|
||||
/// Update the shared pages in the state.
|
||||
const factory SharedSectionEvent.updateSharedPages({
|
||||
required SharedPages sharedPages,
|
||||
}) = SharedSectionUpdateSharedPagesEvent;
|
||||
|
||||
/// Toggle the expanded status of the shared section.
|
||||
const factory SharedSectionEvent.toggleExpanded() =
|
||||
SharedSectionToggleExpandedEvent;
|
||||
|
||||
/// Leave shared page.
|
||||
const factory SharedSectionEvent.leaveSharedPage({
|
||||
required String pageId,
|
||||
}) = SharedSectionLeaveSharedPageEvent;
|
||||
}
|
||||
|
||||
class SharedSectionInitEvent extends SharedSectionEvent {
|
||||
const SharedSectionInitEvent();
|
||||
}
|
||||
|
||||
class SharedSectionRefreshEvent extends SharedSectionEvent {
|
||||
const SharedSectionRefreshEvent();
|
||||
}
|
||||
|
||||
class SharedSectionUpdateSharedPagesEvent extends SharedSectionEvent {
|
||||
const SharedSectionUpdateSharedPagesEvent({
|
||||
required this.sharedPages,
|
||||
});
|
||||
|
||||
final SharedPages sharedPages;
|
||||
}
|
||||
|
||||
class SharedSectionToggleExpandedEvent extends SharedSectionEvent {
|
||||
const SharedSectionToggleExpandedEvent();
|
||||
}
|
||||
|
||||
class SharedSectionLeaveSharedPageEvent extends SharedSectionEvent {
|
||||
const SharedSectionLeaveSharedPageEvent({
|
||||
required this.pageId,
|
||||
});
|
||||
|
||||
final String pageId;
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
import 'package:appflowy/features/shared_section/models/shared_page.dart';
|
||||
|
||||
class SharedSectionState {
|
||||
factory SharedSectionState.initial() => const SharedSectionState();
|
||||
|
||||
const SharedSectionState({
|
||||
this.sharedPages = const [],
|
||||
this.isLoading = false,
|
||||
this.errorMessage = '',
|
||||
this.isExpanded = true,
|
||||
});
|
||||
|
||||
final SharedPages sharedPages;
|
||||
final bool isLoading;
|
||||
final String errorMessage;
|
||||
final bool isExpanded;
|
||||
|
||||
SharedSectionState copyWith({
|
||||
SharedPages? sharedPages,
|
||||
bool? isLoading,
|
||||
String? errorMessage,
|
||||
bool? isExpanded,
|
||||
}) {
|
||||
return SharedSectionState(
|
||||
sharedPages: sharedPages ?? this.sharedPages,
|
||||
isLoading: isLoading ?? this.isLoading,
|
||||
errorMessage: errorMessage ?? this.errorMessage,
|
||||
isExpanded: isExpanded ?? this.isExpanded,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (identical(this, other)) return true;
|
||||
return other is SharedSectionState &&
|
||||
other.sharedPages == sharedPages &&
|
||||
other.isLoading == isLoading &&
|
||||
other.errorMessage == errorMessage &&
|
||||
other.isExpanded == isExpanded;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
return Object.hash(
|
||||
sharedPages,
|
||||
isLoading,
|
||||
errorMessage,
|
||||
isExpanded,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'SharedSectionState(sharedPages: $sharedPages, isLoading: $isLoading, errorMessage: $errorMessage, isExpanded: $isExpanded)';
|
||||
}
|
||||
}
|
||||
-92
@@ -1,92 +0,0 @@
|
||||
import 'package:appflowy/features/shared_section/data/repositories/rust_shared_pages_repository_impl.dart';
|
||||
import 'package:appflowy/features/shared_section/logic/shared_section_bloc.dart';
|
||||
import 'package:appflowy/features/shared_section/presentation/widgets/m_shared_page_list.dart';
|
||||
import 'package:appflowy/features/shared_section/presentation/widgets/m_shared_section_header.dart';
|
||||
import 'package:appflowy/features/shared_section/presentation/widgets/refresh_button.dart';
|
||||
import 'package:appflowy/features/shared_section/presentation/widgets/shared_section_empty.dart';
|
||||
import 'package:appflowy/features/shared_section/presentation/widgets/shared_section_error.dart';
|
||||
import 'package:appflowy/features/shared_section/presentation/widgets/shared_section_loading.dart';
|
||||
import 'package:appflowy/mobile/application/mobile_router.dart';
|
||||
import 'package:appflowy/shared/icon_emoji_picker/tab.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/home_sizes.dart';
|
||||
import 'package:flowy_infra_ui/widget/spacing.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
class MSharedSection extends StatelessWidget {
|
||||
const MSharedSection({
|
||||
super.key,
|
||||
required this.workspaceId,
|
||||
});
|
||||
|
||||
final String workspaceId;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final repository = RustSharePagesRepositoryImpl();
|
||||
|
||||
return BlocProvider(
|
||||
create: (_) => SharedSectionBloc(
|
||||
workspaceId: workspaceId,
|
||||
repository: repository,
|
||||
enablePolling: true,
|
||||
)..add(const SharedSectionInitEvent()),
|
||||
child: BlocBuilder<SharedSectionBloc, SharedSectionState>(
|
||||
builder: (context, state) {
|
||||
if (state.isLoading) {
|
||||
return const SharedSectionLoading();
|
||||
}
|
||||
|
||||
if (state.errorMessage.isNotEmpty) {
|
||||
return SharedSectionError(errorMessage: state.errorMessage);
|
||||
}
|
||||
|
||||
// hide the shared section if there are no shared pages
|
||||
if (state.sharedPages.isEmpty) {
|
||||
return const SharedSectionEmpty();
|
||||
}
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const VSpace(HomeSpaceViewSizes.mVerticalPadding),
|
||||
|
||||
// Shared header
|
||||
MSharedSectionHeader(),
|
||||
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
left: HomeSpaceViewSizes.mHorizontalPadding,
|
||||
),
|
||||
child: MSharedPageList(
|
||||
sharedPages: state.sharedPages,
|
||||
onSelected: (view) {
|
||||
context.pushView(
|
||||
view,
|
||||
tabs: [
|
||||
PickerTabType.emoji,
|
||||
PickerTabType.icon,
|
||||
PickerTabType.custom,
|
||||
].map((e) => e.name).toList(),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
|
||||
// Refresh button, for debugging only
|
||||
if (kDebugMode)
|
||||
RefreshSharedSectionButton(
|
||||
onTap: () {
|
||||
context.read<SharedSectionBloc>().add(
|
||||
const SharedSectionEvent.refresh(),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
+6
-98
@@ -1,26 +1,12 @@
|
||||
import 'package:appflowy/features/shared_section/data/repositories/rust_shared_pages_repository_impl.dart';
|
||||
import 'package:appflowy/features/shared_section/data/rust_share_pagers_repository.dart';
|
||||
import 'package:appflowy/features/shared_section/logic/shared_section_bloc.dart';
|
||||
import 'package:appflowy/features/shared_section/presentation/widgets/refresh_button.dart';
|
||||
import 'package:appflowy/features/shared_section/presentation/widgets/shared_page_list.dart';
|
||||
import 'package:appflowy/features/shared_section/presentation/widgets/shared_pages_list.dart';
|
||||
import 'package:appflowy/features/shared_section/presentation/widgets/shared_section_error.dart';
|
||||
import 'package:appflowy/features/shared_section/presentation/widgets/shared_section_header.dart';
|
||||
import 'package:appflowy/features/shared_section/presentation/widgets/shared_section_loading.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/view/view_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/view/view_ext.dart';
|
||||
import 'package:appflowy/workspace/application/view/view_service.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/shared_widget.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/view/view_action_type.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/dialog_v2.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
|
||||
import 'package:appflowy_ui/appflowy_ui.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/widget/spacing.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
class SharedSection extends StatelessWidget {
|
||||
@@ -33,15 +19,12 @@ class SharedSection extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = AppFlowyTheme.of(context);
|
||||
final repository = RustSharePagesRepositoryImpl();
|
||||
|
||||
return BlocProvider(
|
||||
create: (_) => SharedSectionBloc(
|
||||
workspaceId: workspaceId,
|
||||
repository: repository,
|
||||
repository: RustSharePagesRepository(),
|
||||
enablePolling: true,
|
||||
)..add(const SharedSectionInitEvent()),
|
||||
)..add(const SharedSectionEvent.init()),
|
||||
child: BlocBuilder<SharedSectionBloc, SharedSectionState>(
|
||||
builder: (context, state) {
|
||||
if (state.isLoading) {
|
||||
@@ -61,83 +44,10 @@ class SharedSection extends StatelessWidget {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Shared header
|
||||
SharedSectionHeader(
|
||||
onTap: () {
|
||||
// expand or collapse the shared section
|
||||
context.read<SharedSectionBloc>().add(
|
||||
const SharedSectionToggleExpandedEvent(),
|
||||
);
|
||||
},
|
||||
),
|
||||
const SharedSectionHeader(),
|
||||
|
||||
// Shared pages list
|
||||
if (state.isExpanded)
|
||||
SharedPageList(
|
||||
sharedPages: state.sharedPages,
|
||||
onSetEditing: (context, value) {
|
||||
context.read<ViewBloc>().add(ViewEvent.setIsEditing(value));
|
||||
},
|
||||
onAction: (action, view, data) async {
|
||||
switch (action) {
|
||||
case ViewMoreActionType.favorite:
|
||||
case ViewMoreActionType.unFavorite:
|
||||
context
|
||||
.read<FavoriteBloc>()
|
||||
.add(FavoriteEvent.toggle(view));
|
||||
break;
|
||||
case ViewMoreActionType.openInNewTab:
|
||||
context.read<TabsBloc>().openTab(view);
|
||||
break;
|
||||
case ViewMoreActionType.rename:
|
||||
await showAFTextFieldDialog(
|
||||
context: context,
|
||||
title: LocaleKeys.disclosureAction_rename.tr(),
|
||||
initialValue: view.nameOrDefault,
|
||||
maxLength: 256,
|
||||
onConfirm: (newValue) {
|
||||
// can not use bloc here because it has been disposed.
|
||||
ViewBackendService.updateView(
|
||||
viewId: view.id,
|
||||
name: newValue,
|
||||
);
|
||||
},
|
||||
);
|
||||
break;
|
||||
case ViewMoreActionType.leaveSharedPage:
|
||||
// show a dialog to confirm the action
|
||||
await showConfirmDialog(
|
||||
context: context,
|
||||
title: 'Remove your own access',
|
||||
titleStyle: theme.textStyle.body.standard(
|
||||
color: theme.textColorScheme.primary,
|
||||
),
|
||||
description: '',
|
||||
style: ConfirmPopupStyle.cancelAndOk,
|
||||
confirmLabel: 'Remove',
|
||||
onConfirm: (_) {
|
||||
context.read<SharedSectionBloc>().add(
|
||||
SharedSectionEvent.leaveSharedPage(
|
||||
pageId: view.id,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
break;
|
||||
default:
|
||||
// Other actions are not allowed for read-only access
|
||||
break;
|
||||
}
|
||||
},
|
||||
onSelected: (context, view) {
|
||||
if (HardwareKeyboard.instance.isControlPressed) {
|
||||
context.read<TabsBloc>().openTab(view);
|
||||
}
|
||||
context.read<TabsBloc>().openPlugin(view);
|
||||
},
|
||||
onTertiarySelected: (context, view) {
|
||||
context.read<TabsBloc>().openTab(view);
|
||||
},
|
||||
),
|
||||
SharedPagesList(sharedPages: state.sharedPages),
|
||||
|
||||
// Refresh button, for debugging only
|
||||
if (kDebugMode)
|
||||
@@ -148,8 +58,6 @@ class SharedSection extends StatelessWidget {
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
const VSpace(16.0),
|
||||
],
|
||||
);
|
||||
},
|
||||
|
||||
-38
@@ -1,38 +0,0 @@
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/home_sizes.dart';
|
||||
import 'package:appflowy_ui/appflowy_ui.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/text.dart';
|
||||
import 'package:flowy_infra_ui/widget/spacing.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class MSharedSectionHeader extends StatelessWidget {
|
||||
const MSharedSectionHeader({
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = AppFlowyTheme.of(context);
|
||||
return SizedBox(
|
||||
height: 48,
|
||||
child: Row(
|
||||
children: [
|
||||
const HSpace(HomeSpaceViewSizes.mHorizontalPadding),
|
||||
FlowySvg(
|
||||
FlowySvgs.shared_with_me_m,
|
||||
color: theme.badgeColorScheme.color13Thick2,
|
||||
),
|
||||
const HSpace(10.0),
|
||||
FlowyText.medium(
|
||||
LocaleKeys.shareSection_shared.tr(),
|
||||
lineHeight: 1.15,
|
||||
fontSize: 16.0,
|
||||
),
|
||||
const HSpace(HomeSpaceViewSizes.mHorizontalPadding),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
+3
-1
@@ -1,5 +1,7 @@
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy_ui/appflowy_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
|
||||
class RefreshSharedSectionButton extends StatelessWidget {
|
||||
const RefreshSharedSectionButton({
|
||||
@@ -18,7 +20,7 @@ class RefreshSharedSectionButton extends StatelessWidget {
|
||||
size: 20,
|
||||
color: theme.iconColorScheme.secondary,
|
||||
),
|
||||
title: 'Refresh',
|
||||
title: LocaleKeys.shareSection_refresh.tr(),
|
||||
onTap: onTap,
|
||||
);
|
||||
}
|
||||
|
||||
-158
@@ -1,158 +0,0 @@
|
||||
import 'package:appflowy/features/share_tab/data/models/share_access_level.dart';
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/view/view_action_type.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
import 'package:appflowy_ui/appflowy_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
typedef SharedPageActionsButtonCallback = void Function(
|
||||
ViewMoreActionType type,
|
||||
ViewPB view,
|
||||
dynamic data,
|
||||
);
|
||||
|
||||
typedef SharedPageActionsButtonSetEditingCallback = void Function(
|
||||
BuildContext context,
|
||||
bool value,
|
||||
);
|
||||
|
||||
class SharedPageActionsButton extends StatefulWidget {
|
||||
const SharedPageActionsButton({
|
||||
super.key,
|
||||
required this.view,
|
||||
required this.accessLevel,
|
||||
required this.onAction,
|
||||
required this.buildChild,
|
||||
required this.onSetEditing,
|
||||
this.showAtCursor = false,
|
||||
});
|
||||
|
||||
final ViewPB view;
|
||||
final ShareAccessLevel accessLevel;
|
||||
final SharedPageActionsButtonCallback onAction;
|
||||
final SharedPageActionsButtonSetEditingCallback onSetEditing;
|
||||
final bool showAtCursor;
|
||||
final Widget Function(AFPopoverController) buildChild;
|
||||
|
||||
@override
|
||||
State<SharedPageActionsButton> createState() =>
|
||||
_SharedPageActionsButtonState();
|
||||
}
|
||||
|
||||
class _SharedPageActionsButtonState extends State<SharedPageActionsButton> {
|
||||
AFPopoverController controller = AFPopoverController();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
controller.addListener(() {
|
||||
if (!controller.isOpen) {
|
||||
widget.onSetEditing(context, false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
controller.dispose();
|
||||
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AFPopover(
|
||||
controller: controller,
|
||||
padding: EdgeInsets.zero,
|
||||
decoration: BoxDecoration(), // the AFMenu has a border
|
||||
anchor: const AFAnchorAuto(
|
||||
offset: Offset(98, 8),
|
||||
),
|
||||
popover: (context) => AFMenu(
|
||||
width: 240,
|
||||
backgroundColor:
|
||||
Theme.of(context).cardColor, // to compatible with the old design
|
||||
children: _buildMenuItems(context),
|
||||
),
|
||||
child: widget.buildChild(controller),
|
||||
);
|
||||
}
|
||||
|
||||
List<Widget> _buildMenuItems(BuildContext context) {
|
||||
final actionTypes = _buildActionTypes();
|
||||
final menuItems = <Widget>[];
|
||||
|
||||
for (final actionType in actionTypes) {
|
||||
if (actionType == ViewMoreActionType.divider) {
|
||||
if (menuItems.isNotEmpty) {
|
||||
menuItems.add(const AFDivider(spacing: 4));
|
||||
}
|
||||
} else {
|
||||
menuItems.add(
|
||||
AFTextMenuItem(
|
||||
leading: FlowySvg(
|
||||
actionType.leftIconSvg,
|
||||
size: const Size.square(16),
|
||||
color: actionType == ViewMoreActionType.delete
|
||||
? Theme.of(context).colorScheme.error
|
||||
: null,
|
||||
),
|
||||
title: actionType.name,
|
||||
titleColor: actionType == ViewMoreActionType.delete
|
||||
? Theme.of(context).colorScheme.error
|
||||
: null,
|
||||
trailing: actionType.rightIcon,
|
||||
onTap: () {
|
||||
widget.onAction(actionType, widget.view, null);
|
||||
controller.hide();
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return menuItems;
|
||||
}
|
||||
|
||||
List<ViewMoreActionType> _buildActionTypes() {
|
||||
final List<ViewMoreActionType> actionTypes = [];
|
||||
|
||||
// Always allow add to favorites and open in new tab
|
||||
actionTypes.add(
|
||||
widget.view.isFavorite
|
||||
? ViewMoreActionType.unFavorite
|
||||
: ViewMoreActionType.favorite,
|
||||
);
|
||||
|
||||
actionTypes.add(
|
||||
ViewMoreActionType.leaveSharedPage,
|
||||
);
|
||||
|
||||
// Only show editable actions if access level allows it
|
||||
if (widget.accessLevel != ShareAccessLevel.readOnly) {
|
||||
actionTypes.addAll([
|
||||
ViewMoreActionType.divider,
|
||||
ViewMoreActionType.rename,
|
||||
]);
|
||||
|
||||
// Chat doesn't change icon and duplicate
|
||||
if (widget.view.layout != ViewLayoutPB.Chat) {
|
||||
actionTypes.addAll([
|
||||
ViewMoreActionType.changeIcon,
|
||||
]);
|
||||
}
|
||||
|
||||
if (widget.accessLevel == ShareAccessLevel.fullAccess) {
|
||||
actionTypes.addAll([
|
||||
ViewMoreActionType.delete,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
actionTypes.add(ViewMoreActionType.divider);
|
||||
actionTypes.add(ViewMoreActionType.openInNewTab);
|
||||
|
||||
return actionTypes;
|
||||
}
|
||||
}
|
||||
-82
@@ -1,82 +0,0 @@
|
||||
import 'package:appflowy/features/share_tab/data/models/share_access_level.dart';
|
||||
import 'package:appflowy/features/shared_section/models/shared_page.dart';
|
||||
import 'package:appflowy/features/shared_section/presentation/widgets/shared_page_actions_button.dart';
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/home_sizes.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// Shared pages on desktop
|
||||
class SharedPageList extends StatelessWidget {
|
||||
const SharedPageList({
|
||||
super.key,
|
||||
required this.sharedPages,
|
||||
required this.onAction,
|
||||
required this.onSelected,
|
||||
required this.onTertiarySelected,
|
||||
required this.onSetEditing,
|
||||
});
|
||||
|
||||
final SharedPages sharedPages;
|
||||
final ViewItemOnSelected onSelected;
|
||||
final ViewItemOnSelected onTertiarySelected;
|
||||
final SharedPageActionsButtonCallback onAction;
|
||||
final SharedPageActionsButtonSetEditingCallback onSetEditing;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: sharedPages.map((sharedPage) {
|
||||
final view = sharedPage.view;
|
||||
final accessLevel = sharedPage.accessLevel;
|
||||
return ViewItem(
|
||||
key: ValueKey(view.id),
|
||||
spaceType: FolderSpaceType.public,
|
||||
isFirstChild: view.id == sharedPages.first.view.id,
|
||||
view: view,
|
||||
level: 0,
|
||||
isDraggable: false, // disable draggable for shared pages
|
||||
leftPadding: HomeSpaceViewSizes.leftPadding,
|
||||
isFeedback: false,
|
||||
onSelected: onSelected,
|
||||
onTertiarySelected: onTertiarySelected,
|
||||
rightIconsBuilder: (context, view) => [
|
||||
IntrinsicWidth(
|
||||
child: _buildSharedPageMoreActionButton(
|
||||
context,
|
||||
view,
|
||||
accessLevel,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 4.0),
|
||||
],
|
||||
);
|
||||
}).toList(),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSharedPageMoreActionButton(
|
||||
BuildContext context,
|
||||
ViewPB view,
|
||||
ShareAccessLevel accessLevel,
|
||||
) {
|
||||
return SharedPageActionsButton(
|
||||
view: view,
|
||||
accessLevel: accessLevel,
|
||||
onAction: onAction,
|
||||
onSetEditing: onSetEditing,
|
||||
buildChild: (controller) => FlowyIconButton(
|
||||
width: 24,
|
||||
icon: const FlowySvg(FlowySvgs.workspace_three_dots_s),
|
||||
onPressed: () {
|
||||
onSetEditing(context, true);
|
||||
controller.show();
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
+15
-8
@@ -1,19 +1,19 @@
|
||||
import 'package:appflowy/features/shared_section/models/shared_page.dart';
|
||||
import 'package:appflowy/mobile/presentation/page_item/mobile_view_item.dart';
|
||||
import 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/home_sizes.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
/// Shared pages on mobile
|
||||
class MSharedPageList extends StatelessWidget {
|
||||
const MSharedPageList({
|
||||
class SharedPagesList extends StatelessWidget {
|
||||
const SharedPagesList({
|
||||
super.key,
|
||||
required this.sharedPages,
|
||||
required this.onSelected,
|
||||
});
|
||||
|
||||
final SharedPages sharedPages;
|
||||
final ViewItemOnSelected onSelected;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -21,7 +21,7 @@ class MSharedPageList extends StatelessWidget {
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: sharedPages.map((sharedPage) {
|
||||
final view = sharedPage.view;
|
||||
return MobileViewItem(
|
||||
return ViewItem(
|
||||
key: ValueKey(view.id),
|
||||
spaceType: FolderSpaceType.public,
|
||||
isFirstChild: view.id == sharedPages.first.view.id,
|
||||
@@ -30,7 +30,14 @@ class MSharedPageList extends StatelessWidget {
|
||||
isDraggable: false, // disable draggable for shared pages
|
||||
leftPadding: HomeSpaceViewSizes.leftPadding,
|
||||
isFeedback: false,
|
||||
onSelected: onSelected,
|
||||
onSelected: (context, view) {
|
||||
if (HardwareKeyboard.instance.isControlPressed) {
|
||||
context.read<TabsBloc>().openTab(view);
|
||||
}
|
||||
context.read<TabsBloc>().openPlugin(view);
|
||||
},
|
||||
onTertiarySelected: (context, view) =>
|
||||
context.read<TabsBloc>().openTab(view),
|
||||
);
|
||||
}).toList(),
|
||||
);
|
||||
-41
@@ -1,41 +0,0 @@
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy_ui/appflowy_ui.dart';
|
||||
import 'package:flowy_infra_ui/widget/spacing.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class SharedSectionEmpty extends StatelessWidget {
|
||||
const SharedSectionEmpty({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = AppFlowyTheme.of(context);
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
FlowySvg(
|
||||
FlowySvgs.empty_shared_section_m,
|
||||
color: theme.iconColorScheme.tertiary,
|
||||
),
|
||||
const VSpace(12),
|
||||
Text(
|
||||
'Nothing shared with you',
|
||||
style: theme.textStyle.heading3.enhanced(
|
||||
color: theme.textColorScheme.secondary,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const VSpace(4),
|
||||
Text(
|
||||
'Pages shared with you will show here',
|
||||
style: theme.textStyle.heading4.standard(
|
||||
color: theme.textColorScheme.tertiary,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const VSpace(kBottomNavigationBarHeight + 60.0),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
+10
-20
@@ -1,34 +1,24 @@
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy_ui/appflowy_ui.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class SharedSectionHeader extends StatelessWidget {
|
||||
const SharedSectionHeader({
|
||||
super.key,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
final VoidCallback onTap;
|
||||
const SharedSectionHeader({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = AppFlowyTheme.of(context);
|
||||
return AFGhostIconTextButton.primary(
|
||||
text: LocaleKeys.shareSection_shared.tr(),
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
size: AFButtonSize.l,
|
||||
onTap: onTap,
|
||||
// todo: ask the designer to provide the token.
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: 4,
|
||||
vertical: 6,
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(
|
||||
left: theme.spacing.xs,
|
||||
bottom: theme.spacing.s,
|
||||
),
|
||||
borderRadius: theme.borderRadius.s,
|
||||
iconBuilder: (context, isHover, disabled) => FlowySvg(
|
||||
FlowySvgs.shared_with_me_m,
|
||||
color: theme.badgeColorScheme.color13Thick2,
|
||||
child: Text(
|
||||
LocaleKeys.shareSection_shared.tr(),
|
||||
style: theme.textStyle.caption.enhanced(
|
||||
color: theme.textColorScheme.tertiary,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -109,18 +109,3 @@ extension ShareRoleExtension on ShareRole {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension SharedSectionTypeExtension on folder.SharedViewSectionPB {
|
||||
SharedSectionType get sharedSectionType {
|
||||
switch (this) {
|
||||
case folder.SharedViewSectionPB.PublicSection:
|
||||
return SharedSectionType.public;
|
||||
case folder.SharedViewSectionPB.PrivateSection:
|
||||
return SharedSectionType.private;
|
||||
case folder.SharedViewSectionPB.SharedSection:
|
||||
return SharedSectionType.shared;
|
||||
default:
|
||||
throw Exception('Unknown shared section type: $this');
|
||||
}
|
||||
}
|
||||
}
|
||||
Whitespace-only changes.
@@ -1 +0,0 @@
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
|
||||
-87
@@ -1,87 +0,0 @@
|
||||
import 'package:appflowy/features/workspace/data/repositories/workspace_repository.dart';
|
||||
import 'package:appflowy/user/application/user_service.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/widgets/setting_appflowy_cloud.dart'
|
||||
as billing_service;
|
||||
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
|
||||
import 'package:appflowy_result/appflowy_result.dart';
|
||||
import 'package:fixnum/fixnum.dart';
|
||||
|
||||
/// Implementation of WorkspaceRepository using UserBackendService.
|
||||
class RustWorkspaceRepositoryImpl implements WorkspaceRepository {
|
||||
RustWorkspaceRepositoryImpl({
|
||||
required Int64 userId,
|
||||
}) : _userService = UserBackendService(userId: userId);
|
||||
|
||||
final UserBackendService _userService;
|
||||
|
||||
@override
|
||||
Future<FlowyResult<WorkspacePB, FlowyError>> getCurrentWorkspace() async {
|
||||
return UserBackendService.getCurrentWorkspace();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<FlowyResult<List<UserWorkspacePB>, FlowyError>> getWorkspaces() async {
|
||||
return _userService.getWorkspaces();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<FlowyResult<UserWorkspacePB, FlowyError>> createWorkspace({
|
||||
required String name,
|
||||
required WorkspaceTypePB workspaceType,
|
||||
}) async {
|
||||
return _userService.createUserWorkspace(name, workspaceType);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<FlowyResult<void, FlowyError>> deleteWorkspace({
|
||||
required String workspaceId,
|
||||
}) async {
|
||||
return _userService.deleteWorkspaceById(workspaceId);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<FlowyResult<void, FlowyError>> openWorkspace({
|
||||
required String workspaceId,
|
||||
required WorkspaceTypePB workspaceType,
|
||||
}) async {
|
||||
return _userService.openWorkspace(workspaceId, workspaceType);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<FlowyResult<void, FlowyError>> renameWorkspace({
|
||||
required String workspaceId,
|
||||
required String name,
|
||||
}) async {
|
||||
return _userService.renameWorkspace(workspaceId, name);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<FlowyResult<void, FlowyError>> updateWorkspaceIcon({
|
||||
required String workspaceId,
|
||||
required String icon,
|
||||
}) async {
|
||||
return _userService.updateWorkspaceIcon(workspaceId, icon);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<FlowyResult<void, FlowyError>> leaveWorkspace({
|
||||
required String workspaceId,
|
||||
}) async {
|
||||
return _userService.leaveWorkspace(workspaceId);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<FlowyResult<WorkspaceSubscriptionInfoPB, FlowyError>>
|
||||
getWorkspaceSubscriptionInfo({
|
||||
required String workspaceId,
|
||||
}) async {
|
||||
return UserBackendService.getWorkspaceSubscriptionInfo(workspaceId);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> isBillingEnabled() async {
|
||||
return billing_service.isBillingEnabled();
|
||||
}
|
||||
}
|
||||
-59
@@ -1,59 +0,0 @@
|
||||
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
|
||||
import 'package:appflowy_result/appflowy_result.dart';
|
||||
|
||||
/// Abstract repository for workspace operations.
|
||||
///
|
||||
/// This abstracts the data source for workspace operations,
|
||||
/// allowing for different implementations (e.g., REST API, gRPC, local storage).
|
||||
abstract class WorkspaceRepository {
|
||||
/// Gets the current workspace for the user.
|
||||
Future<FlowyResult<WorkspacePB, FlowyError>> getCurrentWorkspace();
|
||||
|
||||
/// Gets the list of workspaces for the current user.
|
||||
Future<FlowyResult<List<UserWorkspacePB>, FlowyError>> getWorkspaces();
|
||||
|
||||
/// Creates a new workspace.
|
||||
Future<FlowyResult<UserWorkspacePB, FlowyError>> createWorkspace({
|
||||
required String name,
|
||||
required WorkspaceTypePB workspaceType,
|
||||
});
|
||||
|
||||
/// Deletes a workspace by ID.
|
||||
Future<FlowyResult<void, FlowyError>> deleteWorkspace({
|
||||
required String workspaceId,
|
||||
});
|
||||
|
||||
/// Opens a workspace.
|
||||
Future<FlowyResult<void, FlowyError>> openWorkspace({
|
||||
required String workspaceId,
|
||||
required WorkspaceTypePB workspaceType,
|
||||
});
|
||||
|
||||
/// Renames a workspace.
|
||||
Future<FlowyResult<void, FlowyError>> renameWorkspace({
|
||||
required String workspaceId,
|
||||
required String name,
|
||||
});
|
||||
|
||||
/// Updates workspace icon.
|
||||
Future<FlowyResult<void, FlowyError>> updateWorkspaceIcon({
|
||||
required String workspaceId,
|
||||
required String icon,
|
||||
});
|
||||
|
||||
/// Leaves a workspace.
|
||||
Future<FlowyResult<void, FlowyError>> leaveWorkspace({
|
||||
required String workspaceId,
|
||||
});
|
||||
|
||||
/// Gets workspace subscription information.
|
||||
Future<FlowyResult<WorkspaceSubscriptionInfoPB, FlowyError>>
|
||||
getWorkspaceSubscriptionInfo({
|
||||
required String workspaceId,
|
||||
});
|
||||
|
||||
/// Is billing enabled.
|
||||
Future<bool> isBillingEnabled();
|
||||
}
|
||||
@@ -1,692 +0,0 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:appflowy/features/workspace/data/repositories/workspace_repository.dart';
|
||||
import 'package:appflowy/features/workspace/logic/workspace_event.dart';
|
||||
import 'package:appflowy/features/workspace/logic/workspace_state.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/shared/feature_flags.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy/user/application/reminder/reminder_bloc.dart';
|
||||
import 'package:appflowy/user/application/user_listener.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-error/code.pbenum.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
|
||||
import 'package:appflowy_result/appflowy_result.dart';
|
||||
import 'package:bloc/bloc.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:protobuf/protobuf.dart';
|
||||
|
||||
export 'workspace_event.dart';
|
||||
export 'workspace_state.dart';
|
||||
|
||||
class _WorkspaceFetchResult {
|
||||
const _WorkspaceFetchResult({
|
||||
this.currentWorkspace,
|
||||
required this.workspaces,
|
||||
required this.shouldOpenWorkspace,
|
||||
});
|
||||
|
||||
final UserWorkspacePB? currentWorkspace;
|
||||
final List<UserWorkspacePB> workspaces;
|
||||
final bool shouldOpenWorkspace;
|
||||
}
|
||||
|
||||
class UserWorkspaceBloc extends Bloc<UserWorkspaceEvent, UserWorkspaceState> {
|
||||
UserWorkspaceBloc({
|
||||
required this.repository,
|
||||
required this.userProfile,
|
||||
this.initialWorkspaceId,
|
||||
}) : _listener = UserListener(userProfile: userProfile),
|
||||
super(UserWorkspaceState.initial(userProfile)) {
|
||||
on<WorkspaceEventInitialize>(_onInitialize);
|
||||
on<WorkspaceEventFetchWorkspaces>(_onFetchWorkspaces);
|
||||
on<WorkspaceEventCreateWorkspace>(_onCreateWorkspace);
|
||||
on<WorkspaceEventDeleteWorkspace>(_onDeleteWorkspace);
|
||||
on<WorkspaceEventOpenWorkspace>(_onOpenWorkspace);
|
||||
on<WorkspaceEventRenameWorkspace>(_onRenameWorkspace);
|
||||
on<WorkspaceEventUpdateWorkspaceIcon>(_onUpdateWorkspaceIcon);
|
||||
on<WorkspaceEventLeaveWorkspace>(_onLeaveWorkspace);
|
||||
on<WorkspaceEventFetchWorkspaceSubscriptionInfo>(
|
||||
_onFetchWorkspaceSubscriptionInfo,
|
||||
);
|
||||
on<WorkspaceEventUpdateWorkspaceSubscriptionInfo>(
|
||||
_onUpdateWorkspaceSubscriptionInfo,
|
||||
);
|
||||
on<WorkspaceEventEmitWorkspaces>(_onEmitWorkspaces);
|
||||
on<WorkspaceEventEmitUserProfile>(_onEmitUserProfile);
|
||||
on<WorkspaceEventEmitCurrentWorkspace>(_onEmitCurrentWorkspace);
|
||||
}
|
||||
|
||||
final String? initialWorkspaceId;
|
||||
final WorkspaceRepository repository;
|
||||
final UserProfilePB userProfile;
|
||||
final UserListener _listener;
|
||||
|
||||
@override
|
||||
Future<void> close() {
|
||||
_listener.stop();
|
||||
return super.close();
|
||||
}
|
||||
|
||||
Future<void> _onInitialize(
|
||||
WorkspaceEventInitialize event,
|
||||
Emitter<UserWorkspaceState> emit,
|
||||
) async {
|
||||
await _setupListeners();
|
||||
await _initializeWorkspaces(emit);
|
||||
}
|
||||
|
||||
Future<void> _onFetchWorkspaces(
|
||||
WorkspaceEventFetchWorkspaces event,
|
||||
Emitter<UserWorkspaceState> emit,
|
||||
) async {
|
||||
final result = await _fetchWorkspaces(
|
||||
initialWorkspaceId: event.initialWorkspaceId,
|
||||
);
|
||||
|
||||
final currentWorkspace = result.currentWorkspace;
|
||||
final workspaces = result.workspaces;
|
||||
Log.info(
|
||||
'fetch workspaces: current workspace: ${currentWorkspace?.workspaceId}, workspaces: ${workspaces.map((e) => e.workspaceId)}',
|
||||
);
|
||||
|
||||
emit(
|
||||
state.copyWith(
|
||||
workspaces: workspaces,
|
||||
),
|
||||
);
|
||||
|
||||
if (currentWorkspace != null &&
|
||||
currentWorkspace.workspaceId != state.currentWorkspace?.workspaceId) {
|
||||
Log.info(
|
||||
'fetch workspaces: try to open workspace: ${currentWorkspace.workspaceId}',
|
||||
);
|
||||
add(
|
||||
UserWorkspaceEvent.openWorkspace(
|
||||
workspaceId: currentWorkspace.workspaceId,
|
||||
workspaceType: currentWorkspace.workspaceType,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onCreateWorkspace(
|
||||
WorkspaceEventCreateWorkspace event,
|
||||
Emitter<UserWorkspaceState> emit,
|
||||
) async {
|
||||
emit(
|
||||
state.copyWith(
|
||||
actionResult: const WorkspaceActionResult(
|
||||
actionType: WorkspaceActionType.create,
|
||||
isLoading: true,
|
||||
result: null,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
final result = await repository.createWorkspace(
|
||||
name: event.name,
|
||||
workspaceType: event.workspaceType,
|
||||
);
|
||||
|
||||
final workspaces = result.fold(
|
||||
(s) => [...state.workspaces, s],
|
||||
(e) => state.workspaces,
|
||||
);
|
||||
|
||||
emit(
|
||||
state.copyWith(
|
||||
workspaces: _sortWorkspaces(workspaces),
|
||||
actionResult: WorkspaceActionResult(
|
||||
actionType: WorkspaceActionType.create,
|
||||
isLoading: false,
|
||||
result: result.map((_) {}),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
result
|
||||
..onSuccess((s) {
|
||||
Log.info('create workspace success: $s');
|
||||
add(
|
||||
UserWorkspaceEvent.openWorkspace(
|
||||
workspaceId: s.workspaceId,
|
||||
workspaceType: s.workspaceType,
|
||||
),
|
||||
);
|
||||
})
|
||||
..onFailure((f) {
|
||||
Log.error('create workspace error: $f');
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _onDeleteWorkspace(
|
||||
WorkspaceEventDeleteWorkspace event,
|
||||
Emitter<UserWorkspaceState> emit,
|
||||
) async {
|
||||
Log.info('try to delete workspace: ${event.workspaceId}');
|
||||
emit(
|
||||
state.copyWith(
|
||||
actionResult: const WorkspaceActionResult(
|
||||
actionType: WorkspaceActionType.delete,
|
||||
isLoading: true,
|
||||
result: null,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
final remoteWorkspaces = await _fetchWorkspaces().then(
|
||||
(value) => value.workspaces,
|
||||
);
|
||||
|
||||
if (state.workspaces.length <= 1 || remoteWorkspaces.length <= 1) {
|
||||
final result = FlowyResult.failure(
|
||||
FlowyError(
|
||||
code: ErrorCode.Internal,
|
||||
msg: LocaleKeys.workspace_cannotDeleteTheOnlyWorkspace.tr(),
|
||||
),
|
||||
);
|
||||
return emit(
|
||||
state.copyWith(
|
||||
actionResult: WorkspaceActionResult(
|
||||
actionType: WorkspaceActionType.delete,
|
||||
result: result,
|
||||
isLoading: false,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final result = await repository.deleteWorkspace(
|
||||
workspaceId: event.workspaceId,
|
||||
);
|
||||
final workspacesResult = await _fetchWorkspaces();
|
||||
final workspaces = workspacesResult.workspaces;
|
||||
final containsDeletedWorkspace =
|
||||
_findWorkspaceById(event.workspaceId, workspaces) != null;
|
||||
|
||||
result
|
||||
..onSuccess((_) {
|
||||
Log.info('delete workspace success: ${event.workspaceId}');
|
||||
final firstWorkspace = workspaces.firstOrNull;
|
||||
assert(
|
||||
firstWorkspace != null,
|
||||
'the first workspace must not be null',
|
||||
);
|
||||
if (state.currentWorkspace?.workspaceId == event.workspaceId &&
|
||||
firstWorkspace != null) {
|
||||
Log.info(
|
||||
'delete workspace: open the first workspace: ${firstWorkspace.workspaceId}',
|
||||
);
|
||||
add(
|
||||
UserWorkspaceEvent.openWorkspace(
|
||||
workspaceId: firstWorkspace.workspaceId,
|
||||
workspaceType: firstWorkspace.workspaceType,
|
||||
),
|
||||
);
|
||||
}
|
||||
})
|
||||
..onFailure((f) {
|
||||
Log.error('delete workspace error: $f');
|
||||
if (!containsDeletedWorkspace && workspaces.isNotEmpty) {
|
||||
add(
|
||||
UserWorkspaceEvent.openWorkspace(
|
||||
workspaceId: workspaces.first.workspaceId,
|
||||
workspaceType: workspaces.first.workspaceType,
|
||||
),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
emit(
|
||||
state.copyWith(
|
||||
workspaces: workspaces,
|
||||
actionResult: WorkspaceActionResult(
|
||||
actionType: WorkspaceActionType.delete,
|
||||
result: result,
|
||||
isLoading: false,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _onOpenWorkspace(
|
||||
WorkspaceEventOpenWorkspace event,
|
||||
Emitter<UserWorkspaceState> emit,
|
||||
) async {
|
||||
emit(
|
||||
state.copyWith(
|
||||
actionResult: const WorkspaceActionResult(
|
||||
actionType: WorkspaceActionType.open,
|
||||
isLoading: true,
|
||||
result: null,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
final result = await repository.openWorkspace(
|
||||
workspaceId: event.workspaceId,
|
||||
workspaceType: event.workspaceType,
|
||||
);
|
||||
|
||||
final currentWorkspace = result.fold(
|
||||
(s) => _findWorkspaceById(event.workspaceId),
|
||||
(e) => state.currentWorkspace,
|
||||
);
|
||||
|
||||
result
|
||||
..onSuccess((s) {
|
||||
add(
|
||||
UserWorkspaceEvent.fetchWorkspaceSubscriptionInfo(
|
||||
workspaceId: event.workspaceId,
|
||||
),
|
||||
);
|
||||
|
||||
Log.info(
|
||||
'open workspace success: ${event.workspaceId}, current workspace: ${currentWorkspace?.toProto3Json()}',
|
||||
);
|
||||
})
|
||||
..onFailure((f) {
|
||||
Log.error('open workspace error: $f');
|
||||
});
|
||||
|
||||
emit(
|
||||
state.copyWith(
|
||||
currentWorkspace: currentWorkspace,
|
||||
actionResult: WorkspaceActionResult(
|
||||
actionType: WorkspaceActionType.open,
|
||||
isLoading: false,
|
||||
result: result,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
getIt<ReminderBloc>().add(
|
||||
ReminderEvent.started(),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _onRenameWorkspace(
|
||||
WorkspaceEventRenameWorkspace event,
|
||||
Emitter<UserWorkspaceState> emit,
|
||||
) async {
|
||||
final result = await repository.renameWorkspace(
|
||||
workspaceId: event.workspaceId,
|
||||
name: event.name,
|
||||
);
|
||||
|
||||
final workspaces = result.fold(
|
||||
(s) => _updateWorkspaceInList(event.workspaceId, (workspace) {
|
||||
workspace.freeze();
|
||||
return workspace.rebuild((p0) {
|
||||
p0.name = event.name;
|
||||
});
|
||||
}),
|
||||
(f) => state.workspaces,
|
||||
);
|
||||
|
||||
final currentWorkspace = _findWorkspaceById(
|
||||
state.currentWorkspace?.workspaceId ?? '',
|
||||
workspaces,
|
||||
);
|
||||
|
||||
Log.info('rename workspace: ${event.workspaceId}, name: ${event.name}');
|
||||
|
||||
result.onFailure((f) {
|
||||
Log.error('rename workspace error: $f');
|
||||
});
|
||||
|
||||
emit(
|
||||
state.copyWith(
|
||||
workspaces: workspaces,
|
||||
currentWorkspace: currentWorkspace,
|
||||
actionResult: WorkspaceActionResult(
|
||||
actionType: WorkspaceActionType.rename,
|
||||
isLoading: false,
|
||||
result: result,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _onUpdateWorkspaceIcon(
|
||||
WorkspaceEventUpdateWorkspaceIcon event,
|
||||
Emitter<UserWorkspaceState> emit,
|
||||
) async {
|
||||
final workspace = _findWorkspaceById(event.workspaceId);
|
||||
if (workspace == null) {
|
||||
Log.error('workspace not found: ${event.workspaceId}');
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.icon == workspace.icon) {
|
||||
Log.info('ignore same icon update');
|
||||
return;
|
||||
}
|
||||
|
||||
final result = await repository.updateWorkspaceIcon(
|
||||
workspaceId: event.workspaceId,
|
||||
icon: event.icon,
|
||||
);
|
||||
|
||||
final workspaces = result.fold(
|
||||
(s) => _updateWorkspaceInList(event.workspaceId, (workspace) {
|
||||
workspace.freeze();
|
||||
return workspace.rebuild((p0) {
|
||||
p0.icon = event.icon;
|
||||
});
|
||||
}),
|
||||
(f) => state.workspaces,
|
||||
);
|
||||
|
||||
final currentWorkspace = _findWorkspaceById(
|
||||
state.currentWorkspace?.workspaceId ?? '',
|
||||
workspaces,
|
||||
);
|
||||
|
||||
Log.info(
|
||||
'update workspace icon: ${event.workspaceId}, icon: ${event.icon}',
|
||||
);
|
||||
|
||||
result.onFailure((f) {
|
||||
Log.error('update workspace icon error: $f');
|
||||
});
|
||||
|
||||
emit(
|
||||
state.copyWith(
|
||||
workspaces: workspaces,
|
||||
currentWorkspace: currentWorkspace,
|
||||
actionResult: WorkspaceActionResult(
|
||||
actionType: WorkspaceActionType.updateIcon,
|
||||
isLoading: false,
|
||||
result: result,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _onLeaveWorkspace(
|
||||
WorkspaceEventLeaveWorkspace event,
|
||||
Emitter<UserWorkspaceState> emit,
|
||||
) async {
|
||||
final result = await repository.leaveWorkspace(
|
||||
workspaceId: event.workspaceId,
|
||||
);
|
||||
|
||||
final workspaces = result.fold(
|
||||
(s) => state.workspaces
|
||||
.where((e) => e.workspaceId != event.workspaceId)
|
||||
.toList(),
|
||||
(e) => state.workspaces,
|
||||
);
|
||||
|
||||
result
|
||||
..onSuccess((_) {
|
||||
Log.info('leave workspace success: ${event.workspaceId}');
|
||||
if (state.currentWorkspace?.workspaceId == event.workspaceId &&
|
||||
workspaces.isNotEmpty) {
|
||||
add(
|
||||
UserWorkspaceEvent.openWorkspace(
|
||||
workspaceId: workspaces.first.workspaceId,
|
||||
workspaceType: workspaces.first.workspaceType,
|
||||
),
|
||||
);
|
||||
}
|
||||
})
|
||||
..onFailure((f) {
|
||||
Log.error('leave workspace error: $f');
|
||||
});
|
||||
|
||||
emit(
|
||||
state.copyWith(
|
||||
workspaces: _sortWorkspaces(workspaces),
|
||||
actionResult: WorkspaceActionResult(
|
||||
actionType: WorkspaceActionType.leave,
|
||||
isLoading: false,
|
||||
result: result,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _onFetchWorkspaceSubscriptionInfo(
|
||||
WorkspaceEventFetchWorkspaceSubscriptionInfo event,
|
||||
Emitter<UserWorkspaceState> emit,
|
||||
) async {
|
||||
final enabled = await repository.isBillingEnabled();
|
||||
// If billing is not enabled, we don't need to fetch the workspace subscription info
|
||||
if (!enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
unawaited(
|
||||
repository
|
||||
.getWorkspaceSubscriptionInfo(
|
||||
workspaceId: event.workspaceId,
|
||||
)
|
||||
.fold(
|
||||
(workspaceSubscriptionInfo) {
|
||||
if (isClosed) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (state.currentWorkspace?.workspaceId != event.workspaceId) {
|
||||
return;
|
||||
}
|
||||
|
||||
Log.debug(
|
||||
'fetch workspace subscription info: ${event.workspaceId}, $workspaceSubscriptionInfo',
|
||||
);
|
||||
|
||||
add(
|
||||
UserWorkspaceEvent.updateWorkspaceSubscriptionInfo(
|
||||
workspaceId: event.workspaceId,
|
||||
subscriptionInfo: workspaceSubscriptionInfo,
|
||||
),
|
||||
);
|
||||
},
|
||||
(e) => Log.error('fetch workspace subscription info error: $e'),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _onUpdateWorkspaceSubscriptionInfo(
|
||||
WorkspaceEventUpdateWorkspaceSubscriptionInfo event,
|
||||
Emitter<UserWorkspaceState> emit,
|
||||
) async {
|
||||
emit(
|
||||
state.copyWith(workspaceSubscriptionInfo: event.subscriptionInfo),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _onEmitWorkspaces(
|
||||
WorkspaceEventEmitWorkspaces event,
|
||||
Emitter<UserWorkspaceState> emit,
|
||||
) async {
|
||||
emit(
|
||||
state.copyWith(
|
||||
workspaces: _sortWorkspaces(event.workspaces),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _onEmitUserProfile(
|
||||
WorkspaceEventEmitUserProfile event,
|
||||
Emitter<UserWorkspaceState> emit,
|
||||
) async {
|
||||
emit(
|
||||
state.copyWith(userProfile: event.userProfile),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _onEmitCurrentWorkspace(
|
||||
WorkspaceEventEmitCurrentWorkspace event,
|
||||
Emitter<UserWorkspaceState> emit,
|
||||
) async {
|
||||
emit(
|
||||
state.copyWith(currentWorkspace: event.workspace),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _setupListeners() async {
|
||||
_listener.start(
|
||||
onProfileUpdated: (result) {
|
||||
if (!isClosed) {
|
||||
result.fold(
|
||||
(newProfile) => add(
|
||||
UserWorkspaceEvent.emitUserProfile(userProfile: newProfile),
|
||||
),
|
||||
(error) => Log.error("Failed to get user profile: $error"),
|
||||
);
|
||||
}
|
||||
},
|
||||
onUserWorkspaceListUpdated: (workspaces) {
|
||||
if (!isClosed) {
|
||||
add(
|
||||
UserWorkspaceEvent.emitWorkspaces(
|
||||
workspaces: _sortWorkspaces(workspaces.items),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
onUserWorkspaceUpdated: (workspace) {
|
||||
if (!isClosed) {
|
||||
if (state.currentWorkspace?.workspaceId == workspace.workspaceId) {
|
||||
add(UserWorkspaceEvent.emitCurrentWorkspace(workspace: workspace));
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _initializeWorkspaces(Emitter<UserWorkspaceState> emit) async {
|
||||
final result = await _fetchWorkspaces(
|
||||
initialWorkspaceId: initialWorkspaceId,
|
||||
);
|
||||
final currentWorkspace = result.currentWorkspace;
|
||||
final workspaces = result.workspaces;
|
||||
final isCollabWorkspaceOn =
|
||||
state.userProfile.userAuthType == AuthTypePB.Server &&
|
||||
FeatureFlag.collaborativeWorkspace.isOn;
|
||||
|
||||
Log.info(
|
||||
'init workspace, current workspace: ${currentWorkspace?.workspaceId}, '
|
||||
'workspaces: ${workspaces.map((e) => e.workspaceId)}, isCollabWorkspaceOn: $isCollabWorkspaceOn',
|
||||
);
|
||||
|
||||
if (currentWorkspace != null) {
|
||||
add(
|
||||
UserWorkspaceEvent.fetchWorkspaceSubscriptionInfo(
|
||||
workspaceId: currentWorkspace.workspaceId,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (currentWorkspace != null && result.shouldOpenWorkspace == true) {
|
||||
Log.info('init open workspace: ${currentWorkspace.workspaceId}');
|
||||
await repository.openWorkspace(
|
||||
workspaceId: currentWorkspace.workspaceId,
|
||||
workspaceType: currentWorkspace.workspaceType,
|
||||
);
|
||||
}
|
||||
|
||||
emit(
|
||||
state.copyWith(
|
||||
currentWorkspace: currentWorkspace,
|
||||
workspaces: workspaces,
|
||||
isCollabWorkspaceOn: isCollabWorkspaceOn,
|
||||
actionResult: const WorkspaceActionResult(
|
||||
actionType: WorkspaceActionType.none,
|
||||
isLoading: false,
|
||||
result: null,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Helper methods
|
||||
List<UserWorkspacePB> _sortWorkspaces(List<UserWorkspacePB> workspaces) {
|
||||
final sorted = [...workspaces];
|
||||
sorted.sort(
|
||||
(a, b) => a.createdAtTimestamp.compareTo(b.createdAtTimestamp),
|
||||
);
|
||||
return sorted;
|
||||
}
|
||||
|
||||
UserWorkspacePB? _findWorkspaceById(
|
||||
String id, [
|
||||
List<UserWorkspacePB>? workspacesList,
|
||||
]) {
|
||||
final workspaces = workspacesList ?? state.workspaces;
|
||||
return workspaces.firstWhereOrNull((e) => e.workspaceId == id);
|
||||
}
|
||||
|
||||
List<UserWorkspacePB> _updateWorkspaceInList(
|
||||
String workspaceId,
|
||||
UserWorkspacePB Function(UserWorkspacePB workspace) updater,
|
||||
) {
|
||||
final workspaces = [...state.workspaces];
|
||||
final index = workspaces.indexWhere((e) => e.workspaceId == workspaceId);
|
||||
if (index != -1) {
|
||||
workspaces[index] = updater(workspaces[index]);
|
||||
}
|
||||
return workspaces;
|
||||
}
|
||||
|
||||
Future<_WorkspaceFetchResult> _fetchWorkspaces({
|
||||
String? initialWorkspaceId,
|
||||
}) async {
|
||||
try {
|
||||
final currentWorkspaceResult = await repository.getCurrentWorkspace();
|
||||
final currentWorkspace = currentWorkspaceResult.fold(
|
||||
(s) => s,
|
||||
(e) => null,
|
||||
);
|
||||
final currentWorkspaceId = initialWorkspaceId ?? currentWorkspace?.id;
|
||||
final workspacesResult = await repository.getWorkspaces();
|
||||
final workspaces = workspacesResult.getOrThrow();
|
||||
|
||||
if (workspaces.isEmpty && currentWorkspace != null) {
|
||||
workspaces.add(
|
||||
_convertWorkspacePBToUserWorkspace(currentWorkspace),
|
||||
);
|
||||
}
|
||||
|
||||
final currentWorkspaceInList = _findWorkspaceById(
|
||||
currentWorkspaceId ?? '',
|
||||
workspaces,
|
||||
) ??
|
||||
workspaces.firstOrNull;
|
||||
|
||||
final sortedWorkspaces = _sortWorkspaces(workspaces);
|
||||
|
||||
Log.info(
|
||||
'fetch workspaces: current workspace: ${currentWorkspaceInList?.workspaceId}, sorted workspaces: ${sortedWorkspaces.map((e) => '${e.name}: ${e.workspaceId}')}',
|
||||
);
|
||||
|
||||
return _WorkspaceFetchResult(
|
||||
currentWorkspace: currentWorkspaceInList,
|
||||
workspaces: sortedWorkspaces,
|
||||
shouldOpenWorkspace:
|
||||
currentWorkspaceInList?.workspaceId != currentWorkspaceId,
|
||||
);
|
||||
} catch (e) {
|
||||
Log.error('fetch workspace error: $e');
|
||||
return _WorkspaceFetchResult(
|
||||
currentWorkspace: state.currentWorkspace,
|
||||
workspaces: state.workspaces,
|
||||
shouldOpenWorkspace: false,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
UserWorkspacePB _convertWorkspacePBToUserWorkspace(WorkspacePB workspace) {
|
||||
return UserWorkspacePB.create()
|
||||
..workspaceId = workspace.id
|
||||
..name = workspace.name
|
||||
..createdAtTimestamp = workspace.createTime;
|
||||
}
|
||||
}
|
||||
@@ -1,202 +0,0 @@
|
||||
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
|
||||
|
||||
sealed class UserWorkspaceEvent {
|
||||
UserWorkspaceEvent();
|
||||
|
||||
// Factory functions for creating events
|
||||
factory UserWorkspaceEvent.initialize() => WorkspaceEventInitialize();
|
||||
|
||||
factory UserWorkspaceEvent.fetchWorkspaces({
|
||||
String? initialWorkspaceId,
|
||||
}) =>
|
||||
WorkspaceEventFetchWorkspaces(initialWorkspaceId: initialWorkspaceId);
|
||||
|
||||
factory UserWorkspaceEvent.createWorkspace({
|
||||
required String name,
|
||||
required WorkspaceTypePB workspaceType,
|
||||
}) =>
|
||||
WorkspaceEventCreateWorkspace(name: name, workspaceType: workspaceType);
|
||||
|
||||
factory UserWorkspaceEvent.deleteWorkspace({
|
||||
required String workspaceId,
|
||||
}) =>
|
||||
WorkspaceEventDeleteWorkspace(workspaceId: workspaceId);
|
||||
|
||||
factory UserWorkspaceEvent.openWorkspace({
|
||||
required String workspaceId,
|
||||
required WorkspaceTypePB workspaceType,
|
||||
}) =>
|
||||
WorkspaceEventOpenWorkspace(
|
||||
workspaceId: workspaceId,
|
||||
workspaceType: workspaceType,
|
||||
);
|
||||
|
||||
factory UserWorkspaceEvent.renameWorkspace({
|
||||
required String workspaceId,
|
||||
required String name,
|
||||
}) =>
|
||||
WorkspaceEventRenameWorkspace(workspaceId: workspaceId, name: name);
|
||||
|
||||
factory UserWorkspaceEvent.updateWorkspaceIcon({
|
||||
required String workspaceId,
|
||||
required String icon,
|
||||
}) =>
|
||||
WorkspaceEventUpdateWorkspaceIcon(
|
||||
workspaceId: workspaceId,
|
||||
icon: icon,
|
||||
);
|
||||
|
||||
factory UserWorkspaceEvent.leaveWorkspace({
|
||||
required String workspaceId,
|
||||
}) =>
|
||||
WorkspaceEventLeaveWorkspace(workspaceId: workspaceId);
|
||||
|
||||
factory UserWorkspaceEvent.fetchWorkspaceSubscriptionInfo({
|
||||
required String workspaceId,
|
||||
}) =>
|
||||
WorkspaceEventFetchWorkspaceSubscriptionInfo(workspaceId: workspaceId);
|
||||
|
||||
factory UserWorkspaceEvent.updateWorkspaceSubscriptionInfo({
|
||||
required String workspaceId,
|
||||
required WorkspaceSubscriptionInfoPB subscriptionInfo,
|
||||
}) =>
|
||||
WorkspaceEventUpdateWorkspaceSubscriptionInfo(
|
||||
workspaceId: workspaceId,
|
||||
subscriptionInfo: subscriptionInfo,
|
||||
);
|
||||
|
||||
factory UserWorkspaceEvent.emitWorkspaces({
|
||||
required List<UserWorkspacePB> workspaces,
|
||||
}) =>
|
||||
WorkspaceEventEmitWorkspaces(workspaces: workspaces);
|
||||
|
||||
factory UserWorkspaceEvent.emitUserProfile({
|
||||
required UserProfilePB userProfile,
|
||||
}) =>
|
||||
WorkspaceEventEmitUserProfile(userProfile: userProfile);
|
||||
|
||||
factory UserWorkspaceEvent.emitCurrentWorkspace({
|
||||
required UserWorkspacePB workspace,
|
||||
}) =>
|
||||
WorkspaceEventEmitCurrentWorkspace(workspace: workspace);
|
||||
}
|
||||
|
||||
/// Initializes the workspace bloc.
|
||||
class WorkspaceEventInitialize extends UserWorkspaceEvent {
|
||||
WorkspaceEventInitialize();
|
||||
}
|
||||
|
||||
/// Fetches the list of workspaces for the current user.
|
||||
class WorkspaceEventFetchWorkspaces extends UserWorkspaceEvent {
|
||||
WorkspaceEventFetchWorkspaces({
|
||||
this.initialWorkspaceId,
|
||||
});
|
||||
|
||||
final String? initialWorkspaceId;
|
||||
}
|
||||
|
||||
/// Creates a new workspace.
|
||||
class WorkspaceEventCreateWorkspace extends UserWorkspaceEvent {
|
||||
WorkspaceEventCreateWorkspace({
|
||||
required this.name,
|
||||
required this.workspaceType,
|
||||
});
|
||||
|
||||
final String name;
|
||||
final WorkspaceTypePB workspaceType;
|
||||
}
|
||||
|
||||
/// Deletes a workspace.
|
||||
class WorkspaceEventDeleteWorkspace extends UserWorkspaceEvent {
|
||||
WorkspaceEventDeleteWorkspace({
|
||||
required this.workspaceId,
|
||||
});
|
||||
|
||||
final String workspaceId;
|
||||
}
|
||||
|
||||
/// Opens a workspace.
|
||||
class WorkspaceEventOpenWorkspace extends UserWorkspaceEvent {
|
||||
WorkspaceEventOpenWorkspace({
|
||||
required this.workspaceId,
|
||||
required this.workspaceType,
|
||||
});
|
||||
|
||||
final String workspaceId;
|
||||
final WorkspaceTypePB workspaceType;
|
||||
}
|
||||
|
||||
/// Renames a workspace.
|
||||
class WorkspaceEventRenameWorkspace extends UserWorkspaceEvent {
|
||||
WorkspaceEventRenameWorkspace({
|
||||
required this.workspaceId,
|
||||
required this.name,
|
||||
});
|
||||
|
||||
final String workspaceId;
|
||||
final String name;
|
||||
}
|
||||
|
||||
/// Updates workspace icon.
|
||||
class WorkspaceEventUpdateWorkspaceIcon extends UserWorkspaceEvent {
|
||||
WorkspaceEventUpdateWorkspaceIcon({
|
||||
required this.workspaceId,
|
||||
required this.icon,
|
||||
});
|
||||
|
||||
final String workspaceId;
|
||||
final String icon;
|
||||
}
|
||||
|
||||
/// Leaves a workspace.
|
||||
class WorkspaceEventLeaveWorkspace extends UserWorkspaceEvent {
|
||||
WorkspaceEventLeaveWorkspace({
|
||||
required this.workspaceId,
|
||||
});
|
||||
|
||||
final String workspaceId;
|
||||
}
|
||||
|
||||
/// Fetches workspace subscription info.
|
||||
class WorkspaceEventFetchWorkspaceSubscriptionInfo extends UserWorkspaceEvent {
|
||||
WorkspaceEventFetchWorkspaceSubscriptionInfo({
|
||||
required this.workspaceId,
|
||||
});
|
||||
|
||||
final String workspaceId;
|
||||
}
|
||||
|
||||
/// Updates workspace subscription info.
|
||||
class WorkspaceEventUpdateWorkspaceSubscriptionInfo extends UserWorkspaceEvent {
|
||||
WorkspaceEventUpdateWorkspaceSubscriptionInfo({
|
||||
required this.workspaceId,
|
||||
required this.subscriptionInfo,
|
||||
});
|
||||
|
||||
final String workspaceId;
|
||||
final WorkspaceSubscriptionInfoPB subscriptionInfo;
|
||||
}
|
||||
|
||||
class WorkspaceEventEmitWorkspaces extends UserWorkspaceEvent {
|
||||
WorkspaceEventEmitWorkspaces({
|
||||
required this.workspaces,
|
||||
});
|
||||
|
||||
final List<UserWorkspacePB> workspaces;
|
||||
}
|
||||
|
||||
class WorkspaceEventEmitUserProfile extends UserWorkspaceEvent {
|
||||
WorkspaceEventEmitUserProfile({
|
||||
required this.userProfile,
|
||||
});
|
||||
|
||||
final UserProfilePB userProfile;
|
||||
}
|
||||
|
||||
class WorkspaceEventEmitCurrentWorkspace extends UserWorkspaceEvent {
|
||||
WorkspaceEventEmitCurrentWorkspace({
|
||||
required this.workspace,
|
||||
});
|
||||
|
||||
final UserWorkspacePB workspace;
|
||||
}
|
||||
@@ -1,103 +0,0 @@
|
||||
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
|
||||
import 'package:appflowy_result/appflowy_result.dart';
|
||||
|
||||
enum WorkspaceActionType {
|
||||
none,
|
||||
create,
|
||||
delete,
|
||||
open,
|
||||
rename,
|
||||
updateIcon,
|
||||
fetchWorkspaces,
|
||||
leave,
|
||||
fetchSubscriptionInfo,
|
||||
}
|
||||
|
||||
class WorkspaceActionResult {
|
||||
const WorkspaceActionResult({
|
||||
required this.actionType,
|
||||
required this.isLoading,
|
||||
required this.result,
|
||||
});
|
||||
|
||||
final WorkspaceActionType actionType;
|
||||
final bool isLoading;
|
||||
final FlowyResult<void, FlowyError>? result;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'WorkspaceActionResult(actionType: $actionType, isLoading: $isLoading, result: $result)';
|
||||
}
|
||||
}
|
||||
|
||||
class UserWorkspaceState {
|
||||
factory UserWorkspaceState.initial(UserProfilePB userProfile) =>
|
||||
UserWorkspaceState(
|
||||
userProfile: userProfile,
|
||||
);
|
||||
|
||||
const UserWorkspaceState({
|
||||
this.currentWorkspace,
|
||||
this.workspaces = const [],
|
||||
this.actionResult,
|
||||
this.isCollabWorkspaceOn = false,
|
||||
required this.userProfile,
|
||||
this.workspaceSubscriptionInfo,
|
||||
});
|
||||
|
||||
final UserWorkspacePB? currentWorkspace;
|
||||
final List<UserWorkspacePB> workspaces;
|
||||
final WorkspaceActionResult? actionResult;
|
||||
final bool isCollabWorkspaceOn;
|
||||
final UserProfilePB userProfile;
|
||||
final WorkspaceSubscriptionInfoPB? workspaceSubscriptionInfo;
|
||||
|
||||
UserWorkspaceState copyWith({
|
||||
UserWorkspacePB? currentWorkspace,
|
||||
List<UserWorkspacePB>? workspaces,
|
||||
WorkspaceActionResult? actionResult,
|
||||
bool? isCollabWorkspaceOn,
|
||||
UserProfilePB? userProfile,
|
||||
WorkspaceSubscriptionInfoPB? workspaceSubscriptionInfo,
|
||||
}) {
|
||||
return UserWorkspaceState(
|
||||
currentWorkspace: currentWorkspace ?? this.currentWorkspace,
|
||||
workspaces: workspaces ?? this.workspaces,
|
||||
actionResult: actionResult ?? this.actionResult,
|
||||
isCollabWorkspaceOn: isCollabWorkspaceOn ?? this.isCollabWorkspaceOn,
|
||||
userProfile: userProfile ?? this.userProfile,
|
||||
workspaceSubscriptionInfo:
|
||||
workspaceSubscriptionInfo ?? this.workspaceSubscriptionInfo,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (identical(this, other)) return true;
|
||||
return other is UserWorkspaceState &&
|
||||
other.currentWorkspace == currentWorkspace &&
|
||||
other.workspaces == workspaces &&
|
||||
other.actionResult == actionResult &&
|
||||
other.isCollabWorkspaceOn == isCollabWorkspaceOn &&
|
||||
other.userProfile == userProfile &&
|
||||
other.workspaceSubscriptionInfo == workspaceSubscriptionInfo;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
return Object.hash(
|
||||
currentWorkspace,
|
||||
workspaces,
|
||||
actionResult,
|
||||
isCollabWorkspaceOn,
|
||||
userProfile,
|
||||
workspaceSubscriptionInfo,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'WorkspaceState(currentWorkspace: $currentWorkspace, workspaces: $workspaces, actionResult: $actionResult, isCollabWorkspaceOn: $isCollabWorkspaceOn, userProfile: $userProfile, workspaceSubscriptionInfo: $workspaceSubscriptionInfo)';
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,3 @@
|
||||
import 'package:appflowy/features/page_access_level/logic/page_access_level_bloc.dart';
|
||||
import 'package:appflowy/features/share_tab/data/models/share_access_level.dart';
|
||||
import 'package:appflowy/features/workspace/data/repositories/rust_workspace_repository_impl.dart';
|
||||
import 'package:appflowy/features/workspace/logic/workspace_bloc.dart';
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/mobile/application/base/mobile_view_page_bloc.dart';
|
||||
@@ -21,8 +17,10 @@ import 'package:appflowy/startup/plugin/plugin.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy/user/application/reminder/reminder_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/view/view_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/view/view_ext.dart';
|
||||
import 'package:appflowy/workspace/application/view/view_lock_status_bloc.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/view_title_bar.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
@@ -122,12 +120,9 @@ class _MobileViewPageState extends State<MobileViewPage> {
|
||||
),
|
||||
if (state.userProfilePB != null)
|
||||
BlocProvider(
|
||||
create: (_) => UserWorkspaceBloc(
|
||||
userProfile: state.userProfilePB!,
|
||||
repository: RustWorkspaceRepositoryImpl(
|
||||
userId: state.userProfilePB!.id,
|
||||
),
|
||||
)..add(UserWorkspaceEvent.initialize()),
|
||||
create: (_) =>
|
||||
UserWorkspaceBloc(userProfile: state.userProfilePB!)
|
||||
..add(const UserWorkspaceEvent.initial()),
|
||||
),
|
||||
if (view.layout.isDocumentView)
|
||||
BlocProvider(
|
||||
@@ -136,8 +131,8 @@ class _MobileViewPageState extends State<MobileViewPage> {
|
||||
),
|
||||
if (view.layout.isDocumentView || view.layout.isDatabaseView)
|
||||
BlocProvider(
|
||||
create: (_) => PageAccessLevelBloc(view: view)
|
||||
..add(const PageAccessLevelEvent.initial()),
|
||||
create: (_) => ViewLockStatusBloc(view: view)
|
||||
..add(const ViewLockStatusEvent.initial()),
|
||||
),
|
||||
],
|
||||
child: Builder(
|
||||
@@ -244,8 +239,7 @@ class _MobileViewPageState extends State<MobileViewPage> {
|
||||
final isImmersiveMode =
|
||||
context.read<MobileViewPageBloc>().state.isImmersiveMode;
|
||||
final isLocked =
|
||||
context.read<PageAccessLevelBloc?>()?.state.isLocked ?? false;
|
||||
final accessLevel = context.read<PageAccessLevelBloc>().state.accessLevel;
|
||||
context.read<ViewLockStatusBloc?>()?.state.isLocked ?? false;
|
||||
final actions = <Widget>[];
|
||||
|
||||
if (FeatureFlag.syncDocument.isOn) {
|
||||
@@ -275,7 +269,7 @@ class _MobileViewPageState extends State<MobileViewPage> {
|
||||
]);
|
||||
}
|
||||
|
||||
if (widget.showMoreButton && accessLevel != ShareAccessLevel.readOnly) {
|
||||
if (widget.showMoreButton) {
|
||||
actions.addAll([
|
||||
MobileViewPageMoreButton(
|
||||
view: view,
|
||||
@@ -340,7 +334,7 @@ class _MobileViewPageState extends State<MobileViewPage> {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return BlocConsumer<PageAccessLevelBloc, PageAccessLevelState>(
|
||||
return BlocConsumer<ViewLockStatusBloc, ViewLockStatusState>(
|
||||
listenWhen: (previous, current) =>
|
||||
previous.isLoadingLockStatus == current.isLoadingLockStatus &&
|
||||
current.isLoadingLockStatus == false,
|
||||
@@ -369,7 +363,7 @@ class _MobileViewPageState extends State<MobileViewPage> {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return BlocConsumer<PageAccessLevelBloc, PageAccessLevelState>(
|
||||
return BlocConsumer<ViewLockStatusBloc, ViewLockStatusState>(
|
||||
listenWhen: (previous, current) =>
|
||||
previous.isLoadingLockStatus == current.isLoadingLockStatus &&
|
||||
current.isLoadingLockStatus == false,
|
||||
@@ -385,8 +379,8 @@ class _MobileViewPageState extends State<MobileViewPage> {
|
||||
return GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: () {
|
||||
context.read<PageAccessLevelBloc>().add(
|
||||
const PageAccessLevelEvent.unlock(),
|
||||
context.read<ViewLockStatusBloc>().add(
|
||||
const ViewLockStatusEvent.unlock(),
|
||||
);
|
||||
},
|
||||
child: Padding(
|
||||
@@ -405,8 +399,8 @@ class _MobileViewPageState extends State<MobileViewPage> {
|
||||
return GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: () {
|
||||
context.read<PageAccessLevelBloc>().add(
|
||||
const PageAccessLevelEvent.lock(),
|
||||
context.read<ViewLockStatusBloc>().add(
|
||||
const ViewLockStatusEvent.lock(),
|
||||
);
|
||||
},
|
||||
child: Padding(
|
||||
|
||||
+7
-2
@@ -1,4 +1,3 @@
|
||||
import 'package:appflowy/features/page_access_level/logic/page_access_level_bloc.dart';
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/mobile/application/base/mobile_view_page_bloc.dart';
|
||||
@@ -13,6 +12,7 @@ import 'package:appflowy/plugins/shared/share/share_bloc.dart';
|
||||
import 'package:appflowy/shared/icon_emoji_picker/tab.dart';
|
||||
import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/view/prelude.dart';
|
||||
import 'package:appflowy/workspace/application/view/view_lock_status_bloc.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra/theme_extension.dart';
|
||||
@@ -111,7 +111,12 @@ class MobileViewPageMoreButton extends StatelessWidget {
|
||||
BlocProvider.value(value: context.read<FavoriteBloc>()),
|
||||
BlocProvider.value(value: context.read<MobileViewPageBloc>()),
|
||||
BlocProvider.value(value: context.read<ShareBloc>()),
|
||||
BlocProvider.value(value: context.read<PageAccessLevelBloc>()),
|
||||
BlocProvider(
|
||||
create: (context) => ViewLockStatusBloc(view: view)
|
||||
..add(
|
||||
ViewLockStatusEvent.initial(),
|
||||
),
|
||||
),
|
||||
],
|
||||
child: MobileViewPageMoreBottomSheet(view: view),
|
||||
),
|
||||
|
||||
+7
-15
@@ -1,7 +1,6 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:appflowy/core/helpers/url_launcher.dart';
|
||||
import 'package:appflowy/features/page_access_level/logic/page_access_level_bloc.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
|
||||
import 'package:appflowy/mobile/presentation/home/workspaces/create_workspace_menu.dart';
|
||||
@@ -15,6 +14,7 @@ import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy/util/string_extension.dart';
|
||||
import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/view/prelude.dart';
|
||||
import 'package:appflowy/workspace/application/view/view_lock_status_bloc.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
||||
@@ -44,16 +44,10 @@ class MobileViewPageMoreBottomSheet extends StatelessWidget {
|
||||
},
|
||||
child: ViewPageBottomSheet(
|
||||
view: view,
|
||||
onAction: (action, {arguments}) async => _onAction(
|
||||
context,
|
||||
action,
|
||||
arguments,
|
||||
),
|
||||
onAction: (action, {arguments}) async =>
|
||||
_onAction(context, action, arguments),
|
||||
onRename: (name) {
|
||||
_onRename(
|
||||
context,
|
||||
name,
|
||||
);
|
||||
_onRename(context, name);
|
||||
context.pop();
|
||||
},
|
||||
),
|
||||
@@ -134,13 +128,11 @@ class MobileViewPageMoreBottomSheet extends StatelessWidget {
|
||||
required bool isLocked,
|
||||
}) async {
|
||||
if (isLocked) {
|
||||
context
|
||||
.read<PageAccessLevelBloc>()
|
||||
.add(const PageAccessLevelEvent.lock());
|
||||
context.read<ViewLockStatusBloc>().add(const ViewLockStatusEvent.lock());
|
||||
} else {
|
||||
context
|
||||
.read<PageAccessLevelBloc>()
|
||||
.add(const PageAccessLevelEvent.unlock());
|
||||
.read<ViewLockStatusBloc>()
|
||||
.add(const ViewLockStatusEvent.unlock());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
import 'package:appflowy/features/page_access_level/logic/page_access_level_bloc.dart';
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/mobile/presentation/widgets/widgets.dart';
|
||||
import 'package:appflowy/workspace/application/view/view_lock_status_bloc.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
@@ -43,7 +43,7 @@ class MobileViewItemBottomSheetBody extends StatelessWidget {
|
||||
MobileViewItemBottomSheetBodyAction action,
|
||||
) {
|
||||
final isLocked =
|
||||
context.read<PageAccessLevelBloc?>()?.state.isLocked ?? false;
|
||||
context.read<ViewLockStatusBloc?>()?.state.isLocked ?? false;
|
||||
switch (action) {
|
||||
case MobileViewItemBottomSheetBodyAction.rename:
|
||||
return FlowyOptionTile.text(
|
||||
|
||||
+10
-10
@@ -1,4 +1,3 @@
|
||||
import 'package:appflowy/features/page_access_level/logic/page_access_level_bloc.dart';
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/mobile/application/base/mobile_view_page_bloc.dart';
|
||||
@@ -6,6 +5,7 @@ import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
|
||||
import 'package:appflowy/mobile/presentation/widgets/flowy_mobile_quick_action_button.dart';
|
||||
import 'package:appflowy/plugins/shared/share/share_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/view/view_ext.dart';
|
||||
import 'package:appflowy/workspace/application/view/view_lock_status_bloc.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
@@ -112,8 +112,8 @@ class MobileViewBottomSheetBody extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isFavorite = view.isFavorite;
|
||||
final isEditable =
|
||||
context.watch<PageAccessLevelBloc?>()?.state.isEditable ?? false;
|
||||
final isLocked =
|
||||
context.watch<ViewLockStatusBloc?>()?.state.isLocked ?? false;
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
@@ -121,7 +121,7 @@ class MobileViewBottomSheetBody extends StatelessWidget {
|
||||
text: LocaleKeys.button_rename.tr(),
|
||||
icon: FlowySvgs.view_item_rename_s,
|
||||
iconSize: const Size.square(18),
|
||||
enable: isEditable,
|
||||
enable: !isLocked,
|
||||
onTap: () => onAction(
|
||||
MobileViewBottomSheetBodyAction.rename,
|
||||
),
|
||||
@@ -150,12 +150,12 @@ class MobileViewBottomSheetBody extends StatelessWidget {
|
||||
),
|
||||
onTap: () {
|
||||
final isLocked =
|
||||
context.read<PageAccessLevelBloc?>()?.state.isLocked ?? false;
|
||||
context.read<ViewLockStatusBloc?>()?.state.isLocked ?? false;
|
||||
onAction(
|
||||
MobileViewBottomSheetBodyAction.lockPage,
|
||||
arguments: {
|
||||
MobileViewBottomSheetBodyActionArguments.isLockedKey:
|
||||
isLocked,
|
||||
!isLocked,
|
||||
},
|
||||
);
|
||||
},
|
||||
@@ -189,7 +189,7 @@ class MobileViewBottomSheetBody extends StatelessWidget {
|
||||
icon: FlowySvgs.trash_s,
|
||||
iconColor: Theme.of(context).colorScheme.error,
|
||||
iconSize: const Size.square(18),
|
||||
enable: isEditable,
|
||||
enable: !isLocked,
|
||||
onTap: () => onAction(
|
||||
MobileViewBottomSheetBodyAction.delete,
|
||||
),
|
||||
@@ -264,15 +264,15 @@ class _LockPageRightIconBuilder extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isEditable =
|
||||
context.watch<PageAccessLevelBloc?>()?.state.isEditable ?? false;
|
||||
final isLocked =
|
||||
context.watch<ViewLockStatusBloc?>()?.state.isLocked ?? false;
|
||||
return SizedBox(
|
||||
width: 46,
|
||||
height: 30,
|
||||
child: FittedBox(
|
||||
fit: BoxFit.fill,
|
||||
child: CupertinoSwitch(
|
||||
value: isEditable,
|
||||
value: isLocked,
|
||||
activeTrackColor: Theme.of(context).colorScheme.primary,
|
||||
onChanged: (value) {
|
||||
onAction(
|
||||
|
||||
+3
-3
@@ -1,4 +1,3 @@
|
||||
import 'package:appflowy/features/page_access_level/logic/page_access_level_bloc.dart';
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
|
||||
@@ -9,6 +8,7 @@ import 'package:appflowy/workspace/application/recent/recent_views_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/view/view_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/view/view_ext.dart';
|
||||
import 'package:appflowy/workspace/application/view/view_lock_status_bloc.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
@@ -132,8 +132,8 @@ enum MobilePaneActionType {
|
||||
BlocProvider.value(value: recentViewsBloc),
|
||||
BlocProvider(
|
||||
create: (_) =>
|
||||
PageAccessLevelBloc(view: viewBloc.state.view)
|
||||
..add(const PageAccessLevelEvent.initial()),
|
||||
ViewLockStatusBloc(view: viewBloc.state.view)
|
||||
..add(const ViewLockStatusEvent.initial()),
|
||||
),
|
||||
],
|
||||
child: BlocBuilder<ViewBloc, ViewState>(
|
||||
|
||||
+12
-10
@@ -1,4 +1,3 @@
|
||||
import 'package:appflowy/features/page_access_level/logic/page_access_level_bloc.dart';
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/mobile/presentation/database/board/board.dart';
|
||||
@@ -12,6 +11,7 @@ import 'package:appflowy/plugins/database/widgets/cell/card_cell_style_maps/mobi
|
||||
import 'package:appflowy/shared/flowy_error_page.dart';
|
||||
import 'package:appflowy/util/field_type_extension.dart';
|
||||
import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart';
|
||||
import 'package:appflowy/workspace/application/view/view_lock_status_bloc.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';
|
||||
import 'package:appflowy_board/appflowy_board.dart';
|
||||
@@ -143,9 +143,8 @@ class _BoardContentState extends State<_BoardContent> {
|
||||
return state.maybeMap(
|
||||
orElse: () => const SizedBox.shrink(),
|
||||
ready: (state) {
|
||||
final isEditable =
|
||||
context.watch<PageAccessLevelBloc?>()?.state.isEditable ??
|
||||
false;
|
||||
final isLocked =
|
||||
context.watch<ViewLockStatusBloc?>()?.state.isLocked ?? false;
|
||||
final showCreateGroupButton = context
|
||||
.read<BoardBloc>()
|
||||
.groupingFieldType
|
||||
@@ -163,12 +162,15 @@ class _BoardContentState extends State<_BoardContent> {
|
||||
padding: config.groupHeaderPadding,
|
||||
)
|
||||
: const HSpace(16),
|
||||
trailing: showCreateGroupButton && isEditable
|
||||
trailing: showCreateGroupButton && !isLocked
|
||||
? const MobileBoardTrailing()
|
||||
: const HSpace(16),
|
||||
headerBuilder: (_, groupData) {
|
||||
final isLocked =
|
||||
context.read<ViewLockStatusBloc?>()?.state.isLocked ??
|
||||
false;
|
||||
return IgnorePointer(
|
||||
ignoring: !isEditable,
|
||||
ignoring: isLocked,
|
||||
child: GroupCardHeader(
|
||||
groupData: groupData,
|
||||
),
|
||||
@@ -189,15 +191,15 @@ class _BoardContentState extends State<_BoardContent> {
|
||||
}
|
||||
|
||||
Widget _buildFooter(BuildContext context, AppFlowyGroupData columnData) {
|
||||
final isEditable =
|
||||
context.read<PageAccessLevelBloc?>()?.state.isEditable ?? false;
|
||||
final isLocked =
|
||||
context.read<ViewLockStatusBloc?>()?.state.isLocked ?? false;
|
||||
final style = Theme.of(context);
|
||||
|
||||
return SizedBox(
|
||||
height: 42,
|
||||
width: double.infinity,
|
||||
child: IgnorePointer(
|
||||
ignoring: !isEditable,
|
||||
ignoring: isLocked,
|
||||
child: TextButton.icon(
|
||||
style: TextButton.styleFrom(
|
||||
padding: const EdgeInsets.only(left: 8),
|
||||
@@ -242,7 +244,7 @@ class _BoardContentState extends State<_BoardContent> {
|
||||
|
||||
final groupItemId = groupItem.row.id + groupData.group.groupId;
|
||||
final isLocked =
|
||||
context.read<PageAccessLevelBloc?>()?.state.isLocked ?? false;
|
||||
context.read<ViewLockStatusBloc?>()?.state.isLocked ?? false;
|
||||
|
||||
return Container(
|
||||
key: ValueKey(groupItemId),
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ import 'package:appflowy/mobile/presentation/home/favorite_folder/mobile_home_fa
|
||||
import 'package:appflowy/mobile/presentation/widgets/flowy_mobile_state_container.dart';
|
||||
import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/menu/sidebar_sections_bloc.dart';
|
||||
import 'package:appflowy/features/workspace/logic/workspace_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
|
||||
+4
-9
@@ -1,11 +1,10 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:appflowy/features/workspace/data/repositories/rust_workspace_repository_impl.dart';
|
||||
import 'package:appflowy/features/workspace/logic/workspace_bloc.dart';
|
||||
import 'package:appflowy/mobile/presentation/favorite/mobile_favorite_folder.dart';
|
||||
import 'package:appflowy/mobile/presentation/home/mobile_home_page_header.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy/user/application/auth/auth_service.dart';
|
||||
import 'package:appflowy/workspace/application/user/prelude.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/errors/workspace_failed_screen.dart';
|
||||
import 'package:appflowy_backend/dispatch/dispatch.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/workspace.pb.dart';
|
||||
@@ -54,13 +53,9 @@ class MobileFavoriteScreen extends StatelessWidget {
|
||||
return Scaffold(
|
||||
body: SafeArea(
|
||||
child: BlocProvider(
|
||||
create: (_) => UserWorkspaceBloc(
|
||||
userProfile: userProfile,
|
||||
repository: RustWorkspaceRepositoryImpl(
|
||||
userId: userProfile.id,
|
||||
),
|
||||
)..add(
|
||||
UserWorkspaceEvent.initialize(),
|
||||
create: (_) => UserWorkspaceBloc(userProfile: userProfile)
|
||||
..add(
|
||||
const UserWorkspaceEvent.initial(),
|
||||
),
|
||||
child: BlocBuilder<UserWorkspaceBloc, UserWorkspaceState>(
|
||||
buildWhen: (previous, current) =>
|
||||
|
||||
+1
-1
@@ -1,10 +1,10 @@
|
||||
import 'package:appflowy/features/workspace/logic/workspace_bloc.dart';
|
||||
import 'package:appflowy/mobile/application/mobile_router.dart';
|
||||
import 'package:appflowy/mobile/presentation/home/shared/empty_placeholder.dart';
|
||||
import 'package:appflowy/mobile/presentation/home/shared/mobile_page_card.dart';
|
||||
import 'package:appflowy/util/theme_extension.dart';
|
||||
import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/menu/sidebar_sections_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/user/prelude.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/home_sizes.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import 'package:appflowy/features/workspace/logic/workspace_bloc.dart';
|
||||
import 'package:appflowy/mobile/presentation/home/mobile_folders.dart';
|
||||
import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/home_sizes.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import 'package:appflowy/features/workspace/logic/workspace_bloc.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/mobile/presentation/home/section_folder/mobile_home_section_folder.dart';
|
||||
import 'package:appflowy/mobile/presentation/home/space/mobile_space.dart';
|
||||
import 'package:appflowy/workspace/application/menu/sidebar_sections_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/sidebar/space/space_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import 'package:appflowy/features/workspace/data/repositories/rust_workspace_repository_impl.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/mobile/presentation/home/mobile_home_page_header.dart';
|
||||
import 'package:appflowy/mobile/presentation/home/tab/mobile_space_tab.dart';
|
||||
@@ -122,12 +121,8 @@ class _MobileHomePageState extends State<MobileHomePage> {
|
||||
return MultiBlocProvider(
|
||||
providers: [
|
||||
BlocProvider(
|
||||
create: (_) => UserWorkspaceBloc(
|
||||
userProfile: widget.userProfile,
|
||||
repository: RustWorkspaceRepositoryImpl(
|
||||
userId: widget.userProfile.id,
|
||||
),
|
||||
)..add(UserWorkspaceEvent.initialize()),
|
||||
create: (_) => UserWorkspaceBloc(userProfile: widget.userProfile)
|
||||
..add(const UserWorkspaceEvent.initial()),
|
||||
),
|
||||
BlocProvider(
|
||||
create: (context) =>
|
||||
@@ -239,7 +234,7 @@ class _HomePageState extends State<_HomePage> {
|
||||
),
|
||||
),
|
||||
],
|
||||
child: MobileHomePageTab(
|
||||
child: MobileSpaceTab(
|
||||
userProfile: widget.userProfile,
|
||||
),
|
||||
),
|
||||
@@ -283,13 +278,13 @@ class _HomePageState extends State<_HomePage> {
|
||||
final String? message;
|
||||
ToastificationType toastType = ToastificationType.success;
|
||||
switch (actionType) {
|
||||
case WorkspaceActionType.open:
|
||||
case UserWorkspaceActionType.open:
|
||||
message = result.onFailure((e) {
|
||||
toastType = ToastificationType.error;
|
||||
return '${LocaleKeys.workspace_openFailed.tr()}: ${e.msg}';
|
||||
});
|
||||
break;
|
||||
case WorkspaceActionType.delete:
|
||||
case UserWorkspaceActionType.delete:
|
||||
message = result.fold(
|
||||
(s) {
|
||||
toastType = ToastificationType.success;
|
||||
@@ -301,7 +296,7 @@ class _HomePageState extends State<_HomePage> {
|
||||
},
|
||||
);
|
||||
break;
|
||||
case WorkspaceActionType.leave:
|
||||
case UserWorkspaceActionType.leave:
|
||||
message = result.fold(
|
||||
(s) {
|
||||
toastType = ToastificationType.success;
|
||||
@@ -315,7 +310,7 @@ class _HomePageState extends State<_HomePage> {
|
||||
},
|
||||
);
|
||||
break;
|
||||
case WorkspaceActionType.rename:
|
||||
case UserWorkspaceActionType.rename:
|
||||
message = result.fold(
|
||||
(s) {
|
||||
toastType = ToastificationType.success;
|
||||
|
||||
+10
-9
@@ -1,4 +1,3 @@
|
||||
import 'package:appflowy/features/workspace/logic/workspace_bloc.dart';
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/mobile/presentation/base/animated_gesture.dart';
|
||||
@@ -10,6 +9,7 @@ import 'package:appflowy/shared/icon_emoji_picker/tab.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy/util/built_in_svgs.dart';
|
||||
import 'package:appflowy/workspace/application/user/settings_user_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_icon.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
@@ -116,25 +116,26 @@ class _MobileWorkspace extends StatelessWidget {
|
||||
alignment: Alignment.centerLeft,
|
||||
onTapUp: () {
|
||||
context.read<UserWorkspaceBloc>().add(
|
||||
UserWorkspaceEvent.fetchWorkspaces(),
|
||||
const UserWorkspaceEvent.fetchWorkspaces(),
|
||||
);
|
||||
_showSwitchWorkspacesBottomSheet(context);
|
||||
},
|
||||
child: Row(
|
||||
children: [
|
||||
WorkspaceIcon(
|
||||
workspaceIcon: currentWorkspace.icon,
|
||||
workspaceName: currentWorkspace.name,
|
||||
workspace: currentWorkspace,
|
||||
iconSize: 36,
|
||||
fontSize: 18.0,
|
||||
isEditable: true,
|
||||
enableEdit: true,
|
||||
alignment: Alignment.centerLeft,
|
||||
figmaLineHeight: 26.0,
|
||||
emojiSize: 24.0,
|
||||
borderRadius: 12.0,
|
||||
showBorder: false,
|
||||
onSelected: (result) => context.read<UserWorkspaceBloc>().add(
|
||||
UserWorkspaceEvent.updateWorkspaceIcon(
|
||||
workspaceId: currentWorkspace.workspaceId,
|
||||
icon: result.emoji,
|
||||
currentWorkspace.workspaceId,
|
||||
result.emoji,
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -192,8 +193,8 @@ class _MobileWorkspace extends StatelessWidget {
|
||||
|
||||
context.read<UserWorkspaceBloc>().add(
|
||||
UserWorkspaceEvent.openWorkspace(
|
||||
workspaceId: workspace.workspaceId,
|
||||
workspaceType: workspace.workspaceType,
|
||||
workspace.workspaceId,
|
||||
workspace.workspaceType,
|
||||
),
|
||||
);
|
||||
},
|
||||
|
||||
+2
-9
@@ -1,7 +1,5 @@
|
||||
import 'package:appflowy/env/cloud_env.dart';
|
||||
import 'package:appflowy/env/env.dart';
|
||||
import 'package:appflowy/features/workspace/data/repositories/rust_workspace_repository_impl.dart';
|
||||
import 'package:appflowy/features/workspace/logic/workspace_bloc.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/mobile/presentation/base/app_bar/app_bar.dart';
|
||||
import 'package:appflowy/mobile/presentation/presentation.dart';
|
||||
@@ -9,7 +7,6 @@ import 'package:appflowy/mobile/presentation/setting/ai/ai_settings_group.dart';
|
||||
import 'package:appflowy/mobile/presentation/setting/cloud/cloud_setting_group.dart';
|
||||
import 'package:appflowy/mobile/presentation/setting/user_session_setting_group.dart';
|
||||
import 'package:appflowy/mobile/presentation/setting/workspace/workspace_setting_group.dart';
|
||||
import 'package:appflowy/mobile/presentation/widgets/flowy_mobile_state_container.dart';
|
||||
import 'package:appflowy/mobile/presentation/widgets/widgets.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy/user/application/auth/auth_service.dart';
|
||||
@@ -75,12 +72,8 @@ class _MobileHomeSettingPageState extends State<MobileHomeSettingPage> {
|
||||
|
||||
Widget _buildSettingsWidget(UserProfilePB userProfile) {
|
||||
return BlocProvider(
|
||||
create: (context) => UserWorkspaceBloc(
|
||||
userProfile: userProfile,
|
||||
repository: RustWorkspaceRepositoryImpl(
|
||||
userId: userProfile.id,
|
||||
),
|
||||
)..add(UserWorkspaceEvent.initialize()),
|
||||
create: (context) => UserWorkspaceBloc(userProfile: userProfile)
|
||||
..add(const UserWorkspaceEvent.initial()),
|
||||
child: BlocBuilder<UserWorkspaceBloc, UserWorkspaceState>(
|
||||
builder: (context, state) {
|
||||
final currentWorkspaceId = state.currentWorkspace?.workspaceId ?? '';
|
||||
|
||||
+1
-1
@@ -1,10 +1,10 @@
|
||||
import 'package:appflowy/features/workspace/logic/workspace_bloc.dart';
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
|
||||
import 'package:appflowy/mobile/presentation/home/recent_folder/mobile_recent_view.dart';
|
||||
import 'package:appflowy/mobile/presentation/widgets/widgets.dart';
|
||||
import 'package:appflowy/workspace/application/recent/prelude.dart';
|
||||
import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra/theme_extension.dart';
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import 'package:appflowy/features/shared_section/presentation/m_shared_section.dart';
|
||||
import 'package:appflowy/features/workspace/logic/workspace_bloc.dart';
|
||||
import 'package:appflowy/mobile/application/mobile_router.dart';
|
||||
import 'package:appflowy/mobile/presentation/home/favorite_folder/favorite_space.dart';
|
||||
import 'package:appflowy/mobile/presentation/home/home_space/home_space.dart';
|
||||
@@ -12,6 +10,7 @@ import 'package:appflowy/shared/icon_emoji_picker/tab.dart';
|
||||
import 'package:appflowy/workspace/application/menu/sidebar_sections_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/sidebar/space/space_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
|
||||
@@ -24,8 +23,8 @@ import 'ai_bubble_button.dart';
|
||||
|
||||
final ValueNotifier<int> mobileCreateNewAIChatNotifier = ValueNotifier(0);
|
||||
|
||||
class MobileHomePageTab extends StatefulWidget {
|
||||
const MobileHomePageTab({
|
||||
class MobileSpaceTab extends StatefulWidget {
|
||||
const MobileSpaceTab({
|
||||
super.key,
|
||||
required this.userProfile,
|
||||
});
|
||||
@@ -33,10 +32,10 @@ class MobileHomePageTab extends StatefulWidget {
|
||||
final UserProfilePB userProfile;
|
||||
|
||||
@override
|
||||
State<MobileHomePageTab> createState() => _MobileHomePageTabState();
|
||||
State<MobileSpaceTab> createState() => _MobileSpaceTabState();
|
||||
}
|
||||
|
||||
class _MobileHomePageTabState extends State<MobileHomePageTab>
|
||||
class _MobileSpaceTabState extends State<MobileSpaceTab>
|
||||
with SingleTickerProviderStateMixin {
|
||||
TabController? tabController;
|
||||
|
||||
@@ -179,18 +178,6 @@ class _MobileHomePageTabState extends State<MobileHomePageTab>
|
||||
);
|
||||
case MobileSpaceTabType.favorites:
|
||||
return MobileFavoriteSpace(userProfile: widget.userProfile);
|
||||
case MobileSpaceTabType.shared:
|
||||
final workspaceId = context
|
||||
.read<UserWorkspaceBloc>()
|
||||
.state
|
||||
.currentWorkspace
|
||||
?.workspaceId;
|
||||
if (workspaceId == null) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
return MSharedSection(
|
||||
workspaceId: workspaceId,
|
||||
);
|
||||
}
|
||||
}).toList();
|
||||
}
|
||||
@@ -229,6 +216,6 @@ class _MobileHomePageTabState extends State<MobileHomePageTab>
|
||||
}
|
||||
context
|
||||
.read<UserWorkspaceBloc>()
|
||||
.add(UserWorkspaceEvent.leaveWorkspace(workspaceId: workspaceId));
|
||||
.add(UserWorkspaceEvent.leaveWorkspace(workspaceId));
|
||||
}
|
||||
}
|
||||
Loaded 100 of 284 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user