feat(dialog): experimental sharing dialog for the unified sharing API#239
feat(dialog): experimental sharing dialog for the unified sharing API#239skjnldsv wants to merge 21 commits into
Conversation
Switch the build from plain tsc to @nextcloud/vite-config createLibConfig so the package can ship Vue SFCs in upcoming subpath exports. The three existing entry points (., ./public, ./ui) are preserved; only the dist file names change (.js -> .mjs, ui/index.js -> ui.mjs). Declarations are generated by vite-plugin-dts and rolled up in production mode. Also fixes the stale lib/publicShare.ts reference in build:doc. Assisted-by: ClaudeCode:claude-fable-5
… API Add a new ./dialog entry point shipping the sharing dialog built for the unified sharing API (Nextcloud >= 35): - openSharingDialog(node) spawns the dialog, gated on the sharing capability being present - SharingDialog component export for embedding - typed API client (createShare, addShareRecipient, selectSharePermissionPreset, ...) and response types - own gettext domain, translations bundled at build time The dialog is marked experimental: the server API is new and the module API may change in any minor release. Assisted-by: ClaudeCode:claude-fable-5
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #239 +/- ##
===========================================
- Coverage 95.65% 83.42% -12.23%
===========================================
Files 4 11 +7
Lines 69 181 +112
Branches 25 68 +43
===========================================
+ Hits 66 151 +85
- Misses 3 24 +21
- Partials 0 6 +6 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
Declaration rollup only ran in production mode, so dev/watch builds emitted nested d.ts files that did not match the flat paths in the exports map. Drop the rollup and point exports at the nested output, identical in both modes. Assisted-by: ClaudeCode:claude-fable-5
The unified sharing API replaced the view/edit preset enum with
registered preset classes carrying translated display names:
- capabilities now expose sharing.permission_presets with
{class, display_name, hint}, so the hardcoded preset label map is
gone and the dropdown is fully backend-driven
- permission.presets and permission_preset carry preset class strings
- the preset endpoint parameter was renamed to permissionPresetClass
Assisted-by: ClaudeCode:claude-fable-5
Render the date property with NcTextField (type=date) instead of NcDateTimePickerNative so it shares the floating-label design of the other fields (label, password). ISO values are reduced to their date part for the native input and re-expanded to an ATOM datetime on save. type=date needs the NcInputField date-type support (@nextcloud/vue > 9.9.0); it already works at runtime on 9.9.0, so the type-check is suppressed with a versioned @vue-expect-error until that lands. Assisted-by: ClaudeCode:claude-fable-5
The floating-label design now lives in NcDateTimePickerNative itself, so the dialog no longer needs the NcTextField workaround (and its type-check suppression). Restore the Date-based picker usage. Assisted-by: ClaudeCode:claude-fable-5
- word-break: break-all on the title so long file names wrap instead of overflowing - scroll the share form instead of the whole dialog content, so the scrollbar no longer overlaps the fixed header and its close button Assisted-by: ClaudeCode:claude-fable-5
Admin-enforced properties (required, no toggle) now show a small info icon next to the field with a native tooltip carrying the property hint, e.g. why an expiration date or password is mandatory. The wording is provided by the backend property hint, so it stays accurate per share type and translatable. Assisted-by: ClaudeCode:claude-fable-5
The earlier form-scroll override fought the content-sized modal: it collapsed the height (constricted layout) and showed a scrollbar even when everything fit. Revert to the dialog's native content scrolling and instead pad the form bottom to match the dialog's inline padding. Assisted-by: ClaudeCode:claude-fable-5
Cap the form with a max-height and scroll it, instead of flexing it against the content-sized modal. The header and its close button stay fixed above the scroll region, while the dialog still sizes to its content (no spurious scrollbar) when short. Assisted-by: ClaudeCode:claude-fable-5
In a scrolling flex column the fields were shrinking (flex-shrink) to fit the max-height, compressing the inputs. Pin the direct children to their natural height so the form scrolls instead. Assisted-by: ClaudeCode:claude-fable-5
- drop the form margin in favour of a dialog content gap - form gap 4x -> 3x the grid baseline, unified with the content gap and bottom padding (all calc(var(--default-grid-baseline) * 3)) - express the header gap, settings-toggle offset and empty-content padding as grid-baseline multiples instead of raw pixels Assisted-by: ClaudeCode:claude-fable-5
Floating input labels overshoot upward into the form gap, so the 3x baseline gap looked cramped between selects. Bump the form gap to 4x the baseline (the content/header gap stays at 3x, no floating label there). Assisted-by: ClaudeCode:claude-fable-5
The recipient model (selectedRecipient) and its watcher are single-value (a new pick replaces the previous), but NcSelectUsers defaulted to multiple mode. Multiple mode has no height boost, so the picker rendered short (~33px). Set multiple=false to match the model and get the proper 42px single-select height. Also reverts the temporary form-gap bump; the spacing was a symptom of the short picker, not the gap. Assisted-by: ClaudeCode:claude-fable-5
The inactive-slot styles were lost in the port: the disabled input swallowed the click so the field never enabled, and there was no pointer cursor. Restore the inactive rule — pointer cursor + pointer-events:none on the slot content so the click reaches the container's enable handler. Assisted-by: ClaudeCode:claude-fable-5
Assisted-by: ClaudeCode:claude-fable-5
Replace the hand-rolled OCS envelope shapes (unwrapOcs, the OCS error type, and the test helper) with the shared OCSResponse<T> type from @nextcloud/typings/ocs. Declare the (types-only) dev dependency. Assisted-by: ClaudeCode:claude-fable-5
Addresses PR review note. Assisted-by: ClaudeCode:claude-opus-4-8
openSharingDialog no longer throws when the unified sharing API is missing; it shows an error toast and resolves to undefined, so callers can invoke it unconditionally. isSharingDialogAvailable stays exported for gating the entry point. Addresses PR review note. Assisted-by: ClaudeCode:claude-opus-4-8
Generate recipient display variations on the frontend, mirroring the legacy files_sharing SharingInput: email/group/team recipients get a type-specific mdi icon, federated recipients show their instance on a second line, and email recipients show the address. Users keep native avatar loading; unknown types fall back to a backend-provided icon. Adds recipient type constants and unit tests for the mapping. Assisted-by: ClaudeCode:claude-opus-4-8
Outline mdi variants are the current design pattern. Switch the recipient type icons (email/group/team) and the send action icon to their -outline variants. content-copy and arrow-left have no outline variant and are left unchanged. Assisted-by: ClaudeCode:claude-opus-4-8
| const props = defineProps<{ | ||
| /** Accessible label of the field group and its toggle */ | ||
| label: string | ||
| /** Whether the field is enabled */ | ||
| modelValue: boolean | ||
| /** Align the toggle to the first line of a multi-line field (e.g. textarea) */ | ||
| longText?: boolean | ||
| }>() | ||
|
|
||
| const emit = defineEmits<{ | ||
| (e: 'update:modelValue', value: boolean): void | ||
| }>() |
There was a problem hiding this comment.
| const props = defineProps<{ | |
| /** Accessible label of the field group and its toggle */ | |
| label: string | |
| /** Whether the field is enabled */ | |
| modelValue: boolean | |
| /** Align the toggle to the first line of a multi-line field (e.g. textarea) */ | |
| longText?: boolean | |
| }>() | |
| const emit = defineEmits<{ | |
| (e: 'update:modelValue', value: boolean): void | |
| }>() | |
| /** Whether the field is enabled */ | |
| const modelValue = defineModel<boolean>() | |
| const props = defineProps<{ | |
| /** Accessible label of the field group and its toggle */ | |
| label: string | |
| /** Align the toggle to the first line of a multi-line field (e.g. textarea) */ | |
| longText?: boolean | |
| }>() |
consistent with other component
| import { computed, nextTick, ref } from 'vue' | ||
| import NcCheckboxRadioSwitch from '@nextcloud/vue/components/NcCheckboxRadioSwitch' | ||
|
|
||
| defineOptions({ name: 'InlineToggleField' }) |
There was a problem hiding this comment.
Should already be the case for Vue3? It should be inherited by the filename
There was a problem hiding this comment.
For me it never works 😅😅
But with the two different Vue extensions, it might be the root cause tbh
| :aria-label="t('Back to sharing options')" | ||
| @click="inSettings = false"> | ||
| <template #icon> | ||
| <NcIconSvgWrapper :svg="IconArrowLeft" name="Back" :size="20" /> |
There was a problem hiding this comment.
| <NcIconSvgWrapper :svg="IconArrowLeft" name="Back" :size="20" /> | |
| <NcIconSvgWrapper :svg="IconArrowLeft" directional /> |
- no name because button has already an accessible name
- make it directional as on RTL it should be mirrored
- 20px is default
There was a problem hiding this comment.
Never knew about directional, very cool 😎
Thanks!!
| :inSettings="inSettings" | ||
| :share="share" | ||
| :shareDialogTab="shareDialogTab" | ||
| @settingsWarning="(v) => (settingsHasWarning = v)" |
There was a problem hiding this comment.
fine but if you like you can shorten it
| @settingsWarning="(v) => (settingsHasWarning = v)" | |
| @settingsWarning="settingsHasWarning = $event" |
| variant="tertiary" | ||
| @click="inSettings = true"> | ||
| <template #icon> | ||
| <NcIconSvgWrapper :svg="IconCogOutline" name="Settings" :size="20" /> |
There was a problem hiding this comment.
same here no name if the button already has a label (and its not translated)
| <NcIconSvgWrapper :svg="IconCogOutline" name="Settings" :size="20" /> | |
| <NcIconSvgWrapper :svg="IconCogOutline" /> |
| const textarea = document.createElement('textarea') | ||
| textarea.value = text | ||
| textarea.style.position = 'fixed' | ||
| textarea.style.opacity = '0' | ||
| document.body.appendChild(textarea) | ||
| textarea.select() | ||
| try { | ||
| document.execCommand('copy') | ||
| } finally { | ||
| document.body.removeChild(textarea) | ||
| } |
There was a problem hiding this comment.
In many browsers document.execCommand('copy') does not work anymore as deprecated.
| const textarea = document.createElement('textarea') | |
| textarea.value = text | |
| textarea.style.position = 'fixed' | |
| textarea.style.opacity = '0' | |
| document.body.appendChild(textarea) | |
| textarea.select() | |
| try { | |
| document.execCommand('copy') | |
| } finally { | |
| document.body.removeChild(textarea) | |
| } | |
| window.prompt(t('Please copy the share link manually'), text) |
| * | ||
| * @param node The file or folder to share | ||
| */ | ||
| export async function openSharingDialog(node: INode): Promise<unknown> { |
There was a problem hiding this comment.
| export async function openSharingDialog(node: INode): Promise<unknown> { | |
| export async function openSharingDialog(node: INode[]): Promise<unknown> { |
Should use an array to later properly have support for multi-node shares without api break
| import { openSharingDialog, isSharingDialogAvailable } from '@nextcloud/sharing/dialog' | ||
|
|
||
| // Safe to call directly: shows an error toast if the API is unavailable | ||
| await openSharingDialog(node) |
There was a problem hiding this comment.
| await openSharingDialog(node) | |
| await openSharingDialog([node]) |
Needs