diff --git a/web_fullcalendar_resource/README.rst b/web_fullcalendar_resource/README.rst new file mode 100644 index 00000000000..de6e7163abe --- /dev/null +++ b/web_fullcalendar_resource/README.rst @@ -0,0 +1,128 @@ +========================= +Web Fullcalendar Resource +========================= + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:e81f0f03494be410f440285a3427bf35765f79d535198e36b2c0334d35842584 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fweb-lightgray.png?logo=github + :target: https://github.com/OCA/web/tree/18.0/web_fullcalendar_resource + :alt: OCA/web +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/web-18-0/web-18-0-web_fullcalendar_resource + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png + :target: https://runboat.odoo-community.org/builds?repo=OCA/web&target_branch=18.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This module adds a new ``resource`` view type to the web client: an OWL +calendar that displays events in vertical columns, one per resource, +based on the `FullCalendar +Scheduler `__ +plugins. + +It reuses Odoo's standard calendar view as much as possible. Only the +arch parser (to read the ``resource_field`` attribute), the model (to +load the resources) and the renderer (to enable the ``resourceTimeGrid`` +views) are specialized. + +The bundled FullCalendar Scheduler plugins +(``static/lib/fullcalendar/``, v6.1.11) are tri-licensed (commercial / +CC BY-NC-ND / GPLv3). They are used here under the GPLv3 option, enabled +through the ``schedulerLicenseKey = "GPL-My-Project-Is-Open-Source"`` +setting, which is compatible with the AGPL-3 license of this module. The +FullCalendar core (shipped by the ``web`` module) is MIT-licensed; only +the Scheduler plugins are covered by this tri-license. + +**Table of contents** + +.. contents:: + :local: + +Usage +===== + +To use the resource view, declare an ``ir.ui.view`` of type ``resource`` +whose arch root is a ```` tag. It accepts the same attributes +as the standard ```` view, plus the mandatory +``resource_field`` attribute, which points to the field used to split +events into columns. + +.. code:: xml + + + + + + +The ``resource_field`` may be a ``many2one``, ``many2many`` or +``one2many`` field; an event spanning several resources is then shown in +every matching column. + +Add the ``resource`` view mode to the related window action so the view +becomes selectable: + +.. code:: xml + + resource,calendar,list,form + +By default, only the resources actually referenced by the loaded records +are displayed as columns. A business module can show every resource of a +domain by extending ``ResourceCalendarModel`` and overriding +``showAllResources`` and ``resourceDomain()``. + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +------- + +* Le Filament + +Contributors +------------ + +- `Le Filament `__: + + - Hugo Trentesaux + +Maintainers +----------- + +This module is maintained by the OCA. + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +This module is part of the `OCA/web `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/web_fullcalendar_resource/__init__.py b/web_fullcalendar_resource/__init__.py new file mode 100644 index 00000000000..0650744f6bc --- /dev/null +++ b/web_fullcalendar_resource/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/web_fullcalendar_resource/__manifest__.py b/web_fullcalendar_resource/__manifest__.py new file mode 100644 index 00000000000..2bfa163e402 --- /dev/null +++ b/web_fullcalendar_resource/__manifest__.py @@ -0,0 +1,32 @@ +# Copyright 2026 Le Filament (https://le-filament.com) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +{ + "name": "Web Fullcalendar Resource", + "summary": "OWL calendar view with one vertical column per resource " + "(based on FullCalendar Scheduler plugins)", + "version": "18.0.1.0.0", + "author": "Le Filament, Odoo Community Association (OCA)", + "website": "https://github.com/OCA/web", + "license": "AGPL-3", + "category": "Productivity", + "development_status": "Beta", + "depends": [ + "web", + ], + "assets": { + # The Scheduler plugins must be loaded AFTER the FullCalendar core. + # They self-register on FullCalendar.globalPlugins. + # Order: premium-common -> resource -> resource-daygrid -> resource-timegrid + "web.fullcalendar_lib": [ + "web_fullcalendar_resource/static/lib/fullcalendar/premium-common/index.global.js", + "web_fullcalendar_resource/static/lib/fullcalendar/resource/index.global.js", + "web_fullcalendar_resource/static/lib/fullcalendar/resource-daygrid/index.global.js", + "web_fullcalendar_resource/static/lib/fullcalendar/resource-timegrid/index.global.js", + ], + "web.assets_backend": [ + "web_fullcalendar_resource/static/src/resource_calendar/**/*", + ], + }, + "installable": True, + "application": False, +} diff --git a/web_fullcalendar_resource/models/__init__.py b/web_fullcalendar_resource/models/__init__.py new file mode 100644 index 00000000000..7517544d507 --- /dev/null +++ b/web_fullcalendar_resource/models/__init__.py @@ -0,0 +1,2 @@ +from . import ir_ui_view +from . import ir_actions_act_window_view diff --git a/web_fullcalendar_resource/models/ir_actions_act_window_view.py b/web_fullcalendar_resource/models/ir_actions_act_window_view.py new file mode 100644 index 00000000000..18c19c38b74 --- /dev/null +++ b/web_fullcalendar_resource/models/ir_actions_act_window_view.py @@ -0,0 +1,13 @@ +# Copyright 2026 Le Filament (https://le-filament.com) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class IrActionsActWindowView(models.Model): + _inherit = "ir.actions.act_window.view" + + view_mode = fields.Selection( + selection_add=[("resource", "Resource")], + ondelete={"resource": "cascade"}, + ) diff --git a/web_fullcalendar_resource/models/ir_ui_view.py b/web_fullcalendar_resource/models/ir_ui_view.py new file mode 100644 index 00000000000..deec9f6a4f3 --- /dev/null +++ b/web_fullcalendar_resource/models/ir_ui_view.py @@ -0,0 +1,19 @@ +# Copyright 2026 Le Filament (https://le-filament.com) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class IrUiView(models.Model): + _inherit = "ir.ui.view" + + type = fields.Selection(selection_add=[("resource", "Resource")]) + + def _get_view_info(self): + # Declare the "resource" view type to the web client (icon, etc.). + # Without this, session.view_info does not contain "resource" and the + # action service rejects the action ("View types not defined resource"). + return { + **super()._get_view_info(), + "resource": {"icon": "fa fa-calendar-check-o"}, + } diff --git a/web_fullcalendar_resource/pyproject.toml b/web_fullcalendar_resource/pyproject.toml new file mode 100644 index 00000000000..4231d0cccb3 --- /dev/null +++ b/web_fullcalendar_resource/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/web_fullcalendar_resource/readme/CONTRIBUTORS.md b/web_fullcalendar_resource/readme/CONTRIBUTORS.md new file mode 100644 index 00000000000..77ff4898466 --- /dev/null +++ b/web_fullcalendar_resource/readme/CONTRIBUTORS.md @@ -0,0 +1,2 @@ +- [Le Filament](https://le-filament.com): + - Hugo Trentesaux diff --git a/web_fullcalendar_resource/readme/DESCRIPTION.md b/web_fullcalendar_resource/readme/DESCRIPTION.md new file mode 100644 index 00000000000..bd476773858 --- /dev/null +++ b/web_fullcalendar_resource/readme/DESCRIPTION.md @@ -0,0 +1,17 @@ +This module adds a new `resource` view type to the web client: an OWL +calendar that displays events in vertical columns, one per resource, +based on the [FullCalendar +Scheduler](https://fullcalendar.io/docs/resource-timegrid-view) plugins. + +It reuses Odoo's standard calendar view as much as possible. Only the +arch parser (to read the `resource_field` attribute), the model (to load +the resources) and the renderer (to enable the `resourceTimeGrid` views) +are specialized. + +The bundled FullCalendar Scheduler plugins (`static/lib/fullcalendar/`, +v6.1.11) are tri-licensed (commercial / CC BY-NC-ND / GPLv3). They are +used here under the GPLv3 option, enabled through the +`schedulerLicenseKey = "GPL-My-Project-Is-Open-Source"` setting, which +is compatible with the AGPL-3 license of this module. The FullCalendar +core (shipped by the `web` module) is MIT-licensed; only the Scheduler +plugins are covered by this tri-license. diff --git a/web_fullcalendar_resource/readme/USAGE.md b/web_fullcalendar_resource/readme/USAGE.md new file mode 100644 index 00000000000..b046a6097ad --- /dev/null +++ b/web_fullcalendar_resource/readme/USAGE.md @@ -0,0 +1,28 @@ +To use the resource view, declare an `ir.ui.view` of type `resource` +whose arch root is a `` tag. It accepts the same attributes as +the standard `` view, plus the mandatory `resource_field` +attribute, which points to the field used to split events into columns. + +``` xml + + + + +``` + +The `resource_field` may be a `many2one`, `many2many` or `one2many` +field; an event spanning several resources is then shown in every +matching column. + +Add the `resource` view mode to the related window action so the view +becomes selectable: + +``` xml +resource,calendar,list,form +``` + +By default, only the resources actually referenced by the loaded records +are displayed as columns. A business module can show every resource of a +domain by extending `ResourceCalendarModel` and overriding +`showAllResources` and `resourceDomain()`. diff --git a/web_fullcalendar_resource/static/description/index.html b/web_fullcalendar_resource/static/description/index.html new file mode 100644 index 00000000000..870f317c46f --- /dev/null +++ b/web_fullcalendar_resource/static/description/index.html @@ -0,0 +1,469 @@ + + + + + +Web Fullcalendar Resource + + + +
+

Web Fullcalendar Resource

+ + +

Beta License: AGPL-3 OCA/web Translate me on Weblate Try me on Runboat

+

This module adds a new resource view type to the web client: an OWL +calendar that displays events in vertical columns, one per resource, +based on the FullCalendar +Scheduler +plugins.

+

It reuses Odoo’s standard calendar view as much as possible. Only the +arch parser (to read the resource_field attribute), the model (to +load the resources) and the renderer (to enable the resourceTimeGrid +views) are specialized.

+

The bundled FullCalendar Scheduler plugins +(static/lib/fullcalendar/, v6.1.11) are tri-licensed (commercial / +CC BY-NC-ND / GPLv3). They are used here under the GPLv3 option, enabled +through the schedulerLicenseKey = "GPL-My-Project-Is-Open-Source" +setting, which is compatible with the AGPL-3 license of this module. The +FullCalendar core (shipped by the web module) is MIT-licensed; only +the Scheduler plugins are covered by this tri-license.

+

Table of contents

+ +
+

Usage

+

To use the resource view, declare an ir.ui.view of type resource +whose arch root is a <resource> tag. It accepts the same attributes +as the standard <calendar> view, plus the mandatory +resource_field attribute, which points to the field used to split +events into columns.

+
+<resource date_start="date_start" date_stop="date_stop"
+          resource_field="resource_id" mode="day">
+    <field name="name"/>
+    <field name="resource_id" filters="1"/>
+</resource>
+
+

The resource_field may be a many2one, many2many or +one2many field; an event spanning several resources is then shown in +every matching column.

+

Add the resource view mode to the related window action so the view +becomes selectable:

+
+<field name="view_mode">resource,calendar,list,form</field>
+
+

By default, only the resources actually referenced by the loaded records +are displayed as columns. A business module can show every resource of a +domain by extending ResourceCalendarModel and overriding +showAllResources and resourceDomain().

+
+
+

Bug Tracker

+

Bugs are tracked on GitHub Issues. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +feedback.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • Le Filament
  • +
+
+
+

Contributors

+ +
+
+

Maintainers

+

This module is maintained by the OCA.

+ +Odoo Community Association + +

OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use.

+

This module is part of the OCA/web project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+ + diff --git a/web_fullcalendar_resource/static/lib/fullcalendar/LICENSE.txt b/web_fullcalendar_resource/static/lib/fullcalendar/LICENSE.txt new file mode 100644 index 00000000000..dbcf277b848 --- /dev/null +++ b/web_fullcalendar_resource/static/lib/fullcalendar/LICENSE.txt @@ -0,0 +1,10 @@ +FullCalendar Scheduler v6.1.11 — (c) Adam Shaw — https://fullcalendar.io +Paquets : premium-common, resource, resource-daygrid, resource-timegrid. + +Tri-licencié : commerciale / CC BY-NC-ND 4.0 / GPLv3. +Utilisé ici sous GPLv3 (schedulerLicenseKey = "GPL-My-Project-Is-Open-Source"). +=> copyleft : tout projet redistribuant ces plugins doit rester sous licence +compatible GPL (GPLv3/AGPLv3). Usage propriétaire = licence commerciale. +Voir https://fullcalendar.io/license + +Le cœur de FullCalendar (module Odoo "web") est sous licence MIT. diff --git a/web_fullcalendar_resource/static/lib/fullcalendar/premium-common/index.global.js b/web_fullcalendar_resource/static/lib/fullcalendar/premium-common/index.global.js new file mode 100644 index 00000000000..3f2c6c23d68 --- /dev/null +++ b/web_fullcalendar_resource/static/lib/fullcalendar/premium-common/index.global.js @@ -0,0 +1,6 @@ +/*! +FullCalendar Premium Common v6.1.11 +Docs & License: https://fullcalendar.io/docs/premium +(c) 2023 Adam Shaw +*/ +FullCalendar.PremiumCommon=function(e,n,t,l){"use strict";const r=["GPL-My-Project-Is-Open-Source","CC-Attribution-NonCommercial-NoDerivatives"],i={position:"absolute",zIndex:99999,bottom:"1px",left:"1px",background:"#eee",borderColor:"#ddd",borderStyle:"solid",borderWidth:"1px 1px 0 0",padding:"2px 4px",fontSize:"12px",borderTopRightRadius:"3px"};const o={schedulerLicenseKey:String};var a=n.createPlugin({name:"@fullcalendar/premium-common",premiumReleaseDate:"2024-02-20",optionRefiners:o,viewContainerAppends:[function(e){let n=e.options.schedulerLicenseKey,o="undefined"!=typeof window?window.location.href:"";if(!/\w+:\/\/fullcalendar\.io\/|\/examples\/[\w-]+\.html$/.test(o)){let o=function(e,n){if(-1!==r.indexOf(e))return"valid";const l=(e||"").match(/^(\d+)-fcs-(\d+)$/);if(l&&10===l[1].length){const e=new Date(1e3*parseInt(l[2],10)),r=t.config.mockSchedulerReleaseDate||n;if(t.isValidDate(r)){return t.addDays(r,-372)Object.assign(Object.assign(Object.assign({},e),t),{isStart:e.isStart&&t.isStart,isEnd:e.isEnd&&t.isEnd}))}}class R extends l.DateComponent{constructor(){super(...arguments),this.splitter=new n.VResourceSplitter,this.slicers={},this.joiner=new y,this.tableRef=o.createRef(),this.isHitComboAllowed=(e,t)=>1===this.props.resourceDayTableModel.dayTableModel.colCnt||e.dateSpan.resourceId===t.dateSpan.resourceId}render(){let{props:e,context:t}=this,{resourceDayTableModel:r,nextDayThreshold:a,dateProfile:s}=e,n=this.splitter.splitProps(e);this.slicers=l.mapHash(n,(e,t)=>this.slicers[t]||new i.DayTableSlicer);let d=l.mapHash(this.slicers,(e,l)=>e.sliceProps(n[l],s,a,t,r.dayTableModel));return o.createElement(i.Table,Object.assign({forPrint:e.forPrint,ref:this.tableRef},this.joiner.joinProps(d,r),{cells:r.cells,dateProfile:s,colGroupNode:e.colGroupNode,tableMinWidth:e.tableMinWidth,renderRowIntro:e.renderRowIntro,dayMaxEvents:e.dayMaxEvents,dayMaxEventRows:e.dayMaxEventRows,showWeekNumbers:e.showWeekNumbers,expandRows:e.expandRows,headerAlignElRef:e.headerAlignElRef,clientWidth:e.clientWidth,clientHeight:e.clientHeight,isHitComboAllowed:this.isHitComboAllowed}))}}class b extends i.TableView{constructor(){super(...arguments),this.flattenResources=l.memoize(n.flattenResources),this.buildResourceDayTableModel=l.memoize(f),this.headerRef=o.createRef(),this.tableRef=o.createRef()}render(){let{props:e,context:t}=this,{options:r}=t,a=r.resourceOrder||n.DEFAULT_RESOURCE_ORDER,s=this.flattenResources(e.resourceStore,a),l=this.buildResourceDayTableModel(e.dateProfile,t.dateProfileGenerator,s,r.datesAboveResources,t),i=r.dayHeaders&&o.createElement(n.ResourceDayHeader,{ref:this.headerRef,resources:s,dateProfile:e.dateProfile,dates:l.dayTableModel.headerDates,datesRepDistinctDays:!0}),d=t=>o.createElement(R,{ref:this.tableRef,dateProfile:e.dateProfile,resourceDayTableModel:l,businessHours:e.businessHours,eventStore:e.eventStore,eventUiBases:e.eventUiBases,dateSelection:e.dateSelection,eventSelection:e.eventSelection,eventDrag:e.eventDrag,eventResize:e.eventResize,nextDayThreshold:r.nextDayThreshold,tableMinWidth:t.tableMinWidth,colGroupNode:t.tableColGroupNode,dayMaxEvents:r.dayMaxEvents,dayMaxEventRows:r.dayMaxEventRows,showWeekNumbers:r.weekNumbers,expandRows:!e.isHeightAuto,headerAlignElRef:this.headerElRef,clientWidth:t.clientWidth,clientHeight:t.clientHeight,forPrint:e.forPrint});return r.dayMinWidth?this.renderHScrollLayout(i,d,l.colCnt,r.dayMinWidth):this.renderSimpleLayout(i,d)}}function f(e,t,r,a,s){let l=i.buildDayTableModel(e,t);return a?new n.DayResourceTableModel(l,r,s):new n.ResourceDayTableModel(l,r,s)}var p=t.createPlugin({name:"@fullcalendar/resource-daygrid",premiumReleaseDate:"2024-02-20",deps:[u.default,c.default,h.default],initialView:"resourceDayGridDay",views:{resourceDayGrid:{type:"dayGrid",component:b,needsResourceData:!0},resourceDayGridDay:{type:"resourceDayGrid",duration:{days:1}},resourceDayGridWeek:{type:"resourceDayGrid",duration:{weeks:1}},resourceDayGridMonth:{type:"resourceDayGrid",duration:{months:1},fixedWeekCount:!0}}}),D={__proto__:null,ResourceDayTableView:b,ResourceDayTable:R};return t.globalPlugins.push(p),e.Internal=D,e.default=p,Object.defineProperty(e,"__esModule",{value:!0}),e}({},FullCalendar,FullCalendar.PremiumCommon,FullCalendar.Resource,FullCalendar.DayGrid,FullCalendar.Internal,FullCalendar.Preact,FullCalendar.DayGrid.Internal,FullCalendar.Resource.Internal); \ No newline at end of file diff --git a/web_fullcalendar_resource/static/lib/fullcalendar/resource-timegrid/index.global.js b/web_fullcalendar_resource/static/lib/fullcalendar/resource-timegrid/index.global.js new file mode 100644 index 00000000000..d4a87ce2270 --- /dev/null +++ b/web_fullcalendar_resource/static/lib/fullcalendar/resource-timegrid/index.global.js @@ -0,0 +1,6 @@ +/*! +FullCalendar Resource Time Grid Plugin v6.1.11 +Docs & License: https://fullcalendar.io/docs/vertical-resource-view +(c) 2023 Adam Shaw +*/ +FullCalendar.ResourceTimeGrid=function(e,t,l,o,r,s,i,a,n,d){"use strict";function u(e){return e&&e.__esModule?e:{default:e}}var c=u(l),h=u(o),m=u(r);class p extends n.VResourceJoiner{transformSeg(e,t,l){return[Object.assign(Object.assign({},e),{col:t.computeCol(e.col,l)})]}}class R extends s.DateComponent{constructor(){super(...arguments),this.buildDayRanges=s.memoize(a.buildDayRanges),this.splitter=new n.VResourceSplitter,this.slicers={},this.joiner=new p,this.timeColsRef=i.createRef(),this.isHitComboAllowed=(e,t)=>1===this.dayRanges.length||e.dateSpan.resourceId===t.dateSpan.resourceId}render(){let{props:e,context:t}=this,{dateEnv:l,options:o}=t,{dateProfile:r,resourceDayTableModel:n}=e,d=this.dayRanges=this.buildDayRanges(n.dayTableModel,r,l),u=this.splitter.splitProps(e);this.slicers=s.mapHash(u,(e,t)=>this.slicers[t]||new a.DayTimeColsSlicer);let c=s.mapHash(this.slicers,(e,l)=>e.sliceProps(u[l],r,null,t,d));return i.createElement(s.NowTimer,{unit:o.nowIndicator?"minute":"day"},(t,l)=>i.createElement(a.TimeCols,Object.assign({ref:this.timeColsRef},this.joiner.joinProps(c,n),{dateProfile:r,axis:e.axis,slotDuration:e.slotDuration,slatMetas:e.slatMetas,cells:n.cells[0],tableColGroupNode:e.tableColGroupNode,tableMinWidth:e.tableMinWidth,clientWidth:e.clientWidth,clientHeight:e.clientHeight,expandRows:e.expandRows,nowDate:t,nowIndicatorSegs:o.nowIndicator&&this.buildNowIndicatorSegs(t),todayRange:l,onScrollTopRequest:e.onScrollTopRequest,forPrint:e.forPrint,onSlatCoords:e.onSlatCoords,isHitComboAllowed:this.isHitComboAllowed})))}buildNowIndicatorSegs(e){let t=this.slicers[""].sliceNowDate(e,this.props.dateProfile,this.context.options.nextDayThreshold,this.context,this.dayRanges);return this.joiner.expandSegs(this.props.resourceDayTableModel,t)}}class b extends a.TimeColsView{constructor(){super(...arguments),this.flattenResources=s.memoize(n.flattenResources),this.buildResourceTimeColsModel=s.memoize(y),this.buildSlatMetas=s.memoize(a.buildSlatMetas)}render(){let{props:e,context:t}=this,{options:l,dateEnv:o}=t,{dateProfile:r}=e,s=this.allDaySplitter.splitProps(e),a=l.resourceOrder||n.DEFAULT_RESOURCE_ORDER,u=this.flattenResources(e.resourceStore,a),c=this.buildResourceTimeColsModel(r,t.dateProfileGenerator,u,l.datesAboveResources,t),h=this.buildSlatMetas(r.slotMinTime,r.slotMaxTime,l.slotLabelInterval,l.slotDuration,o),{dayMinWidth:m}=l,p=!m,b=m,y=l.dayHeaders&&i.createElement(n.ResourceDayHeader,{resources:u,dates:c.dayTableModel.headerDates,dateProfile:r,datesRepDistinctDays:!0,renderIntro:p?this.renderHeadAxis:null}),D=!1!==l.allDaySlot&&(t=>i.createElement(d.ResourceDayTable,Object.assign({},s.allDay,{dateProfile:r,resourceDayTableModel:c,nextDayThreshold:l.nextDayThreshold,tableMinWidth:t.tableMinWidth,colGroupNode:t.tableColGroupNode,renderRowIntro:p?this.renderTableRowAxis:null,showWeekNumbers:!1,expandRows:!1,headerAlignElRef:this.headerElRef,clientWidth:t.clientWidth,clientHeight:t.clientHeight,forPrint:e.forPrint},this.getAllDayMaxEventProps()))),C=t=>i.createElement(R,Object.assign({},s.timed,{dateProfile:r,axis:p,slotDuration:l.slotDuration,slatMetas:h,resourceDayTableModel:c,tableColGroupNode:t.tableColGroupNode,tableMinWidth:t.tableMinWidth,clientWidth:t.clientWidth,clientHeight:t.clientHeight,onSlatCoords:this.handleSlatCoords,expandRows:t.expandRows,forPrint:e.forPrint,onScrollTopRequest:this.handleScrollTopRequest}));return b?this.renderHScrollLayout(y,D,C,c.colCnt,m,h,this.state.slatCoords):this.renderSimpleLayout(y,D,C)}}function y(e,t,l,o,r){let s=a.buildTimeColsModel(e,t);return o?new n.DayResourceTableModel(s,l,r):new n.ResourceDayTableModel(s,l,r)}var D=t.createPlugin({name:"@fullcalendar/resource-timegrid",premiumReleaseDate:"2024-02-20",deps:[c.default,h.default,m.default],initialView:"resourceTimeGridDay",views:{resourceTimeGrid:{type:"timeGrid",component:b,needsResourceData:!0},resourceTimeGridDay:{type:"resourceTimeGrid",duration:{days:1}},resourceTimeGridWeek:{type:"resourceTimeGrid",duration:{weeks:1}}}}),C={__proto__:null,ResourceDayTimeColsView:b,ResourceDayTimeCols:R};return t.globalPlugins.push(D),e.Internal=C,e.default=D,Object.defineProperty(e,"__esModule",{value:!0}),e}({},FullCalendar,FullCalendar.PremiumCommon,FullCalendar.Resource,FullCalendar.TimeGrid,FullCalendar.Internal,FullCalendar.Preact,FullCalendar.TimeGrid.Internal,FullCalendar.Resource.Internal,FullCalendar.ResourceDayGrid.Internal); \ No newline at end of file diff --git a/web_fullcalendar_resource/static/lib/fullcalendar/resource/index.global.js b/web_fullcalendar_resource/static/lib/fullcalendar/resource/index.global.js new file mode 100644 index 00000000000..f107f35724b --- /dev/null +++ b/web_fullcalendar_resource/static/lib/fullcalendar/resource/index.global.js @@ -0,0 +1,6 @@ +/*! +FullCalendar Resource Plugin v6.1.11 +Docs & License: https://fullcalendar.io/docs/premium +(c) 2023 Adam Shaw +*/ +FullCalendar.Resource=function(e,t,r,s,n){"use strict";function o(e){return e&&e.__esModule?e:{default:e}}var i=o(r);function u(e,t){let{resourceEditable:r}=e;if(null==r){let s=e.sourceId&&t.getCurrentData().eventSources[e.sourceId];s&&(r=s.extendedProps.resourceEditable),null==r&&(r=t.options.eventResourceEditable,null==r&&(r=t.options.editable))}return r}function a(e,t,r,n){if(t){let t=function(e,t){let r={};for(let s in e){let n=e[s];for(let e of t[n.defId].resourceIds)r[e]=!0}return r}(function(e,t){return s.filterHash(e,e=>s.rangesIntersect(e.range,t))}(r.instances,n),r.defs);return Object.assign(t,function(e,t){let r={};for(let s in e){let e;for(;(e=t[s])&&(s=e.parentId,s);)r[s]=!0}return r}(t,e)),s.filterHash(e,(e,r)=>t[r])}return e}function c(e){return s.mapHash(e,e=>e.ui)}function l(e,t,r){return s.mapHash(e,(e,n)=>n?function(e,t,r){let n=[];for(let e of t.resourceIds)r[e]&&n.unshift(r[e]);return n.unshift(e),s.combineEventUis(n)}(e,t[n],r):e)}let d=[];function p(e){d.push(e)}function f(e){return d[e]}function g(){return d}const h={id:String,resources:s.identity,url:String,method:String,startParam:String,endParam:String,timeZoneParam:String,extraParams:s.identity};function m(e){let t;if("string"==typeof e?t={url:e}:"function"==typeof e||Array.isArray(e)?t={resources:e}:"object"==typeof e&&e&&(t=e),t){let{refined:r,extra:n}=s.refineProps(t,h);!function(e){for(let t in e)console.warn(`Unknown resource prop '${t}'`)}(n);let o=function(e){let t=g();for(let r=t.length-1;r>=0;r-=1){let s=t[r].parseMeta(e);if(s)return{meta:s,sourceDefId:r}}return null}(r);if(o)return{_raw:e,sourceId:s.guid(),sourceDefId:o.sourceDefId,meta:o.meta,publicId:r.id||"",isFetching:!1,latestFetchId:"",fetchRange:null}}return null}function R(e,t,r){let{options:n,dateProfile:o}=r;if(!e||!t)return E(n.initialResources||n.resources,o.activeRange,n.refetchResourcesOnNavigate,r);switch(t.type){case"RESET_RESOURCE_SOURCE":return E(t.resourceSourceInput,o.activeRange,n.refetchResourcesOnNavigate,r);case"PREV":case"NEXT":case"CHANGE_DATE":case"CHANGE_VIEW_TYPE":return function(e,t,r,n){if(r&&!function(e){return Boolean(f(e.sourceDefId).ignoreRange)}(e)&&(!e.fetchRange||!s.rangesEqual(e.fetchRange,t)))return S(e,t,n);return e}(e,o.activeRange,n.refetchResourcesOnNavigate,r);case"RECEIVE_RESOURCES":case"RECEIVE_RESOURCE_ERROR":return function(e,t,r){if(t===e.latestFetchId)return Object.assign(Object.assign({},e),{isFetching:!1,fetchRange:r});return e}(e,t.fetchId,t.fetchRange);case"REFETCH_RESOURCES":return S(e,o.activeRange,r);default:return e}}function E(e,t,r,s){if(e){let n=m(e);return n=S(n,r?t:null,s),n}return null}function S(e,t,r){let n=f(e.sourceDefId),o=s.guid();return n.fetch({resourceSource:e,range:t,context:r},e=>{r.dispatch({type:"RECEIVE_RESOURCES",fetchId:o,fetchRange:t,rawResources:e.rawResources})},e=>{r.dispatch({type:"RECEIVE_RESOURCE_ERROR",fetchId:o,fetchRange:t,error:e})}),Object.assign(Object.assign({},e),{isFetching:!0,latestFetchId:o})}const C={id:String,parentId:String,children:s.identity,title:String,businessHours:s.identity,extendedProps:s.identity,eventEditable:Boolean,eventStartEditable:Boolean,eventDurationEditable:Boolean,eventConstraint:s.identity,eventOverlap:Boolean,eventAllow:s.identity,eventClassNames:s.parseClassNames,eventBackgroundColor:String,eventBorderColor:String,eventTextColor:String,eventColor:String};function b(e,t="",r,n){let{refined:o,extra:i}=s.refineProps(e,C),u={id:o.id||"_fc:"+s.guid(),parentId:o.parentId||t,title:o.title||"",businessHours:o.businessHours?s.parseBusinessHours(o.businessHours,n):null,ui:s.createEventUi({editable:o.eventEditable,startEditable:o.eventStartEditable,durationEditable:o.eventDurationEditable,constraint:o.eventConstraint,overlap:o.eventOverlap,allow:o.eventAllow,classNames:o.eventClassNames,backgroundColor:o.eventBackgroundColor,borderColor:o.eventBorderColor,textColor:o.eventTextColor,color:o.eventColor},n),extendedProps:Object.assign(Object.assign({},i),o.extendedProps)};if(Object.freeze(u.ui.classNames),Object.freeze(u.extendedProps),r[u.id]);else if(r[u.id]=u,o.children)for(let e of o.children)b(e,u.id,r,n);return u}function v(e){return 0===e.indexOf("_fc:")?"":e}function y(e,t,r,s){if(!e||!t)return{};switch(t.type){case"RECEIVE_RESOURCES":return function(e,t,r,s,n){if(s.latestFetchId===r){let e={};for(let r of t)b(r,"",e,n);return e}return e}(e,t.rawResources,t.fetchId,r,s);case"ADD_RESOURCE":return n=e,o=t.resourceHash,Object.assign(Object.assign({},n),o);case"REMOVE_RESOURCE":return function(e,t){let r=Object.assign({},e);delete r[t];for(let e in r)r[e].parentId===t&&(r[e]=Object.assign(Object.assign({},r[e]),{parentId:""}));return r}(e,t.resourceId);case"SET_RESOURCE_PROP":return function(e,t,r,s){let n=e[t];if(n)return Object.assign(Object.assign({},e),{[t]:Object.assign(Object.assign({},n),{[r]:s})});return e}(e,t.resourceId,t.propName,t.propValue);case"SET_RESOURCE_EXTENDED_PROP":return function(e,t,r,s){let n=e[t];if(n)return Object.assign(Object.assign({},e),{[t]:Object.assign(Object.assign({},n),{extendedProps:Object.assign(Object.assign({},n.extendedProps),{[r]:s})})});return e}(e,t.resourceId,t.propName,t.propValue);default:return e}var n,o}const I={resourceId:String,resourceIds:s.identity,resourceEditable:Boolean};class O{constructor(e,t){this._context=e,this._resource=t}setProp(e,t){let r=this._resource;this._context.dispatch({type:"SET_RESOURCE_PROP",resourceId:r.id,propName:e,propValue:t}),this.sync(r)}setExtendedProp(e,t){let r=this._resource;this._context.dispatch({type:"SET_RESOURCE_EXTENDED_PROP",resourceId:r.id,propName:e,propValue:t}),this.sync(r)}sync(e){let t=this._context,r=e.id;this._resource=t.getCurrentData().resourceStore[r],t.emitter.trigger("resourceChange",{oldResource:new O(t,e),resource:this,revert(){t.dispatch({type:"ADD_RESOURCE",resourceHash:{[r]:e}})}})}remove(){let e=this._context,t=this._resource,r=t.id;e.dispatch({type:"REMOVE_RESOURCE",resourceId:r}),e.emitter.trigger("resourceRemove",{resource:this,revert(){e.dispatch({type:"ADD_RESOURCE",resourceHash:{[r]:t}})}})}getParent(){let e=this._context,t=this._resource.parentId;return t?new O(e,e.getCurrentData().resourceStore[t]):null}getChildren(){let e=this._resource.id,t=this._context,{resourceStore:r}=t.getCurrentData(),s=[];for(let n in r)r[n].parentId===e&&s.push(new O(t,r[n]));return s}getEvents(){let e=this._resource.id,t=this._context,{defs:r,instances:n}=t.getCurrentData().eventStore,o=[];for(let i in n){let u=n[i],a=r[u.defId];-1!==a.resourceIds.indexOf(e)&&o.push(new s.EventImpl(t,a,u))}return o}get id(){return v(this._resource.id)}get title(){return this._resource.title}get eventConstraint(){return this._resource.ui.constraints[0]||null}get eventOverlap(){return this._resource.ui.overlap}get eventAllow(){return this._resource.ui.allows[0]||null}get eventBackgroundColor(){return this._resource.ui.backgroundColor}get eventBorderColor(){return this._resource.ui.borderColor}get eventTextColor(){return this._resource.ui.textColor}get eventClassNames(){return this._resource.ui.classNames}get extendedProps(){return this._resource.extendedProps}toPlainObject(e={}){let t=this._resource,{ui:r}=t,s=this.id,n={};return s&&(n.id=s),t.title&&(n.title=t.title),e.collapseEventColor&&r.backgroundColor&&r.backgroundColor===r.borderColor?n.eventColor=r.backgroundColor:(r.backgroundColor&&(n.eventBackgroundColor=r.backgroundColor),r.borderColor&&(n.eventBorderColor=r.borderColor)),r.textColor&&(n.eventTextColor=r.textColor),r.classNames.length&&(n.eventClassNames=r.classNames),Object.keys(t.extendedProps).length&&(e.collapseExtendedProps?Object.assign(n,t.extendedProps):n.extendedProps=t.extendedProps),n}toJSON(){return this.toPlainObject()}}s.CalendarImpl.prototype.addResource=function(e,t=!0){let r,s,n=this.getCurrentData();e instanceof O?(s=e._resource,r={[s.id]:s}):(r={},s=b(e,"",r,n)),this.dispatch({type:"ADD_RESOURCE",resourceHash:r}),t&&this.trigger("_scrollRequest",{resourceId:s.id});let o=new O(n,s);return n.emitter.trigger("resourceAdd",{resource:o,revert:()=>{this.dispatch({type:"REMOVE_RESOURCE",resourceId:s.id})}}),o},s.CalendarImpl.prototype.getResourceById=function(e){e=String(e);let t=this.getCurrentData();if(t.resourceStore){let r=t.resourceStore[e];if(r)return new O(t,r)}return null},s.CalendarImpl.prototype.getResources=function(){let e=this.getCurrentData(),{resourceStore:t}=e,r=[];if(t)for(let s in t)r.push(new O(e,t[s]));return r},s.CalendarImpl.prototype.getTopLevelResources=function(){let e=this.getCurrentData(),{resourceStore:t}=e,r=[];if(t)for(let s in t)t[s].parentId||r.push(new O(e,t[s]));return r},s.CalendarImpl.prototype.refetchResources=function(){this.dispatch({type:"REFETCH_RESOURCES"})};class D extends s.Splitter{getKeyInfo(e){return Object.assign({"":{}},e.resourceStore)}getKeysForDateSpan(e){return[e.resourceId||""]}getKeysForEventDef(e){let t=e.resourceIds;return t.length?t:[""]}}function x(e,t){return Object.assign(Object.assign({},t),{constraints:_(e,t.constraints)})}function _(e,t){return t.map(t=>{let r=t.defs;if(r)for(let t in r){let s=r[t].resourceIds;if(s.length&&-1===s.indexOf(e))return!1}return t})}const P={resources:function(e,t){t.getCurrentData().resourceSource._raw!==e&&t.dispatch({type:"RESET_RESOURCE_SOURCE",resourceSourceInput:e})}};const j=s.parseFieldSpecs("id,title");const w={initialResources:s.identity,resources:s.identity,eventResourceEditable:Boolean,refetchResourcesOnNavigate:Boolean,resourceOrder:s.parseFieldSpecs,filterResourcesWithEvents:Boolean,resourceGroupField:String,resourceAreaWidth:s.identity,resourceAreaColumns:s.identity,resourcesInitiallyExpanded:Boolean,datesAboveResources:Boolean,needsResourceData:Boolean,resourceAreaHeaderClassNames:s.identity,resourceAreaHeaderContent:s.identity,resourceAreaHeaderDidMount:s.identity,resourceAreaHeaderWillUnmount:s.identity,resourceGroupLabelClassNames:s.identity,resourceGroupLabelContent:s.identity,resourceGroupLabelDidMount:s.identity,resourceGroupLabelWillUnmount:s.identity,resourceLabelClassNames:s.identity,resourceLabelContent:s.identity,resourceLabelDidMount:s.identity,resourceLabelWillUnmount:s.identity,resourceLaneClassNames:s.identity,resourceLaneContent:s.identity,resourceLaneDidMount:s.identity,resourceLaneWillUnmount:s.identity,resourceGroupLaneClassNames:s.identity,resourceGroupLaneContent:s.identity,resourceGroupLaneDidMount:s.identity,resourceGroupLaneWillUnmount:s.identity},T={resourcesSet:s.identity,resourceAdd:s.identity,resourceChange:s.identity,resourceRemove:s.identity};s.EventImpl.prototype.getResources=function(){let{calendarApi:e}=this._context;return this._def.resourceIds.map(t=>e.getResourceById(t))},s.EventImpl.prototype.setResources=function(e){let t=[];for(let r of e){let e=null;"string"==typeof r?e=r:"number"==typeof r?e=String(r):r instanceof O?e=r.id:console.warn("unknown resource type: "+r),e&&t.push(e)}this.mutate({standardProps:{resourceIds:t}})},p({ignoreRange:!0,parseMeta:e=>Array.isArray(e.resources)?e.resources:null,fetch(e,t){t({rawResources:e.resourceSource.meta})}}),p({parseMeta:e=>"function"==typeof e.resources?e.resources:null,fetch(e,t,r){const n=e.context.dateEnv,o=e.resourceSource.meta,i=e.range?{start:n.toDate(e.range.start),end:n.toDate(e.range.end),startStr:n.formatIso(e.range.start),endStr:n.formatIso(e.range.end),timeZone:n.timeZone}:{};s.unpromisify(o.bind(null,i),e=>t({rawResources:e}),r)}}),p({parseMeta:e=>e.url?{url:e.url,method:(e.method||"GET").toUpperCase(),extraParams:e.extraParams}:null,fetch(e,t,r){const n=e.resourceSource.meta,o=function(e,t,r){let s,n,o,i,{dateEnv:u,options:a}=r,c={};t&&(s=e.startParam,null==s&&(s=a.startParam),n=e.endParam,null==n&&(n=a.endParam),o=e.timeZoneParam,null==o&&(o=a.timeZoneParam),c[s]=u.formatIso(t.start),c[n]=u.formatIso(t.end),"local"!==u.timeZone&&(c[o]=u.timeZone));i="function"==typeof e.extraParams?e.extraParams():e.extraParams||{};return Object.assign(c,i),c}(n,e.range,e.context);s.requestJson(n.method,n.url,o).then(([e,r])=>{t({rawResources:e,response:r})},r)}});var U=t.createPlugin({name:"@fullcalendar/resource",premiumReleaseDate:"2024-02-20",deps:[i.default],reducers:[function(e,t,r){let s=R(e&&e.resourceSource,t,r);return{resourceSource:s,resourceStore:y(e&&e.resourceStore,t,s,r),resourceEntityExpansions:function(e,t){if(!e||!t)return{};switch(t.type){case"SET_RESOURCE_ENTITY_EXPANDED":return Object.assign(Object.assign({},e),{[t.id]:t.isExpanded});default:return e}}(e&&e.resourceEntityExpansions,t)}}],isLoadingFuncs:[e=>e.resourceSource&&e.resourceSource.isFetching],eventRefiners:I,eventDefMemberAdders:[function(e){return{resourceIds:(t=e.resourceIds,(t||[]).map(e=>String(e))).concat(e.resourceId?[e.resourceId]:[]),resourceEditable:e.resourceEditable};var t}],isDraggableTransformers:[function(e,t,r,s){if(!e){let e=s.getCurrentData();if(e.viewSpecs[e.currentViewType].optionDefaults.needsResourceData&&u(t,s))return!0}return e}],eventDragMutationMassagers:[function(e,t,r){let s=t.dateSpan.resourceId,n=r.dateSpan.resourceId;s&&n&&s!==n&&(e.resourceMutation={matchResourceId:s,setResourceId:n})}],eventDefMutationAppliers:[function(e,t,r){let s=t.resourceMutation;if(s&&u(e,r)){let t=e.resourceIds.indexOf(s.matchResourceId);if(-1!==t){let r=e.resourceIds.slice();r.splice(t,1),-1===r.indexOf(s.setResourceId)&&r.push(s.setResourceId),e.resourceIds=r}}}],dateSelectionTransformers:[function(e,t){let r=e.dateSpan.resourceId,s=t.dateSpan.resourceId;return r&&s?{resourceId:r}:null}],datePointTransforms:[function(e,t){return e.resourceId?{resource:t.calendarApi.getResourceById(e.resourceId)}:{}}],dateSpanTransforms:[function(e,t){return e.resourceId?{resource:t.calendarApi.getResourceById(e.resourceId)}:{}}],viewPropsTransformers:[class{constructor(){this.filterResources=s.memoize(a)}transform(e,t){return t.viewSpec.optionDefaults.needsResourceData?{resourceStore:this.filterResources(t.resourceStore,t.options.filterResourcesWithEvents,t.eventStore,t.dateProfile.activeRange),resourceEntityExpansions:t.resourceEntityExpansions}:null}},class{constructor(){this.buildResourceEventUis=s.memoize(c,s.isPropsEqual),this.injectResourceEventUis=s.memoize(l)}transform(e,t){return t.viewSpec.optionDefaults.needsResourceData?null:{eventUiBases:this.injectResourceEventUis(e.eventUiBases,e.eventStore.defs,this.buildResourceEventUis(t.resourceStore))}}}],isPropsValid:function(e,t){let r=(new D).splitProps(Object.assign(Object.assign({},e),{resourceStore:t.getCurrentData().resourceStore}));for(let e in r){let n=r[e];if(e&&r[""]&&(n=Object.assign(Object.assign({},n),{eventStore:s.mergeEventStores(r[""].eventStore,n.eventStore),eventUiBases:Object.assign(Object.assign({},r[""].eventUiBases),n.eventUiBases)})),!s.isPropsValid(n,t,{resourceId:e},x.bind(null,e)))return!1}return!0},externalDefTransforms:[function(e){return e.resourceId?{resourceId:e.resourceId}:{}}],eventDropTransformers:[function(e,t){let{resourceMutation:r}=e;if(r){let{calendarApi:e}=t;return{oldResource:e.getResourceById(r.matchResourceId),newResource:e.getResourceById(r.setResourceId)}}return{oldResource:null,newResource:null}}],optionChangeHandlers:P,optionRefiners:w,listenerRefiners:T,propSetHandlers:{resourceStore:function(e,t){let{emitter:r}=t;r.hasHandlers("resourcesSet")&&r.trigger("resourcesSet",function(e,t){let r=[];for(let s in e)r.push(new O(t,e[s]));return r}(e,t))}}});class A extends s.BaseComponent{constructor(){super(...arguments),this.refineRenderProps=s.memoizeObjArg(F)}render(){const{props:e}=this;return n.createElement(s.ViewContextType.Consumer,null,t=>{let{options:r}=t,o=this.refineRenderProps({resource:e.resource,date:e.date,context:t});return n.createElement(s.ContentContainer,Object.assign({},e,{elAttrs:Object.assign(Object.assign({},e.elAttrs),{"data-resource-id":e.resource.id,"data-date":e.date?s.formatDayString(e.date):void 0}),renderProps:o,generatorName:"resourceLabelContent",customGenerator:r.resourceLabelContent,defaultGenerator:B,classNameGenerator:r.resourceLabelClassNames,didMount:r.resourceLabelDidMount,willUnmount:r.resourceLabelWillUnmount}))})}}function B(e){return e.resource.title||e.resource.id}function F(e){return{resource:new O(e.context,e.resource),date:e.date?e.context.dateEnv.toDate(e.date):null,view:e.context.viewApi}}class N extends s.BaseComponent{render(){let{props:e}=this;return n.createElement(A,{elTag:"th",elClasses:["fc-col-header-cell","fc-resource"],elAttrs:{role:"columnheader",colSpan:e.colSpan},resource:e.resource,date:e.date},t=>n.createElement("div",{className:"fc-scrollgrid-sync-inner"},n.createElement(t,{elTag:"span",elClasses:["fc-col-header-cell-cushion",e.isSticky&&"fc-sticky"]})))}}class H extends s.BaseComponent{constructor(){super(...arguments),this.buildDateFormat=s.memoize(k)}render(){let{props:e,context:t}=this,r=this.buildDateFormat(t.options.dayHeaderFormat,e.datesRepDistinctDays,e.dates.length);return n.createElement(s.NowTimer,{unit:"day"},(s,n)=>1===e.dates.length?this.renderResourceRow(e.resources,e.dates[0]):t.options.datesAboveResources?this.renderDayAndResourceRows(e.dates,r,n,e.resources):this.renderResourceAndDayRows(e.resources,e.dates,r,n))}renderResourceRow(e,t){let r=e.map(e=>n.createElement(N,{key:e.id,resource:e,colSpan:1,date:t}));return this.buildTr(r,"resources")}renderDayAndResourceRows(e,t,r,s){let o=[],i=[];for(let u of e){o.push(this.renderDateCell(u,t,r,s.length,null,!0));for(let e of s)i.push(n.createElement(N,{key:e.id+":"+u.toISOString(),resource:e,colSpan:1,date:u}))}return n.createElement(n.Fragment,null,this.buildTr(o,"day"),this.buildTr(i,"resources"))}renderResourceAndDayRows(e,t,r,s){let o=[],i=[];for(let u of e){o.push(n.createElement(N,{key:u.id,resource:u,colSpan:t.length,isSticky:!0}));for(let e of t)i.push(this.renderDateCell(e,r,s,1,u))}return n.createElement(n.Fragment,null,this.buildTr(o,"resources"),this.buildTr(i,"day"))}renderDateCell(e,t,r,o,i,u){let{props:a}=this,c=i?":"+i.id:"",l=i?{resource:new O(this.context,i)}:{},d=i?{"data-resource-id":i.id}:{};return a.datesRepDistinctDays?n.createElement(s.TableDateCell,{key:e.toISOString()+c,date:e,dateProfile:a.dateProfile,todayRange:r,colCnt:a.dates.length*a.resources.length,dayHeaderFormat:t,colSpan:o,isSticky:u,extraRenderProps:l,extraDataAttrs:d}):n.createElement(s.TableDowCell,{key:e.getUTCDay()+c,dow:e.getUTCDay(),dayHeaderFormat:t,colSpan:o,isSticky:u,extraRenderProps:l,extraDataAttrs:d})}buildTr(e,t){let{renderIntro:r}=this.props;return e.length||(e=[n.createElement("td",{key:0}," ")]),n.createElement("tr",{key:t,role:"row"},r&&r(t),e)}}function k(e,t,r){return e||s.computeFallbackHeaderFormat(t,r)}class M{constructor(e){let t={},r=[];for(let s=0;st.resources[e]);return r[""]={},r}getKeysForDateSpan(e){return[e.resourceId||""]}getKeysForEventDef(e){let t=e.resourceIds;return t.length?t:[""]}}function G(e,t,r,s,n,o){let i=[];return function e(t,r,s,n,o,i,u){for(let a=0;a0)break}t.splice(n,0,e)}function K(e){let t=Object.assign(Object.assign(Object.assign({},e.extendedProps),e.ui),e);return delete t.ui,delete t.extendedProps,t}var q={__proto__:null,refineRenderProps:function(e){return{resource:new O(e.context,e.resource)}},DEFAULT_RESOURCE_ORDER:j,ResourceDayHeader:H,AbstractResourceDayTableModel:L,ResourceDayTableModel:class extends L{computeCol(e,t){return t*this.dayTableModel.colCnt+e}computeColRanges(e,t,r){return[{firstCol:this.computeCol(e,r),lastCol:this.computeCol(t,r),isStart:!0,isEnd:!0}]}},DayResourceTableModel:class extends L{computeCol(e,t){return e*this.resources.length+t}computeColRanges(e,t,r){let s=[];for(let n=e;n<=t;n+=1){let o=this.computeCol(n,r);s.push({firstCol:o,lastCol:o,isStart:n===e,isEnd:n===t})}return s}},VResourceJoiner:class{constructor(){this.joinDateSelection=s.memoize(this.joinSegs),this.joinBusinessHours=s.memoize(this.joinSegs),this.joinFgEvents=s.memoize(this.joinSegs),this.joinBgEvents=s.memoize(this.joinSegs),this.joinEventDrags=s.memoize(this.joinInteractions),this.joinEventResizes=s.memoize(this.joinInteractions)}joinProps(e,t){let r=[],s=[],n=[],o=[],i=[],u=[],a="",c=t.resourceIndex.ids.concat([""]);for(let t of c){let c=e[t];r.push(c.dateSelectionSegs),s.push(t?c.businessHourSegs:V),n.push(t?c.fgEventSegs:V),o.push(c.bgEventSegs),i.push(c.eventDrag),u.push(c.eventResize),a=a||c.eventSelection}return{dateSelectionSegs:this.joinDateSelection(t,...r),businessHourSegs:this.joinBusinessHours(t,...s),fgEventSegs:this.joinFgEvents(t,...n),bgEventSegs:this.joinBgEvents(t,...o),eventDrag:this.joinEventDrags(t,...i),eventResize:this.joinEventResizes(t,...u),eventSelection:a}}joinSegs(e,...t){let r=e.resources.length,s=[];for(let n=0;ne.resource)},isGroupsEqual:function(e,t){return e.spec===t.spec&&e.value===t.value},buildRowNodes:G,buildResourceFields:K,ResourceSplitter:D,ResourceLabelContainer:A};return t.globalPlugins.push(U),e.Internal=q,e.ResourceApi=O,e.default=U,Object.defineProperty(e,"__esModule",{value:!0}),e}({},FullCalendar,FullCalendar.PremiumCommon,FullCalendar.Internal,FullCalendar.Preact); \ No newline at end of file diff --git a/web_fullcalendar_resource/static/src/resource_calendar/resource_calendar.scss b/web_fullcalendar_resource/static/src/resource_calendar/resource_calendar.scss new file mode 100644 index 00000000000..658d12e3b01 --- /dev/null +++ b/web_fullcalendar_resource/static/src/resource_calendar/resource_calendar.scss @@ -0,0 +1,12 @@ +// Minimum width of the resource columns + horizontal scrolling when there are +// many resources. +.o_calendar_renderer { + .fc-resource-timegrid, + .fc-resourceTimeGridDay-view, + .fc-resourceTimeGridWeek-view { + .fc-col-header-cell, + .fc-timegrid-col { + min-width: 80px; + } + } +} diff --git a/web_fullcalendar_resource/static/src/resource_calendar/resource_calendar_arch_parser.esm.js b/web_fullcalendar_resource/static/src/resource_calendar/resource_calendar_arch_parser.esm.js new file mode 100644 index 00000000000..e3acc4fa75e --- /dev/null +++ b/web_fullcalendar_resource/static/src/resource_calendar/resource_calendar_arch_parser.esm.js @@ -0,0 +1,48 @@ +import {CalendarArchParser} from "@web/views/calendar/calendar_arch_parser"; +import {parseXML} from "@web/core/utils/xml"; + +const RESOURCE_SCALES = ["day", "week"]; + +/** + * Arch parser for the "resource" view. + * + * The root arch node is `` instead of ``. The core + * parser only reacts to the "calendar" tag, so we relabel the root as + * "calendar" before delegating, then read the specific `resource_field` + * attribute. + */ +export class ResourceCalendarArchParser extends CalendarArchParser { + parse(arch, models, modelName) { + const root = typeof arch === "string" ? parseXML(arch) : arch; + const resourceField = root.getAttribute("resource_field"); + if (!resourceField) { + throw new Error( + `Resource view has not defined "resource_field" attribute.` + ); + } + + // Relabel as to reuse the standard parser. + const calendarNode = root.ownerDocument.createElement("calendar"); + for (const {name, value} of Array.from(root.attributes)) { + calendarNode.setAttribute(name, value); + } + while (root.firstChild) { + calendarNode.appendChild(root.firstChild); + } + + const archInfo = super.parse(calendarNode, models, modelName); + + // The resource view only handles day / week scales. + archInfo.scales = archInfo.scales.filter((s) => RESOURCE_SCALES.includes(s)); + if (!archInfo.scales.length) { + archInfo.scales = ["day"]; + } + if (!archInfo.scales.includes(archInfo.scale)) { + archInfo.scale = archInfo.scales[0]; + } + + archInfo.resourceField = resourceField; + archInfo.fieldNames = [...new Set([...archInfo.fieldNames, resourceField])]; + return archInfo; + } +} diff --git a/web_fullcalendar_resource/static/src/resource_calendar/resource_calendar_common_renderer.esm.js b/web_fullcalendar_resource/static/src/resource_calendar/resource_calendar_common_renderer.esm.js new file mode 100644 index 00000000000..9372115cad5 --- /dev/null +++ b/web_fullcalendar_resource/static/src/resource_calendar/resource_calendar_common_renderer.esm.js @@ -0,0 +1,39 @@ +import {CalendarCommonRenderer} from "@web/views/calendar/calendar_common/calendar_common_renderer"; + +const SCALE_TO_RESOURCE_VIEW = { + day: "resourceTimeGridDay", + week: "resourceTimeGridWeek", +}; + +/** + * FullCalendar renderer for the day/week scales of the "resource" view. + * + * Extends the core common renderer by: + * - switching to the `resourceTimeGrid*` views (columns = resources); + * - providing the `resources` list; + * - attaching the `resourceIds` to each event. + */ +export class ResourceCalendarCommonRenderer extends CalendarCommonRenderer { + get options() { + return { + ...super.options, + initialView: + SCALE_TO_RESOURCE_VIEW[this.props.model.scale] || "resourceTimeGridDay", + // GPL key: free to use in an open source project (AGPL). + schedulerLicenseKey: "GPL-My-Project-Is-Open-Source", + resources: this.props.model.resources, + datesAboveResources: true, + filterResourcesWithEvents: false, + // The core week-number column hack breaks the resource grid, so we + // disable it. + weekNumbers: false, + }; + } + + convertRecordToEvent(record) { + return { + ...super.convertRecordToEvent(record), + resourceIds: record.resourceIds || [], + }; + } +} diff --git a/web_fullcalendar_resource/static/src/resource_calendar/resource_calendar_model.esm.js b/web_fullcalendar_resource/static/src/resource_calendar/resource_calendar_model.esm.js new file mode 100644 index 00000000000..12e46e59828 --- /dev/null +++ b/web_fullcalendar_resource/static/src/resource_calendar/resource_calendar_model.esm.js @@ -0,0 +1,100 @@ +import {CalendarModel} from "@web/views/calendar/calendar_model"; + +/** + * Model for the "resource" view. + * + * On top of the records, it builds the list of resources (the columns) from the + * relation pointed to by `resource_field`, and associates each record with the + * list of resources it belongs to (`resourceIds`). + * + * By default, the displayed columns are the resources actually present in the + * loaded records (scalable, including for a many2many field such as + * `partner_ids`). A business module can display ALL the resources of a domain + * by setting `showAllResources = true` and overriding `resourceDomain()`. + */ +export class ResourceCalendarModel extends CalendarModel { + setup(params, services) { + super.setup(params, services); + this.data.resources = []; + } + + get resourceField() { + return this.meta.resourceField; + } + get resources() { + return this.data.resources; + } + + /** + * If true, display every resource returned by `resourceDomain()` (even + * without any event). Otherwise, only display the ones present in the loaded + * records. + */ + get showAllResources() { + return false; + } + + /** + * Extension point: domain applied when `showAllResources` is true (e.g. + * filtering by regional unit). + * @returns {Array} + */ + resourceDomain() { + return []; + } + + async updateData(data) { + // Super loads data.records; the columns are then derived from them. + await super.updateData(data); + data.resources = await this.loadResources(data.records); + } + + /** + * @protected + * @param {Object} records the loaded records (key = id) + * @returns {Promise>} + */ + async loadResources(records) { + const field = this.meta.fields[this.resourceField]; + let domain = null; + if (this.showAllResources) { + domain = this.resourceDomain(); + } else { + const ids = new Set(); + for (const record of Object.values(records)) { + for (const id of record.resourceIds || []) { + ids.add(parseInt(id, 10)); + } + } + if (!ids.size) { + return []; + } + domain = [["id", "in", [...ids]]]; + } + const resources = await this.orm.searchRead(field.relation, domain, [ + "display_name", + ]); + return resources.map((rec) => ({ + id: String(rec.id), + title: rec.display_name, + })); + } + + normalizeRecord(rawRecord) { + const record = super.normalizeRecord(rawRecord); + const field = this.meta.fields[this.resourceField]; + const rawValue = rawRecord[this.resourceField]; + let ids = []; + if (rawValue) { + if (["many2many", "one2many"].includes(field.type)) { + ids = rawValue; + } else if (Array.isArray(rawValue)) { + ids = [rawValue[0]]; + } else { + ids = [rawValue]; + } + } + record.resourceIds = ids.map(String); + return record; + } +} diff --git a/web_fullcalendar_resource/static/src/resource_calendar/resource_calendar_renderer.esm.js b/web_fullcalendar_resource/static/src/resource_calendar/resource_calendar_renderer.esm.js new file mode 100644 index 00000000000..98ae89fe5ce --- /dev/null +++ b/web_fullcalendar_resource/static/src/resource_calendar/resource_calendar_renderer.esm.js @@ -0,0 +1,14 @@ +import {CalendarRenderer} from "@web/views/calendar/calendar_renderer"; +import {ResourceCalendarCommonRenderer} from "@web_fullcalendar_resource/resource_calendar/resource_calendar_common_renderer.esm"; + +/** + * Rendering dispatcher: routes the day/week scales to the resource renderer + * (columns per resource). + */ +export class ResourceCalendarRenderer extends CalendarRenderer { + static components = { + ...CalendarRenderer.components, + day: ResourceCalendarCommonRenderer, + week: ResourceCalendarCommonRenderer, + }; +} diff --git a/web_fullcalendar_resource/static/src/resource_calendar/resource_calendar_view.esm.js b/web_fullcalendar_resource/static/src/resource_calendar/resource_calendar_view.esm.js new file mode 100644 index 00000000000..a443b39c423 --- /dev/null +++ b/web_fullcalendar_resource/static/src/resource_calendar/resource_calendar_view.esm.js @@ -0,0 +1,24 @@ +import {ResourceCalendarArchParser} from "@web_fullcalendar_resource/resource_calendar/resource_calendar_arch_parser.esm"; +import {ResourceCalendarModel} from "@web_fullcalendar_resource/resource_calendar/resource_calendar_model.esm"; +import {ResourceCalendarRenderer} from "@web_fullcalendar_resource/resource_calendar/resource_calendar_renderer.esm"; +import {calendarView} from "@web/views/calendar/calendar_view"; +import {registry} from "@web/core/registry"; + +/** + * "resource" view: a variant of Odoo's standard calendar view that displays + * events in vertical columns per resource, thanks to the FullCalendar Scheduler + * plugins (resource-timegrid / resource-daygrid). + * + * We reuse the core calendar view as much as possible: only the ArchParser (to + * read the `resource_field` attribute), the Model (to load the resources) and + * the Renderer (to enable the resourceTimeGrid views) are specialized. + */ +export const resourceCalendarView = { + ...calendarView, + type: "resource", + ArchParser: ResourceCalendarArchParser, + Model: ResourceCalendarModel, + Renderer: ResourceCalendarRenderer, +}; + +registry.category("views").add("resource", resourceCalendarView); diff --git a/web_fullcalendar_resource_demo/README.rst b/web_fullcalendar_resource_demo/README.rst new file mode 100644 index 00000000000..1844b297a49 --- /dev/null +++ b/web_fullcalendar_resource_demo/README.rst @@ -0,0 +1,83 @@ +================================ +Web Fullcalendar Resource - Demo +================================ + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:960bae72420ae441fb14155e7549967344b2fbde8bcbe2d97eb4b31ba523d692 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fweb-lightgray.png?logo=github + :target: https://github.com/OCA/web/tree/18.0/web_fullcalendar_resource_demo + :alt: OCA/web +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/web-18-0/web-18-0-web_fullcalendar_resource_demo + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png + :target: https://runboat.odoo-community.org/builds?repo=OCA/web&target_branch=18.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This module provides a demonstration model (a simple resource booking) +together with sample data and views to test and showcase the +``resource`` view added by ``web_fullcalendar_resource``. + +It is meant for demonstration and testing purposes only; do not install +it on a production database. + +**Table of contents** + +.. contents:: + :local: + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +------- + +* Le Filament + +Contributors +------------ + +- `Le Filament `__: + + - Hugo Trentesaux + +Maintainers +----------- + +This module is maintained by the OCA. + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +This module is part of the `OCA/web `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/web_fullcalendar_resource_demo/__init__.py b/web_fullcalendar_resource_demo/__init__.py new file mode 100644 index 00000000000..0650744f6bc --- /dev/null +++ b/web_fullcalendar_resource_demo/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/web_fullcalendar_resource_demo/__manifest__.py b/web_fullcalendar_resource_demo/__manifest__.py new file mode 100644 index 00000000000..e03d0b5d858 --- /dev/null +++ b/web_fullcalendar_resource_demo/__manifest__.py @@ -0,0 +1,25 @@ +# Copyright 2026 Le Filament (https://le-filament.com) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +{ + "name": "Web Fullcalendar Resource - Demo", + "summary": "Demonstration model and data for the resource calendar view", + "version": "18.0.1.0.0", + "author": "Le Filament, Odoo Community Association (OCA)", + "website": "https://github.com/OCA/web", + "license": "AGPL-3", + "category": "Productivity", + "development_status": "Beta", + "depends": [ + "web_fullcalendar_resource", + ], + "data": [ + "demo/fc_demo_data.xml", + "security/ir.model.access.csv", + "views/fc_demo_views.xml", + ], + "demo": [ + "demo/fc_demo_data.xml", + ], + "installable": True, + "application": False, +} diff --git a/web_fullcalendar_resource_demo/demo/fc_demo_data.xml b/web_fullcalendar_resource_demo/demo/fc_demo_data.xml new file mode 100644 index 00000000000..762bc21c273 --- /dev/null +++ b/web_fullcalendar_resource_demo/demo/fc_demo_data.xml @@ -0,0 +1,95 @@ + + + + + + Room A + room + + + Room B + room + + + Vehicle 1 + car + + + Video call + video + + + + + Management committee + + + + + + Recruitment interview + + + + + + Product workshop + + + + + + Customer visit + + + + + + Weekly video meeting + + + + + + Internal training + + + + + diff --git a/web_fullcalendar_resource_demo/models/__init__.py b/web_fullcalendar_resource_demo/models/__init__.py new file mode 100644 index 00000000000..d13aed93f5b --- /dev/null +++ b/web_fullcalendar_resource_demo/models/__init__.py @@ -0,0 +1 @@ +from . import fc_demo diff --git a/web_fullcalendar_resource_demo/models/fc_demo.py b/web_fullcalendar_resource_demo/models/fc_demo.py new file mode 100644 index 00000000000..892e59e9668 --- /dev/null +++ b/web_fullcalendar_resource_demo/models/fc_demo.py @@ -0,0 +1,37 @@ +# Copyright 2026 Le Filament (https://le-filament.com) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class FcDemoResource(models.Model): + _name = "fc.demo.resource" + _description = "Demonstration resource (room, vehicle, ...)" + + name = fields.Char(required=True) + resource_type = fields.Selection( + [ + ("room", "Room"), + ("car", "Vehicle"), + ("video", "Video call"), + ("other", "Other"), + ], + string="Type", + default="room", + ) + active = fields.Boolean(default=True) + + +class FcDemoBooking(models.Model): + _name = "fc.demo.booking" + _description = "Demonstration booking" + _order = "date_start" + + name = fields.Char(required=True) + resource_id = fields.Many2one("fc.demo.resource", string="Resource", required=True) + user_id = fields.Many2one( + "res.users", string="Responsible", default=lambda self: self.env.user + ) + date_start = fields.Datetime(string="Start", required=True) + date_stop = fields.Datetime(string="Stop", required=True) + description = fields.Text() diff --git a/web_fullcalendar_resource_demo/pyproject.toml b/web_fullcalendar_resource_demo/pyproject.toml new file mode 100644 index 00000000000..4231d0cccb3 --- /dev/null +++ b/web_fullcalendar_resource_demo/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/web_fullcalendar_resource_demo/readme/CONTRIBUTORS.md b/web_fullcalendar_resource_demo/readme/CONTRIBUTORS.md new file mode 100644 index 00000000000..77ff4898466 --- /dev/null +++ b/web_fullcalendar_resource_demo/readme/CONTRIBUTORS.md @@ -0,0 +1,2 @@ +- [Le Filament](https://le-filament.com): + - Hugo Trentesaux diff --git a/web_fullcalendar_resource_demo/readme/DESCRIPTION.md b/web_fullcalendar_resource_demo/readme/DESCRIPTION.md new file mode 100644 index 00000000000..37536533b1a --- /dev/null +++ b/web_fullcalendar_resource_demo/readme/DESCRIPTION.md @@ -0,0 +1,6 @@ +This module provides a demonstration model (a simple resource booking) +together with sample data and views to test and showcase the `resource` +view added by `web_fullcalendar_resource`. + +It is meant for demonstration and testing purposes only; do not install +it on a production database. diff --git a/web_fullcalendar_resource_demo/security/ir.model.access.csv b/web_fullcalendar_resource_demo/security/ir.model.access.csv new file mode 100644 index 00000000000..e43a5bf45db --- /dev/null +++ b/web_fullcalendar_resource_demo/security/ir.model.access.csv @@ -0,0 +1,3 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_fc_demo_resource_user,fc.demo.resource.user,model_fc_demo_resource,base.group_user,1,1,1,1 +access_fc_demo_booking_user,fc.demo.booking.user,model_fc_demo_booking,base.group_user,1,1,1,1 diff --git a/web_fullcalendar_resource_demo/static/description/index.html b/web_fullcalendar_resource_demo/static/description/index.html new file mode 100644 index 00000000000..dbc95df418f --- /dev/null +++ b/web_fullcalendar_resource_demo/static/description/index.html @@ -0,0 +1,430 @@ + + + + + +Web Fullcalendar Resource - Demo + + + +
+

Web Fullcalendar Resource - Demo

+ + +

Beta License: AGPL-3 OCA/web Translate me on Weblate Try me on Runboat

+

This module provides a demonstration model (a simple resource booking) +together with sample data and views to test and showcase the +resource view added by web_fullcalendar_resource.

+

It is meant for demonstration and testing purposes only; do not install +it on a production database.

+

Table of contents

+ +
+

Bug Tracker

+

Bugs are tracked on GitHub Issues. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +feedback.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • Le Filament
  • +
+
+
+

Contributors

+ +
+
+

Maintainers

+

This module is maintained by the OCA.

+ +Odoo Community Association + +

OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use.

+

This module is part of the OCA/web project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+ + diff --git a/web_fullcalendar_resource_demo/views/fc_demo_views.xml b/web_fullcalendar_resource_demo/views/fc_demo_views.xml new file mode 100644 index 00000000000..29240426f9c --- /dev/null +++ b/web_fullcalendar_resource_demo/views/fc_demo_views.xml @@ -0,0 +1,153 @@ + + + + + + fc.demo.resource.list + fc.demo.resource + + + + + + + + + + fc.demo.resource.form + fc.demo.resource + +
+ + + + + + + +
+
+
+ + + Resources (demo) + fc.demo.resource + list,form + + + + + fc.demo.booking.list + fc.demo.booking + + + + + + + + + + + + + fc.demo.booking.form + fc.demo.booking + +
+ + + + + + + + + + + + + + + + +
+
+
+ + + + fc.demo.booking.calendar + fc.demo.booking + + + + + + + + + + + + fc.demo.booking.resource + fc.demo.booking + + + + + + + + + + + Bookings (demo) + fc.demo.booking + resource,calendar,list,form + + + + + + + +