mirror of https://github.com/nextcloud/server.git
Merge branch 'master' into auth-token-commands
commit
79bc6ba06c
@ -1,191 +0,0 @@
|
||||
/**
|
||||
* @copyright Copyright (c) 2023 John Molakvoæ <skjnldsv@protonmail.com>
|
||||
*
|
||||
* @author John Molakvoæ <skjnldsv@protonmail.com>
|
||||
*
|
||||
* @license AGPL-3.0-or-later
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
import type { Node } from '@nextcloud/files'
|
||||
import logger from '../logger'
|
||||
import type { Navigation } from './Navigation'
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
OC: any;
|
||||
_nc_fileactions: FileAction[] | undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export enum DefaultType {
|
||||
DEFAULT = 'default',
|
||||
HIDDEN = 'hidden',
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO: remove and move to @nextcloud/files
|
||||
* @see https://github.com/nextcloud/nextcloud-files/pull/608
|
||||
*/
|
||||
interface FileActionData {
|
||||
/** Unique ID */
|
||||
id: string
|
||||
/** Translatable string displayed in the menu */
|
||||
displayName: (files: Node[], view: Navigation) => string
|
||||
/** Svg as inline string. <svg><path fill="..." /></svg> */
|
||||
iconSvgInline: (files: Node[], view: Navigation) => string
|
||||
/** Condition wether this action is shown or not */
|
||||
enabled?: (files: Node[], view: Navigation) => boolean
|
||||
/**
|
||||
* Function executed on single file action
|
||||
* @returns true if the action was executed, false otherwise
|
||||
* @throws Error if the action failed
|
||||
*/
|
||||
exec: (file: Node, view: Navigation, dir: string) => Promise<boolean|null>,
|
||||
/**
|
||||
* Function executed on multiple files action
|
||||
* @returns true if the action was executed successfully,
|
||||
* false otherwise and null if the action is silent/undefined.
|
||||
* @throws Error if the action failed
|
||||
*/
|
||||
execBatch?: (files: Node[], view: Navigation, dir: string) => Promise<(boolean|null)[]>
|
||||
/** This action order in the list */
|
||||
order?: number,
|
||||
/** Make this action the default */
|
||||
default?: DefaultType,
|
||||
/**
|
||||
* If true, the renderInline function will be called
|
||||
*/
|
||||
inline?: (file: Node, view: Navigation) => boolean,
|
||||
/**
|
||||
* If defined, the returned html element will be
|
||||
* appended before the actions menu.
|
||||
*/
|
||||
renderInline?: (file: Node, view: Navigation) => Promise<HTMLElement | null>,
|
||||
}
|
||||
|
||||
export class FileAction {
|
||||
|
||||
private _action: FileActionData
|
||||
|
||||
constructor(action: FileActionData) {
|
||||
this.validateAction(action)
|
||||
this._action = action
|
||||
}
|
||||
|
||||
get id() {
|
||||
return this._action.id
|
||||
}
|
||||
|
||||
get displayName() {
|
||||
return this._action.displayName
|
||||
}
|
||||
|
||||
get iconSvgInline() {
|
||||
return this._action.iconSvgInline
|
||||
}
|
||||
|
||||
get enabled() {
|
||||
return this._action.enabled
|
||||
}
|
||||
|
||||
get exec() {
|
||||
return this._action.exec
|
||||
}
|
||||
|
||||
get execBatch() {
|
||||
return this._action.execBatch
|
||||
}
|
||||
|
||||
get order() {
|
||||
return this._action.order
|
||||
}
|
||||
|
||||
get default() {
|
||||
return this._action.default
|
||||
}
|
||||
|
||||
get inline() {
|
||||
return this._action.inline
|
||||
}
|
||||
|
||||
get renderInline() {
|
||||
return this._action.renderInline
|
||||
}
|
||||
|
||||
private validateAction(action: FileActionData) {
|
||||
if (!action.id || typeof action.id !== 'string') {
|
||||
throw new Error('Invalid id')
|
||||
}
|
||||
|
||||
if (!action.displayName || typeof action.displayName !== 'function') {
|
||||
throw new Error('Invalid displayName function')
|
||||
}
|
||||
|
||||
if (!action.iconSvgInline || typeof action.iconSvgInline !== 'function') {
|
||||
throw new Error('Invalid iconSvgInline function')
|
||||
}
|
||||
|
||||
if (!action.exec || typeof action.exec !== 'function') {
|
||||
throw new Error('Invalid exec function')
|
||||
}
|
||||
|
||||
// Optional properties --------------------------------------------
|
||||
if ('enabled' in action && typeof action.enabled !== 'function') {
|
||||
throw new Error('Invalid enabled function')
|
||||
}
|
||||
|
||||
if ('execBatch' in action && typeof action.execBatch !== 'function') {
|
||||
throw new Error('Invalid execBatch function')
|
||||
}
|
||||
|
||||
if ('order' in action && typeof action.order !== 'number') {
|
||||
throw new Error('Invalid order')
|
||||
}
|
||||
|
||||
if (action.default && !Object.values(DefaultType).includes(action.default)) {
|
||||
throw new Error('Invalid default')
|
||||
}
|
||||
|
||||
if ('inline' in action && typeof action.inline !== 'function') {
|
||||
throw new Error('Invalid inline function')
|
||||
}
|
||||
|
||||
if ('renderInline' in action && typeof action.renderInline !== 'function') {
|
||||
throw new Error('Invalid renderInline function')
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export const registerFileAction = function(action: FileAction): void {
|
||||
if (typeof window._nc_fileactions === 'undefined') {
|
||||
window._nc_fileactions = []
|
||||
logger.debug('FileActions initialized')
|
||||
}
|
||||
|
||||
// Check duplicates
|
||||
if (window._nc_fileactions.find(search => search.id === action.id)) {
|
||||
logger.error(`FileAction ${action.id} already registered`, { action })
|
||||
return
|
||||
}
|
||||
|
||||
window._nc_fileactions.push(action)
|
||||
}
|
||||
|
||||
export const getFileActions = function(): FileAction[] {
|
||||
return window._nc_fileactions || []
|
||||
}
|
@ -1,241 +0,0 @@
|
||||
/**
|
||||
* @copyright Copyright (c) 2022 John Molakvoæ <skjnldsv@protonmail.com>
|
||||
*
|
||||
* @author John Molakvoæ <skjnldsv@protonmail.com>
|
||||
*
|
||||
* @license AGPL-3.0-or-later
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
/* eslint-disable no-use-before-define */
|
||||
import type { Folder, Node } from '@nextcloud/files'
|
||||
import isSvg from 'is-svg'
|
||||
|
||||
import logger from '../logger.js'
|
||||
|
||||
export type ContentsWithRoot = {
|
||||
folder: Folder,
|
||||
contents: Node[]
|
||||
}
|
||||
|
||||
export interface Column {
|
||||
/** Unique column ID */
|
||||
id: string
|
||||
/** Translated column title */
|
||||
title: string
|
||||
/** The content of the cell. The element will be appended within */
|
||||
render: (node: Node, view: Navigation) => HTMLElement
|
||||
/** Function used to sort Nodes between them */
|
||||
sort?: (nodeA: Node, nodeB: Node) => number
|
||||
/**
|
||||
* Custom summary of the column to display at the end of the list.
|
||||
* Will not be displayed if nothing is provided
|
||||
*/
|
||||
summary?: (node: Node[], view: Navigation) => string
|
||||
}
|
||||
|
||||
export interface Navigation {
|
||||
/** Unique view ID */
|
||||
id: string
|
||||
/** Translated view name */
|
||||
name: string
|
||||
/** Translated accessible description of the view */
|
||||
caption?: string
|
||||
|
||||
/** Translated title of the empty view */
|
||||
emptyTitle?: string
|
||||
/** Translated description of the empty view */
|
||||
emptyCaption?: string
|
||||
|
||||
/**
|
||||
* Method return the content of the provided path
|
||||
* This ideally should be a cancellable promise.
|
||||
* promise.cancel(reason) will be called when the directory
|
||||
* change and the promise is not resolved yet.
|
||||
* You _must_ also return the current directory
|
||||
* information alongside with its content.
|
||||
*/
|
||||
getContents: (path: string) => Promise<ContentsWithRoot>
|
||||
/** The view icon as an inline svg */
|
||||
icon: string
|
||||
/** The view order */
|
||||
order: number
|
||||
|
||||
/**
|
||||
* This view column(s). Name and actions are
|
||||
* by default always included
|
||||
*/
|
||||
columns?: Column[]
|
||||
/** The empty view element to render your empty content into */
|
||||
emptyView?: (div: HTMLDivElement) => void
|
||||
/** The parent unique ID */
|
||||
parent?: string
|
||||
/** This view is sticky (sent at the bottom) */
|
||||
sticky?: boolean
|
||||
|
||||
/**
|
||||
* This view has children and is expanded or not,
|
||||
* will be overridden by user config.
|
||||
*/
|
||||
expanded?: boolean
|
||||
|
||||
/**
|
||||
* Will be used as default if the user
|
||||
* haven't customized their sorting column
|
||||
*/
|
||||
defaultSortKey?: string
|
||||
}
|
||||
|
||||
export class NavigationService {
|
||||
|
||||
private _views: Navigation[] = []
|
||||
private _currentView: Navigation | null = null
|
||||
|
||||
constructor() {
|
||||
logger.debug('Navigation service initialized')
|
||||
}
|
||||
|
||||
register(view: Navigation) {
|
||||
try {
|
||||
isValidNavigation(view)
|
||||
isUniqueNavigation(view, this._views)
|
||||
} catch (e) {
|
||||
if (e instanceof Error) {
|
||||
logger.error(e.message, { view })
|
||||
}
|
||||
throw e
|
||||
}
|
||||
|
||||
this._views.push(view)
|
||||
}
|
||||
|
||||
remove(id: string) {
|
||||
const index = this._views.findIndex(view => view.id === id)
|
||||
if (index !== -1) {
|
||||
this._views.splice(index, 1)
|
||||
}
|
||||
}
|
||||
|
||||
get views(): Navigation[] {
|
||||
return this._views
|
||||
}
|
||||
|
||||
setActive(view: Navigation | null) {
|
||||
this._currentView = view
|
||||
}
|
||||
|
||||
get active(): Navigation | null {
|
||||
return this._currentView
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Make sure the given view is unique
|
||||
* and not already registered.
|
||||
*/
|
||||
const isUniqueNavigation = function(view: Navigation, views: Navigation[]): boolean {
|
||||
if (views.find(search => search.id === view.id)) {
|
||||
throw new Error(`Navigation id ${view.id} is already registered`)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Typescript cannot validate an interface.
|
||||
* Please keep in sync with the Navigation interface requirements.
|
||||
*/
|
||||
const isValidNavigation = function(view: Navigation): boolean {
|
||||
if (!view.id || typeof view.id !== 'string') {
|
||||
throw new Error('Navigation id is required and must be a string')
|
||||
}
|
||||
|
||||
if (!view.name || typeof view.name !== 'string') {
|
||||
throw new Error('Navigation name is required and must be a string')
|
||||
}
|
||||
|
||||
if (view.columns && view.columns.length > 0
|
||||
&& (!view.caption || typeof view.caption !== 'string')) {
|
||||
throw new Error('Navigation caption is required for top-level views and must be a string')
|
||||
}
|
||||
|
||||
if (!view.getContents || typeof view.getContents !== 'function') {
|
||||
throw new Error('Navigation getContents is required and must be a function')
|
||||
}
|
||||
|
||||
if (!view.icon || typeof view.icon !== 'string' || !isSvg(view.icon)) {
|
||||
throw new Error('Navigation icon is required and must be a valid svg string')
|
||||
}
|
||||
|
||||
if (!('order' in view) || typeof view.order !== 'number') {
|
||||
throw new Error('Navigation order is required and must be a number')
|
||||
}
|
||||
|
||||
// Optional properties
|
||||
if (view.columns) {
|
||||
view.columns.forEach(isValidColumn)
|
||||
}
|
||||
|
||||
if (view.emptyView && typeof view.emptyView !== 'function') {
|
||||
throw new Error('Navigation emptyView must be a function')
|
||||
}
|
||||
|
||||
if (view.parent && typeof view.parent !== 'string') {
|
||||
throw new Error('Navigation parent must be a string')
|
||||
}
|
||||
|
||||
if ('sticky' in view && typeof view.sticky !== 'boolean') {
|
||||
throw new Error('Navigation sticky must be a boolean')
|
||||
}
|
||||
|
||||
if ('expanded' in view && typeof view.expanded !== 'boolean') {
|
||||
throw new Error('Navigation expanded must be a boolean')
|
||||
}
|
||||
|
||||
if (view.defaultSortKey && typeof view.defaultSortKey !== 'string') {
|
||||
throw new Error('Navigation defaultSortKey must be a string')
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Typescript cannot validate an interface.
|
||||
* Please keep in sync with the Column interface requirements.
|
||||
*/
|
||||
const isValidColumn = function(column: Column): boolean {
|
||||
if (!column.id || typeof column.id !== 'string') {
|
||||
throw new Error('A column id is required')
|
||||
}
|
||||
|
||||
if (!column.title || typeof column.title !== 'string') {
|
||||
throw new Error('A column title is required')
|
||||
}
|
||||
|
||||
if (!column.render || typeof column.render !== 'function') {
|
||||
throw new Error('A render function is required')
|
||||
}
|
||||
|
||||
// Optional properties
|
||||
if (column.sort && typeof column.sort !== 'function') {
|
||||
throw new Error('Column sortFunction must be a function')
|
||||
}
|
||||
|
||||
if (column.summary && typeof column.summary !== 'function') {
|
||||
throw new Error('Column summary must be a function')
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
@ -0,0 +1,29 @@
|
||||
OC.L10N.register(
|
||||
"files_reminders",
|
||||
{
|
||||
"File reminders" : "Recordatorios de archivo",
|
||||
"Reminder for {name}" : "Recordatorio para {name}",
|
||||
"View file" : "Ver archivo",
|
||||
"View folder" : "Ver carpeta",
|
||||
"Set file reminders" : "Establecer recordatorios de archivo",
|
||||
"**📣 File reminders**\n\nSet file reminders." : "**📣 Recordatorios de archivo**\n\nEstablecer recordatorios de archivo.",
|
||||
"Back" : "Volver",
|
||||
"Clear reminder" : "Borrar recordatorio",
|
||||
"Set custom reminder" : "Configurar recordatorio personalizado",
|
||||
"Later today" : "Más tarde hoy",
|
||||
"Set reminder for later today" : "Configurar recordatorio para hoy, más tarde",
|
||||
"Tomorrow" : "Mañana",
|
||||
"Set reminder for tomorrow" : "Configurar recordatorio para mañana",
|
||||
"This weekend" : "Este fin de semana",
|
||||
"Set reminder for this weekend" : "Configurar recordatorio para este fin de semana",
|
||||
"Next week" : "Semana siguiente",
|
||||
"Set reminder for next week" : "Configurar recordatorio para la semana que viene",
|
||||
"Set reminder at custom date & time" : "Establecer recordatorio a una fecha y hora personalizada",
|
||||
"Reminder set for \"{fileName}\"" : "Se estableció recordatorio para \"{fileName}\"",
|
||||
"Failed to set reminder" : "No se pudo configurar el recordatorio",
|
||||
"Please choose a valid date & time" : "Por favor, escoja una fecha y hora válidas",
|
||||
"Reminder cleared" : "Se quitó el recordatorio",
|
||||
"Failed to clear reminder" : "Fallo al quitar el recordatorio",
|
||||
"Failed to load reminder" : "Fallo al cargar el recordatorio"
|
||||
},
|
||||
"nplurals=3; plural=n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;");
|
@ -0,0 +1,27 @@
|
||||
{ "translations": {
|
||||
"File reminders" : "Recordatorios de archivo",
|
||||
"Reminder for {name}" : "Recordatorio para {name}",
|
||||
"View file" : "Ver archivo",
|
||||
"View folder" : "Ver carpeta",
|
||||
"Set file reminders" : "Establecer recordatorios de archivo",
|
||||
"**📣 File reminders**\n\nSet file reminders." : "**📣 Recordatorios de archivo**\n\nEstablecer recordatorios de archivo.",
|
||||
"Back" : "Volver",
|
||||
"Clear reminder" : "Borrar recordatorio",
|
||||
"Set custom reminder" : "Configurar recordatorio personalizado",
|
||||
"Later today" : "Más tarde hoy",
|
||||
"Set reminder for later today" : "Configurar recordatorio para hoy, más tarde",
|
||||
"Tomorrow" : "Mañana",
|
||||
"Set reminder for tomorrow" : "Configurar recordatorio para mañana",
|
||||
"This weekend" : "Este fin de semana",
|
||||
"Set reminder for this weekend" : "Configurar recordatorio para este fin de semana",
|
||||
"Next week" : "Semana siguiente",
|
||||
"Set reminder for next week" : "Configurar recordatorio para la semana que viene",
|
||||
"Set reminder at custom date & time" : "Establecer recordatorio a una fecha y hora personalizada",
|
||||
"Reminder set for \"{fileName}\"" : "Se estableció recordatorio para \"{fileName}\"",
|
||||
"Failed to set reminder" : "No se pudo configurar el recordatorio",
|
||||
"Please choose a valid date & time" : "Por favor, escoja una fecha y hora válidas",
|
||||
"Reminder cleared" : "Se quitó el recordatorio",
|
||||
"Failed to clear reminder" : "Fallo al quitar el recordatorio",
|
||||
"Failed to load reminder" : "Fallo al cargar el recordatorio"
|
||||
},"pluralForm" :"nplurals=3; plural=n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;"
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue