diff --git a/MODULE_REPORT.md b/MODULE_REPORT.md deleted file mode 100644 index e3d527fb2..000000000 --- a/MODULE_REPORT.md +++ /dev/null @@ -1,14 +0,0 @@ -## Module Report -### Unknown Global - -**Global**: `Ember.Copyable` - -**Location**: `app/models/tagging.js` at line 11 - -```js - - -export default Model.extend(Ember.Copyable, Auditable, { - workspace: belongsTo('workspace', { async: false }), - selection: belongsTo('selection'), -``` diff --git a/README.md b/README.md index 9292f9a1b..45ecc9700 100644 --- a/README.md +++ b/README.md @@ -76,7 +76,7 @@ If you run into the following error while running tests: ## Client `/app` -EnCOMPASS uses Ember for the client and was recently migrated to Ember Octane from v2.14. Portions are not fully migrated. +EnCOMPASS uses Ember for the client and was recently migrated to Ember Octane from v2.14. Portions are not fully migrated. See [UPGRADE_NOTES](./UPGRADE_NOTES.md) for more information. ### workflow @@ -90,9 +90,6 @@ EnCOMPASS uses Ember for the client and was recently migrated to Ember Octane fr Ember is switching to Glimmer for its component engine. Components that have their templates (.hbs files) in `app/components` have been migrated. Their classes (.js files) will look like native JS classes. Components with templates in `app/templates/components` have not been migrated and still use Classic Ember component classes. I tried to combine similar components when possible. -- `admin-problem-filter` and `admin-workspace-filter` could be comined -- `problem-filter` and `workspace-filter` could be combined - ### mixins usage of mixins (found in `app/mixins`) are deprecated - they still work for classic components but should be refactored away @@ -122,9 +119,7 @@ Each route has a corresponding template that gets rendered. It should be in the Libraries that are not managed by npm are added in the `/vendor` directory and configured into the bundle in `/ember-cli-build.js` including: -1. selectize input library (see `app/components/selectize-input.js`) -2. typeahead library (see `app/components/twitter-typeahead.js`) -3. selection libraries (`vendor/image-tagging.js` and `vendor/selection-highlighting.js`) that are used in `app/components/workspace-submission.js` +- selection libraries (`vendor/image-tagging.js` and `vendor/selection-highlighting.js`) that are used in `app/components/workspace-submission.js` ### `/helpers` diff --git a/UPGRADE_NOTES.md b/UPGRADE_NOTES.md index 989b7a25b..5151cd256 100644 --- a/UPGRADE_NOTES.md +++ b/UPGRADE_NOTES.md @@ -1,4 +1,4 @@ -This are notes taken as I attempt to upgrade Encompass to Ember 4.5. Note that there is a fair bit of redundancy in this file as these notes are being created as I do the work. +These are notes taken as I attempt to upgrade Encompass to Ember 4.5. Note that there is a fair bit of redundancy in this file as these notes are being created as I do the work. # Backstory @@ -6,20 +6,45 @@ There has been various attempts to upgrade this app to modern Ember (Octane, Emb This file is an attempt to document what has and has not been done, as well as suggestions for future developers if I (like all others) leave an incomplete upgrade process. +# Notes about the current state (1 Dec 2024) + +Enc-test has been updated with the latest version of the work that I've done over the last couple of months, as represented in this file. Of course, there is plenty that does not work; mostly parts of the system that have not yet been upgraded, upgrades that have not been adequately tested, and a few items that I document below that represent my current work when this contracted ended. To help the next developer, there are two files beyond this one: + +1. component audit.xls -- contains notes about all components in the Encompass system, including which have been upgraded or deleted. +2. componentFinder.js -- script (run with "node componentFinder") that produces a report of all the components used in the different routes, all the components used by other components, etc. This should help the next developer in understanding how the Encompass app is organized. + +## Items in progress + +1. user-info component -- mostly works except for a few of the updates: seen tour, authorized, etc. +2. problem-list-container and related components -- trashed problems might not be showing up correctly. Deleting and some actions might not be working. +3. workspace-list-container and related components -- trashed and hidden workspaces might not be showing up correctly. Many of the actions in the three-dot (more) menu are not working. + # Upgrades needed globally +## Use of this.model or @model + +There are several components (js or template) that reference the model. In standard Ember practice, only the route templates should access the model and pass along the specific bits of the model needed by each component. That way, there's a single source of model truth (from the route) and we don't have components knowing about what's in the model. + +## Use to 'toXXX' methods + +There are several, largely unused, methods throughout the codebase such as 'toProblemInfo' that seem to be intended to transition to certain routes. It seems like these are legacy code so should be removed because the more modern approach -- using LinkTo in templates -- seems to be used throughout the app. The use of LinkTo is much better than prop-drilling these 'toXXX' methods. + ## Removal of Mixins A fair number of mixins have been removed, replaced by services or component superclasses. There are still more mixins on some legacy components and elsewhere. The plan is to step through all mixins in app/mixins, replacing them with existing or new services everywhere they are used. See below: on 11/10/2024 I deleted most of them and identified just three that need refactoring before removal. ## Error handling service -The error handling service was previously implemented incorrectly. The ErrorHandling superclass worked, but was a bit obscure in terms of creating properties for the component behind-the-scenes. The corrected error handling service is more explict in how it works and how components should use it. Note that all components that use this service should create a getter to access the errors generated. Also, the component should clean up those errors when it unmounts or at least regularly clean out the errors when we have a successful action. +The error handling service was previously implemented incorrectly. The ErrorHandling superclass worked, but was a bit obscure in terms of creating properties for the component behind-the-scenes. The corrected error handling service is more explict in how it works and how components should use it. Note that all components that use this service should create a getter to access the errors generated. Also, the component should clean up those errors when it unmounts or at least regularly clean out the errors when we have a successful action. (In January 2025, I added a timer to the ErrorHandling service such that errors automatically clear after 3 minutes.) Note that there are many classic components that use the error handling service but it does not exactly behave as these components expect, so this needs to be fixed. But the above is not enough! Apparently the display of errors was a work-in-progresss. There are several components (e.g., workspace-list-container, problem-list-container) that incorporate either the error-handling service, superclass, or mixin, but don't display the errors (albiet they do nicely keep track of them). +## .slice() replaces .toArray() + +To convert a RecordArray to a plain JS array, Ember docs now recommend using .slice() because .toArray() has been deprecated. As of 1/2/2025 I've made this replacement in selectize-input.js and troubleshooting.md, but still have to change it in about 55 places among 31 files. + ## Store Store is a service, so there is no need for it to be passed as an argument to a component, route, or controller. This has been corrected for many cases (selectize-input, various aspects of the workspace and problem subsystems), but there are several more uses of @store={{this.store}} that need to be refactored. @@ -32,23 +57,25 @@ These should be replaced with usage of htmlSafe(). A number of components used two-way data binding, which is discouraged in modern Ember. In two-way data binding, it's possible to mutate the value of a property and have that property's value be changed in the property of the original component. Difficulties arise because it's tricky to recognize when two-way data binding is being used and therefore tricky for a new developer to see how (or that) certain property values are being updated. -Instead, modern Ember uses the Data down, actions up pattern. Parent components send data down to their child components. A parent also sends actions (functions) to the child when updates are needed. When the child wants to update the property's value, it uses the appropriate action (essentially a callback to the parent). In this way, the parent is responsible for all of its properties' values, both getting and setting them. +Instead, modern Ember uses the Data down, Actions up pattern. Parent components send data down to their child components. A parent also sends actions (functions) to the child when updates are needed. When the child wants to update the property's value, it uses the appropriate action (essentially a callback to the parent). In this way, the parent is responsible for all of its properties' values, both getting and setting them. ## Template and Component files -Should be co-located in the app/components folder rather than in the app/templates/components folder. The app/templates folder should be for route templates only. Note that many of the still-to be upgraded components are split between the folders; the upgraded ones have their hbs files in app/components. +Should be co-located in the app/components folder rather than in the app/templates/components folder. The app/templates folder should be for route templates only. Note that many of the still-to-be upgraded components are split between the folders; the upgraded ones have their hbs files in app/components. ## Vendor imports Currently, ember-cli-build.js and the vendor/ folder reflects an older-style of imports. Wouldn't it be better to include the necessary packages and import them as needed in the various components? -This has been done for randomcolor, jQuery (older versions), typeahead.js, and selectize. Imports that were unused were deleted, including ajax (part of Ember), daterangePicker, error, ie_hacks, and jquery.mousewheel. +This has been done for randomcolor, jQuery (older versions), typeahead.js, and selectize. Imports that were unused were deleted, including ajax (it's already part of Ember so no need to import it), daterangePicker, error, ie_hacks, and jquery.mousewheel. Changing these vendor imports involved installing the corresponding packages (selectize and typeahead.js), then importing those packages into the wrapping components (selectize-input and twitter-typeahead). +There are also several packages that are essentially polyfills for outdated browsers, such as IE. + ## Move from built-in Ember components (Input, TextArea, Select, etc.) to the plain HTML versions -As possible, I will be replacing with , and so forth. Although the Ember built-ins provide some convenience for assessibility options, they also encourage older-style approachs such as two-way data binding. +As possible, I will be replacing with , and so forth. Although the Ember built-ins provide some convenience for assessibility options, they also encourage older-style approachs such as two-way data binding. [NOTE: I'm not certain this is always a good idea.] ## Helpers @@ -60,6 +87,14 @@ In Ember 4.5, helpers can now be regular functions rather than wrapped in a mana 11/14/2024: Upgraded to 4.5 and simplified all helpers. +## Controllers + +Controllers may be slated for deprecation. Best practice is to refactor the logic and properties in the controller, distributing them as appropriate to the route (for building the model), a service (for application state that will be used elsewhere), and a component (for everything else). The idea is that a route templates should be simple and reference just one or more components that contain much of the work that had been done by the controller. + +## Route Templates + +Route templates should refer to the model via @model rather than this.model as per the Ember Octane upgrade guide. + ## EmberTable There are other packages that are more aligned with Glimmer and Octane approaches to Ember. However, depending on the needs, perhaps the 5 uses of EmberTable could be replaced with vanilla JS. @@ -75,18 +110,34 @@ The current code includes several subsystems of components that are tightly coup # Possible future upgrades +## Model definitions + +All hasMany and belongsTo relationships should specify inverse and async options explicitly. Not doing so is deprecated. + ## DB document timestamps Currently, all timestamping of db documents (users, problems, workspaces, etc.) appears to be done manually primarily on the client side. This approach could cause issues because the clients clocks might be wrong. Also, because the dates are updated manually (all over the codebase), there is a higher likelihood of errors. -Instead, we could leverage the {timestamps: true} option when defining all the Mongoose Schemas. This option has the db (a single ) +Instead, we could leverage the {timestamps: true} option when defining all the Mongoose Schemas. This option has the db (a single source of truth) automatically insert and maintain createdAt and updatedAt fields. + +## Route organization + +There are several routes directly under the routes/ folder that have corresponding subfolders. For example, there is app/routes/assignments.js as well as app/routes/assignments/assignment.js and app/routes/assignments/new.js. A more consistent naming scheme would be to move these top-level routes to be index.js files in their corresponding folders (e.g., assignments.js file would become app/routes/assignments/index.js). That way, all the related routing files are grouped together. + +Another argument for this approach is that this is how the corresponding template files are organized. For example, there is a app/templates/assignments/ folder with index.hbs, assignment.hbs, and new.hbs. + +On the other hand, the current structure reflects a parent / child route structure because there are also files such as app/templates/assignments.hbs in addition to app/templates/assignments/index.hbs. On the other hand, we have to ask if the complexity is needed, but that is an issue for another day. ## Component organization -# UI Elements +### UI Elements There is now the folder app/components/ui that contains the form-field and expandable-cell components. The purpose of this folder is a place for generic UI components. Other generic UI components include: my-select, selectize-input, twitter-typeahead, radio-group (and radio-group-item), toggle-control, checkbox-list (and checkbox-list-item), collapsible-list, and quill-container. Once these get moved into that folder, every usage must reference the "Ui" namespace, such as or . +### Other components + +Similar to the Ui example above, usage of Namespaces is encouraged in Ember moving forward. Thus, we should reorganize the app/components folder with subfolders representing the distinct subsystems of Encompass. The components in the folders would then be referenced in templates via namespaces, such as which refers to app/components/users/user-list.js and user-list.hbs. + ## New Workspaces The components workspace-new, workspace-new-enc, and workspace-new-pows are currently not used. They seem to reflect some type of new functionality (rather than template/workspaces/new.hbs and workspace-new-container, which are used) that Pedro was working on but never finished. I'm leaving these files in the codebase with the hope that someday someone will use them to figure out what was being done and to finish the work. Likely the intent was to have tempalte/workspaces/new.hbs use the workspace-new component. @@ -97,13 +148,13 @@ Both lodash and underscore are used extensively throughout the app. These cases Underscore is not as well maintained as is lodash, so lodash should be used as needed. Note that underscore is used extensively in the app_server. It is used in about 15 files in the client code. -There are certainly cases where lodash is helpful, but uses of underscore could be replaced by lodash. Also, rather than globally making the underscore character a reference to the entire lodash library, it would be better to import just the lodash functions needed. Also, rather than lodash, using native JS functions such as map, filter, etc. would be good. +There are certainly cases where lodash is helpful, but uses of underscore could be replaced by lodash. Also, rather than globally making the underscore character a reference to the entire lodash library, it would be better to import just the lodash functions needed. -Also, of course, as possible we should replace the use of lodash functions with native JS equivalents. +Also, rather than lodash, using native JS functions such as map, filter, etc. would be good. ## Removal of jQuery -Modern Ember recommends removing jQuery, using standard DOM access routines instead. Our file app.js sets $ globally to jQuery, so finding all instances will involve both searching for imports of jQuery and for $ (whether "$." or "$("). Note that we cannot completely eliminate jQuery because the selectize package depends on it. +Modern Ember recommends removing jQuery, using standard DOM access routines instead. Our file app.js sets $ globally to jQuery, so finding all instances will involve both searching for imports of jQuery and for $ (whether "$." or "$("). Note that we cannot completely remove jQuery because it's a dependency of selectize. But we can eliminate its use in our own code to be conistent with modern Ember best practices. @@ -129,7 +180,7 @@ There are a variety of superclasses I need to figure out if something needs to be done about this. I know that the Component superclasses should be made into services. -In particular, the ErrorHandling superclass works, but it obscures how errors are accumulated (i.e., handleErrors creates a new property in the component that typically the template accesses). There is an error-handling service that handles errors more explicitly, requiring the component to create a getter to access the error variable created. Also, components should clean these up when they unmount. +In particular, the ErrorHandling superclass works, but it obscures how errors are accumulated (i.e., handleErrors creates a new property in the component that typically the template accesses). See earlier notes on the ErrorHandling service. ## Cleaning up packages and unused elements @@ -158,11 +209,12 @@ There are several README.md files scattered through the /test folder. - **Routes** Many of the routes are in the classic style, so should be upgraded to JS classes, although they all seem to work fine in Ember 4.5. - **Services** Most of the services are still in the classic style. +- **Models** Several models include component-specific logic, including both derived properties (via get) and functions. Modern Ember encourages leaner models that focus on the data and their relationships. It would be best to trim down several of the models, pushing the specific logic out into the components. # Gotchas - For the built-in component , the id argument should be id= rather than @id=. If you do "@id", the component will not respond to clicks. -- If a classic component receives @store={{this.store}} but this.store is undefined, then that component will see this.store as undefined even if you added store as a service. +- If a classic component receives @store={{this.store}} but this.store (i.e., the argument) is undefined, then that component will see this.store as undefined even if you added store as a service. - "Error: Expected a dynamic component definition, but received an object or function that did not have a component manager associated with it. The dynamic invocation was <(result of a unknown helper)> or {{(result of a unknown helper)}}, and the incorrect definition is the value at the path (result of a unknown helper), which was: Object". This error has nothing to do with helpers. For some reason, a template file didn't like that I used the standard tag. When I switched to the Ember tag, the problem went away. - Occasionally, Ember will fail quietly. For example, @@ -177,14 +229,26 @@ There are several README.md files scattered through the /test folder. If this.removeMessages is undefined, Ember might **not** show an error in the console or indicate anywhere that it failed. Subsequent lines will simply not execute but the app will continue running as if everything is fine. - Be careful around the use of objects that are being tracked. One must be careful to update their references so that they are reactive. Just setting a property won't be enough unless you use TrackedObject from tracked-built-ins. +- The error "Error while processing route: assignments.new Assertion Failed: Expected hash or Mixin instance, got [object Function]" was just caused by a syntax error in a model. The assertion failed because when hydrating a model, trying get all the documents from the store in that model failed. +- Not really a gotcha, just something about Ember. If there is an async relationship in the route when the model is being put together, there are cases where you'll need to `await` a property access to ensure that the value has arrived. +- a gotcha related to above: 'hash' waits for the resolution of regular Promises but not Ember ProxyPromises. Thus, if there's an async relationship in a model, if you use dot notation you must wait for that access to fully resolve (i.e., use await). 'hash' will not wait for that type of Promise. You'll see this via an error message such as: + +`Error while processing route: problems.problem.index Assertion Failed: You attempted to access the recommendedProblems property (of <(unknown):ember236>). +Since Ember 3.1, this is usually fine as you no longer need to use .get() to access computed properties. However, in this case, the object in question is a special kind of Ember object (a proxy). Therefore, it is still necessary to use .get('recommendedProblems') in this case.` + +- related to above, in components, dot notation might result in a PromiseProxy. However, if it's used in a template, Ember will re-render once the value is fully resolved, so you don't have to worry about it. However, if you are using the value in js, such as wanting to loop over an array, you need to use await. (Of course, if you are just defining a convenience getter that uses dot notiation for use by a template, you don't have to await.) # Current Progress +## CurrentUser service + +Today (Feb 10, 2025) I upgraded the currentUser service so that clients don't have to always dig in to the user object it provides. Now you can do this.currentUser.isAdmin, etc., instead of this.currentUser.user.isAdmin. In other words, you can get directly to the most popular properties of user, most of which are getters in the model. Really, though, the model should be simply the specs of the data and the currentUser service should be calculating all the things that right now the user model is doing. Before I do that, I need to make sure that I replace accessing the user object (e.g., this.user.XXX) with accessing the service (e.g., this.currentUser.XXX, but really we could name the service 'user' in the components) in the entire codebase for those calculated properties. + ## Upgrade of elements - **Components** - most of the discussion in this file focuses on the upgrading of components from classic to modern (Glimmer, Octane, Ember 4.5). - **Adapters** - Upgraded -- **Controllers** - Seem to have already been upgraded. I've not tested these. +- **Controllers** - Controllers might be slated to be deprecated in favor of using components. That is, the template for a route would simply contain invocations of one or ore components. Thus, working through the controllers and refactoring to remove them all is another goal of the upgrade. - **Helpers** - as document elsewhere in this document, all upgraded. - **Initializers** - only one and I believe it's not needed for production. I upgraded it, however. - **Mixins** - as documented elsewhere, I'm in the process of eliminating these and double-checking others' work on removing these from components, etc. @@ -213,3 +277,47 @@ These mixins are slated to be removed: ## Upgrade of Helpers With the upgrade to Ember 4.5 (11/14/2024), all helpers have been simplified to regular functions. + +## .gitkeep + +Removed all the unnecessary .gitkeep files that were created when the project began 6 years ago. This empty file is just a convention so that git will keep an otherwise empty folder in the history. + +## UI folder + +As of 12/9, the components/ui folder contains the following: + +- checkbox-list, checkbox-list-item +- error-box +- expandable-cell +- form-field +- radio-group, radio-group-item +- twitter-typeahead +- wordcloud-container + +Components potentially to include in the ui folder: + +- bread-crumbs, bread-crumbs-item +- draggable-selection, DragNDrop, Droppable +- my-select +- pagination-control +- quill-container +- radio-filter +- selectize-input +- toggle-control + +The following components, included above, are actually wrappers for third-party packages, so might go into their own folder: (uiwrappers?): + +- quill-container +- selectize-input +- twitter-typeahead +- wordcloud-container + +## New Routes + +- I upgraded the problems parent route: + + - problems/problem/ + - problems/problem//edit + - problems/problem//assignment + + Previously, edit and assignment were handled by flags in the db inside of a problem. When we transitioned to the problem, we first set the correct flag so that would render correctly. With this change, we don't use the db for local application state, which is poor practice. diff --git a/app/components/.gitkeep b/app/components/.gitkeep deleted file mode 100644 index e69de29bb..000000000 diff --git a/app/components/admin-filter.hbs b/app/components/admin-filter.hbs index 5f369aa6b..755050e16 100644 --- a/app/components/admin-filter.hbs +++ b/app/components/admin-filter.hbs @@ -1,4 +1,4 @@ -
+
Find by diff --git a/app/components/admin-filter.js b/app/components/admin-filter.js index 062c220d1..ab9d95caf 100644 --- a/app/components/admin-filter.js +++ b/app/components/admin-filter.js @@ -16,7 +16,7 @@ Admin filter expects the subOptions to be an object with the following structure val2: {type: 'checkbox', options: [{value, icon, label}, {value, icon, label, etc...]} */ -export default class AdminWorkspaceFilterComponent extends Component { +export default class AdminFilterComponent extends Component { @service('utility-methods') utils; @service inputState; @tracked dropdownSelections = []; diff --git a/app/components/answer-new.hbs b/app/components/answer-new.hbs index b63733724..79e7f7a6a 100644 --- a/app/components/answer-new.hbs +++ b/app/components/answer-new.hbs @@ -24,7 +24,7 @@
{{#each this.briefSummaryErrors as |error|}} -
{{#each this.explanationErrors as |error|}} - {{#if this.isMissingRequiredFields}} - {{/if}} {{#if this.overSizedFileError}} - {{/if}} {{#if this.isExplanationTooLarge}} - {{#each this.loadAnswerErrors as |error|}} - Revise {{/if}} {{#if this.showRespondButton}} - {{/if}} diff --git a/app/components/assignment-info-teacher.hbs b/app/components/assignment-info-teacher.hbs index c5c6d1386..44b3625b8 100644 --- a/app/components/assignment-info-teacher.hbs +++ b/app/components/assignment-info-teacher.hbs @@ -24,7 +24,8 @@ name='daterange' id='assignedDate' @type='date' - @value={{(format-date @assignment.assignedDate 'YYYY-MM-DD')}} {{on "change" (action 'updateAssignedDate')}} + @value={{(format-date @assignment.assignedDate 'YYYY-MM-DD')}} + {{on 'change' (action 'updateAssignedDate')}} /> {{else}}

@@ -41,7 +42,8 @@ id='dueDate' name='daterange' @type='date' - @value={{(format-date @assignment.dueDate 'YYYY-MM-DD')}} {{on "change" (action 'updateDueDate')}} + @value={{(format-date @assignment.dueDate 'YYYY-MM-DD')}} + {{on 'change' (action 'updateDueDate')}} /> {{else}}

@@ -54,16 +56,12 @@

{{! Class, Problem }}
- {{!-- {{#if this.showSectionInput}} - - {{else}} --}}

Class: {{@section.name}}

- {{!-- {{/if}} --}}

@@ -72,10 +70,6 @@ {{@problem.title}}

- {{!-- {{#if this.showProblemInput}} - - {{else}} --}} - {{!-- {{/if}} --}}

@@ -109,14 +103,15 @@ @section={{@section}} @problem={{@problem}} @onCancel={{action (mut showLinkedWsForm) false}} - @store={{this.store}} @handleResults={{action 'handleCreatedLinkedWs'}} /> {{/if}} {{#if this.showAddLinkedWsBtn}} {{/if}} {{#if this.showNoParentWsMsg}} @@ -188,7 +184,9 @@ {{#if this.showEditButton}} - {{/if}} diff --git a/app/components/assignment-info-teacher.js b/app/components/assignment-info-teacher.js index 4c07e8edc..fc2966b20 100644 --- a/app/components/assignment-info-teacher.js +++ b/app/components/assignment-info-teacher.js @@ -6,6 +6,12 @@ import $ from 'jquery'; import moment from 'moment'; export default class AssignmentInfoTeacherComponent extends ErrorHandlingComponent { + @service store; + @service router; + @service currentUser; + @service('sweet-alert') alert; + @service('assignment-permissions') permissions; + @service('utility-methods') utils; @tracked formattedDueDate = null; @tracked formattedAssignedDate = null; @tracked isEditing = false; @@ -21,6 +27,11 @@ export default class AssignmentInfoTeacherComponent extends ErrorHandlingCompone @tracked showLinkedWsForm = false; @tracked areSubmissionsExpanded = true; @tracked cachedProblem = []; + + get user() { + return this.currentUser.user; + } + get showProblemInput() { return this.isEditing && this.canEditProblem; } @@ -45,11 +56,6 @@ export default class AssignmentInfoTeacherComponent extends ErrorHandlingCompone get allGroupsHaveWs() { return this.groupsWithoutWorkspaces.length === 0; } - @service store; - @service router; - @service('sweet-alert') alert; - @service('assignment-permissions') permissions; - @service('utility-methods') utils; get hasLinkedWorkspaces() { return this.args.assignment.linkedWorkspaces.length > 0; @@ -86,7 +92,7 @@ export default class AssignmentInfoTeacherComponent extends ErrorHandlingCompone this.args.assignment, 'createdBy' ); - return this.args.currentUser.id === creatorId; + return this.user.id === creatorId; } get isDirty() { @@ -103,7 +109,7 @@ export default class AssignmentInfoTeacherComponent extends ErrorHandlingCompone } get canEdit() { - const isAdmin = this.args.currentUser.isAdmin; + const isAdmin = this.user.isAdmin; const isClean = this.isClean; const isYourOwn = this.isYourOwn; @@ -140,7 +146,7 @@ export default class AssignmentInfoTeacherComponent extends ErrorHandlingCompone } get canEditDate() { - const isAdmin = this.args.currentUser.isAdmin; + const isAdmin = this.user.isAdmin; const canEdit = this.canEdit; const isBeforeAssignedDate = this.isBeforeAssignedDate; return isAdmin || (canEdit && isBeforeAssignedDate); diff --git a/app/components/assignment-info.hbs b/app/components/assignment-info.hbs index b101ded89..d225aed1d 100644 --- a/app/components/assignment-info.hbs +++ b/app/components/assignment-info.hbs @@ -1,18 +1,22 @@ -

Assignment Details

-
-{{#each this.initialLoadErrors as |error|}} -

{{error}}

-{{/each}} -{{#if @currentUser.isStudent}} - +

Assignment Details

+
+{{#if @isStudent}} + {{else}} - -{{/if}} - - - - - - - - + +{{/if}} \ No newline at end of file diff --git a/app/components/assignment-info.js b/app/components/assignment-info.js deleted file mode 100644 index 43bfd074c..000000000 --- a/app/components/assignment-info.js +++ /dev/null @@ -1,22 +0,0 @@ -import ErrorHandlingComponent from './error-handling'; -import { inject as service } from '@ember/service'; - -export default class AssignmentInfoComponent extends ErrorHandlingComponent { - @service('utility-methods') utils; - - get currentProblem() { - let assignment = this.args.assignment; - if (!assignment) { - return null; - } - return assignment.get('problem.content'); - } - - get currentSection() { - let assignment = this.args.assignment; - if (!assignment) { - return null; - } - return assignment.get('section.content'); - } -} diff --git a/app/components/assignment-list.hbs b/app/components/assignment-list.hbs index 9570ffaeb..1f6940ea7 100644 --- a/app/components/assignment-list.hbs +++ b/app/components/assignment-list.hbs @@ -2,7 +2,7 @@

Assignments

- {{#unless @currentUser.isStudent}} + {{#unless this.user.isStudent}}
@@ -96,17 +85,6 @@ @topLevelQueryParams='searchBy' @preload={{this.problemsPreloadValue}} /> - {{#each this.problemFormErrors as |error|}} - - {{/each}} {{/if}} @@ -128,11 +106,6 @@ @value={{this.assignedDate}} {{on 'input' this.cancelDateError}} /> - {{#each this.assignedDateFormErrors as |error|}} -

- {{error}} -

- {{/each}}
  • @@ -152,13 +125,8 @@ @value={{this.dueDate}} {{on 'input' this.cancelDateError}} /> - {{#each this.dueDateFormErrors as |error|}} -

    - {{error}} -

    - {{/each}} {{#if this.invalidDateRange}} - - {{#each this.nameFormErrors as |error|}} -

    - {{error}} -

    - {{/each}}
  • @@ -200,7 +163,7 @@
    - - - - - {{#each this.createRecordErrors as |error|}} + {{#each this.errors as |error|}}

    {{error}}

    diff --git a/app/components/assignment-new.js b/app/components/assignment-new.js index 047aa643c..6704d6a2a 100644 --- a/app/components/assignment-new.js +++ b/app/components/assignment-new.js @@ -1,30 +1,26 @@ -import ErrorHandlingComponent from './error-handling'; +import Component from '@glimmer/component'; import { tracked } from '@glimmer/tracking'; import { action } from '@ember/object'; -import { inject as service } from '@ember/service'; -import $ from 'jquery'; +import { service } from '@ember/service'; import moment from 'moment'; -export default class AssignmentNewComponent extends ErrorHandlingComponent { +export default class AssignmentNewComponent extends Component { @service router; @service store; @service('sweet-alert') alert; - @tracked createAssignmentError = null; + @service currentUser; + @service errorHandling; @tracked isMissingRequiredFields = null; @tracked selectedSection = null; @tracked selectedProblem = null; @tracked problemList = null; @tracked formId = null; - @tracked createRecordErrors = []; - @tracked queryErrors = []; @tracked linkedWorkspacesMode = 'individual'; @tracked doCreateLinkedWorkspaces = false; @tracked doCreateParentWorkspace = false; @tracked fromProblemInfo = false; @tracked parentWorkspaceAccess = false; @tracked allSelected = true; - @tracked problemFormErrors = []; - @tracked sectionFormErrors = []; @tracked invalidDateRange = false; @tracked assignedDate = moment(new Date()).format('YYYY-MM-DD'); tooltips = { @@ -154,6 +150,10 @@ export default class AssignmentNewComponent extends ErrorHandlingComponent { } } + get user() { + return this.currentUser.user; + } + get hasSelectedSection() { return !!this.selectedSection; } @@ -210,13 +210,14 @@ export default class AssignmentNewComponent extends ErrorHandlingComponent { } } + get errors() { + return this.errorHandling.getErrors('createRecordErrors'); + } + constructor() { super(...arguments); - let selectedProblem = this.args.selectedProblem; - if (selectedProblem && selectedProblem.isForAssignment) { - this.fromProblemInfo = true; - this.selectedProblem = selectedProblem; - } + if (this.args.fromProbleInfo) + this.selectedProblem = this.args.selectedProblem; if (this.args.fromSectionInfo) { this.fromSectionInfo = true; this.selectedSection = this.args.selectedSection; @@ -233,7 +234,7 @@ export default class AssignmentNewComponent extends ErrorHandlingComponent { createAssignment(formValues) { let { section, problem, assignedDate, dueDate, name } = formValues; - const createdBy = this.args.currentUser; + const createdBy = this.user; if (!name) { // let nameDate = $('#assignedDate') @@ -269,14 +270,14 @@ export default class AssignmentNewComponent extends ErrorHandlingComponent { const doCreateLinkedWorkspaces = this.doCreateLinkedWorkspaces; const doCreateParentWorkspace = this.doCreateParentWorkspace; - let linkedFormatInput = $('#linked-ws-new-name'); + let linkedFormatInput = document.getElementById('linked-ws-new-name'); let linkedNameFormat; if (linkedFormatInput) { linkedNameFormat = linkedFormatInput.val(); } - let parentFormatInput = $('#parent-ws-new-name'); + let parentFormatInput = document.getElementById('parent-ws-new-name'); let parentNameFormat; if (parentFormatInput) { @@ -315,7 +316,11 @@ export default class AssignmentNewComponent extends ErrorHandlingComponent { ); }) .catch((err) => { - this.handleErrors(err, 'createRecordErrors', createAssignmentData); + this.errorHandling.handleErrors( + err, + 'createRecordErrors', + createAssignmentData + ); }); } @@ -380,8 +385,8 @@ export default class AssignmentNewComponent extends ErrorHandlingComponent { @action validate() { const section = this.selectedSection; const problem = this.selectedProblem; - let assignedDate = $('#assignedDate').val(); - let dueDate = $('#dueDate').val(); + let assignedDate = document.getElementById('assignedDate')?.value; + let dueDate = document.getElementById('dueDate')?.value; const name = this.name; const values = { diff --git a/app/components/categories-menu.hbs b/app/components/categories-menu.hbs new file mode 100644 index 000000000..5fe5cb556 --- /dev/null +++ b/app/components/categories-menu.hbs @@ -0,0 +1,7 @@ +
    +
      + {{#each this.categories as |category|}} + + {{/each}} +
    +
    \ No newline at end of file diff --git a/app/components/categories-menu.js b/app/components/categories-menu.js index 5ab655484..bb74b68ee 100644 --- a/app/components/categories-menu.js +++ b/app/components/categories-menu.js @@ -1,22 +1,73 @@ import Component from '@glimmer/component'; +import { service } from '@ember/service'; +import { tracked } from '@glimmer/tracking'; import { action } from '@ember/object'; +export default class CategoriesMenuComponent extends Component { + @service store; + @tracked categories = []; -export default class CategoriesMenu extends Component { - // didReceiveAttrs: function () { - // this._super(...arguments); - // let categories = this.get('categories.meta'); - // this.set('categories', categories.categories); - // } - addCategoryAction(cat) { - this.addCategories(cat); + constructor() { + super(...arguments); + this.loadCategoryTree().then((categories) => { + this.categories = this.normalizeCategories(categories); + }); } + + async loadCategoryTree() { + const queryCats = await this.store.query('category', {}); + const categories = queryCats.meta; + return categories.categories; + } + + normalizeCategories(categories = []) { + return categories.map((category) => { + return { + ...category, + url: null, + children: this.normalizeDomains(category.domains), + }; + }); + } + + normalizeDomains(domains = []) { + return domains.map((domain) => { + return { + ...domain, + children: this.normalizeTopics(domain.topics), + }; + }); + } + + normalizeTopics(topics = []) { + return topics.map((topic) => { + return { + ...topic, + url: null, + children: this.normalizeStandards(topic.standards), + }; + }); + } + + normalizeStandards(standards = []) { + return standards.map((standard) => { + return { + ...standard, + url: null, + children: standard.substandards || [], + }; + }); + } + @action - addCategory(category) { - let identifier = category.identifier; - this.store - .queryRecord('category', { identifier: identifier }) - .then((cat) => { - this.addCategoryAction('addCategories', cat); + async addCategory(category) { + if (category) { + const [categ] = await this.store.query('category', { + identifier: category.identifier, }); + console.log('categ', categ); + if (categ && this.args.addCategory) { + this.args.addCategory(categ); + } + } } } diff --git a/app/components/category-filter.hbs b/app/components/category-filter.hbs new file mode 100644 index 000000000..2b99982f9 --- /dev/null +++ b/app/components/category-filter.hbs @@ -0,0 +1,63 @@ +
    +
      + +
    • + +
    • +
      Selected Categories
      + + {{#if this.hasSelectedCategories}} +
        + {{#each this.selectedCategories as |category|}} +
      • + {{category.identifier}} + +
      • + {{/each}} +
      + {{else}} +

      No categories selected.

      + {{/if}} +
    + + {{#if this.showCategoryList}} +
    + +
    + {{/if}} +
    \ No newline at end of file diff --git a/app/components/category-filter.js b/app/components/category-filter.js new file mode 100644 index 000000000..cf02c54d3 --- /dev/null +++ b/app/components/category-filter.js @@ -0,0 +1,96 @@ +import Component from '@glimmer/component'; +import { action } from '@ember/object'; +import { service } from '@ember/service'; +import { tracked } from '@glimmer/tracking'; + +export default class CategoryFilterComponent extends Component { + @service store; + @service inputState; + @tracked showCategoryList = false; + @tracked selectedCategories = []; + + /** + * + + The category filter is assumed to have a single main option, which is selected by default. + The list of selected categories is stored in the listState of that main option. + The single main option has a single suboption, which is whether to include + subcategories in the filter. + */ + + get hasSelectedCategories() { + return this.selectedCategories.length > 0; + } + + get includeSubCatsOption() { + return this.inputState.getSubOptions(this.args.filterName)[0].value; + } + + get includeSubCats() { + return this.inputState + .getSubSelections(this.args.filterName) + .includes(this.includeSubCatsOption); + } + + @action + toggleIncludeSubCats() { + this.inputState.setSubSelection( + this.args.filterName, + this.includeSubCatsOption, + !this.includeSubCats + ); + if (this.args.onUpdate) { + this.args.onUpdate(); + } + } + + @action + handleAddCategoryIdentifier(identifier) { + // get the category by identifier from the store + // then call handleAddCategory with the full category object + this.store.query('category', { identifier }).then((categories) => { + if (categories.length > 0) { + this.handleAddCategory(categories.firstObject); + } + }); + } + + @action + handleAddCategory(category) { + console.log('category', category); + if (category && !this.selectedCategories.includes(category)) { + this.selectedCategories = [...this.selectedCategories, category]; + this.inputState.setListState( + this.args.filterName, + this.selectedCategories + ); + if (this.args.onUpdate) { + this.args.onUpdate(); + } + } + } + + @action + handleRemoveCategory(category) { + this.selectedCategories = this.selectedCategories.filter( + (selectedCategory) => selectedCategory.id !== category.id + ); + this.inputState.setListState(this.args.filterName, this.selectedCategories); + if (this.args.onUpdate) { + this.args.onUpdate(); + } + } + + @action + handleOpenCategoryList() { + this.showCategoryList = true; + } + + @action + handleCloseCategoryList() { + this.showCategoryList = false; + } +} diff --git a/app/components/image-upload.hbs b/app/components/image-upload.hbs new file mode 100644 index 000000000..ed37b4d79 --- /dev/null +++ b/app/components/image-upload.hbs @@ -0,0 +1,70 @@ +
    +
    + + + {{#unless @hideSubmit}} + {{#if this.filesToBeUploaded}} + + {{/if}} + {{/unless}} +
    + + {{#if this.missingFilesError}} + + {{/if}} + + {{#if this.isOverSizeLimit}} + {{#if this.isOverPdfLimit}} + + {{/if}} + {{#if this.isOverImageLimit}} + + {{/if}} + {{/if}} + + {{#if this.overSizedFileError}} + + {{/if}} + + {{#unless this.uploadResults}} + {{#if this.showLoadingMessage}} +

    Uploading images... Thank you for your + patience.

    + {{/if}} + {{/unless}} + + {{#each this.uploadErrors as |error|}} +

    {{error}}

    + {{/each}} + + {{#if this.uploadResults}} +

    + {{this.uploadResults.length}} + {{#if (greater-equal this.uploadResults.length 2)}} + files + {{else}} + file + {{/if}} + uploaded successfully! +

    + {{/if}} +
    \ No newline at end of file diff --git a/app/components/image-upload.js b/app/components/image-upload.js index d67947457..faf9f7cfc 100644 --- a/app/components/image-upload.js +++ b/app/components/image-upload.js @@ -1,36 +1,30 @@ -import Component from '@ember/component'; -import { computed, observer } from '@ember/object'; +import Component from '@glimmer/component'; +import { action } from '@ember/object'; +import { service } from '@ember/service'; +import { tracked } from '@glimmer/tracking'; import { later } from '@ember/runloop'; -import { inject as service } from '@ember/service'; -import $ from 'jquery'; -import CurrentUserMixin from '../mixins/current_user_mixin'; - -export default Component.extend(CurrentUserMixin, { - elementId: 'image-upload', - - alert: service('sweet-alert'), - store: service(), - errorHandling: service('error-handling'), - isHidden: false, - //uploadedFiles: null, - filesToBeUploaded: null, - uploadResults: null, - uploadError: null, - missingFilesError: false, - acceptMultiple: false, - uploadErrors: [], - singleFileSizeLimit: 10485760, // 10MB - totalPdfSizeLimit: 52428800, // 50MB - totalImageSizeLimit: 52428800, // 50MB - - didReceiveAttrs() { - this._super(); - let acceptableFileTypes = 'image/png,image/jpeg,application/pdf'; - if (this.isPdfOnly) { - acceptableFileTypes = 'application/pdf'; - } - this.set('acceptableFileTypes', acceptableFileTypes); - }, +export default class ImageUploadComponent extends Component { + @service('sweet-alert') alert; + @service store; + @service currentUser; + @service errorHandling; + + @tracked isHidden = false; + @tracked filesToBeUploaded = null; + @tracked uploadResults = null; + @tracked uploadError = null; + @tracked missingFilesError = false; + @tracked acceptMultiple = false; + @tracked uploadErrors = []; + @tracked singleFileSizeLimit = 10485760; // 10MB + @tracked totalPdfSizeLimit = 52428800; // 50MB + @tracked totalImageSizeLimit = 52428800; // 50MB + @tracked acceptableFileTypes = this.args.isPdfOnly + ? 'application/pdf' + : 'image/png,image/jpeg,application/pdf'; + @tracked showLoadingMessage = false; + loadingMessageTimer = null; + _fileInputEl = null; returnSizeDisplay(bytes) { if (bytes < 1024) { @@ -40,7 +34,7 @@ export default Component.extend(CurrentUserMixin, { } else if (bytes >= 1048576) { return (bytes / 1048576).toFixed(1) + 'MB'; } - }, + } getOverSizedFileMsg(fileSize, fileName) { let limit = this.singleFileSizeLimit; @@ -49,90 +43,100 @@ export default Component.extend(CurrentUserMixin, { let limitDisplay = this.returnSizeDisplay(limit); return `The file ${fileName} (${actualDisplay}) was not accepted due to exceeding the size limit of ${limitDisplay}`; - }, + } - overPdfLimitMsg: computed('totalPdfSizeLimit', 'totalPdfSize', function () { + get overPdfLimitMsg() { let limit = this.totalPdfSizeLimit; let actual = this.totalPdfSize; let actualDisplay = this.returnSizeDisplay(actual); let limitDisplay = this.returnSizeDisplay(limit); return `Sorry, the total size of your PDF uploads (${actualDisplay}) exceeds the maximum of ${limitDisplay}`; - }), - - overImageLimitMsg: computed( - 'totalImageSizeLimit', - 'totalImageSize', - function () { - let limit = this.totalImageSizeLimit; - let actual = this.totalImageSize; - let actualDisplay = this.returnSizeDisplay(actual); - let limitDisplay = this.returnSizeDisplay(limit); - - return `Sorry, the total size of your image uploads (${actualDisplay}) exceeds the maximum of ${limitDisplay}`; - } - ), + } - handleLoadingMessage: observer('isUploading', function () { - const that = this; - if (!this.isUploading) { - this.set('showLoadingMessage', false); - return; - } - later(function () { - if (that.isDestroyed || that.isDestroying) { - return; - } - that.set('showLoadingMessage', that.get('isUploading')); - }, 500); - }), - - uploadImage: function (currentUser, formData) { - const that = this; - return $.post({ - url: '/image', - processData: false, - contentType: false, - // createdBy: currentUser, - data: formData, + get overImageLimitMsg() { + let limit = this.totalImageSizeLimit; + let actual = this.totalImageSize; + let actualDisplay = this.returnSizeDisplay(actual); + let limitDisplay = this.returnSizeDisplay(limit); + + return `Sorry, the total size of your image uploads (${actualDisplay}) exceeds the maximum of ${limitDisplay}`; + } + + uploadImage(formData) { + return fetch('/image', { + method: 'POST', + body: formData, }) + .then((response) => { + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + return response.json(); + }) .then((res) => { let images = res.images; - that.set('uploadedImages', images); - that.get('store').pushPayload({ images }); + this.uploadedImages = images; + this.store.pushPayload({ images }); return res.images; }) .catch((err) => { - that.set('isUploading', false); - that.errorHandling.handleErrors(err, 'uploadErrors', err); + this.setShowLoadingMessage(false); + this.errorHandling.handleErrors(err, 'uploadErrors', err); return err; }); - }, - - uploadPdf: function (currentUser, formData) { - const that = this; - return $.post({ - url: '/pdf', - processData: false, - contentType: false, - data: formData, - // createdBy: currentUser - }) - .then(function (res) { - let images = res.images; - that.set('uploadedPdfs', images); - that.get('store').pushPayload({ images }); - return res.images; - }) - .catch((err) => { - that.set('isUploading', false); - that.errorHandling.handleErrors(err, 'uploadErrors', err); - return; + } + + @action + async uploadPdf(formData) { + // Show a loading state if needed + this.setShowLoadingMessage(true); + + try { + let response = await fetch('/pdf', { + method: 'POST', + body: formData, }); - }, - totalPdfSize: computed('filesToBeUploaded', function () { + if (!response.ok) { + // Read the error body to provide a meaningful message + let errorText = await response.text(); + throw new Error(errorText); + } + + let data = await response.json(); + let { images } = data; + this.uploadedPdfs = images; + this.store.pushPayload({ images }); + return images; + } catch (err) { + this.setShowLoadingMessage(false); + this.errorHandling.handleErrors(err, 'uploadErrors', err); + } + } + + setShowLoadingMessage(shouldShow) { + if (shouldShow) { + // Schedule a delayed toggle to show the message after 500 ms + this.loadingMessageTimer = later( + this, + () => { + this.showLoadingMessage = true; + }, + 500 + ); + } else { + // Hide immediately and cancel any pending timer + this.showLoadingMessage = false; + if (this.loadingMessageTimer) { + this.loadingMessageTimer.cancel(); + this.loadingMessageTimer = null; + } + } + } + + get totalPdfSize() { let total = 0; let files = this.filesToBeUploaded; if (!files) { @@ -145,9 +149,9 @@ export default Component.extend(CurrentUserMixin, { } } return total; - }), + } - totalImageSize: computed('filesToBeUploaded', function () { + get totalImageSize() { let total = 0; let files = this.filesToBeUploaded; if (!files) { @@ -160,152 +164,150 @@ export default Component.extend(CurrentUserMixin, { } } return total; - }), + } - isOverPdfLimit: computed('totalPdfSize', 'totalPdfSizeLimit', function () { + get isOverPdfLimit() { return this.totalPdfSize > this.totalPdfSizeLimit; - }), + } - isOverImageLimit: computed( - 'totalImageSize', - 'totalImageSizeLimit', - function () { - return this.totalImageSize > this.totalImageSizeLimit; - } - ), + get isOverImageLimit() { + return this.totalImageSize > this.totalImageSizeLimit; + } resetFileInput() { - let input = $('input.image-upload'); - if (input) { - this.set('filesToBeUploaded', null); - input.val(''); + if (this._fileInputEl) { + this.filesToBeUploaded = null; + this._fileInputEl.value = ''; // Clear the real DOM input } - }, - - actions: { - uploadImages: function () { - const that = this; - const currentUser = that.get('currentUser'); - const uploadData = that.get('filesToBeUploaded'); - if (!uploadData) { - this.set('isUploading', false); - this.set('missingFilesError', true); - return; - } - if (this.isOverPdfLimit || this.isOverImageLimit) { - this.set('isUploading', false); - this.set('isOverSizeLimit', true); + } + + @action + storeFileInputEl(el) { + this._fileInputEl = el; + } + + @action + resetMissingFilesError() { + this.missingFilesError = false; + } + + @action + resetOverSizedFileError() { + this.overSizedFileError = null; + } + + @action + uploadImages() { + const uploadData = this.filesToBeUploaded; + if (!uploadData) { + this.setShowLoadingMessage(false); + this.missingFilesError = true; + return; + } + if (this.isOverPdfLimit || this.isOverImageLimit) { + this.setShowLoadingMessage(false); + this.isOverSizeLimit = true; + return; + } + this.setShowLoadingMessage(true); + + let formData = new FormData(); + let pdfFormData = new FormData(); + + let imageCount = 0; + let pdfCount = 0; + + for (let f of uploadData) { + let fileSize = f.size; + if (fileSize > this.singleFileSizeLimit) { + this.setShowLoadingMessage(false); + this.overSizedFileError = this.getOverSizedFileMsg(f.size, f.name); + this.filesToBeUploaded = null; return; + } else if (f.type === 'application/pdf') { + pdfFormData.append('photo', f); + pdfCount++; + } else { + formData.append('photo', f); + imageCount++; } - this.set('isUploading', true); - - let formData = new FormData(); - let pdfFormData = new FormData(); - - let imageCount = 0; - let pdfCount = 0; - - for (let f of uploadData) { - let fileSize = f.size; - if (fileSize > this.singleFileSizeLimit) { - this.set('isUploading', false); - this.set( - 'overSizedFileError', - this.getOverSizedFileMsg(f.size, f.name) - ); - this.set('filesToBeUploaded', null); - return; - } else if (f.type === 'application/pdf') { - pdfFormData.append('photo', f); - pdfCount++; - } else { - formData.append('photo', f); - imageCount++; - } - } + } - if (imageCount > 0) { - return this.uploadImage(currentUser, formData).then((res) => { - if (pdfCount > 0) { - return this.uploadPdf(currentUser, pdfFormData).then((res) => { - let results; - if (this.uploadedPdfs && this.uploadedImages) { - results = this.uploadedPdfs.concat(this.uploadedImages); - this.set('isUploading', false); - let fileModifier = results.length === 1 ? 'file' : 'files'; - - let msg = `Uploaded ${results.length} ${fileModifier} successfully`; - this.alert.showToast( - 'success', - msg, - 'bottom-end', - 3000, - false, - null - ); - if (this.handleUploadResults) { - this.handleUploadResults(results); - } - this.set('uploadResults', results); - if (this.doResetFilesAfterUpload) { - this.resetFileInput(); - } + if (imageCount > 0) { + return this.uploadImage(formData).then(() => { + if (pdfCount > 0) { + return this.uploadPdf(pdfFormData).then(() => { + let results; + if (this.uploadedPdfs && this.uploadedImages) { + results = this.uploadedPdfs.concat(this.uploadedImages); + this.setShowLoadingMessage(false); + let fileModifier = results.length === 1 ? 'file' : 'files'; + + let msg = `Uploaded ${results.length} ${fileModifier} successfully`; + this.alert.showToast( + 'success', + msg, + 'bottom-end', + 3000, + false, + null + ); + if (this.handleUploadResults) { + this.handleUploadResults(results); + } + this.uploadResults = results; + if (this.doResetFilesAfterUpload) { + this.resetFileInput(); } - }); - } else { - this.set('isUploading', false); - - let images = this.uploadedImages; - let fileModifier = images.length === 1 ? 'file' : 'files'; - - let msg = `Uploaded ${images.length} ${fileModifier} successfully`; - this.alert.showToast( - 'success', - msg, - 'bottom-end', - 3000, - false, - null - ); - if (this.handleUploadResults) { - this.handleUploadResults(images); - } - this.set('uploadResults', this.uploadedImages); - if (this.doResetFilesAfterUpload) { - this.resetFileInput(); } - } - }); - } else if (pdfCount > 0) { - return this.uploadPdf(currentUser, pdfFormData).then((res) => { - this.set('isUploading', false); - - let pdfs = this.uploadedPdfs; + }); + } else { + this.setShowLoadingMessage(false); - let fileModifier = pdfs.length === 1 ? 'file' : 'files'; + let images = this.uploadedImages; + let fileModifier = images.length === 1 ? 'file' : 'files'; - let msg = `Uploaded ${pdfs.length} ${fileModifier} successfully`; + let msg = `Uploaded ${images.length} ${fileModifier} successfully`; this.alert.showToast('success', msg, 'bottom-end', 3000, false, null); if (this.handleUploadResults) { - this.handleUploadResults(pdfs); + this.handleUploadResults(images); } - this.set('uploadResults', this.uploadedPdfs); + this.uploadResults = this.uploadedImages; if (this.doResetFilesAfterUpload) { this.resetFileInput(); } - }); - } - }, + } + }); + } else if (pdfCount > 0) { + return this.uploadPdf(pdfFormData).then(() => { + this.setShowLoadingMessage(false); - updateFiles(event) { - if (this.missingFilesError) { - this.set('missingFilesError', false); - } + let pdfs = this.uploadedPdfs; - this.set('filesToBeUploaded', event.target.form.firstElementChild.files); - if (this.storeFiles) { - this.storeFiles(event.target.form.firstElementChild.files); - } - }, - }, -}); + let fileModifier = pdfs.length === 1 ? 'file' : 'files'; + + let msg = `Uploaded ${pdfs.length} ${fileModifier} successfully`; + this.alert.showToast('success', msg, 'bottom-end', 3000, false, null); + if (this.handleUploadResults) { + this.handleUploadResults(pdfs); + } + this.uploadResults = this.uploadedPdfs; + if (this.doResetFilesAfterUpload) { + this.resetFileInput(); + } + }); + } + } + + @action + updateFiles(event) { + if (this.missingFilesError) { + this.missingFilesError = false; + } + + this.filesToBeUploaded = event.target.form.firstElementChild.files; + if (this.storeFiles) { + this.storeFiles(event.target.form.firstElementChild.files); + } + } +} diff --git a/app/components/linked-workspaces-new.hbs b/app/components/linked-workspaces-new.hbs index 669399ad2..8cdbdca16 100644 --- a/app/components/linked-workspaces-new.hbs +++ b/app/components/linked-workspaces-new.hbs @@ -49,7 +49,7 @@
    {{#each this.workspaceNameErrors as |error|}} - {{#if this.createWorkspaceError}} - {{/unless}} diff --git a/app/components/log-in.hbs b/app/components/log-in.hbs index 7d8d17a29..00083d706 100644 --- a/app/components/log-in.hbs +++ b/app/components/log-in.hbs @@ -9,7 +9,7 @@
  • - {{#if this.showCategoryFilters}} -
      - -
    • - -
    • -
      Selected Categories
      - - {{#if @categoriesFilter}} -
        - {{#each @categoriesFilter as |category|}} -
      • - {{category.identifier}} - -
      • - {{/each}} -
      - {{else}} -

      No categories selected.

      - {{/if}} -
    + {{/if}} - {{#if this.isAdminUser}} + {{#if this.userIsAdmin}}

    - Show All Trashed Problems + Show All the Trashed Problems {{/if}} diff --git a/app/components/problem-filter.js b/app/components/problem-filter.js index 34026d089..cb6fe80be 100644 --- a/app/components/problem-filter.js +++ b/app/components/problem-filter.js @@ -4,18 +4,15 @@ import { service } from '@ember/service'; import { tracked } from '@glimmer/tracking'; /** - * + @categoryFilterName={{this.categoryFilterName}} + /> */ export default class ProblemFilterComponent extends Component { @service currentUser; @@ -56,34 +53,11 @@ export default class ProblemFilterComponent extends Component { this.closedMenu = !this.closedMenu; } - @action - toggleTrashedProblems() { - if (this.args.triggerShowTrashed) { - this.args.triggerShowTrashed(); - } - } - @action toggleCategoryFilters() { this.showCategoryFilters = !this.showCategoryFilters; } - @action - addCategory(val) { - if (!val) { - return; - } - let category = this.args.store.peekRecord('category', val); - if (category) { - this.args.categoriesFilter.addObject(category); - } - } - - @action - removeCategory(category) { - this.args.categoriesFilter.removeObject(category); - } - @action toggleIncludeSubCats() { this.args.onUpdate(); diff --git a/app/components/problem-info.hbs b/app/components/problem-info.hbs index b3153207f..35f30156b 100644 --- a/app/components/problem-info.hbs +++ b/app/components/problem-info.hbs @@ -1,160 +1,104 @@ -
    - {{#if this.isEditing}} - - {{{public-private this.privacySettingIcon}}} - -
    - + + + + +
    + {{else}} + - - - - - - {{else}} - - {{{public-private @problem.privacySetting}}} + {{public-private this.problem.privacySetting}} + + {{/if}} + + - {{/if}} - - - -
    + -
    -
    -
    -
    - {{#if this.isEditing}} - +
    +
    +
    + - {{else}} - {{{@problem.title}}} - {{/if}} -
    -
    {{format-date - @problem.createDate - 'MM/DD/YYYY' - }}
    -
    See Reports
    -
    -
    - - - - -
    -
    - {{#if this.showGeneral}} - -
    - {{#if this.isEditing}} - - {{else}} - {{{@problem.text}}} - {{/if}}
    - {{#if this.isEditing}} +
    + {{format-date @problem.createDate 'MM/DD/YYYY'}} +
    +
    See Reports
    +
    + +
    + {{! General tab}} + {{#if this.showGeneral}} + +
    + {{#if @isEditing}} + {{! The starting text is from the argument; we change the local value }} + + {{else}} + {{make-html-safe @problem.text}} + {{/if}} +
    - {{#if @problem.author}} -
    - -
    - {{else}} -
    - + +
    + {{#if @problem.organization}} + {{#if this.user.isAdmin}} + +
    + {{#if @problem.organization}} + {{@problem.organization.name}} + {{/if}}
    {{/if}} -
    - {{else}} - {{#if @problem.author}} - -
    - {{@problem.author}} -
    - {{/if}} - {{/if}} - {{#if @problem.organization}} - {{#if @currentUser.isAdmin}} - -
    - {{#if @problem.organization}} - {{@problem.organization.name}} - {{/if}} -
    {{/if}} - {{/if}} - -
    - {{#if this.isEditing}} - {{#if @currentUser.isTeacher}} - - - - - {{@problem.status}} - - {{else}} - - - - - {{/if}} - {{else}} + + +
    - - {{@problem.status}} - - {{#if (is-equal @problem.status 'flagged')}} - {{#if this.showFlagReason}} - Hide -
    -

    Reason: - {{@problem.flagReason.reason}}

    -

    By - {{@flaggedBy.username}} - on - {{format-date @flaggedDate 'MMM Do YYYY'}}

    -
    - {{else}} - Why? - {{/if}} + + {{#if this.canEditStatus}} + + {{else}} + + {{@problem.status}} + {{/if}} - {{/if}} -
    - {{/if}} + {{#unless @isEditing}} + {{#if (is-equal @problem.status 'flagged')}} + {{#if this.showFlagReason}} + Hide +
    +

    Reason: + {{@problem.flagReason.reason}}

    +

    By + {{@flaggedBy.username}} + on + {{format-date @flaggedDate 'MMM Do YYYY'}}

    +
    + {{else}} + Why? + {{/if}} + {{/if}} + {{/unless}} +
    + {{/if}} - {{#if this.showCats}} - -
    - {{#if this.isEditing}} - {{#if @problem.categories}} -
      - {{#each @problem.categories as |category|}} -
    • - {{category.identifier}} - -

      {{category.description}}

      -
    • - {{/each}} -
    - {{#unless this.showCategories}} -
    - -
    - {{/unless}} - {{#if this.showCategories}} -
    - -
    - + {{! Categories tab}} + {{#if this.showCats}} + +
    + {{#if @isEditing}} + {{#if this.problem.categories}} +
      + {{#each this.problem.categories as |category|}} +
    • + {{category.identifier}} + +

      {{category.description}}

      +
    • + {{/each}} +
    {{/if}} - {{else}} - {{#unless this.showCategories}} -
    - -
    - {{/unless}} +
    + +
    {{#if this.showCategories}} -
    - -
    - {{#if this.categoryTree}} - - {{/if}} + {{/if}} - {{/if}} - {{else}} - {{#if @problem.categories}} -
      - {{#each @problem.categories as |category|}} -
    • - {{category.identifier}} -

      {{category.description}}

      -
    • - {{/each}} -
    {{else}} - No Problem Categories + {{#if @problem.categories}} +
      + {{#each @problem.categories as |category|}} +
    • + {{category.identifier}} +

      {{category.description}}

      +
    • + {{/each}} +
    + {{else}} + No Problem Categories + {{/if}} {{/if}} - {{/if}} -
    - -
    - {{#if this.isEditing}} - - {{else}} - {{#if @problem.keywords}} -
      - {{#each @problem.keywords as |keyword|}} -
    • - {{keyword}} -
    • - {{/each}} -
    +
    + +
    + {{#if @isEditing}} + {{else}} - No Problem Keywords + {{#if @problem.keywords}} +
      + {{#each @problem.keywords as |keyword|}} +
    • + {{keyword}} +
    • + {{/each}} +
    + {{else}} + No Problem Keywords + {{/if}} {{/if}} - {{/if}} - -
    - {{/if}} +
    + {{/if}} - {{#if this.showAdditional}} - - {{#if this.isEditing}} + {{! Additional tab}} + {{#if this.showAdditional}} +
    - + {{else if (is-equal @type 'button')}} + {{else}} + {{#if @label}} + + {{/if}} {{/if}} @@ -28,19 +54,21 @@ {{#if (is-equal @type 'checkbox')}} {{if @value 'Yes' 'No'}} {{else}} - {{@value}} + {{make-html-safe @value}} {{/if}} {{/if}} - {{! Render a button if provided }} + {{! Render a post-field button if provided }} {{#if @buttonLabel}} - + {{#if @isEditing}} + + {{/if}} {{/if}}
    \ No newline at end of file diff --git a/app/components/ui/form-field.js b/app/components/ui/form-field.js index e4ed262eb..38806c0e5 100644 --- a/app/components/ui/form-field.js +++ b/app/components/ui/form-field.js @@ -3,7 +3,11 @@ import { action } from '@ember/object'; import { tracked } from '@glimmer/tracking'; export default class UiFormFieldComponent extends Component { - @tracked currentValue = this.args.value; // Track the value for inline editing + @tracked currentValue = this.args.value; + + get id() { + return this.args.id || this.args.name || 'xyzzy'; + } @action handleInput(event) { @@ -27,4 +31,9 @@ export default class UiFormFieldComponent extends Component { this.args.onClick(); } } + + @action + resetEditingValue() { + this.currentValue = this.args.value; + } } diff --git a/app/components/ui/my-select.hbs b/app/components/ui/my-select.hbs new file mode 100644 index 000000000..36bc19508 --- /dev/null +++ b/app/components/ui/my-select.hbs @@ -0,0 +1,31 @@ +
    + + {{#unless @cannotBeNull}} + {{#if @selectedValue}} + + {{/if}} + {{/unless}} +
    \ No newline at end of file diff --git a/app/components/ui/my-select.js b/app/components/ui/my-select.js new file mode 100644 index 000000000..05d4dff3f --- /dev/null +++ b/app/components/ui/my-select.js @@ -0,0 +1,24 @@ +import Component from '@glimmer/component'; +import { action } from '@ember/object'; + +export default class MySelectComponent extends Component { + @action + selectChange(event) { + let selectedIndex = event.target.selectedIndex; + if (this.args.prompt && selectedIndex > 0) { + selectedIndex -= 1; + } + + const selectedItem = this.args.content[selectedIndex]; + if (this.args.action) { + this.args.action(selectedItem); + } + } + + @action + clearSelection() { + if (this.args.action) { + this.args.action(null); + } + } +} diff --git a/app/components/radio-filter.hbs b/app/components/ui/radio-filter.hbs similarity index 100% rename from app/components/radio-filter.hbs rename to app/components/ui/radio-filter.hbs diff --git a/app/components/radio-filter.js b/app/components/ui/radio-filter.js similarity index 100% rename from app/components/radio-filter.js rename to app/components/ui/radio-filter.js diff --git a/app/components/radio-group-item.hbs b/app/components/ui/radio-group-item.hbs similarity index 100% rename from app/components/radio-group-item.hbs rename to app/components/ui/radio-group-item.hbs diff --git a/app/components/radio-group.hbs b/app/components/ui/radio-group.hbs similarity index 96% rename from app/components/radio-group.hbs rename to app/components/ui/radio-group.hbs index 2e2501a5a..14c245405 100644 --- a/app/components/radio-group.hbs +++ b/app/components/ui/radio-group.hbs @@ -14,7 +14,7 @@ {{#each @options.inputs as |input|}}
    1. - + {{! We expect @tabs to be an array of objects like: + [{ label: "General", key: "general" }, ... ] }} + {{#each @tabs as |tab|}} + + {{/each}} +
    \ No newline at end of file diff --git a/app/components/wordcloud-container.hbs b/app/components/ui/wordcloud-container.hbs similarity index 100% rename from app/components/wordcloud-container.hbs rename to app/components/ui/wordcloud-container.hbs diff --git a/app/components/wordcloud-container.js b/app/components/ui/wordcloud-container.js similarity index 100% rename from app/components/wordcloud-container.js rename to app/components/ui/wordcloud-container.js diff --git a/app/components/collapsible-list.hbs b/app/components/user-collapsible-list.hbs similarity index 100% rename from app/components/collapsible-list.hbs rename to app/components/user-collapsible-list.hbs diff --git a/app/components/collapsible-list.js b/app/components/user-collapsible-list.js similarity index 100% rename from app/components/collapsible-list.js rename to app/components/user-collapsible-list.js diff --git a/app/components/user-info.hbs b/app/components/user-info.hbs index 830c615b4..52333ef94 100644 --- a/app/components/user-info.hbs +++ b/app/components/user-info.hbs @@ -1,5 +1,7 @@ -

    {{@user.username}}'s Account Details

    -{{#each this.errorHandling.loadOrgsErrors as |error|}} +

    + {{@user.username}}'s Account Details +

    +{{#each this.loadOrgsErrors as |error|}}

    {{error}}

    {{/each}}
    @@ -11,36 +13,38 @@ Username - {{#if this.isEditing}} - - {{else}} - {{@user.username}} - {{/if}} + First Name - {{#if this.isEditing}} - - {{else}} - {{@user.firstName}} - {{/if}} + Last Name - {{#if this.isEditing}} - - {{else}} - {{@user.lastName}} - {{/if}} + @@ -52,11 +56,13 @@ Email - {{#if this.isEditing}} - - {{else}} - {{@user.email}} - {{/if}} + @@ -64,22 +70,20 @@ Email Confirmed - {{#if this.isEditing}} - {{#if this.unconfirmedEmail}} - {{#if @user.email}} - - - {{/if}} - {{else}} - {{@user.isEmailConfirmed}} - {{/if}} + {{#if this.noEmail}} + No email address provided + {{else if @user.isEmailConfirmed}} + {{! Note: Use the original value so can undo during editing }} + Yes {{else}} - {{@user.isEmailConfirmed}} + {{/if}} @@ -87,8 +91,23 @@ {{#if this.isEditing}} Password - + + {{#if this.isResettingPassword}} + + {{else}} + + {{/if}} + {{/if}} {{#unless @user.organizationRequest}} @@ -97,14 +116,16 @@ {{#if this.isEditing}}*{{/if}} {{#if this.isEditing}} {{#if @currentUser.isAdmin}} - + + /> + {{else}} {{@user.organization.name}} {{/if}} @@ -121,8 +142,11 @@ Would you like to create {{@user.organizationRequest}}
    - - + + {{else}} {{@user.organizationRequest}} @@ -139,10 +163,11 @@ User's Sections {{#each @userSections as |section|}} -

    {{section.name}}

    +

    + + {{section.name}} + +

    {{/each}} @@ -150,32 +175,36 @@ Location - {{#if this.isEditing}} - - {{else}} - {{@user.location}} - {{/if}} + Seen Tour - {{#if this.isEditing}} - {{#if this.seenTour}} - - {{else}} - - {{/if}} - {{/if}} - {{this.tourDate}} + Last Seen {{#if @user.lastSeen}} - {{format-date @user.lastSeen 'MMM Do YYYY'}} + + {{format-date @user.lastSeen 'MMM Do YYYY'}} at - {{format-date @user.lastSeen 'hh:mma'}} + {{format-date @user.lastSeen 'hh:mma'}} + {{else}} Never {{/if}} @@ -183,9 +212,11 @@ {{#if @user.lastModifiedBy}} Last Modified - {{format-date @user.lastModifiedDate 'MMM Do YYYY'}} + + {{format-date @user.lastModifiedDate 'MMM Do YYYY'}} by - {{@user.lastModifiedBy.username}} + {{@user.lastModifiedBy.username}} + {{/if}} @@ -194,16 +225,11 @@ {{#if this.isEditing}} {{#if this.canEdit}} - - {{/if}} @@ -211,112 +237,87 @@ {{account-types @user.accountType}} {{/if}} - {{#if @user.isAuthorized}} + + Authorized + + {{#if this.canEdit}} + + {{else}} + {{yes-no @user.isAuthorized}} + {{/if}} + + + {{#if @user.authorizedBy}} - Authorized - - {{#if this.isEditing}} - {{#if this.canEdit}} - - {{else}} - {{@user.isAuthorized}} - {{/if}} - {{else}} - {{@user.isAuthorized}} - {{/if}} - + Authorized By + {{@user.authorizedBy.username}} - {{#if @user.authorizedBy}} - - Authorized By - {{@user.authorizedBy.username}} - - {{/if}} - {{else}} + {{/if}} + {{#if @user.requestReason}} - Request Reason + Reason for Authorization Requst {{@user.requestReason}} - {{#if this.isEditing}} - - Authorize - - {{#if this.canEdit}} - - {{else}} - {{@user.isAuthorized}} - {{/if}} - - - {{else}} - - Authorized - - No - - - {{/if}} {{/if}} {{#if this.canEdit}} {{#if this.isEditing}} - {{#if this.isResettingPassword}} - - {{/if}} - {{#unless this.isResettingPassword}} -
    - - -
    - {{/unless}} +
    + + +
    {{else}}
    {{#if @currentUser.isAdmin}} {{#if @user.isTrashed}} {{else}} {{/if}} {{/if}}
    {{/if}} {{else}} -

    You can't edit users you haven't created. Contact - an admin to make changes.

    +

    You may not edit users whom you did not create. + Contact an admin to make changes.

    {{/if}}
    -{{#each this.errorHandling.findRecordErrors as |error|}} +{{#each this.findRecordErrors as |error|}}

    {{error}}

    {{/each}} -{{#each this.errorHandling.createRecordErrors as |error|}} +{{#each this.createRecordErrors as |error|}}

    {{error}}

    {{/each}} -{{#each this.errorHandling.updateRecordErrors as |error|}} +{{#each this.updateRecordErrors as |error|}}

    {{error}}

    {{/each}} \ No newline at end of file diff --git a/app/components/user-info.js b/app/components/user-info.js index 0db8cbb15..14fc8c77f 100644 --- a/app/components/user-info.js +++ b/app/components/user-info.js @@ -9,125 +9,118 @@ export default class UserInfoComponent extends Component { @service('utility-methods') utils; @service('error-handling') errorHandling; @service('edit-permissions') basePermissions; + @service currentUser; @service store; @tracked isEditing = false; - @tracked authorized = null; - @tracked selectedType = null; - @tracked willOveride = null; - @tracked fieldType = 'password'; - @tracked loadOrgsErrors = []; - @tracked updateRecordErrors = []; - @tracked createRecordErrors = []; - @tracked findRecordErrors = []; @tracked isResettingPassword = false; + @tracked user = null; - get canEdit() { - let user = this.args.user; - let currentUser = this.args.currentUser; - if (!user || !currentUser) { - return false; - } - - // is Admin - if (this.args.currentUser.accountType === 'A') { - return true; - } + get loadOrgsErrors() { + return this.errorHandling.getErrors('loadOrgsErrors'); + } - // is self - if (user.get('id') === currentUser.get('id')) { - return true; - } + get updateRecordErrors() { + return this.errorHandling.getErrors('updateRecordErrors'); + } - let creatorId = this.utils.getBelongsToId(user, 'createdBy'); + get createRecordErrors() { + return this.errorHandling.getErrors('createRecordErrors'); + } - // is creator - if (currentUser.get('id') === creatorId) { - return true; - } + get findRecordErrors() { + return this.errorHandling.getErrors('findRecordErrors'); + } - // pd admin for user's org - if (this.basePermissions.isRecordInPdDomain(user)) { - return true; - } - return false; + get noEmail() { + return !this.args.user.email; } - get canConfirm() { - if (this.basePermissions.isActingAdmin) { - return true; - } - if (this.basePermissions.isRecordInPdDomain(this.args.user)) { - return true; - } - return false; + get canEdit() { + const user = this.args.user; + if (!user) return false; + + const creatorId = this.utils.getBelongsToId(user, 'createdBy'); + + // can edit if: + // - current user is an admin + // - current user is editing their own profile + // - current user is the creator of the user + // - current user is a PD admin for the user's org + return ( + this.currentUser.isAdmin || + user.id === this.currentUser.id || + creatorId === this.currentUser.id || + this.basePermissions.isRecordInPdDomain(user) + ); } - get unconfirmedEmail() { - return !this.args.user.isEmailConfirmed; + get canConfirm() { + return ( + this.currentUser.isActingAdmin || + this.basePermissions.isRecordInPdDomain(this.args.user) + ); } get accountTypes() { - let accountType = this.args.currentUser.get('accountType'); - let accountTypes; - - if (accountType === 'A') { - accountTypes = ['Teacher', 'Student', 'Pd Admin', 'Admin']; - } else if (accountType === 'P') { - accountTypes = ['Teacher', 'Student']; - } else if (accountType === 'T') { - accountTypes = ['Student']; + if (this.currentUser.isAdmin) { + return ['Teacher', 'Student', 'Pd Admin', 'Admin']; + } else if (this.currentUser.isPdAdmin) { + return ['Teacher', 'Student']; + } else if (this.currentUser.isTeacher) { + return ['Student']; } else { - accountTypes = []; + return []; } + } - return accountTypes; + get seenTour() { + return this.user?.seenTour ?? this.args.user.seenTour; } get tourDate() { - var date = this.seenTour; + var date = this.args.user.seenTour; if (date) { return moment(date).fromNow(); } - return 'no'; + return 'No'; } - @action editUser() { - let user = this.args.user; - this.userEmail = user.email; - - let accountType = user.get('accountType'); - if (accountType === 'S') { - this.selectedType = 'Student'; - } else if (accountType === 'T') { - this.selectedType = 'Teacher'; - } else if (accountType === 'A') { - this.selectedType = 'Admin'; - } else if (accountType === 'P') { - this.selectedType = 'Pd Admin'; - } else { - this.selectedType = null; - } + @action + editUser() { this.isEditing = true; - let isAuth = user.get('isAuthorized'); - this.authorized = isAuth; + this.user = this.extractEditableProperties(this.args.user); + } + + @action + handleCancelEdit() { + this.isEditing = false; + this.user = this.extractEditableProperties(this.args.user); + } + + @action + handleSave() { + this.checkOrgExists(); + } + + @action + handleChange(property, value) { + this.user = { ...this.user, [property]: value }; + } + + @action + handleAccountTypeChange(value) { + this.handleChange('accountType', value[0]); } @action checkOrgExists() { - let user = this.args.user; - let userOrg = user.get('organization.content'); - let userOrgRequest = user.get('organizationRequest'); + let user = this.user; + let userOrg = user.organization?.content; + let userOrgRequest = user.organizationRequest; let org = this.org; let orgReq = this.orgReq; - let options = [ - Boolean(userOrg), - Boolean(userOrgRequest), - Boolean(org), - Boolean(orgReq), - ]; - - if (options.includes(true)) { + if (userOrg || userOrgRequest || org || orgReq) { this.saveUser(); } else { this.alert @@ -146,38 +139,38 @@ export default class UserInfoComponent extends Component { } saveUser() { - let currentUser = this.args.currentUser; - let user = this.args.user; - let org = this.org; - let orgReq = this.orgReq; + const user = this.args.user; + + // set the isConfirmingEmail flag if the current user has manually confirmed this user's email + // so server knows whether to make request to sso server + user.isConfirmingEmail = + this.user.isEmailConfirmed && !user.isEmailConfirmed; + + // update user with the values from the properties in this.user + Object.keys(this.user).forEach((key) => { + user.set(key, this.user[key]); + }); + + const org = this.org; + const orgReq = this.orgReq; let orgs = this.args.orgList; let matchingOrg = orgs.findBy('name', orgReq); if (matchingOrg) { - org = matchingOrg; - orgReq = null; + this.org = matchingOrg; + this.orgReq = null; } - // should we check to see if any information was actually updated before updating modified by/date? - let accountType = this.selectedType; - let accountTypeLetter = accountType.charAt(0).toUpperCase(); - user.set('accountType', accountTypeLetter); - if (org) { - user.set('organization', org); + user.org = org; } if (orgReq) { - user.set('organizationRequest', orgReq); + user.organizationRequest = orgReq; } - user.set('email', this.userEmail); //if is authorized is now true, then we need to set the value of authorized by to current user - let newDate = new Date(); - user.set('lastModifiedBy', currentUser); - user.set('lastModifiedDate', newDate); - - // so server knows whether to make request to sso server - user.set('isConfirmingEmail', this.isConfirmingEmail); + user.lastModifiedBy = this.currentUser.user; + user.lastModifiedDate = new Date(); user .save() @@ -190,7 +183,6 @@ export default class UserInfoComponent extends Component { false, null ); - this.isEditing = false; this.errorHandling.removeMessages('updateRecordErrors'); }) .catch((err) => { @@ -211,14 +203,11 @@ export default class UserInfoComponent extends Component { } } - @action resetPassword() { + @action + resetPassword() { this.isResettingPassword = true; } - @action authEmail() { - this.willOveride = true; - } - @action confirmOrgModal() { let user = this.args.user; let reqOrg = user.get('organizationRequest'); @@ -238,11 +227,10 @@ export default class UserInfoComponent extends Component { @action createNewOrg() { let user = this.args.user; - let currentUser = this.args.currentUser; let reqOrg = user.get('organizationRequest'); let newOrg = this.store.createRecord('organization', { name: reqOrg, - createdBy: currentUser, + createdBy: this.currentUser.user, }); newOrg .save() @@ -255,7 +243,7 @@ export default class UserInfoComponent extends Component { user.set('organizationRequest', null); user .save() - .then((user) => { + .then(() => { this.alert.showToast( 'success', `${orgName} Created`, @@ -281,7 +269,7 @@ export default class UserInfoComponent extends Component { user.set('organizationRequest', null); user .save() - .then((res) => { + .then(() => { // handle success this.errorHandling.removeMessages('updateRecordErrors'); }) @@ -370,29 +358,37 @@ export default class UserInfoComponent extends Component { this.isResettingPassword = false; } - @action handleResetSuccess(updatedUser) { + @action handleResetSuccess() { this.isResettingPassword = false; this.resetPasswordSuccess = true; this.errorHandling.removeMessages('findRecordErrors'); this.args.refresh(); } - @action clearTour() { - this.args.user.seenTour = null; + @action + clearTour() { + this.handleChange('seenTour', null); } - @action doneTour() { - this.args.user.seenTour = new Date(); + @action + doneTour() { + this.handleChange('seenTour', new Date()); } - @action onManualConfirm() { - // clicked manual confirm email box - let user = this.args.user; - - if (user) { - let isConfirmingEmail = !user.get('isEmailConfirmed'); - this.isConfirmingEmail = isConfirmingEmail; - user.toggleProperty('isEmailConfirmed'); - } + extractEditableProperties(user) { + return { + username: user.username, + firstName: user.firstName, + lastName: user.lastName, + email: user.email, + isEmailConfirmed: user.isEmailConfirmed, + organizationRequest: user.organizationRequest, + location: user.location, + accountType: user.accountType, + organization: user.organization, + isAuthorized: user.isAuthorized, + isTrashed: user.isTrashed, + seenTour: user.seenTour, + }; } } diff --git a/app/components/user-list.hbs b/app/components/user-list.hbs index d2b4a27fb..a8d4ba270 100644 --- a/app/components/user-list.hbs +++ b/app/components/user-list.hbs @@ -7,57 +7,57 @@
    -
    - {{#if (is-equal this.submissions.value 'custom')}} @@ -54,11 +53,10 @@ Selections
    -
    @@ -68,11 +66,10 @@ Comments
    -
    @@ -82,11 +79,10 @@ Folders
    -
    @@ -96,11 +92,10 @@ Feedback
    -
    @@ -108,7 +103,7 @@ {{/if}} {{#if this.missingUserError}}
    - -
    {{/if}} @@ -115,7 +117,9 @@
    {{#if - (not (is-equal collaborator.userObj this.selectedCollaborator)) + (not + (is-equal collaborator.userObj this.selectedCollaborator) + ) }}
    Permissions Type @@ -125,7 +129,7 @@ {{#if (is-equal collaborator.userObj this.selectedCollaborator) }} -
    - {{#if (is-equal this.submissions.value 'custom')}} @@ -185,11 +188,10 @@ Selections
    -
    @@ -199,11 +201,10 @@ Comments
    -
    @@ -213,11 +214,10 @@ Folders
    -
    @@ -227,11 +227,10 @@ Feedback
    -
    @@ -250,11 +249,10 @@ collaborator.userObj this.selectedCollaborator ) }} - {{#if (is-equal this.submissions.value 'custom')}} @@ -320,10 +318,14 @@ }}
    {{/if}} diff --git a/app/components/workspace-info-settings.hbs b/app/components/workspace-info-settings.hbs index d51215610..d291cd3d2 100644 --- a/app/components/workspace-info-settings.hbs +++ b/app/components/workspace-info-settings.hbs @@ -86,7 +86,7 @@
    {{#if this.isEditing}} -
    {{#if this.isEditing}} - Update Workspace
    {{#if this.missingLinkedAssignment}} - {{/if}} {{#each this.updateErrors as |error|}} - {{/each}} {{#each this.serverErrors as |error|}} -
    {{#if this.isEditing}} - Update Parent Workspace
    {{#if this.missingChildWorkspaces}} - {{/if}} {{#each this.updateErrors as |error|}} - {{/each}} {{#each this.serverErrors as |error|}} - @@ -102,7 +102,7 @@
      {{#each this.modeOptions as |option|}}
    • - = 0) { let endTime = this.currentSelection.vmtInfo.endTime; this.setVmtReplayerTime(vmtStartTime, true, endTime); @@ -353,7 +353,7 @@ export default class WorkspaceSubmissionCompComponent extends Component { get isVmt() { return this.utils.isValidMongoId( - this.args.currentSubmission.vmtRoomInfo.roomId + this.args.currentSubmission?.vmtRoomInfo?.roomId ); } diff --git a/app/controllers/.gitkeep b/app/controllers/.gitkeep deleted file mode 100644 index e69de29bb..000000000 diff --git a/app/controllers/index.js b/app/controllers/index.js index f9d8907a9..2cc8a826b 100644 --- a/app/controllers/index.js +++ b/app/controllers/index.js @@ -12,18 +12,15 @@ import { inject as service } from '@ember/service'; import config from 'encompass/config/environment'; export default class IndexController extends Controller { - @service('edit-permissions') basePermissions; - @tracked dataToShow = this.basePermissions.isStudent - ? 'assignment' - : 'workspace'; + @service currentUser; + @tracked dataToShow = this.currentUser.isStudent ? 'assignment' : 'workspace'; @tracked currentBound = 'oneWeek'; @tracked showTable = true; // this changes when user changes the tab. initially starts at "mine" @tracked selectedData = this.data[0].details; - @tracked activeDetailTab = this.basePermissions.isStudent + @tracked activeDetailTab = this.currentUser.isStudent ? 'Assigned To Me' : 'Mine'; - @service('edit-permissions') basePermissions; version = config.APP.VERSION; buildDate = config.APP.BUILD_DATE; diff --git a/app/controllers/landing-page.js b/app/controllers/landing-page.js deleted file mode 100644 index 0ccb3ca84..000000000 --- a/app/controllers/landing-page.js +++ /dev/null @@ -1,7 +0,0 @@ -import Controller from '@ember/controller'; -import config from 'encompass/config/environment'; - -export default class LandingPageController extends Controller { - version = config.APP.VERSION; - buildDate = config.APP.BUILD_DATE; -} diff --git a/app/controllers/metrics/workspace.js b/app/controllers/metrics/workspace.js index 785e36fb6..3323119d4 100644 --- a/app/controllers/metrics/workspace.js +++ b/app/controllers/metrics/workspace.js @@ -14,10 +14,12 @@ export default class MetricsWorkspaceController extends Controller { ]; get workspaceCsv() { - return this.workspaceReports.submissionReport(this.model); + return encodeURIComponent( + this.workspaceReports.submissionReport(this.model) + ); } get responseCsv() { - return this.workspaceReports.responseReport(this.model); + return encodeURIComponent(this.workspaceReports.responseReport(this.model)); } @action handleToggle(prop) { diff --git a/app/controllers/problems/edit.js b/app/controllers/problems/edit.js new file mode 100644 index 000000000..8b151833c --- /dev/null +++ b/app/controllers/problems/edit.js @@ -0,0 +1,5 @@ +import Controller from '@ember/controller'; + +export default class ProblemsEditController extends Controller { + queryParams = ['tab']; +} diff --git a/app/controllers/problems/problem.js b/app/controllers/problems/problem.js new file mode 100644 index 000000000..69fd99ace --- /dev/null +++ b/app/controllers/problems/problem.js @@ -0,0 +1,5 @@ +import Controller from '@ember/controller'; + +export default class ProblemsProblemController extends Controller { + queryParams = ['tab']; +} diff --git a/app/controllers/problems/problem/assignment.js b/app/controllers/problems/problem/assignment.js new file mode 100644 index 000000000..e627b8df8 --- /dev/null +++ b/app/controllers/problems/problem/assignment.js @@ -0,0 +1,12 @@ +import Controller from '@ember/controller'; +import { action } from '@ember/object'; +import { service } from '@ember/service'; + +export default class ProblemsProblemAssignmentController extends Controller { + @service router; + + @action + handleHideAssignment() { + this.router.transitionTo('problems.problem', this.model.problem.id); + } +} diff --git a/app/controllers/workspace.js b/app/controllers/workspace.js index 38efb81cf..b25c86173 100644 --- a/app/controllers/workspace.js +++ b/app/controllers/workspace.js @@ -6,15 +6,11 @@ */ import Controller from '@ember/controller'; import { action } from '@ember/object'; -import { computed } from '@ember/object'; import { tracked } from '@glimmer/tracking'; export default class WorkspaceController extends Controller { - // comments: controller(); - @tracked currentSelection = null; //ENC-397, ENC-398 - @computed('makingSelection', 'taggingSelection') get showOverlay() { return this.makingSelection || this.taggingSelection; } @@ -23,9 +19,4 @@ export default class WorkspaceController extends Controller { popupMaskClicked() { this.transitionToRoute('workspace.submission', this.currentSubmission); } - // Was this ever used? If not, remove it. - // @action - // tagSelection(selection, tags) { - // // Your implementation here - // } } diff --git a/app/helpers/.gitkeep b/app/helpers/.gitkeep deleted file mode 100644 index e69de29bb..000000000 diff --git a/app/helpers/is-empty.js b/app/helpers/is-empty.js new file mode 100644 index 000000000..d4c52120d --- /dev/null +++ b/app/helpers/is-empty.js @@ -0,0 +1,5 @@ +import isEmpty from 'lodash/isEmpty'; + +export default function (value) { + return isEmpty(value); +} diff --git a/app/helpers/not-empty.js b/app/helpers/not-empty.js new file mode 100644 index 000000000..7c574eaff --- /dev/null +++ b/app/helpers/not-empty.js @@ -0,0 +1,5 @@ +import isEmpty from 'lodash/isEmpty'; + +export default function (value) { + return !isEmpty(value); +} diff --git a/app/models/.gitkeep b/app/models/.gitkeep deleted file mode 100644 index e69de29bb..000000000 diff --git a/app/models/answer.js b/app/models/answer.js index a3c8d6085..d12168524 100644 --- a/app/models/answer.js +++ b/app/models/answer.js @@ -1,5 +1,4 @@ import { attr, belongsTo, hasMany } from '@ember-data/model'; -import _ from 'underscore'; import AuditableModel from './auditable'; export default class AnswerModel extends AuditableModel { @@ -7,16 +6,16 @@ export default class AnswerModel extends AuditableModel { return this.id; } @attr('string') studentName; - @belongsTo('problem', { async: true }) problem; + @belongsTo('problem', { inverse: null, async: true }) problem; @attr('string') answer; @attr('string') explanation; @belongsTo('image', { inverse: null, async: true }) explanationImage; - @belongsTo('section', { async: true }) section; + @belongsTo('section', { inverse: null, async: true }) section; @attr('boolean') isSubmitted; @hasMany('users', { inverse: null, async: true }) students; @attr studentNames; @belongsTo('answer', { inverse: null, async: true }) priorAnswer; - @belongsTo('assignment', { async: true }) assignment; + @belongsTo('assignment', { inverse: 'answers', async: true }) assignment; @belongsTo('image', { inverse: null, async: true }) additionalImage; @attr workspacesToUpdate; @attr vmtRoomInfo; @@ -45,7 +44,7 @@ export default class AnswerModel extends AuditableModel { const names = this.studentNames; if (Array.isArray(names)) { - let firstStringName = _.find(names, _.isString); + const firstStringName = names.find((name) => typeof name === 'string'); if (firstStringName) { return firstStringName.trim(); } diff --git a/app/models/assignment.js b/app/models/assignment.js index 22c73ca70..87d99e1af 100644 --- a/app/models/assignment.js +++ b/app/models/assignment.js @@ -6,10 +6,10 @@ export default class AssignmentModel extends AuditableModel { return this.id; } @attr('string') name; - @hasMany('answer', { async: true }) answers; + @hasMany('answer', { inverse: 'assignment', async: true }) answers; @hasMany('user', { inverse: null, async: true }) students; - @belongsTo('section', { async: true }) section; - @belongsTo('problem', { async: true }) problem; + @belongsTo('section', { inverse: 'assignments', async: true }) section; + @belongsTo('problem', { inverse: null, async: true }) problem; @attr('date') assignedDate; @attr('date') dueDate; @belongsTo('workspace', { inverse: null, async: true }) taskWorkspace; diff --git a/app/models/folder.js b/app/models/folder.js index 9ad85c21f..388a62ab2 100644 --- a/app/models/folder.js +++ b/app/models/folder.js @@ -4,7 +4,7 @@ import AuditableModel from './auditable'; export default class FolderModel extends AuditableModel { @attr('string') name; @attr('number') weight; - @hasMany('tagging', { async: true }) taggings; + @hasMany('tagging', { inverse: 'folder', async: true }) taggings; @belongsTo('folder', { inverse: 'children', async: true }) parent; @hasMany('folder', { inverse: 'parent', async: true }) children; @belongsTo('workspace', { async: true }) workspace; diff --git a/app/models/group.js b/app/models/group.js index e7df1ebf0..27ca1e034 100644 --- a/app/models/group.js +++ b/app/models/group.js @@ -1,7 +1,7 @@ -import Model, { attr, hasMany, belongsTo } from '@ember-data/model'; +import { attr, hasMany, belongsTo } from '@ember-data/model'; import AuditableModel from './auditable'; -export default class GroupModel extends Model.extend(AuditableModel) { +export default class GroupModel extends AuditableModel { @attr name; @belongsTo('section', { inverse: null, async: true }) section; @hasMany('user', { inverse: null, async: true }) students; diff --git a/app/models/import_request.js b/app/models/import_request.js index d035ea71b..e9de0007b 100644 --- a/app/models/import_request.js +++ b/app/models/import_request.js @@ -4,7 +4,7 @@ */ import Model, { attr } from '@ember-data/model'; import Auditable from './auditable'; -export default class ImportRequestModel extends Model(Auditable) { +export default class ImportRequestModel extends Auditable { @attr('string') teacher; @attr('string') submitter; @attr('number') publication; diff --git a/app/models/notification.js b/app/models/notification.js index 421ff1933..5dd2ef252 100644 --- a/app/models/notification.js +++ b/app/models/notification.js @@ -5,12 +5,12 @@ export default class NotificationModel extends Auditable { @attr('string') primaryRecordType; @attr('string') notificationType; @belongsTo('submission', { inverse: null, async: true }) submission; - @belongsTo('workspace', { async: true }) workspace; - @belongsTo('response', { async: true }) response; - @belongsTo('user', { async: true }) recipient; - @belongsTo('assignment', { async: true }) assignment; - @belongsTo('problem', { async: true }) problem; - @belongsTo('section', { async: true }) section; - @belongsTo('organization', { async: true }) organization; + @belongsTo('workspace', { inverse: null, async: true }) workspace; + @belongsTo('response', { inverse: null, async: true }) response; + @belongsTo('user', { inverse: 'notifications', async: true }) recipient; + @belongsTo('assignment', { inverse: null, async: true }) assignment; + @belongsTo('problem', { inverse: null, async: true }) problem; + @belongsTo('section', { inverse: null, async: true }) section; + @belongsTo('organization', { inverse: null, async: true }) organization; @attr('boolean', { defaultValue: false }) wasSeen; } diff --git a/app/models/problem.js b/app/models/problem.js index 36808419c..a55148f93 100644 --- a/app/models/problem.js +++ b/app/models/problem.js @@ -16,15 +16,13 @@ export default class ProblemModel extends Auditable { @attr('string') additionalInfo; @attr('string') privacySetting; @hasMany('category', { inverse: null, async: true }) categories; - @attr keywords; + @attr keywords; // an array of strings @attr('string') copyrightNotice; @attr('string') sharingAuth; @attr('string') author; @attr('string') error; - @attr('boolean') isUsed; + @attr('boolean') isUsed; // indicates if a problem has associated answers @attr('string') status; @attr flagReason; - @attr('boolean', { defaultValue: false }) isForEdit; - @attr('boolean', { defaultValue: false }) isForAssignment; @attr contexts; } diff --git a/app/models/section.js b/app/models/section.js index 427ad8b70..246a910ca 100644 --- a/app/models/section.js +++ b/app/models/section.js @@ -9,6 +9,6 @@ export default class SectionModel extends Auditable { @hasMany('user', { inverse: null, async: true }) teachers; @attr('string') sectionPassword; @hasMany('user', { inverse: null, async: true }) students; - @hasMany('problem', { async: true }) problems; - @hasMany('assignment', { async: true }) assignments; + @hasMany('problem', { inverse: null, async: true }) problems; + @hasMany('assignment', { inverse: 'section', async: true }) assignments; } diff --git a/app/models/tagging.js b/app/models/tagging.js index 8d4d4c259..3d265e6a4 100644 --- a/app/models/tagging.js +++ b/app/models/tagging.js @@ -3,9 +3,9 @@ import { belongsTo } from '@ember-data/model'; import AuditableModel from './auditable'; export default class TaggingModel extends AuditableModel { - @belongsTo('workspace', { async: false }) workspace; - @belongsTo('selection', { async: true }) selection; - @belongsTo('folder', { async: true }) folder; + @belongsTo('workspace', { inverse: 'taggings', async: false }) workspace; + @belongsTo('selection', { inverse: 'taggings', async: true }) selection; + @belongsTo('folder', { inverse: 'taggings', async: true }) folder; @belongsTo('tagging', { inverse: null, async: true }) originalTagging; copy() { diff --git a/app/models/user.js b/app/models/user.js index 739789eb8..c2832e302 100644 --- a/app/models/user.js +++ b/app/models/user.js @@ -49,6 +49,17 @@ export default class UserModel extends AuditableModel { get isStudent() { return this.accountType === 'S' || this.actingRole === 'student'; } + get isPseudoStudent() { + return this.actingRole === 'S'; + } + + get isActingAdmin() { + return !this.isPseudoStudent && this.isAdmin; + } + + get isActingPdAdmin() { + return !this.isPseudoStudent && this.isPdAdmin; + } get isPdAdmin() { return this.accountType === 'P'; } diff --git a/app/models/workspace.js b/app/models/workspace.js index bd3a06d9a..ab967673e 100644 --- a/app/models/workspace.js +++ b/app/models/workspace.js @@ -12,7 +12,7 @@ export default class WorkspaceModel extends AuditableModel { @belongsTo('user', { async: true }) owner; @hasMany('user', { async: true }) editors; @hasMany('folder', { async: true }) folders; - @attr() group; + @belongsTo('group', { inverse: null, async: true }) group; @hasMany('submission', { async: true }) submissions; @hasMany('response', { async: true }) responses; @hasMany('selection', { async: true }) selections; diff --git a/app/router.js b/app/router.js index 0532dcbef..87c271a8c 100644 --- a/app/router.js +++ b/app/router.js @@ -21,7 +21,10 @@ Router.map(function () { // PROBLEMS PARENT ROUTE this.route('problems', function () { - this.route('problem', { path: '/:problem_id' }); + this.route('problem', { path: '/:problem_id' }, function () { + this.route('assignment'); + }); + this.route('edit', { path: '/:problem_id/edit' }); this.route('new'); }); diff --git a/app/routes/application.js b/app/routes/application.js index 6bb87f802..33299ffbb 100644 --- a/app/routes/application.js +++ b/app/routes/application.js @@ -13,11 +13,10 @@ import { action } from '@ember/object'; export default class Application extends Route { //the application route can't require authentication since it's getting the user - @service('user-ntfs') userNtfs; + @service userNtfs; @service store; - @service('workspace-permissions') workspacePermissions; - @service('edit-permissions') editPermissions; - @service('current-user') currentUser; + @service router; + @service currentUser; beforeModel() { let that = this; window.addEventListener( @@ -32,8 +31,6 @@ export default class Application extends Route { async model() { let user = await this.store.queryRecord('user', { alias: 'current' }); - this.workspacePermissions.setUser(user); - this.editPermissions.setUser(user); this.currentUser.setUser(user); return user; } @@ -62,11 +59,11 @@ export default class Application extends Route { // should be extending AuthenticatedRoute. if (!user.get('isAuthenticated')) { this.store.unloadAll(); - this.transitionTo('welcome'); + this.router.transitionTo('welcome'); } else if (!user.get('isEmailConfirmed') && !user.get('isStudent')) { - this.transitionTo('unconfirmed'); + this.router.transitionTo('unconfirmed'); } else if (!user.get('isAuthz')) { - this.transitionTo('unauthorized'); + this.router.transitionTo('unauthorized'); } } diff --git a/app/routes/assignments.js b/app/routes/assignments.js index 96c581dba..148710e80 100644 --- a/app/routes/assignments.js +++ b/app/routes/assignments.js @@ -1,6 +1,6 @@ import AuthenticatedRoute from './_authenticated_route'; import { hash } from 'rsvp'; -import { inject as service } from '@ember/service'; +import { service } from '@ember/service'; /** * # Assignments Route @@ -8,18 +8,11 @@ import { inject as service } from '@ember/service'; */ export default class AssignmentsRoute extends AuthenticatedRoute { @service store; - - async model() { - let currentUser = this.modelFor('application'); - let assignments = await this.store.findAll('assignment'); - let filtered = assignments.filter((assignment) => { - return assignment.id && !assignment.get('isTrashed'); - }); - filtered = filtered.sortBy('createDate').reverse(); + @service currentUser; + model() { return hash({ - currentUser, - assignments, - filtered, + isStudent: this.currentUser.isStudent, + assignments: this.store.findAll('assignment'), }); } } diff --git a/app/routes/assignments/assignment.js b/app/routes/assignments/assignment.js index 4e0336d04..82f8415e8 100644 --- a/app/routes/assignments/assignment.js +++ b/app/routes/assignments/assignment.js @@ -1,32 +1,46 @@ import AuthenticatedRoute from '../_authenticated_route'; import { action } from '@ember/object'; import { hash } from 'rsvp'; -import { inject as service } from '@ember/service'; +import { service } from '@ember/service'; export default class AssignmentsAssignmentRoute extends AuthenticatedRoute { @service store; + @service router; + @service currentUser; async model(params) { - let currentUser = this.modelFor('application'); const assignment = await this.store.findRecord( 'assignment', params.assignment_id ); - const sections = await this.store.findAll('section'); - const section = await assignment.get('section.id'); - const groups = await this.store.query('group', { - section: section, + const section = await assignment.section; + const sectionId = section?.id ?? null; + const groups = this.store.query('group', { + section: sectionId, isTrashed: false, }); - const students = await assignment.get('students'); return hash({ - currentUser, assignment, + currentSection: section, + currentProblem: assignment.problem, groups, - students, - sections, + students: assignment.students, + sections: this.store.findAll('section'), + linkedWorkspaces: assignment.linkedWorkspaces, + parentWorkspace: assignment.parentWorkspace, + answers: assignment.answers, + isStudent: this.currentUser.isStudent, }); } - @action toAssignments() { - this.transitionTo('assignments'); + + @action + async onAnswerCreated(answer) { + const { assignment } = this.model; + assignment.answers.pushObject(answer); + + try { + await assignment.save(); + } catch (error) { + console.error('Error saving assignment:', error); + } } } diff --git a/app/routes/assignments/new.js b/app/routes/assignments/new.js index eb8ad1d41..a6ca2d38f 100644 --- a/app/routes/assignments/new.js +++ b/app/routes/assignments/new.js @@ -4,27 +4,26 @@ import AuthenticatedRoute from '../_authenticated_route'; import { inject as service } from '@ember/service'; export default class AssignmentsNewRoute extends AuthenticatedRoute { @service store; + @service router; beforeModel() { const user = this.modelFor('application'); const isStudent = user.get('isStudent'); - if (isStudent) { - this.transitionTo('assignments'); + this.router.transitionTo('assignments'); } } - async model() { - let currentUser = this.modelFor('application'); + model() { return hash({ - currentUser, - sections: await this.store.findAll('section'), - groups: await this.store.findAll('group'), - cachedProblems: await this.store.findAll('problem'), + currentUser: this.modelFor('application'), + sections: this.store.findAll('section'), + groups: this.store.findAll('group'), + cachedProblems: this.store.findAll('problem'), }); } @action toAssignmentInfo(model) { - this.transitionTo('assignment', model); + this.router.transitionTo('assignment', model); } @action toAssignmentsHome() { - this.transitionTo('assignments'); + this.router.transitionTo('assignments'); } } diff --git a/app/routes/folders_index.js b/app/routes/folders_index.js index a17c88c26..024c757f1 100644 --- a/app/routes/folders_index.js +++ b/app/routes/folders_index.js @@ -4,15 +4,17 @@ * @since 1.0.0 */ import Route from '@ember/routing/route'; +import { inject as service } from '@ember/service'; export default class FoldersIndexRoute extends Route { + @service router; model() { return this.store.findAll('folder'); } afterModel(folders, transition) { if (folders.length === 1) { - this.transitionTo('workspaceFolder', folders.firstObject); + this.router.transitionTo('workspaceFolder', folders.firstObject); } } } diff --git a/app/routes/index.js b/app/routes/index.js index 689611b5a..d81d0eaa9 100644 --- a/app/routes/index.js +++ b/app/routes/index.js @@ -32,21 +32,23 @@ export default class IndexRoute extends Route { const user = this.currentUser.user; const sections = await this.store.findAll('section'); - const teacherSections = sections.filter((section) => - section.teachers.includes(user) - ); - - const studentSections = sections.filter((section) => - section.students.includes(user) - ); - - const teacherAssignments = teacherSections.flatMap((section) => - section.assignments.toArray() - ); + const teacherAssignments = ( + await Promise.all( + sections.map(async (section) => { + const teachers = await section.teachers; + return teachers.includes(user) ? section.assignments.toArray() : []; + }) + ) + ).flat(); - const studentAssignments = studentSections.flatMap((section) => - section.assignments.toArray() - ); + const studentAssignments = ( + await Promise.all( + sections.map(async (section) => { + const students = await section.students; + return students.includes(user) ? section.assignments.toArray() : []; + }) + ) + ).flat(); const teacherClasses = user.sections.map((section) => section.sectionId); diff --git a/app/routes/problems/edit.js b/app/routes/problems/edit.js new file mode 100644 index 000000000..f0f410f03 --- /dev/null +++ b/app/routes/problems/edit.js @@ -0,0 +1,12 @@ +// app/routes/problems/edit.js +import AuthenticatedRoute from '../_authenticated_route'; +import { service } from '@ember/service'; + +export default class ProblemsEditRoute extends AuthenticatedRoute { + @service store; + @service problemUtils; + + async model(params) { + return this.problemUtils.fetchProblemData(params.problem_id); + } +} diff --git a/app/routes/problems/new.js b/app/routes/problems/new.js index 1fbd7f8ae..8232f123d 100644 --- a/app/routes/problems/new.js +++ b/app/routes/problems/new.js @@ -1,23 +1,13 @@ import { hash } from 'rsvp'; -import { action } from '@ember/object'; -import { inject as service } from '@ember/service'; +import { service } from '@ember/service'; import AuthenticatedRoute from '../_authenticated_route'; export default class ProblemsNewRoute extends AuthenticatedRoute { @service store; model() { - let currentUser = this.modelFor('application'); return hash({ - currentUser, problems: this.store.findAll('problem'), organizations: this.store.findAll('organization'), }); } - - @action toProblemInfo(problem) { - this.transitionTo('problems.problem', problem.id); - } - @action toProblemList() { - this.transitionTo('problems'); - } } diff --git a/app/routes/problems/problem.js b/app/routes/problems/problem.js index 46295ec80..061038b1d 100644 --- a/app/routes/problems/problem.js +++ b/app/routes/problems/problem.js @@ -1,35 +1,12 @@ +// app/routes/problems/problem.js import AuthenticatedRoute from '../_authenticated_route'; -import { hash } from 'rsvp'; -import { inject as service } from '@ember/service'; +import { service } from '@ember/service'; export default class ProblemsProblemRoute extends AuthenticatedRoute { @service store; + @service problemUtils; + async model(params) { - const currentUser = this.modelFor('application'); - const userOrg = await currentUser.get('organization'); - const recommendedProblems = await userOrg.get('recommendedProblems'); - const problem = await this.store.findRecord('problem', params.problem_id); - const sectionList = await this.store.findAll('section'); - const orgList = await this.store.findAll('organization'); - let flaggedBy; - let flaggedDate; - if (problem.flagReason?.flaggedBy) { - flaggedBy = await this.store.findRecord( - 'user', - problem.flagReason.flaggedBy - ); - } - if (problem.flagReason?.flaggedBy) { - flaggedDate = new Date(problem.flagReason.flaggedDate); - } - return hash({ - currentUser, - problem, - sectionList, - orgList, - recommendedProblems, - flaggedBy, - flaggedDate, - }); + return this.problemUtils.fetchProblemData(params.problem_id); } } diff --git a/app/routes/problems/problem/assignment.js b/app/routes/problems/problem/assignment.js new file mode 100644 index 000000000..7e17ce975 --- /dev/null +++ b/app/routes/problems/problem/assignment.js @@ -0,0 +1,17 @@ +import AuthenticatedRoute from 'encompass/routes/_authenticated_route'; + +export default class ProblemsProblemAssignmentRoute extends AuthenticatedRoute { + activate() { + super.activate(...arguments); + requestAnimationFrame(() => { + let outletEl = document.getElementById('problem-info-outlet'); + if (outletEl) { + outletEl.scrollIntoView({ behavior: 'smooth' }); + } + }); + } + model() { + // This reuses the model already loaded by problems/problem.js (must be a parent route) + return this.modelFor('problems.problem'); + } +} diff --git a/app/routes/responses.js b/app/routes/responses.js index ef0c88aa7..e4230374a 100644 --- a/app/routes/responses.js +++ b/app/routes/responses.js @@ -1,15 +1,16 @@ import AuthenticatedRoute from './_authenticated_route'; import { action } from '@ember/object'; - +import { inject as service } from '@ember/service'; export default class ResponsesRoute extends AuthenticatedRoute { + @service router; @action toSubmissionResponse(subId) { - this.transitionTo('responses.submission', subId); + this.router.transitionTo('responses.submission', subId); } @action toResponses() { this.refresh(); } @action toResponse(submissionId, responseId) { - this.transitionTo('responses.submission', submissionId, { + this.router.transitionTo('responses.submission', submissionId, { queryParams: { responseId: responseId }, }); } diff --git a/app/routes/responses/new/submission.js b/app/routes/responses/new/submission.js index ed49f78e6..535d22dc1 100644 --- a/app/routes/responses/new/submission.js +++ b/app/routes/responses/new/submission.js @@ -1,14 +1,12 @@ -import Route from '@ember/routing/route'; import { inject as service } from '@ember/service'; import { hash, resolve } from 'rsvp'; import ConfirmLeavingRoute from '../../_confirm_leaving_route'; import { action } from '@ember/object'; -export default class ResponsesNewSubmissionRoute extends Route.extend( - ConfirmLeavingRoute -) { +export default class ResponsesNewSubmissionRoute extends ConfirmLeavingRoute { @service('utility-methods') utils; @service store; + @service router; renderTemplate() { this.render('responses/response'); } @@ -130,18 +128,18 @@ export default class ResponsesNewSubmissionRoute extends Route.extend( afterModel(model) { if (model.isDraft) { - this.transitionTo('responses.submission', model.submissionId, { + this.router.transitionTo('responses.submission', model.submissionId, { queryParams: { responseId: model.responseId }, }); } } @action toResponse(submissionId, responseId) { - this.transitionTo('responses.submission', submissionId, { + this.router.transitionTo('responses.submission', submissionId, { queryParams: { responseId: responseId }, }); } @action toResponseSubmission(subId) { - this.transitionTo('responses.submission', subId); + this.router.transitionTo('responses.submission', subId); } } diff --git a/app/routes/responses/submission.js b/app/routes/responses/submission.js index 7318894d4..064246382 100644 --- a/app/routes/responses/submission.js +++ b/app/routes/responses/submission.js @@ -5,6 +5,7 @@ import { action } from '@ember/object'; export default class ResponsesRoute extends AuthenticatedRoute { @service('utility-methods') utils; @service store; + @service router; queryParams = { responseId: { refreshModel: true, @@ -86,26 +87,26 @@ export default class ResponsesRoute extends AuthenticatedRoute { redirect(model, transition) { if (!model) { - this.transitionTo('responses'); + this.router.transitionTo('responses'); } } @action toResponseSubmission(subId) { - this.transitionTo('responses.submission', subId); + this.router.transitionTo('responses.submission', subId); } @action toResponse(submissionId, responseId) { - this.transitionTo('responses.submission', submissionId, { + this.router.transitionTo('responses.submission', submissionId, { queryParams: { responseId: responseId }, }); } @action toResponses() { - this.transitionTo('responses'); + this.router.transitionTo('responses'); } @action toNewResponse(submissionId, workspaceId) { - this.transitionTo('responses.new.submission', submissionId, { + this.router.transitionTo('responses.new.submission', submissionId, { queryParams: { workspaceId: workspaceId }, }); } diff --git a/app/routes/sections/new.js b/app/routes/sections/new.js index 1dff6df38..40fbc3b16 100644 --- a/app/routes/sections/new.js +++ b/app/routes/sections/new.js @@ -4,12 +4,13 @@ import { inject as service } from '@ember/service'; export default class SectionsNewRoute extends AuthenticatedRoute { @service store; + @service router; beforeModel() { const user = this.modelFor('application'); const isStudent = user.get('isStudent'); if (isStudent) { - this.transitionTo('sections'); + this.router.transitionTo('sections'); } } diff --git a/app/routes/sections/section.js b/app/routes/sections/section.js index 2fdb8a271..59d20db75 100644 --- a/app/routes/sections/section.js +++ b/app/routes/sections/section.js @@ -5,6 +5,7 @@ import { inject as service } from '@ember/service'; export default class SectionsSectionRoute extends AuthenticatedRoute { @service store; + @service router; async model(params) { let section = await this.store.findRecord('section', params.section_id); let groups = await this.store.query('group', { @@ -23,10 +24,10 @@ export default class SectionsSectionRoute extends AuthenticatedRoute { } @action toSectionList() { - this.transitionTo('sections'); + this.router.transitionTo('sections'); } @action toAssignmentInfo(assignment) { - this.transitionTo('assignments.assignment', assignment); + this.router.transitionTo('assignments.assignment', assignment); } @action refreshModel() { this.refresh(); diff --git a/app/routes/submission/response.js b/app/routes/submission/response.js index a6fd3953d..f9659d480 100644 --- a/app/routes/submission/response.js +++ b/app/routes/submission/response.js @@ -4,33 +4,34 @@ import { action } from '@ember/object'; export default class ResponsesRoute extends ConfirmLeavingRoute { @service('utility-methods') utils; @service store; + @service router; model(params) { return this.store.findRecord('response', params.response_id); } redirect(model, transition) { if (!model) { - this.transitionTo('responses'); + this.router.transitionTo('responses'); } else { let submissionId = this.utils.getBelongsToId(model, 'submission'); if (this.utils.isValidMongoId(submissionId)) { - this.transitionTo('responses.submission', submissionId, { + this.router.transitionTo('responses.submission', submissionId, { queryParams: { responseId: model.get('id') }, }); } else { - this.transitionTo('responses'); + this.router.transitionTo('responses'); } } } @action toResponseInfo(response) { - this.transitionTo('response', response.get('id')); + this.router.transitionTo('response', response.get('id')); } @action toResponses() { - this.transitionTo('responses'); + this.router.transitionTo('responses'); } @action toNewResponse(submissionId, workspaceId) { - this.transitionTo('responses.new.submission', submissionId, { + this.router.transitionTo('responses.new.submission', submissionId, { queryParams: { workspaceId: workspaceId }, }); } diff --git a/app/routes/submissions/first.js b/app/routes/submissions/first.js index 67c8ecf09..af73e7cbf 100644 --- a/app/routes/submissions/first.js +++ b/app/routes/submissions/first.js @@ -10,6 +10,7 @@ import { inject as service } from '@ember/service'; export default Route.extend({ utils: service('utility-methods'), alert: service('sweet-alert'), + router: service(), model: function () { return this.modelFor('workspace.submissions'); @@ -22,7 +23,7 @@ export default Route.extend({ let firstStudent = sorted.get('firstObject.student'); let lastRevision = sorted.getEach('student').lastIndexOf(firstStudent); - this.transitionTo( + this.router.transitionTo( 'workspace.submissions.submission', workspace, sorted.objectAt(lastRevision).get('id') @@ -38,7 +39,7 @@ export default Route.extend({ null ); - this.transitionTo('workspace.info'); + this.router.transitionTo('workspace.info'); } }, }); diff --git a/app/routes/submissions/submission.js b/app/routes/submissions/submission.js index 72a70d21e..3858769c5 100644 --- a/app/routes/submissions/submission.js +++ b/app/routes/submissions/submission.js @@ -18,6 +18,7 @@ import VmtHostMixin from '../mixins/vmt-host'; export default Route.extend(CurrentUserMixin, VmtHostMixin, { alert: service('sweet-alert'), utils: service('utility-methods'), + router: service(), queryParams: 'vmtRoomId', @@ -39,9 +40,13 @@ export default Route.extend(CurrentUserMixin, VmtHostMixin, { // so links to selections still work if (transition.intent.name === 'workspace.submissions.submission') { - this.transitionTo('workspace.submissions.submission', submission, { - queryParams: { vmtRoomId }, - }); + this.router.transitionTo( + 'workspace.submissions.submission', + submission, + { + queryParams: { vmtRoomId }, + } + ); } }); }, diff --git a/app/routes/unauthorized.js b/app/routes/unauthorized.js index 96857a9a5..3a49aa925 100644 --- a/app/routes/unauthorized.js +++ b/app/routes/unauthorized.js @@ -4,12 +4,13 @@ import { action } from '@ember/object'; export default class UnauthorizedRoute extends Route { @service store; + @service router; beforeModel() { // redirect to login if no user logged in const user = this.modelFor('application'); if (!user || !user.get('isAuthenticated')) { - return this.transitionTo('auth.login'); + return this.router.transitionTo('auth.login'); } // redirect to confirm email info page if // email still needs confirming @@ -19,12 +20,12 @@ export default class UnauthorizedRoute extends Route { !user.get('isStudent'); if (doesEmailNeedConfirming) { - return this.transitionTo('unconfirmed'); + return this.router.transitionTo('unconfirmed'); } // redirect to home page if already authorized if (user.get('isAuthz')) { - this.transitionTo('/'); + this.router.transitionTo('/'); } } diff --git a/app/routes/unconfirmed.js b/app/routes/unconfirmed.js index 21ef0706e..5f443a839 100644 --- a/app/routes/unconfirmed.js +++ b/app/routes/unconfirmed.js @@ -1,17 +1,18 @@ import Route from '@ember/routing/route'; - +import { inject as service } from '@ember/service'; export default class UnconfirmedRoute extends Route { + @service router; beforeModel() { // redirect to login if no user logged in const user = this.modelFor('application'); if (!user || !user.get('isAuthenticated')) { - return this.transitionTo('auth.login'); + return this.router.transitionTo('auth.login'); } // redirect to home page if email is already confirmed or user does not have an email if (user.get('isEmailConfirmed') || !user.get('email')) { - this.transitionTo('/'); + this.router.transitionTo('/'); } } } diff --git a/app/routes/users.js b/app/routes/users.js index cecee2ae9..10b203f1c 100644 --- a/app/routes/users.js +++ b/app/routes/users.js @@ -11,20 +11,23 @@ import { inject as service } from '@ember/service'; export default class UsersRoute extends Route { @service store; + @service router; beforeModel() { const user = this.modelFor('application'); const isStudent = user.get('isStudent'); if (isStudent) { - this.transitionTo('/'); + this.router.transitionTo('/'); } } - model() { + async model() { const currentUser = this.modelFor('application'); + const users = await this.store.findAll('user'); return hash({ currentUser, - users: this.store.findAll('user'), + users, organizations: this.store.findAll('organization'), + trashedUsers: users.filter((user) => user.isTrashed), }); } } diff --git a/app/routes/users/user.js b/app/routes/users/user.js index 54f03253e..be02bde38 100644 --- a/app/routes/users/user.js +++ b/app/routes/users/user.js @@ -1,24 +1,18 @@ import Route from '@ember/routing/route'; import { hash } from 'rsvp'; -import { action } from '@ember/object'; import { inject as service } from '@ember/service'; export default class UsersUserRoute extends Route { @service store; async model(params) { const user = await this.store.findRecord('user', params.user_id); - const userSections = await this.store.query('section', { - ids: user.sections.map((section) => section.sectionId), - }); - let currentUser = this.modelFor('application'); return hash({ - currentUser, + currentUser: this.modelFor('application'), user, - userSections, - organizations: await this.store.findAll('organization'), + userSections: this.store.query('section', { + ids: user.sections.map((section) => section.sectionId), + }), + organizations: this.store.findAll('organization'), }); } - @action refresh() { - this.refresh(); - } } diff --git a/app/routes/vmt/import.js b/app/routes/vmt/import.js index 63f472f7b..856467191 100644 --- a/app/routes/vmt/import.js +++ b/app/routes/vmt/import.js @@ -1,8 +1,10 @@ import { hash } from 'rsvp'; import AuthenticatedRoute from '../_authenticated_route'; +import { inject as service } from '@ember/service'; export default AuthenticatedRoute.extend({ controllerName: 'vmt-import', + router: service(), model() { return hash({ @@ -13,7 +15,7 @@ export default AuthenticatedRoute.extend({ actions: { toWorkspaces: function (workspaceId) { - this.transitionTo('workspace.work', workspaceId); + this.router.transitionTo('workspace.work', workspaceId); // window.location.href = `#/workspaces/${workspace._id}/submissions/${workspace.submissions[0]}`; }, }, diff --git a/app/routes/welcome.js b/app/routes/welcome.js new file mode 100644 index 000000000..e5b979d10 --- /dev/null +++ b/app/routes/welcome.js @@ -0,0 +1,12 @@ +// app/routes/welcome.js +import Route from '@ember/routing/route'; +import config from 'encompass/config/environment'; + +export default class WelcomeRoute extends Route { + model() { + return { + version: config.APP.VERSION, + buildDate: config.APP.BUILD_DATE, + }; + } +} diff --git a/app/routes/workspace/submissions/submission.js b/app/routes/workspace/submissions/submission.js index 4ee987996..76702ce60 100644 --- a/app/routes/workspace/submissions/submission.js +++ b/app/routes/workspace/submissions/submission.js @@ -11,12 +11,12 @@ import Route from '@ember/routing/route'; import { schedule } from '@ember/runloop'; import { hash, resolve } from 'rsvp'; import { inject as service } from '@ember/service'; -import $ from 'jquery'; import { action } from '@ember/object'; export default class WorkspaceSubmissionRoute extends Route { @service sweetAlert; @service('utility-methods') utils; @service currentUser; + @service router; queryParams = { vmtRoomId: { @@ -44,9 +44,13 @@ export default class WorkspaceSubmissionRoute extends Route { // so links to selections still work if (transition.intent.name === 'workspace.submissions.submission') { - this.transitionTo('workspace.submissions.submission', submission, { - queryParams: { vmtRoomId }, - }); + this.router.transitionTo( + 'workspace.submissions.submission', + submission, + { + queryParams: { vmtRoomId }, + } + ); } }); } diff --git a/app/routes/workspaces.js b/app/routes/workspaces.js index 1182eb171..5ec6ed47a 100644 --- a/app/routes/workspaces.js +++ b/app/routes/workspaces.js @@ -7,11 +7,13 @@ import AuthenticatedRoute from './_authenticated_route'; import { action } from '@ember/object'; +import { inject as service } from '@ember/service'; export default class WorkspacesRoute extends AuthenticatedRoute { + @service router; @action toCopyWorkspace(workspace) { let workspaceId = workspace.get('id'); - this.transitionTo('workspaces.copy', { + this.router.transitionTo('workspaces.copy', { queryParams: { workspace: workspaceId }, }); } diff --git a/app/routes/workspaces/copy.js b/app/routes/workspaces/copy.js index 6d6bf9e5f..f00117477 100644 --- a/app/routes/workspaces/copy.js +++ b/app/routes/workspaces/copy.js @@ -5,6 +5,7 @@ import AuthenticatedRoute from '../_authenticated_route'; export default class WorkspacesCopyRoute extends AuthenticatedRoute { @service store; + @service router; workspaceToCopy = null; workspaceId = null; beforeModel(transition) { @@ -23,6 +24,6 @@ export default class WorkspacesCopyRoute extends AuthenticatedRoute { } @action toWorkspace(id) { - this.transitionTo('workspace.work', id); + this.router.transitionTo('workspace.work', id); } } diff --git a/app/routes/workspaces/new.js b/app/routes/workspaces/new.js index 65838ccd7..519ac56f5 100644 --- a/app/routes/workspaces/new.js +++ b/app/routes/workspaces/new.js @@ -5,12 +5,13 @@ import AuthenticatedRoute from '../_authenticated_route'; export default class WorkspacesNewRoute extends AuthenticatedRoute { @service store; + @service router; beforeModel() { const user = this.modelFor('application'); const isStudent = user.get('isStudent'); if (isStudent) { - this.transitionTo('/'); + this.router.transitionTo('/'); } } model() { @@ -27,10 +28,10 @@ export default class WorkspacesNewRoute extends AuthenticatedRoute { } // Created workspaceId and is passed from component to redirect @action toWorkspaces(id) { - this.transitionTo('workspace.work', id); + this.router.transitionTo('workspace.work', id); } @action toWorkspace(id) { - this.transitionTo('workspace/work', id); + this.router.transitionTo('workspace/work', id); } } diff --git a/app/services/assignment-permissions.js b/app/services/assignment-permissions.js index 8444e9970..9cea23ec8 100644 --- a/app/services/assignment-permissions.js +++ b/app/services/assignment-permissions.js @@ -3,12 +3,13 @@ import Service, { inject as service } from '@ember/service'; export default class AssignmentPermissionsService extends Service { @service('edit-permissions') base; @service('utility-methods') utils; + @service currentUser; - getPermissionsLevel(assignment, section, user = this.base.user) { + getPermissionsLevel(assignment, section, user = this.currentUser.user) { if (!user) { return 0; } - if (this.base.isActingAdmin) { + if (this.currentUser.isActingAdmin) { return 4; } if (this.base.isRecordInPdDomain(section)) { @@ -37,18 +38,18 @@ export default class AssignmentPermissionsService extends Service { if (!this.utils.isNonEmptyArray(teacherIds)) { return false; } - return teacherIds.includes(this.base.userId); + return teacherIds.includes(this.currentUser.id); } canDelete(assignment) { - if (this.base.isActingAdmin) { + if (this.currentUser.isActingAdmin) { return true; } return !this.haveAnswersBeenSubmitted(assignment); } canEditProblem(assignment, section) { - if (this.base.isActingAdmin) { + if (this.currentUser.isActingAdmin) { return true; } if (this.haveAnswersBeenSubmitted(assignment)) { @@ -58,7 +59,7 @@ export default class AssignmentPermissionsService extends Service { } canEditLinkedWorkspace(assignment) { - if (this.base.isActingAdmin) { + if (this.currentUser.isActingAdmin) { return true; } return this.base.isCreator(assignment); @@ -82,7 +83,7 @@ export default class AssignmentPermissionsService extends Service { } canEditDueDate(assignment) { - if (this.base.isActingAdmin) { + if (this.currentUser.isActingAdmin) { return true; } return this.base.isCreator(assignment); diff --git a/app/services/current-user.js b/app/services/current-user.js index cfcba1ad0..36e4de9da 100644 --- a/app/services/current-user.js +++ b/app/services/current-user.js @@ -7,4 +7,32 @@ export default class CurrentUserService extends Service { setUser(data) { this.user = data; } + + get isAdmin() { + return this.user.isAdmin; + } + + get isPdAdmin() { + return this.user.isPdAdmin; + } + + get isTeacher() { + return this.user.isTeacher; + } + + get isStudent() { + return this.user.isStudent; + } + + get isActingAdmin() { + return this.user.isActingAdmin; + } + + get isActingPdAdmin() { + return this.user.isActingPdAdmin; + } + + get id() { + return this.user.id; + } } diff --git a/app/services/edit-permissions.js b/app/services/edit-permissions.js index 71d3a5300..418d95096 100644 --- a/app/services/edit-permissions.js +++ b/app/services/edit-permissions.js @@ -1,49 +1,29 @@ -import { computed } from '@ember/object'; -import { alias, equal } from '@ember/object/computed'; import Service, { inject as service } from '@ember/service'; +export default class EditPermissionsService extends Service { + @service('utility-methods') utils; + @service currentUser; -export default Service.extend({ - utils: service('utility-methods'), - user: null, - setUser(user) { - this.set('user', user); - }, - userId: alias('user.id'), - userOrg: alias('user.organization'), - accountType: alias('user.accountType'), - actingRole: alias('user.actingRole'), - isAdmin: equal('accountType', 'A'), - isPdAdmin: equal('accountType', 'P'), - isTeacher: equal('accountType', 'T'), - isStudent: equal('accountType', 'S'), - isPseudoStudent: equal('actingRole', 'S'), - - userOrgId: computed('user', function () { - return this.utils.getBelongsToId(this.user, 'organization'); - }), - isActingAdmin: computed('isPseudoStudent', 'isAdmin', function () { - return !this.isPseudoStudent && this.isAdmin; - }), - - isActingPdAdmin: computed('isPseudoStudent', 'isPdAdmin', function () { - return !this.isPseudoStudent && this.isPdAdmin; - }), - - isCreator: function (record, user = this.user) { + isCreator(record, user = this.currentUser.user) { if (!user || !record) { return; } - return this.utils.getBelongsToId(record, 'createdBy') === this.userId; - }, + return ( + this.utils.getBelongsToId(record, 'createdBy') === + this.currentUser.user.id + ); + } - doesRecordBelongToOrg(record, orgId = this.userOrgId) { + doesRecordBelongToOrg( + record, + orgId = this.utils.getBelongsToId(this.user, 'organization') + ) { if (!record || !orgId) { return; } return this.utils.getBelongsToId(record, 'organization') === orgId; - }, + } isRecordInPdDomain(record) { return this.isActingPdAdmin && this.doesRecordBelongToOrg(record); - }, -}); + } +} diff --git a/app/services/error-handling.js b/app/services/error-handling.js index 6249b1ea2..b5fab5ae2 100644 --- a/app/services/error-handling.js +++ b/app/services/error-handling.js @@ -1,114 +1,106 @@ -import Service from '@ember/service'; -import { inject as service } from '@ember/service'; -import { action } from '@ember/object'; +import Service, { service } from '@ember/service'; import { tracked } from '@glimmer/tracking'; +/** + * A service for collecting, storing, and auto-clearing error messages, + * plus optional record rollback and SweetAlert toasts. + */ export default class ErrorHandlingService extends Service { + /** + * The SweetAlert service for showing error toasts, etc. + */ @service('sweet-alert') alert; + /** + * Store of all errors, keyed by a property name (e.g., "problemLoadErrors"). + * Using a TrackedObject so that updates remain reactive in templates. + */ @tracked errors = {}; - isAdapterError(err) { - if (!err) { - return; - } - return err.isAdapterError === true; - } - - isRecordInvalid(record) { - if (!record) { - return; - } - return record.get('isValid') === false; - } - - setErrorMessages(err, propName) { - if (!err || !propName) { - return; - } - - if (err.errors && Array.isArray(err.errors)) { - const details = err.errors.map((e) => e.detail); - this.errors = { ...this.errors, [propName]: details }; - } else if (typeof err.message === 'string') { - this.errors = { ...this.errors, [propName]: [err.message] }; - } else { - this.errors = { ...this.errors, [propName]: ['Unknown Error'] }; - } - } - + /** + * Delays (in ms) after which errors are automatically removed for a given key. + * 3 minutes = 180_000. + */ + AUTO_CLEAR_DELAY = 180_000; + + /** + * Map of propName -> setTimeout handle for auto-clear timers. + */ + _autoClearTimers = {}; + + // --------------------------------------------------------------------------- + // PUBLIC METHODS (API) + // --------------------------------------------------------------------------- + + /** + * Store error messages for `propName`, rolling back the given record(s) if invalid. + * @param {Object} err - The error object (may contain `errors` array, etc.). + * @param {string} propName - The key under which to store these errors. + * @param {DS.Model|null} record - A single record to possibly rollback. + * @param {Array} records - Additional records to possibly rollback. + */ handleErrors(err, propName, record = null, records = []) { - this.setErrorMessages(err, propName); + this._setErrorMessages(err, propName); - if (record) { - if (this.isRecordInvalid(record)) { - record.rollbackAttributes(); - } + if (record && this._isRecordInvalid(record)) { + record.rollbackAttributes(); } + if (records) { - records.forEach((record) => { - if (this.isRecordInvalid(record)) { - record.rollbackAttributes(); + records.forEach((r) => { + if (this._isRecordInvalid(r)) { + r.rollbackAttributes(); } }); } } + /** + * Retrieve the error array for a given key (e.g., "problemLoadErrors"). + * @param {string} prop - Key at which errors are stored. + * @returns {Array|undefined} - Array of error messages (or undefined if none). + */ getErrors(prop) { return this.errors[prop]; } + /** + * Remove error messages for the given key(s). + * e.g., removeMessages('problemLoadErrors') or removeMessages('foo', 'bar'). + * @param {...string|Array} errors - One or more keys (or an array of keys). + */ removeMessages(...errors) { - for (let e of errors) { + for (const e of errors) { this._removeMessages(e); } } - _removeMessages(err) { - if (!err) { - return; - } - - if (Array.isArray(err)) { - for (let e of err) { - if (typeof e === 'string') { - if (this.errors[e]) { - this.errors[e] = null; - } - } - } - } else { - if (typeof err !== 'string') { - return; - } - if (this.errors[err]) { - this.errors[err] = null; - } - } - this.errors = { ...this.errors }; - } - + /** + * Display an error toast (via SweetAlert) and optionally rollback invalid records. + * @param {Object} err - The error object (may contain `errors` array, etc.). + * @param {DS.Model|Array} recordsToRollback - One or more records to rollback. + */ displayErrorToast(err, recordsToRollback) { if (!err) { return; } - let msg; - let errors = err.errors; + let msg; + const { errors } = err; if (Array.isArray(errors) && errors.length > 0) { - let firstError = errors[0]; + const firstError = errors[0]; if (firstError) { - msg = errors[0].detail || 'Unknown Error'; + msg = firstError.detail || 'Unknown Error'; } } - let records = Array.isArray(recordsToRollback) + const records = Array.isArray(recordsToRollback) ? recordsToRollback : [recordsToRollback]; records.forEach((rec) => { - if (this.isRecordInvalid(rec)) { + if (this._isRecordInvalid(rec)) { rec.rollbackAttributes(); } }); @@ -116,13 +108,134 @@ export default class ErrorHandlingService extends Service { this.alert.showToast('error', msg, 'bottom-end', 5000, false, null); } - @action + /** + * Remove a single error string from the array stored at `prop`. + * If removing it empties the array, remove the key entirely (and cancel the auto-clear timer). + * @param {string} prop - The key under which the error array lives. + * @param {string} err - The error string to remove from the array. + */ removeErrorFromArray(prop, err) { if (!this.errors[prop]) { return; } - this.errors[prop].splice(this.errors[prop].indexOf(err), 1); - this.errors = { ...this.errors }; - return; + + const idx = this.errors[prop].indexOf(err); + if (idx !== -1) { + const updatedErrors = [...this.errors[prop]]; + updatedErrors.splice(idx, 1); + if (updatedErrors.length === 0) { + delete this.errors[prop]; + this._cancelTimerFor(prop); + } else { + this.errors = { ...this.errors, [prop]: updatedErrors }; + } + } + } + + // --------------------------------------------------------------------------- + // PRIVATE METHODS + // --------------------------------------------------------------------------- + + /** + * Checks if a record is invalid. We assume `.isValid` is a property on the model. + * @param {DS.Model} record + * @returns {boolean} + */ + _isRecordInvalid(record) { + return !record?.isValid ?? false; + } + + /** + * Sets error messages for a given key, parsing the error object. + * Schedules an auto-clear timer for that key. + * @param {Object} err - The error object + * @param {string} propName - Key under which to store errors + */ + _setErrorMessages(err, propName) { + if (!err || !propName) { + return; + } + + let updatedErrors = { ...this.errors }; + + if (err.errors && Array.isArray(err.errors)) { + // Typically an array of { detail: 'Message...' } + const details = err.errors.map((e) => { + if (typeof e.detail === 'object') { + console.log('Error detail object:', e.detail); + } + return e.detail; + }); + updatedErrors[propName] = details; + } else if (typeof err.message === 'string') { + updatedErrors[propName] = [err.message]; + } else { + updatedErrors[propName] = ['Unknown Error']; + } + + this.errors = updatedErrors; + this._restartAutoClearTimer(propName); + } + + /** + * Internal helper used by removeMessages(...). + * Accepts either a single string key or an array of string keys. + */ + _removeMessages(propOrProps) { + if (!propOrProps) { + return; + } + + if (Array.isArray(propOrProps)) { + propOrProps.forEach((key) => this._clearErrorsForKey(key)); + } else { + this._clearErrorsForKey(propOrProps); + } + } + + /** + * Clears all errors at a particular key and cancels its auto-clear timer. + * @param {string} key + */ + _clearErrorsForKey(key) { + if (typeof key !== 'string') { + return; + } + + if (this.errors[key]) { + const updatedErrors = { ...this.errors }; + delete updatedErrors[key]; + this.errors = updatedErrors; + this._cancelTimerFor(key); + } + } + + /** + * Cancel any existing timer for a given key and start a fresh one. + * If no errors are added before it fires, that key is auto-removed. + * @param {string} key + */ + _restartAutoClearTimer(key) { + this._cancelTimerFor(key); + + this._autoClearTimers[key] = setTimeout(() => { + if (this.errors[key]) { + const updatedErrors = { ...this.errors }; + delete updatedErrors[key]; + this.errors = updatedErrors; + } + delete this._autoClearTimers[key]; + }, this.AUTO_CLEAR_DELAY); + } + + /** + * Cancels the timer for a particular key (if it exists), then deletes it from the map. + * @param {string} key + */ + _cancelTimerFor(key) { + if (this._autoClearTimers[key]) { + clearTimeout(this._autoClearTimers[key]); + delete this._autoClearTimers[key]; + } } } diff --git a/app/services/json-csv.js b/app/services/json-csv.js index d680464bd..d52ce4945 100644 --- a/app/services/json-csv.js +++ b/app/services/json-csv.js @@ -1,25 +1,49 @@ import Service from '@ember/service'; export default class JsonCsvService extends Service { - arrayToCsv(array) { + arrayToCsv(array, columnHeaders) { if (!array.length) { return 'No data to display'; } + try { - const rows = [Object.keys(array[0])].concat(array); - return rows - .map((row) => { - return Object.values(row) - .map((value) => { - return typeof value === 'string' - ? encodeURIComponent(JSON.stringify(value)).replace(/,/g, '') - : value; - }) - .toString(); + // Use given headers or from the first object + const headers = columnHeaders || Object.keys(array[0]); + + // Create CSV header row + const headerRow = headers + .map((header) => { + const escaped = header.replace(/"/g, '""'); + return `"${escaped}"`; }) - .join('\n'); + .join(','); + + // Create data rows + const dataRows = array.map((item) => { + return headers + .map((header) => { + const value = item[header]; + if (value == null) { + return ''; + } + // Convert anything that isn’t a string into a string + let text = String(value); + + // Escape internal quotes by doubling them + text = text.replace(/"/g, '""'); + + // Wrap the entire text in quotes + return `"${text}"`; + }) + .join(','); + }); + + // Combine header and data rows into a proper CSV string + const csvContent = [headerRow, ...dataRows].join('\n'); + + return csvContent; } catch (err) { - console.log(err); + console.error(err); return `error: ${err.message}`; } } diff --git a/app/services/problem-permissions.js b/app/services/problem-permissions.js index c9f383420..0a22fba7d 100644 --- a/app/services/problem-permissions.js +++ b/app/services/problem-permissions.js @@ -1,25 +1,28 @@ import Service, { inject as service } from '@ember/service'; -export default Service.extend({ - base: service('edit-permissions'), - isPublic: function (problem) { - return problem.get('privacySetting') === 'E'; - }, - isPrivate: function (problem) { - return problem.get('privacySetting') === 'M'; - }, +export default class ProblemPermissionsService extends Service { + @service('edit-permissions') base; + @service currentUser; - isApproved(problem) { - return problem.get('status') === 'approved'; - }, + isPublic(problem) { + return problem.privacySetting === 'E'; + } - isUsed(problem) { - return problem.get('isUsed'); - }, + isPrivate(problem) { + return problem.privacySetting === 'M'; + } + + isApproved(problem) { + return problem.status === 'approved'; + } isTrashed(problem) { - return problem.get('isTrashed'); - }, + return problem.isTrashed; + } + + isUsed(problem) { + return problem.isUsed; + } canDelete(problem) { // undefined if no or bad argument passed in @@ -27,7 +30,7 @@ export default Service.extend({ return; } // if admin return true - if (this.get('base.isAdmin')) { + if (this.currentUser.isAdmin) { return true; } @@ -45,7 +48,7 @@ export default Service.extend({ // currently this means that any non PdAdmin would not be able to edit/delete - if (!this.get('base.isPdAdmin')) { + if (!this.currentUser.isPdAdmin) { return false; } @@ -57,7 +60,7 @@ export default Service.extend({ // privacy setting can now only be 'O' or 'M' return this.base.doesRecordBelongToOrg(problem); - }, + } canEdit(problem) { // undefined if no or bad argument passed in @@ -65,7 +68,7 @@ export default Service.extend({ return; } // if admin return true - if (this.get('base.isAdmin')) { + if (this.currentUser.isAdmin) { return true; } @@ -83,7 +86,7 @@ export default Service.extend({ // currently this means that any non PdAdmin would not be able to edit/delete - if (!this.get('base.isPdAdmin')) { + if (!this.currentUser.isPdAdmin) { return false; } @@ -96,7 +99,7 @@ export default Service.extend({ // privacy setting can now only be 'O' or 'M' return this.base.doesRecordBelongToOrg(problem); - }, + } canAssign(problem) { // undefined if no or bad argument passed in @@ -104,19 +107,19 @@ export default Service.extend({ return; } // if admin return true - if (this.get('base.isAdmin')) { + if (this.currentUser.isAdmin) { return true; } return this.isApproved(problem); - }, + } canPend(problem) { if (!problem) { return; } - return this.get('base.isAdmin'); - }, + return this.currentUser.isAdmin; + } writePermissions(problem, isDeleteSameAsEdit = true) { let ret = {}; @@ -134,5 +137,5 @@ export default Service.extend({ ret.canPend = this.canPend(problem); return ret; - }, -}); + } +} diff --git a/app/services/problem-utils.js b/app/services/problem-utils.js new file mode 100644 index 000000000..a29a8496b --- /dev/null +++ b/app/services/problem-utils.js @@ -0,0 +1,149 @@ +import Service from '@ember/service'; +import { service } from '@ember/service'; +import { hash } from 'rsvp'; + +export default class ProblemUtilityService extends Service { + @service store; + @service currentUser; + @service('sweet-alert') alert; + + async fetchProblemData(problemId) { + const problem = await this.store.findRecord('problem', problemId); + const organization = await this.currentUser.user.organization; + const recommendedProblems = organization + ? await organization.recommendedProblems + : []; + + let flaggedBy, flaggedDate; + if (problem.flagReason?.flaggedBy) { + flaggedBy = await this.store.findRecord( + 'user', + problem.flagReason.flaggedBy + ); + flaggedDate = new Date(problem.flagReason.flaggedDate); + } + + return hash({ + problem, + sectionList: this.store.findAll('section'), + orgList: this.store.findAll('organization'), + recommendedProblems, + flaggedBy, + flaggedDate, + }); + } + + // Return a local copy of the problem as an object. + async extractEditableProperties(problem) { + const categories = await problem.categories; // fully load + return { + title: problem.title, + author: problem.author, + text: problem.text, + categories: categories || [], + status: problem.status, + privacySetting: problem.privacySetting, + sharingAuth: problem.sharingAuth, + additionalInfo: problem.additionalInfo, + copyrightNotice: problem.copyrightNotice, + image: problem.image, + keywords: problem.keywords?.slice() || [], + }; + } + + /** + * Converts an Ember Data Problem model into a plain JS object + * suitable for passing to createRecord('problem', { ... }). + * + * Note: We omit fields like 'id', 'isUsed', 'error' because they + * are typically not needed when creating a new problem record. + */ + async convertToObject(problem) { + const categories = await problem.categories; // fully load + return { + title: problem.title, + puzzleId: problem.puzzleId, + text: problem.text, + imageUrl: problem.imageUrl, + sourceUrl: problem.sourceUrl, + image: problem.image, // Ember Data relationship (model instance) if loaded + origin: problem.origin, + modifiedBy: problem.modifiedBy, + organization: problem.organization, + additionalInfo: problem.additionalInfo, + privacySetting: problem.privacySetting, + categories: categories || [], // copy the array of models + keywords: problem.keywords?.slice() ?? [], // copy the array of strings + copyrightNotice: problem.copyrightNotice, + sharingAuth: problem.sharingAuth, + author: problem.author, + status: problem.status, + flagReason: problem.flagReason, + contexts: problem.contexts, + }; + } + + /** + * Creates and saves a new "Problem" record, copying all fields from the given + * `problem` but overwriting any specified in `newProperties`. + * + * @param {DS.Model} problem - An existing Problem model to copy. + * @param {Object} newProperties - Key-value pairs that override the copied fields. + * @returns {Promise} - Resolves with the newly created and saved Problem. + */ + async saveCopy(problem, newProperties = {}) { + // Convert the existing problem into plain JS object + let baseObject = await this.convertToObject(problem); + + // Merge the newProperties over the base fields + let mergedProps = { ...baseObject, ...newProperties }; + + // Create a new problem record + let newProblem = this.store.createRecord('problem', mergedProps); + + // Save and return the promise so callers can await or .then().catch() + return newProblem.save(); + } + + async deleteProblem(problem) { + const { value: shouldDelete } = await this.alert.showModal( + 'warning', + 'Are you sure you want to delete this problem?', + null, + 'Yes, delete it' + ); + + if (!shouldDelete) { + return { wasDeleted: false, wasRestored: false }; + } + + problem.isTrashed = true; + const savedProblem = await problem.save(); + + const { value: shouldRestore } = await this.alert.showToast( + 'success', + 'Problem Deleted', + 'bottom-end', + 5000, + true, + 'Undo' + ); + + if (!shouldRestore) { + return { wasDeleted: true, wasRestored: false }; + } + + savedProblem.isTrashed = false; + savedProblem.save().then(() => { + this.alert.showToast( + 'success', + 'Problem Restored', + 'bottom-end', + 3000, + false, + null + ); + return { wasDeleted: false, wasRestored: true }; + }); + } +} diff --git a/app/services/utility-methods.js b/app/services/utility-methods.js index e12a04b13..23ed4a88b 100644 --- a/app/services/utility-methods.js +++ b/app/services/utility-methods.js @@ -1,5 +1,5 @@ import Service from '@ember/service'; -import { inject as service } from '@ember/service'; +import { service } from '@ember/service'; export default class UtilityService extends Service { @service currentUser; diff --git a/app/services/workspace-permissions.js b/app/services/workspace-permissions.js index ecae584bc..677c0bf84 100644 --- a/app/services/workspace-permissions.js +++ b/app/services/workspace-permissions.js @@ -1,85 +1,82 @@ -/* eslint-disable */ -import Service, { inject as service } from '@ember/service'; +import Service, { service } from '@ember/service'; -export default Service.extend({ - currentUser: null, - setUser(user) { - this.set('currentUser', user); - }, - utils: service('utility-methods'), +export default class WorkspacePermissionsService extends Service { + @service('utility-methods') utils; + @service currentUser; - isAdmin() { - return this.get('currentUser.isAdmin'); - }, + get user() { + return this.currentUser.user; + } - isPdAdmin() { - return this.get('currentUser.isPdAdmin'); - }, + get isAdmin() { + return this.user.isAdmin; + } + + get isPdAdmin() { + return this.user.isPdAdmin; + } isOwner(ws) { let ownerId = this.utils.getBelongsToId(ws, 'owner'); - return ownerId === this.get('currentUser.id'); - }, + return ownerId === this.user.id; + } isCreator(ws) { let creatorId = this.utils.getBelongsToId(ws, 'createdBy'); - return creatorId === this.get('currentUser.id'); - }, + return creatorId === this.user.id; + } isInPdAdminDomain(ws) { - if (!this.isPdAdmin()) { + if (!this.isPdAdmin) { return false; } let utils = this.utils; - let userOrgId = utils.getBelongsToId(this.currentUser, 'organization'); + let userOrgId = utils.getBelongsToId(this.user, 'organization'); let wsOrgId = utils.getBelongsToId(ws, 'organization'); return userOrgId === wsOrgId; - }, + } canDelete(ws) { - return this.isAdmin() || this.isCreator(ws) || this.isOwner(ws); - }, + return this.isAdmin || this.isCreator(ws) || this.isOwner(ws); + } hasOwnerPrivileges(ws) { return ( - this.isAdmin() || + this.isAdmin || this.isOwner(ws) || this.isCreator(ws) || this.isInPdAdminDomain(ws) ); - }, + } canCopy(ws) { // have to add a check is workspace is allowed to be copied - if (this.canDelete(ws) || this.isInPdAdminDomain(ws)) { - return true; - } else { - return false; - } - }, + return this.canDelete(ws) || this.isInPdAdminDomain(ws); + } + isFeedbackApprover(ws) { if (!ws) { return false; } - let approvers = ws.get('feedbackAuthorizers') || []; - return approvers.includes(this.get('currentUser.id')); - }, + let approvers = ws.feedbackAuthorizers || []; + return approvers.includes(this.user.id); + } canApproveFeedback(ws) { if (!ws || ws.workspaceType === 'parent') { return false; } return ( - this.isAdmin() || + this.isAdmin || this.isOwner(ws) || this.isCreator(ws) || this.isFeedbackApprover(ws) || this.isInPdAdminDomain(ws) ); - }, + } canEdit(ws, recordType, requiredPermissionLevel) { const utils = this.utils; @@ -88,7 +85,7 @@ export default Service.extend({ return false; } - let wsType = ws.get('workspaceType'); + let wsType = ws.workspaceType; if (wsType === 'parent') { // cannot create new selections or taggings or edit folders @@ -103,7 +100,7 @@ export default Service.extend({ } if ( - this.isAdmin() || + this.isAdmin || this.isOwner(ws) || this.isCreator(ws) || this.isInPdAdminDomain(ws) @@ -111,7 +108,7 @@ export default Service.extend({ return true; } - let wsMode = ws.get('mode'); + let wsMode = ws.mode; let isPublic = wsMode === 'public' || wsMode === 'internet'; @@ -122,16 +119,13 @@ export default Service.extend({ // check ws permissions - const wsPermissions = ws.get('permissions'); + const wsPermissions = ws.permissions; if (!utils.isNonEmptyArray(wsPermissions)) { return false; } - const userPermissions = wsPermissions.findBy( - 'user', - this.get('currentUser.id') - ); + const userPermissions = wsPermissions.findBy('user', this.user.id); if (!utils.isNonEmptyObject(userPermissions)) { return false; } @@ -169,5 +163,5 @@ export default Service.extend({ } return permissionLevel >= requiredPermissionLevel; - }, -}); + } +} diff --git a/app/services/workspace-reports.js b/app/services/workspace-reports.js index a990b19a5..314b2434a 100644 --- a/app/services/workspace-reports.js +++ b/app/services/workspace-reports.js @@ -6,8 +6,17 @@ export default class WorkspaceReportsService extends Service { @service jsonCsv; @service currentUrl; + getUniqueFolderNames(selection) { + const folderNames = new Set(); + const folders = selection.get('folders') || []; + folders.forEach((folder) => { + folderNames.add(folder.get('name')); + }); + return Array.from(folderNames); + } + submissionReportCsv(model) { - const submissionsArray = model.submissions.toArray(); + const submissionsArray = model.submissions.slice(); // Group submissions by submitter const submissionsByUser = submissionsArray.reduce((acc, submission) => { @@ -34,17 +43,8 @@ export default class WorkspaceReportsService extends Service { // Flatten the grouped submissions back into an array const labeledSubmissions = [].concat(...Object.values(submissionsByUser)); - // Determine the maximum number of selections - let maxSelections = 0; - labeledSubmissions.forEach((submission) => { - const selectionsCount = submission.get('selections').length; - if (selectionsCount > maxSelections) { - maxSelections = selectionsCount; - } - }); - // Generate CSV data with dynamic columns for selections - return labeledSubmissions.flatMap((submission) => { + const data = labeledSubmissions.flatMap((submission) => { const baseData = { 'Name of workspace': submission.get('workspaces.firstObject.name'), 'Workspace URL': this.currentUrl.currentUrl, @@ -65,13 +65,10 @@ export default class WorkspaceReportsService extends Service { }`, 'Submission ID': submission.id, 'Submission or Revision': submission.submissionLabel, - 'Number of Folders': model.workspace.foldersLength, + 'Number of Workspace Folders': model.workspace.foldersLength, 'Number of Notice/Wonder/Feedback': model.workspace.commentsLength, - // Do we need this? Submission order currently inaccurate due to sorting of array. - // 'Submission Order': index + 1, }; - - const selections = submission.get('selections').toArray(); + const selections = submission.get('selections').slice(); if (selections.length === 0) { // For submissions without selections, return the base data only return [baseData]; @@ -85,13 +82,18 @@ export default class WorkspaceReportsService extends Service { [`Selector Date`]: selectorInfo.selectionCreateDate, [`Annotator`]: selectorInfo.annotatorUsername, [`Text of Annotator`]: selectorInfo.annotatorText, - ['Annotator Date']: selectorInfo.annotatorCreateDate, + [`Annotator Date`]: selectorInfo.annotatorCreateDate, + [`Folder(s) for Selection`]: + this.getUniqueFolderNames(selection).join('; '), }; return { ...baseData, ...selectionData }; }); } }); + + const headers = [...new Set(data.flatMap((row) => Object.keys(row)))]; + return { headers, data }; } createSelectorInfo(selector) { @@ -187,7 +189,8 @@ export default class WorkspaceReportsService extends Service { }); } submissionReport(model) { - return this.jsonCsv.arrayToCsv(this.submissionReportCsv(model)); + const { headers, data } = this.submissionReportCsv(model); + return this.jsonCsv.arrayToCsv(data, headers); } responseReport(model) { diff --git a/app/styles/_filter-list.scss b/app/styles/_filter-list.scss index a609b1a06..43686ac02 100644 --- a/app/styles/_filter-list.scss +++ b/app/styles/_filter-list.scss @@ -434,7 +434,7 @@ } } } - #admin-problem-filter, #admin-workspace-filter { + #admin-filter { background-color: $background-body; padding: 10px 0 20px 20px; .choose-filter-admin { @@ -790,7 +790,7 @@ .click-menu { position: absolute; z-index: 100; - margin-top: 28px; + margin-top: 10px; margin-left: -120px; width: 100px; padding: 10px 15px; diff --git a/app/styles/_users.scss b/app/styles/_users.scss index 4821cb3a4..63e357614 100644 --- a/app/styles/_users.scss +++ b/app/styles/_users.scss @@ -97,9 +97,8 @@ } .reset-user-box { - margin: 20px; .auth-form-input { - width: 50%; + width: 80%; margin: 0 auto; label { margin-bottom: 10px; diff --git a/app/templates/assignments.hbs b/app/templates/assignments.hbs index f2317adb9..a56560b67 100644 --- a/app/templates/assignments.hbs +++ b/app/templates/assignments.hbs @@ -1,7 +1,7 @@ - + -
      +
      {{outlet}}
      -
      \ No newline at end of file +
      \ No newline at end of file diff --git a/app/templates/assignments/assignment.hbs b/app/templates/assignments/assignment.hbs index 29aa9da1f..314581a12 100644 --- a/app/templates/assignments/assignment.hbs +++ b/app/templates/assignments/assignment.hbs @@ -1,2 +1,14 @@ -{{page-title this.model.assignment.name}} - \ No newline at end of file +{{page-title @model.assignment.name}} + \ No newline at end of file diff --git a/app/templates/components/categories-menu.hbs b/app/templates/components/categories-menu.hbs deleted file mode 100644 index 4704bfb51..000000000 --- a/app/templates/components/categories-menu.hbs +++ /dev/null @@ -1,71 +0,0 @@ -
        - {{#each categories as |category|}} -
      • - - -
          - {{#each category.domains as |domain|}} -
        • - - -
            - {{#each domain.topics as |topic|}} - {{#if topic.standards}} -
          • - - -
              - {{#each topic.standards as |standard|}} - {{#if standard.substandards}} -
            • - - -
                - {{#each standard.substandards as |sub|}} -
              • {{sub.identifier}} - {{sub.description}}
              • - {{/each}} -
              -
            • - {{else}} -
            • {{standard.identifier}} - {{standard.description}}
            • - {{/if}} - {{/each}} -
            • Other
            • -
            -
          • - {{else}} -
          • {{topic.identifier}} - {{topic.description}}
          • - {{/if}} - {{/each}} -
          • Other
          • -
          -
        • - {{/each}} -
        • - -
        • - {{!--
        • Other
        • --}} -
        -
      • - {{/each}} -
      - - - - \ No newline at end of file diff --git a/app/templates/components/comment-list.hbs b/app/templates/components/comment-list.hbs index 251d81900..d03216e42 100644 --- a/app/templates/components/comment-list.hbs +++ b/app/templates/components/comment-list.hbs @@ -9,7 +9,7 @@ {{#if this.canComment}}
      -
      {{/if}} @@ -87,7 +91,10 @@ @value={{this.sinceDate}} /> {{#if this.showApplyDate}} - + {{/if}} @@ -126,7 +133,7 @@

      {{error}}

      {{/each}} {{#if this.invalidDateError}} - - - - {{#unless hideSubmit}} - {{#if filesToBeUploaded}} - - {{/if}} - {{/unless}} - - -{{#if missingFilesError}} - -{{/if}} - -{{#if isOverSizeLimit}} - {{#if isOverPdfLimit}} - - {{/if}} - {{#if isOverImageLimit}} - - {{/if}} -{{/if}} - -{{#if overSizedFileError}} - -{{/if}} - -{{#unless uploadResults}} - {{#if showLoadingMessage}} -

      Uploading images... Thank you for your patience.

      - {{/if}} -{{/unless}} - -{{#each uploadErrors as |error|}} -

      {{error}}

      -{{/each}} - -{{#if uploadResults}} -

      - {{uploadResults.length}} - {{#if (greater-equal uploadResults.length 2)}} - files - {{else}} - file - {{/if}} - uploaded successfully! -

      -{{/if}} \ No newline at end of file diff --git a/app/templates/components/import-review-answers.hbs b/app/templates/components/import-review-answers.hbs index 1ddf9c179..e7093d31e 100644 --- a/app/templates/components/import-review-answers.hbs +++ b/app/templates/components/import-review-answers.hbs @@ -1,63 +1,83 @@ -
      -

      Please review your submissions to ensure everything is accurate. You can either defer creating a workspace until a later date, or we can automatically create a workspace for you from your uploaded submissions

      - -
      -
      +
      +

      Please review your submissions to ensure everything is + accurate. You can either defer creating a workspace until a later date, or + we can automatically create a workspace for you from your uploaded + submissions

      + +
      +
      -

      Workspace Creation Options

      -
        -
      1. - - -
      2. - - - {{#unless doNotCreateWorkspace}} -
      3. - - -
      4. -
      5. - - -
      6. - - -
      7. - - - - - -
      8. - {{/unless}} -
      +

      Workspace Creation Options

      +
        +
      1. + + +
      2. + + {{#unless doNotCreateWorkspace}} +
      3. + + +
      4. +
      5. + + +
      6. + +
      7. + + + + + +
      8. + {{/unless}} +
      - - -
      - {{#if doNotCreateWorkspace}} -

      Click submit to upload your submissions.

      - {{else}} -

      Click submit to create your {{mode}} workspace.

      - {{/if}} - +
      + {{#if doNotCreateWorkspace}} +

      Click submit to upload your submissions.

      + {{else}} +

      Click submit to create your {{mode}} workspace.

      + {{/if}} + +
      -
      - - -
      +
      {{#each answers as |answer|}} -
      -
      +
      +

      {{answer.explanationImage.originalname}}

      - +
      -
      +
        {{#each answer.students as |student|}} @@ -69,8 +89,4 @@ {{/each}}
      - -
      - - - +
      \ No newline at end of file diff --git a/app/templates/components/import-work-container.hbs b/app/templates/components/import-work-container.hbs index 18ad3044e..d918b627b 100644 --- a/app/templates/components/import-work-container.hbs +++ b/app/templates/components/import-work-container.hbs @@ -1,85 +1,180 @@ -
      -
      -
      +
      +
      +

      Summary

      -
        +
          {{#each detailsItems as |item|}} - + {{/each}}
      -
      -
      -
      +
      +
      +

      Import Work

      -
      -
      -
        -
      • +
        +
        +
          +
        • Select Problem
        • -
        • +
        • Select Class
        • -
        • +
        • Upload Files
        • -
        • +
        • Match Students
        • -
        • +
        • Create Workspace
        • -
        • +
        • Review
        -
        +
        {{#if showSelectProblem}} - + {{/if}} {{#if showSelectClass}} - + {{/if}} {{#if showUploadFiles}} - + {{/if}} {{#if showMatchStudents}} - + {{/if}} {{#if showCreateWs}} - + {{/if}} {{#if showReview}} - + {{/if}}
        - {{#each findRecordErrors as |error|}} - - {{/each}} - {{#each postErrors as |error|}} - - {{/each}} - {{#each createAnswerErrors as |error|}} - - {{/each}} + {{#each findRecordErrors as |error|}} + + {{/each}} + {{#each postErrors as |error|}} + + {{/each}} + {{#each createAnswerErrors as |error|}} + + {{/each}}
      -
      - -
      - +
      + +
      \ No newline at end of file diff --git a/app/templates/components/import-work-step1.hbs b/app/templates/components/import-work-step1.hbs index e5dd54612..9e2aafc24 100644 --- a/app/templates/components/import-work-step1.hbs +++ b/app/templates/components/import-work-step1.hbs @@ -30,7 +30,7 @@ /> {{#if missingProblem}} -

      - +

      Select Files to Upload - - + +

      -

      Supported files are jpeg, png or pdf. Multipage pdfs will convert each page into a single image.

      - +

      Supported files are jpeg, png or pdf. Multipage pdfs + will convert each page into a single image.

      + {{#if missingFiles}} - + {{/if}} {{#if uploadedFiles}}
      Uploaded Files
        - {{#each uploadedFiles as |f| }} + {{#each uploadedFiles as |f|}}
      • - {{f.originalname}} {{#if f.pdfPageNum}}(pg. {{f.pdfPageNum}}){{/if}} + {{f.originalname}} + {{#if f.pdfPageNum}}(pg. {{f.pdfPageNum}}){{/if}} - +
      • {{/each}}
      {{/if}} - + \ No newline at end of file diff --git a/app/templates/components/import-work-step5.hbs b/app/templates/components/import-work-step5.hbs index d5a86c917..dc1823c51 100644 --- a/app/templates/components/import-work-step5.hbs +++ b/app/templates/components/import-work-step5.hbs @@ -1,111 +1,202 @@ -

      +

      Would you like to create a new workspace? - - + +

      - + {{#if creatingWs}} -
      -
      -

      - Workspace Name - * - - - -

      -
      - -
      - {{#if missingNameError}} -
      - +
      +
      +

      + Workspace Name + * + + + +

      +
      + +
      + {{#if missingNameError}} +
      + +
      + {{/if}}
      - {{/if}} -
      - -
      -

      - Workspace Owner - * +

      +

      + Workspace Owner + * - - - -

      - - {{#if missingOwnerError}} -
      - + + + +

      + + {{#if missingOwnerError}} +
      + +
      + {{/if}}
      - {{/if}} -
      - -
      -

      - Privacy Setting - - - -

      - -
      +
      +

      + Privacy Setting + + + +

      + +
      -
      -

      - Folder Set - - - -

      - -
      -

      You can add collaborators and change workspace settings after creating the workspace

      +
      +

      + Folder Set + + + +

      + +
      +

      You can add collaborators and change workspace settings + after creating the workspace

      - {{/if}} +{{/if}} - {{#unless this.currentUser.isStudent}} - {{#if selectedSection}} -
      -

      +{{#unless this.currentUser.isStudent}} + {{#if selectedSection}} +

      +

      Would you like to save this work as an assignment? - - + +

      - +
      - {{/if}} - {{/unless}} + {{/if}} +{{/unless}} - {{#if creatingAssignment}} -
      -
      -

      +{{#if creatingAssignment}} +

      +
      +

      Assignment Name - - + +

      -
      - +
      +
      {{#if missingAssignmentError}} -
      - -
      +
      + +
      {{/if}}
      - {{/if}} - - +{{/if}} -

    - + @@ -107,7 +110,7 @@ {{/if}} - + \ No newline at end of file diff --git a/app/templates/problems.hbs b/app/templates/problems.hbs index 19c65d7af..4d221db82 100644 --- a/app/templates/problems.hbs +++ b/app/templates/problems.hbs @@ -1,8 +1,10 @@ {{page-title 'Problems'}} - \ No newline at end of file +
    + + +
    \ No newline at end of file diff --git a/app/templates/problems/edit.hbs b/app/templates/problems/edit.hbs new file mode 100644 index 000000000..eeeef5ff5 --- /dev/null +++ b/app/templates/problems/edit.hbs @@ -0,0 +1,14 @@ +{{page-title @model.problem.title}} +
    + +
    \ No newline at end of file diff --git a/app/templates/problems/new.hbs b/app/templates/problems/new.hbs index aec96a57d..f4286443d 100644 --- a/app/templates/problems/new.hbs +++ b/app/templates/problems/new.hbs @@ -1,4 +1,4 @@ -{{page-title "New Problem"}} -
    - +{{page-title 'New Problem'}} +
    +
    \ No newline at end of file diff --git a/app/templates/problems/problem.hbs b/app/templates/problems/problem.hbs index cd8463c66..8d691dd27 100644 --- a/app/templates/problems/problem.hbs +++ b/app/templates/problems/problem.hbs @@ -1,12 +1,13 @@ -{{page-title this.model.problem.title}} +{{page-title @model.problem.title}}
    \ No newline at end of file diff --git a/app/templates/problems/problem/assignment.hbs b/app/templates/problems/problem/assignment.hbs new file mode 100644 index 000000000..77511a8f1 --- /dev/null +++ b/app/templates/problems/problem/assignment.hbs @@ -0,0 +1,5 @@ + \ No newline at end of file diff --git a/app/templates/unauthorized.hbs b/app/templates/unauthorized.hbs index 212cdec83..84ba5c92f 100644 --- a/app/templates/unauthorized.hbs +++ b/app/templates/unauthorized.hbs @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/app/templates/users.hbs b/app/templates/users.hbs index df2c9fa08..3a1d82b6a 100644 --- a/app/templates/users.hbs +++ b/app/templates/users.hbs @@ -1,12 +1,14 @@ {{#if this.model.currentUser.isStudent}} -
    FORBIDDEN
    +
    FORBIDDEN
    {{else}} - + {{/if}} - - -
    +
    {{outlet}} -
    - +
    \ No newline at end of file diff --git a/app/templates/users/user.hbs b/app/templates/users/user.hbs index be06db807..d6aa3082f 100644 --- a/app/templates/users/user.hbs +++ b/app/templates/users/user.hbs @@ -1,4 +1,10 @@ {{page-title this.model.user.username}} -
    - +
    +
    \ No newline at end of file diff --git a/app/templates/welcome.hbs b/app/templates/welcome.hbs index 22409c639..b6041e6e0 100644 --- a/app/templates/welcome.hbs +++ b/app/templates/welcome.hbs @@ -83,7 +83,11 @@
    -

    EnCoMPASS +

    EnCoMPASS is part of the Online Reflection and Community-based Instructional Development System for Mathematics Education (ORCIDS), an NSF-funded collaboration between Drexel University, California State University San @@ -91,16 +95,17 @@ Maintained by 21PSTEM.org This material is based upon work supported by the National Science Foundation under Grant No. 2010306. Any opinions, findings, and conclusions or recommendations expressed in this material are those of the author(s) and do not necessarily reflect the views of the National Science Foundation. EnCoMPASS version - {{this.version}} + {{this.model.version}} last updated on - {{this.buildDate}}. + {{this.model.buildDate}}.

    diff --git a/app/templates/workspace/info.hbs b/app/templates/workspace/info.hbs index 7642e06e1..701a84dc5 100644 --- a/app/templates/workspace/info.hbs +++ b/app/templates/workspace/info.hbs @@ -1,5 +1,5 @@ -{{page-title this.model.workspace.name}} +{{page-title @model.workspace.name}} \ No newline at end of file diff --git a/app_server/datasource/schemas/problem.js b/app_server/datasource/schemas/problem.js index 5cb8c415a..c175dd00c 100644 --- a/app_server/datasource/schemas/problem.js +++ b/app_server/datasource/schemas/problem.js @@ -33,7 +33,7 @@ var ProblemSchema = new Schema( organization: { type: ObjectId, ref: 'Organization' }, categories: [{ type: ObjectId, ref: 'Category' }], keywords: [{ type: String }], - isUsed: { type: Boolean, default: false }, + isUsed: { type: Boolean, default: false }, // indicates if a problem has associated answers status: { type: String, enum: ['approved', 'pending', 'flagged'] }, flagReason: { flaggedBy: { type: ObjectId, ref: 'User' }, diff --git a/ember-cli-build.js b/ember-cli-build.js index b0b3a9f1e..961541d4b 100644 --- a/ember-cli-build.js +++ b/ember-cli-build.js @@ -11,16 +11,12 @@ module.exports = function (defaults) { app.import('vendor/image-tagging.js'); app.import('vendor/selection-highlighting.js'); - app.import('vendor/bind.polyfill.js'); /* * These are the dependencies for an Ember application * and they have to be loaded before any application code. */ - app.import('vendor/jquery.cookie.js'); - app.import('vendor/tree.jquery.js'); app.import('vendor/guiders.js'); - app.import('vendor/jq.keys.js'); app.import('vendor/validate.min.js'); app.import('vendor/dropzone.js'); diff --git a/jsconfig.json b/jsconfig.json index a178f9dcd..2c6ea1dc7 100644 --- a/jsconfig.json +++ b/jsconfig.json @@ -1,5 +1,10 @@ { "compilerOptions": { "experimentalDecorators": true - } + }, + "exclude": [ + "node_modules", + "vendor", + "dist" + ] } \ No newline at end of file diff --git a/tests/dummy/app/styles/app.scss b/tests/dummy/app/styles/app.scss deleted file mode 100644 index e69de29bb..000000000 diff --git a/tests/integration/.gitkeep b/tests/integration/.gitkeep deleted file mode 100644 index e69de29bb..000000000 diff --git a/tests/integration/components/problem-list-container-test.js b/tests/integration/components/problem-list-container-test.js index 0656cb00f..bd434fc75 100644 --- a/tests/integration/components/problem-list-container-test.js +++ b/tests/integration/components/problem-list-container-test.js @@ -1,6 +1,6 @@ import { module, test } from 'qunit'; import { setupRenderingTest } from 'ember-qunit'; -import { render, click, findAll } from '@ember/test-helpers'; +import { render, click, findAll, settled } from '@ember/test-helpers'; import { hbs } from 'ember-cli-htmlbars'; import Service from '@ember/service'; @@ -39,7 +39,6 @@ module('Integration | Component | problem-list-container', function (hooks) { ); await render(hbs``); - console.log(this.element.innerHTML); // Verify the error messages are displayed const errors = findAll('.error-message'); assert.strictEqual(errors.length, 2, 'Displays two error messages'); @@ -71,8 +70,8 @@ module('Integration | Component | problem-list-container', function (hooks) { // Remove errors errorHandling.removeMessages('problemLoadErrors'); - // Trigger re-render by interacting with the component - await click('.refresh-icon'); + // Trigger a full re-render of the component + await settled(); // Verify the error is removed assert.dom('.error-message').doesNotExist('Error message is removed'); @@ -102,23 +101,30 @@ module('Integration | Component | problem-list-container', function (hooks) { test('it applies filters using the input-state service', async function (assert) { const inputState = this.owner.lookup('service:input-state'); + const currentUserService = this.owner.lookup('service:current-user'); + + // Set the current user in the current-user service + currentUserService.setUser({ + organization: { id: 'myOrg', get: () => [] }, + }); + // Render the component await render(hbs``); - // Update the selection in input-state - inputState.states['problem-filter'].selectedOption = inputState.states[ - 'problem-filter' - ].options.find((option) => option.value === 'myOrg'); + // Update the selection in input-state using setSelection + inputState.setSelection('problem-filter', 'myOrg'); // Trigger the fetch to apply the new filter - await click('.refresh-icon'); + await settled(); const filterBy = inputState.getFilter('problem-filter'); + + // Ensure that the filter is applied correctly assert.deepEqual( filterBy, { - organization: this.owner.lookup('service:current-user').user - .organization.id, + organization: currentUserService.user.organization.id, + $or: [{ organization: 'myOrg' }], }, 'The filter is applied correctly' ); @@ -140,10 +146,7 @@ module('Integration | Component | problem-list-container', function (hooks) { // Verify initial message assert .dom('.results-message') - .hasText( - 'No results found. Please try expanding your filter criteria.', - 'Displays the correct initial message' - ); + .hasText('0 problems found', 'Displays the correct initial message'); // Set a valid filter inputState.states['problem-filter'].selectedOption = inputState.states[ @@ -155,7 +158,7 @@ module('Integration | Component | problem-list-container', function (hooks) { // Check the updated results message assert .dom('.results-message') - .doesNotHaveText( + .doesNotIncludeText( 'No results found. Please try expanding your filter criteria.', 'The error message is no longer displayed after applying a valid filter' ); diff --git a/tests/unit/.gitkeep b/tests/unit/.gitkeep deleted file mode 100644 index e69de29bb..000000000 diff --git a/troubleshooting.md b/troubleshooting.md index f2ef65fe8..85240f7fa 100644 --- a/troubleshooting.md +++ b/troubleshooting.md @@ -20,7 +20,7 @@ # store query is immutable -- convert to array with `.toArray()` +- convert to array with `.slice()` # tables use [Ember Table](https://opensource.addepar.com/ember-table/docs) diff --git a/vendor/.gitkeep b/vendor/.gitkeep deleted file mode 100644 index e69de29bb..000000000 diff --git a/vendor/bind.polyfill.js b/vendor/bind.polyfill.js deleted file mode 100644 index ad864d8dd..000000000 --- a/vendor/bind.polyfill.js +++ /dev/null @@ -1,10 +0,0 @@ -//http://ryanmorr.com/understanding-scope-and-context-in-javascript/ -//this is used in the workspace listings and apparently phantomjs doesn't have it -if(!('bind' in Function.prototype)){ - Function.prototype.bind = function(){ - var fn = this, context = arguments[0], args = Array.prototype.slice.call(arguments, 1); - return function(){ - return fn.apply(context, args.concat(Array.prototype.slice.call(arguments))); - } - } -} diff --git a/vendor/jq.keys.js b/vendor/jq.keys.js deleted file mode 100644 index 81468cd8c..000000000 --- a/vendor/jq.keys.js +++ /dev/null @@ -1,11 +0,0 @@ -$(document).keydown(function(e){ - if(!($(e.target).is('textarea') || $(e.target).is('input'))) { - if(e.keyCode === 37) { - $('#leftArrow').click(); - } - if(e.keyCode === 39) { - $('#rightArrow').click(); - } - } - return true; -}); diff --git a/vendor/jquery.cookie.js b/vendor/jquery.cookie.js deleted file mode 100644 index 927190008..000000000 --- a/vendor/jquery.cookie.js +++ /dev/null @@ -1,117 +0,0 @@ -/*! - * jQuery Cookie Plugin v1.4.0 - * https://github.com/carhartl/jquery-cookie - * - * Copyright 2013 Klaus Hartl - * Released under the MIT license - */ -(function (factory) { - if (typeof define === 'function' && define.amd) { - // AMD. Register as anonymous module. - define(['jquery'], factory); - } else { - // Browser globals. - factory(jQuery); - } -}(function ($) { - - var pluses = /\+/g; - - function encode(s) { - return config.raw ? s : encodeURIComponent(s); - } - - function decode(s) { - return config.raw ? s : decodeURIComponent(s); - } - - function stringifyCookieValue(value) { - return encode(config.json ? JSON.stringify(value) : String(value)); - } - - function parseCookieValue(s) { - if (s.indexOf('"') === 0) { - // This is a quoted cookie as according to RFC2068, unescape... - s = s.slice(1, -1).replace(/\\"/g, '"').replace(/\\\\/g, '\\'); - } - - try { - // Replace server-side written pluses with spaces. - // If we can't decode the cookie, ignore it, it's unusable. - s = decodeURIComponent(s.replace(pluses, ' ')); - } catch(e) { - return; - } - - try { - // If we can't parse the cookie, ignore it, it's unusable. - return config.json ? JSON.parse(s) : s; - } catch(e) {} - } - - function read(s, converter) { - var value = config.raw ? s : parseCookieValue(s); - return $.isFunction(converter) ? converter(value) : value; - } - - var config = $.cookie = function (key, value, options) { - - // Write - if (value !== undefined && !$.isFunction(value)) { - options = $.extend({}, config.defaults, options); - - if (typeof options.expires === 'number') { - var days = options.expires, t = options.expires = new Date(); - t.setDate(t.getDate() + days); - } - - return (document.cookie = [ - encode(key), '=', stringifyCookieValue(value), - options.expires ? '; expires=' + options.expires.toUTCString() : '', // use expires attribute, max-age is not supported by IE - options.path ? '; path=' + options.path : '', - options.domain ? '; domain=' + options.domain : '', - options.secure ? '; secure' : '' - ].join('')); - } - - // Read - - var result = key ? undefined : {}; - - // To prevent the for loop in the first place assign an empty array - // in case there are no cookies at all. Also prevents odd result when - // calling $.cookie(). - var cookies = document.cookie ? document.cookie.split('; ') : []; - - for (var i = 0, l = cookies.length; i < l; i++) { - var parts = cookies[i].split('='); - var name = decode(parts.shift()); - var cookie = parts.join('='); - - if (key && key === name) { - // If second argument (value) is a function it's a converter... - result = read(cookie, value); - break; - } - - // Prevent storing a cookie that we couldn't decode. - if (!key && (cookie = read(cookie)) !== undefined) { - result[name] = cookie; - } - } - - return result; - }; - - config.defaults = {}; - - $.removeCookie = function (key, options) { - if ($.cookie(key) !== undefined) { - // Must not alter options, thus extending a fresh object... - $.cookie(key, '', $.extend({}, options, { expires: -1 })); - return true; - } - return false; - }; - -})); diff --git a/vendor/tree.jquery.js b/vendor/tree.jquery.js deleted file mode 100755 index 9398faf06..000000000 --- a/vendor/tree.jquery.js +++ /dev/null @@ -1,2765 +0,0 @@ -// Generated by CoffeeScript 1.6.3 -/* -Copyright 2013 Marco Braak - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - - -(function() { - var $, BorderDropHint, DragAndDropHandler, DragElement, FolderElement, GhostDropHint, HitAreasGenerator, JqTreeWidget, KeyHandler, MouseWidget, Node, NodeElement, Position, SaveStateHandler, ScrollHandler, SelectNodeHandler, SimpleWidget, VisibleNodeIterator, html_escape, indexOf, json_escapable, json_meta, json_quote, json_str, _indexOf, _ref, _ref1, _ref2, - __slice = [].slice, - __hasProp = {}.hasOwnProperty, - __extends = function(child, parent) { for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }; - - $ = this.jQuery; - - SimpleWidget = (function() { - SimpleWidget.prototype.defaults = {}; - - function SimpleWidget(el, options) { - this.$el = $(el); - this.options = $.extend({}, this.defaults, options); - } - - SimpleWidget.prototype.destroy = function() { - return this._deinit(); - }; - - SimpleWidget.prototype._init = function() { - return null; - }; - - SimpleWidget.prototype._deinit = function() { - return null; - }; - - SimpleWidget.register = function(widget_class, widget_name) { - var callFunction, createWidget, destroyWidget, getDataKey; - getDataKey = function() { - return "simple_widget_" + widget_name; - }; - createWidget = function($el, options) { - var data_key, el, widget, _i, _len; - data_key = getDataKey(); - for (_i = 0, _len = $el.length; _i < _len; _i++) { - el = $el[_i]; - widget = new widget_class(el, options); - if (!$.data(el, data_key)) { - $.data(el, data_key, widget); - } - widget._init(); - } - return $el; - }; - destroyWidget = function($el) { - var data_key, el, widget, _i, _len, _results; - data_key = getDataKey(); - _results = []; - for (_i = 0, _len = $el.length; _i < _len; _i++) { - el = $el[_i]; - widget = $.data(el, data_key); - if (widget && (widget instanceof SimpleWidget)) { - widget.destroy(); - } - _results.push($.removeData(el, data_key)); - } - return _results; - }; - callFunction = function($el, function_name, args) { - var el, result, widget, widget_function, _i, _len; - result = null; - for (_i = 0, _len = $el.length; _i < _len; _i++) { - el = $el[_i]; - widget = $.data(el, getDataKey()); - if (widget && (widget instanceof SimpleWidget)) { - widget_function = widget[function_name]; - if (widget_function && (typeof widget_function === 'function')) { - result = widget_function.apply(widget, args); - } - } - } - return result; - }; - return $.fn[widget_name] = function() { - var $el, args, argument1, function_name, options; - argument1 = arguments[0], args = 2 <= arguments.length ? __slice.call(arguments, 1) : []; - $el = this; - if (argument1 === void 0 || typeof argument1 === 'object') { - options = argument1; - return createWidget($el, options); - } else if (typeof argument1 === 'string' && argument1[0] !== '_') { - function_name = argument1; - if (function_name === 'destroy') { - return destroyWidget($el); - } else { - return callFunction($el, function_name, args); - } - } - }; - }; - - return SimpleWidget; - - })(); - - this.SimpleWidget = SimpleWidget; - - /* - This widget does the same a the mouse widget in jqueryui. - */ - - - MouseWidget = (function(_super) { - __extends(MouseWidget, _super); - - function MouseWidget() { - _ref = MouseWidget.__super__.constructor.apply(this, arguments); - return _ref; - } - - MouseWidget.is_mouse_handled = false; - - MouseWidget.prototype._init = function() { - this.$el.bind('mousedown.mousewidget', $.proxy(this._mouseDown, this)); - this.$el.bind('touchstart.mousewidget', $.proxy(this._touchStart, this)); - this.is_mouse_started = false; - this.mouse_delay = 0; - this._mouse_delay_timer = null; - this._is_mouse_delay_met = true; - return this.mouse_down_info = null; - }; - - MouseWidget.prototype._deinit = function() { - var $document; - this.$el.unbind('mousedown.mousewidget'); - this.$el.unbind('touchstart.mousewidget'); - $document = $(document); - $document.unbind('mousemove.mousewidget'); - return $document.unbind('mouseup.mousewidget'); - }; - - MouseWidget.prototype._mouseDown = function(e) { - var result; - if (e.which !== 1) { - return; - } - result = this._handleMouseDown(e, this._getPositionInfo(e)); - if (result) { - e.preventDefault(); - } - return result; - }; - - MouseWidget.prototype._handleMouseDown = function(e, position_info) { - if (MouseWidget.is_mouse_handled) { - return; - } - if (this.is_mouse_started) { - this._handleMouseUp(position_info); - } - this.mouse_down_info = position_info; - if (!this._mouseCapture(position_info)) { - return; - } - this._handleStartMouse(); - this.is_mouse_handled = true; - return true; - }; - - MouseWidget.prototype._handleStartMouse = function() { - var $document; - $document = $(document); - $document.bind('mousemove.mousewidget', $.proxy(this._mouseMove, this)); - $document.bind('touchmove.mousewidget', $.proxy(this._touchMove, this)); - $document.bind('mouseup.mousewidget', $.proxy(this._mouseUp, this)); - $document.bind('touchend.mousewidget', $.proxy(this._touchEnd, this)); - if (this.mouse_delay) { - return this._startMouseDelayTimer(); - } - }; - - MouseWidget.prototype._startMouseDelayTimer = function() { - var _this = this; - if (this._mouse_delay_timer) { - clearTimeout(this._mouse_delay_timer); - } - this._mouse_delay_timer = setTimeout(function() { - return _this._is_mouse_delay_met = true; - }, this.mouse_delay); - return this._is_mouse_delay_met = false; - }; - - MouseWidget.prototype._mouseMove = function(e) { - return this._handleMouseMove(e, this._getPositionInfo(e)); - }; - - MouseWidget.prototype._handleMouseMove = function(e, position_info) { - if (this.is_mouse_started) { - this._mouseDrag(position_info); - return e.preventDefault(); - } - if (this.mouse_delay && !this._is_mouse_delay_met) { - return true; - } - this.is_mouse_started = this._mouseStart(this.mouse_down_info) !== false; - if (this.is_mouse_started) { - this._mouseDrag(position_info); - } else { - this._handleMouseUp(position_info); - } - return !this.is_mouse_started; - }; - - MouseWidget.prototype._getPositionInfo = function(e) { - return { - page_x: e.pageX, - page_y: e.pageY, - target: e.target, - original_event: e - }; - }; - - MouseWidget.prototype._mouseUp = function(e) { - return this._handleMouseUp(this._getPositionInfo(e)); - }; - - MouseWidget.prototype._handleMouseUp = function(position_info) { - var $document; - $document = $(document); - $document.unbind('mousemove.mousewidget'); - $document.unbind('touchmove.mousewidget'); - $document.unbind('mouseup.mousewidget'); - $document.unbind('touchend.mousewidget'); - if (this.is_mouse_started) { - this.is_mouse_started = false; - this._mouseStop(position_info); - } - }; - - MouseWidget.prototype._mouseCapture = function(position_info) { - return true; - }; - - MouseWidget.prototype._mouseStart = function(position_info) { - return null; - }; - - MouseWidget.prototype._mouseDrag = function(position_info) { - return null; - }; - - MouseWidget.prototype._mouseStop = function(position_info) { - return null; - }; - - MouseWidget.prototype.setMouseDelay = function(mouse_delay) { - return this.mouse_delay = mouse_delay; - }; - - MouseWidget.prototype._touchStart = function(e) { - var touch; - if (e.originalEvent.touches.length > 1) { - return; - } - touch = e.originalEvent.changedTouches[0]; - return this._handleMouseDown(e, this._getPositionInfo(touch)); - }; - - MouseWidget.prototype._touchMove = function(e) { - var touch; - if (e.originalEvent.touches.length > 1) { - return; - } - touch = e.originalEvent.changedTouches[0]; - return this._handleMouseMove(e, this._getPositionInfo(touch)); - }; - - MouseWidget.prototype._touchEnd = function(e) { - var touch; - if (e.originalEvent.touches.length > 1) { - return; - } - touch = e.originalEvent.changedTouches[0]; - return this._handleMouseUp(this._getPositionInfo(touch)); - }; - - return MouseWidget; - - })(SimpleWidget); - - this.Tree = {}; - - $ = this.jQuery; - - Position = { - getName: function(position) { - return Position.strings[position - 1]; - }, - nameToIndex: function(name) { - var i, _i, _ref1; - for (i = _i = 1, _ref1 = Position.strings.length; 1 <= _ref1 ? _i <= _ref1 : _i >= _ref1; i = 1 <= _ref1 ? ++_i : --_i) { - if (Position.strings[i - 1] === name) { - return i; - } - } - return 0; - } - }; - - Position.BEFORE = 1; - - Position.AFTER = 2; - - Position.INSIDE = 3; - - Position.NONE = 4; - - Position.strings = ['before', 'after', 'inside', 'none']; - - this.Tree.Position = Position; - - Node = (function() { - function Node(o, is_root, node_class) { - if (is_root == null) { - is_root = false; - } - if (node_class == null) { - node_class = Node; - } - this.setData(o); - this.children = []; - this.parent = null; - if (is_root) { - this.id_mapping = {}; - this.tree = this; - this.node_class = node_class; - } - } - - Node.prototype.setData = function(o) { - var key, value, _results; - if (typeof o !== 'object') { - return this.name = o; - } else { - _results = []; - for (key in o) { - value = o[key]; - if (key === 'label') { - _results.push(this.name = value); - } else { - _results.push(this[key] = value); - } - } - return _results; - } - }; - - Node.prototype.initFromData = function(data) { - var addChildren, addNode, - _this = this; - addNode = function(node_data) { - _this.setData(node_data); - if (node_data.children) { - return addChildren(node_data.children); - } - }; - addChildren = function(children_data) { - var child, node, _i, _len; - for (_i = 0, _len = children_data.length; _i < _len; _i++) { - child = children_data[_i]; - node = new _this.tree.node_class(''); - node.initFromData(child); - _this.addChild(node); - } - return null; - }; - addNode(data); - return null; - }; - - /* - Create tree from data. - - Structure of data is: - [ - { - label: 'node1', - children: [ - { label: 'child1' }, - { label: 'child2' } - ] - }, - { - label: 'node2' - } - ] - */ - - - Node.prototype.loadFromData = function(data) { - var node, o, _i, _len; - this.removeChildren(); - for (_i = 0, _len = data.length; _i < _len; _i++) { - o = data[_i]; - node = new this.tree.node_class(o); - this.addChild(node); - if (typeof o === 'object' && o.children) { - node.loadFromData(o.children); - } - } - return null; - }; - - /* - Add child. - - tree.addChild( - new Node('child1') - ); - */ - - - Node.prototype.addChild = function(node) { - this.children.push(node); - return node._setParent(this); - }; - - /* - Add child at position. Index starts at 0. - - tree.addChildAtPosition( - new Node('abc'), - 1 - ); - */ - - - Node.prototype.addChildAtPosition = function(node, index) { - this.children.splice(index, 0, node); - return node._setParent(this); - }; - - Node.prototype._setParent = function(parent) { - this.parent = parent; - this.tree = parent.tree; - return this.tree.addNodeToIndex(this); - }; - - /* - Remove child. This also removes the children of the node. - - tree.removeChild(tree.children[0]); - */ - - - Node.prototype.removeChild = function(node) { - node.removeChildren(); - return this._removeChild(node); - }; - - Node.prototype._removeChild = function(node) { - this.children.splice(this.getChildIndex(node), 1); - return this.tree.removeNodeFromIndex(node); - }; - - /* - Get child index. - - var index = getChildIndex(node); - */ - - - Node.prototype.getChildIndex = function(node) { - return $.inArray(node, this.children); - }; - - /* - Does the tree have children? - - if (tree.hasChildren()) { - // - } - */ - - - Node.prototype.hasChildren = function() { - return this.children.length !== 0; - }; - - Node.prototype.isFolder = function() { - return this.hasChildren() || this.load_on_demand; - }; - - /* - Iterate over all the nodes in the tree. - - Calls callback with (node, level). - - The callback must return true to continue the iteration on current node. - - tree.iterate( - function(node, level) { - console.log(node.name); - - // stop iteration after level 2 - return (level <= 2); - } - ); - */ - - - Node.prototype.iterate = function(callback) { - var _iterate, - _this = this; - _iterate = function(node, level) { - var child, result, _i, _len, _ref1; - if (node.children) { - _ref1 = node.children; - for (_i = 0, _len = _ref1.length; _i < _len; _i++) { - child = _ref1[_i]; - result = callback(child, level); - if (_this.hasChildren() && result) { - _iterate(child, level + 1); - } - } - return null; - } - }; - _iterate(this, 0); - return null; - }; - - /* - Move node relative to another node. - - Argument position: Position.BEFORE, Position.AFTER or Position.Inside - - // move node1 after node2 - tree.moveNode(node1, node2, Position.AFTER); - */ - - - Node.prototype.moveNode = function(moved_node, target_node, position) { - if (moved_node.isParentOf(target_node)) { - return; - } - moved_node.parent._removeChild(moved_node); - if (position === Position.AFTER) { - return target_node.parent.addChildAtPosition(moved_node, target_node.parent.getChildIndex(target_node) + 1); - } else if (position === Position.BEFORE) { - return target_node.parent.addChildAtPosition(moved_node, target_node.parent.getChildIndex(target_node)); - } else if (position === Position.INSIDE) { - return target_node.addChildAtPosition(moved_node, 0); - } - }; - - /* - Get the tree as data. - */ - - - Node.prototype.getData = function() { - var getDataFromNodes, - _this = this; - getDataFromNodes = function(nodes) { - var data, k, node, tmp_node, v, _i, _len; - data = []; - for (_i = 0, _len = nodes.length; _i < _len; _i++) { - node = nodes[_i]; - tmp_node = {}; - for (k in node) { - v = node[k]; - if ((k !== 'parent' && k !== 'children' && k !== 'element' && k !== 'tree') && Object.prototype.hasOwnProperty.call(node, k)) { - tmp_node[k] = v; - } - } - if (node.hasChildren()) { - tmp_node.children = getDataFromNodes(node.children); - } - data.push(tmp_node); - } - return data; - }; - return getDataFromNodes(this.children); - }; - - Node.prototype.getNodeByName = function(name) { - var result; - result = null; - this.iterate(function(node) { - if (node.name === name) { - result = node; - return false; - } else { - return true; - } - }); - return result; - }; - - Node.prototype.addAfter = function(node_info) { - var child_index, node; - if (!this.parent) { - return null; - } else { - node = new this.tree.node_class(node_info); - child_index = this.parent.getChildIndex(this); - this.parent.addChildAtPosition(node, child_index + 1); - return node; - } - }; - - Node.prototype.addBefore = function(node_info) { - var child_index, node; - if (!this.parent) { - return null; - } else { - node = new this.tree.node_class(node_info); - child_index = this.parent.getChildIndex(this); - this.parent.addChildAtPosition(node, child_index); - return node; - } - }; - - Node.prototype.addParent = function(node_info) { - var child, new_parent, original_parent, _i, _len, _ref1; - if (!this.parent) { - return null; - } else { - new_parent = new this.tree.node_class(node_info); - new_parent._setParent(this.tree); - original_parent = this.parent; - _ref1 = original_parent.children; - for (_i = 0, _len = _ref1.length; _i < _len; _i++) { - child = _ref1[_i]; - new_parent.addChild(child); - } - original_parent.children = []; - original_parent.addChild(new_parent); - return new_parent; - } - }; - - Node.prototype.remove = function() { - if (this.parent) { - this.parent.removeChild(this); - return this.parent = null; - } - }; - - Node.prototype.append = function(node_info) { - var node; - node = new this.tree.node_class(node_info); - this.addChild(node); - return node; - }; - - Node.prototype.prepend = function(node_info) { - var node; - node = new this.tree.node_class(node_info); - this.addChildAtPosition(node, 0); - return node; - }; - - Node.prototype.isParentOf = function(node) { - var parent; - parent = node.parent; - while (parent) { - if (parent === this) { - return true; - } - parent = parent.parent; - } - return false; - }; - - Node.prototype.getLevel = function() { - var level, node; - level = 0; - node = this; - while (node.parent) { - level += 1; - node = node.parent; - } - return level; - }; - - Node.prototype.getNodeById = function(node_id) { - return this.id_mapping[node_id]; - }; - - Node.prototype.addNodeToIndex = function(node) { - if (node.id != null) { - return this.id_mapping[node.id] = node; - } - }; - - Node.prototype.removeNodeFromIndex = function(node) { - if (node.id != null) { - return delete this.id_mapping[node.id]; - } - }; - - Node.prototype.removeChildren = function() { - var _this = this; - this.iterate(function(child) { - _this.tree.removeNodeFromIndex(child); - return true; - }); - return this.children = []; - }; - - Node.prototype.getPreviousSibling = function() { - var previous_index; - if (!this.parent) { - return null; - } else { - previous_index = this.parent.getChildIndex(this) - 1; - if (previous_index >= 0) { - return this.parent.children[previous_index]; - } else { - return null; - } - } - }; - - Node.prototype.getNextSibling = function() { - var next_index; - if (!this.parent) { - return null; - } else { - next_index = this.parent.getChildIndex(this) + 1; - if (next_index < this.parent.children.length) { - return this.parent.children[next_index]; - } else { - return null; - } - } - }; - - return Node; - - })(); - - this.Tree.Node = Node; - - /* - Copyright 2013 Marco Braak - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - */ - - - JqTreeWidget = (function(_super) { - __extends(JqTreeWidget, _super); - - function JqTreeWidget() { - _ref1 = JqTreeWidget.__super__.constructor.apply(this, arguments); - return _ref1; - } - - JqTreeWidget.prototype.defaults = { - autoOpen: false, - saveState: false, - dragAndDrop: false, - selectable: true, - useContextMenu: true, - onCanSelectNode: null, - onSetStateFromStorage: null, - onGetStateFromStorage: null, - onCreateLi: null, - onIsMoveHandle: null, - onCanMove: null, - onCanMoveTo: null, - onLoadFailed: null, - autoEscape: true, - dataUrl: null, - closedIcon: '►', - openedIcon: '▼', - slide: true, - nodeClass: Node, - dataFilter: null, - keyboardSupport: true - }; - - JqTreeWidget.prototype.toggle = function(node, slide) { - if (slide == null) { - slide = true; - } - if (node.is_open) { - return this.closeNode(node, slide); - } else { - return this.openNode(node, slide); - } - }; - - JqTreeWidget.prototype.getTree = function() { - return this.tree; - }; - - JqTreeWidget.prototype.selectNode = function(node) { - return this._selectNode(node, false); - }; - - JqTreeWidget.prototype._selectNode = function(node, must_toggle) { - var canSelect, deselected_node, openParents, saveState, - _this = this; - if (must_toggle == null) { - must_toggle = false; - } - if (!this.select_node_handler) { - return; - } - canSelect = function() { - if (_this.options.onCanSelectNode) { - return _this.options.selectable && _this.options.onCanSelectNode(node); - } else { - return _this.options.selectable; - } - }; - openParents = function() { - var parent; - parent = node.parent; - if (parent && parent.parent && !parent.is_open) { - return _this.openNode(parent, false); - } - }; - saveState = function() { - if (_this.options.saveState) { - return _this.save_state_handler.saveState(); - } - }; - if (!node) { - this._deselectCurrentNode(); - saveState(); - return; - } - if (!canSelect()) { - return; - } - if (this.select_node_handler.isNodeSelected(node)) { - if (must_toggle) { - this._deselectCurrentNode(); - this._triggerEvent('tree.select', { - node: null, - previous_node: node - }); - } - } else { - deselected_node = this.getSelectedNode(); - this._deselectCurrentNode(); - this.addToSelection(node); - this._triggerEvent('tree.select', { - node: node, - deselected_node: deselected_node - }); - openParents(); - } - return saveState(); - }; - - JqTreeWidget.prototype.getSelectedNode = function() { - return this.select_node_handler.getSelectedNode(); - }; - - JqTreeWidget.prototype.toJson = function() { - return JSON.stringify(this.tree.getData()); - }; - - JqTreeWidget.prototype.loadData = function(data, parent_node) { - return this._loadData(data, parent_node); - }; - - JqTreeWidget.prototype.loadDataFromUrl = function(url, parent_node, on_finished) { - if ($.type(url) !== 'string') { - on_finished = parent_node; - parent_node = url; - url = null; - } - return this._loadDataFromUrl(url, parent_node, on_finished); - }; - - JqTreeWidget.prototype._loadDataFromUrl = function(url_info, parent_node, on_finished) { - var $el, addLoadingClass, parseUrlInfo, removeLoadingClass, - _this = this; - $el = null; - addLoadingClass = function() { - var folder_element; - if (!parent_node) { - $el = _this.element; - } else { - folder_element = new FolderElement(parent_node, _this); - $el = folder_element.getLi(); - } - return $el.addClass('jqtree-loading'); - }; - removeLoadingClass = function() { - if ($el) { - return $el.removeClass('jqtree-loading'); - } - }; - parseUrlInfo = function() { - if ($.type(url_info) === 'string') { - url_info = { - url: url_info - }; - } - if (!url_info.method) { - return url_info.method = 'get'; - } - }; - addLoadingClass(); - if (!url_info) { - url_info = this._getDataUrlInfo(parent_node); - } - parseUrlInfo(); - return $.ajax({ - url: url_info.url, - data: url_info.data, - type: url_info.method.toUpperCase(), - cache: false, - dataType: 'json', - success: function(response) { - var data; - if ($.isArray(response) || typeof response === 'object') { - data = response; - } else { - data = $.parseJSON(response); - } - if (_this.options.dataFilter) { - data = _this.options.dataFilter(data); - } - removeLoadingClass(); - _this._loadData(data, parent_node); - if (on_finished && $.isFunction(on_finished)) { - return on_finished(); - } - }, - error: function(response) { - removeLoadingClass(); - if (_this.options.onLoadFailed) { - return _this.options.onLoadFailed(response); - } - } - }); - }; - - JqTreeWidget.prototype._loadData = function(data, parent_node) { - var n, selected_nodes_under_parent, _i, _len; - this._triggerEvent('tree.load_data', { - tree_data: data - }); - if (!parent_node) { - this._initTree(data); - } else { - selected_nodes_under_parent = this.select_node_handler.getSelectedNodes(parent_node); - for (_i = 0, _len = selected_nodes_under_parent.length; _i < _len; _i++) { - n = selected_nodes_under_parent[_i]; - this.select_node_handler.removeFromSelection(n); - } - parent_node.loadFromData(data); - parent_node.load_on_demand = false; - this._refreshElements(parent_node.parent); - } - if (this.is_dragging) { - return this.dnd_handler.refreshHitAreas(); - } - }; - - JqTreeWidget.prototype.getNodeById = function(node_id) { - return this.tree.getNodeById(node_id); - }; - - JqTreeWidget.prototype.getNodeByName = function(name) { - return this.tree.getNodeByName(name); - }; - - JqTreeWidget.prototype.openNode = function(node, slide) { - if (slide == null) { - slide = true; - } - return this._openNode(node, slide); - }; - - JqTreeWidget.prototype._openNode = function(node, slide, on_finished) { - var doOpenNode, parent, - _this = this; - if (slide == null) { - slide = true; - } - doOpenNode = function(_node, _slide, _on_finished) { - var folder_element; - folder_element = new FolderElement(_node, _this); - return folder_element.open(_on_finished, _slide); - }; - if (node.isFolder()) { - if (node.load_on_demand) { - return this._loadFolderOnDemand(node, slide, on_finished); - } else { - parent = node.parent; - while (parent && !parent.is_open) { - if (parent.parent) { - doOpenNode(parent, false, null); - } - parent = parent.parent; - } - doOpenNode(node, slide, on_finished); - return this._saveState(); - } - } - }; - - JqTreeWidget.prototype._loadFolderOnDemand = function(node, slide, on_finished) { - var _this = this; - if (slide == null) { - slide = true; - } - return this._loadDataFromUrl(null, node, function() { - return _this._openNode(node, slide, on_finished); - }); - }; - - JqTreeWidget.prototype.closeNode = function(node, slide) { - if (slide == null) { - slide = true; - } - if (node.isFolder()) { - new FolderElement(node, this).close(slide); - return this._saveState(); - } - }; - - JqTreeWidget.prototype.isDragging = function() { - return this.is_dragging; - }; - - JqTreeWidget.prototype.refreshHitAreas = function() { - return this.dnd_handler.refreshHitAreas(); - }; - - JqTreeWidget.prototype.addNodeAfter = function(new_node_info, existing_node) { - var new_node; - new_node = existing_node.addAfter(new_node_info); - this._refreshElements(existing_node.parent); - return new_node; - }; - - JqTreeWidget.prototype.addNodeBefore = function(new_node_info, existing_node) { - var new_node; - new_node = existing_node.addBefore(new_node_info); - this._refreshElements(existing_node.parent); - return new_node; - }; - - JqTreeWidget.prototype.addParentNode = function(new_node_info, existing_node) { - var new_node; - new_node = existing_node.addParent(new_node_info); - this._refreshElements(new_node.parent); - return new_node; - }; - - JqTreeWidget.prototype.removeNode = function(node) { - var parent; - parent = node.parent; - if (parent) { - this.select_node_handler.removeFromSelection(node, true); - node.remove(); - return this._refreshElements(parent.parent); - } - }; - - JqTreeWidget.prototype.appendNode = function(new_node_info, parent_node) { - var is_already_folder_node, node; - if (!parent_node) { - parent_node = this.tree; - } - is_already_folder_node = parent_node.isFolder(); - node = parent_node.append(new_node_info); - if (is_already_folder_node) { - this._refreshElements(parent_node); - } else { - this._refreshElements(parent_node.parent); - } - return node; - }; - - JqTreeWidget.prototype.prependNode = function(new_node_info, parent_node) { - var node; - if (!parent_node) { - parent_node = this.tree; - } - node = parent_node.prepend(new_node_info); - this._refreshElements(parent_node); - return node; - }; - - JqTreeWidget.prototype.updateNode = function(node, data) { - var id_is_changed; - id_is_changed = data.id && data.id !== node.id; - if (id_is_changed) { - this.tree.removeNodeFromIndex(node); - } - node.setData(data); - if (id_is_changed) { - this.tree.addNodeToIndex(node); - } - this._refreshElements(node.parent); - return this._selectCurrentNode(); - }; - - JqTreeWidget.prototype.moveNode = function(node, target_node, position) { - var position_index; - position_index = Position.nameToIndex(position); - this.tree.moveNode(node, target_node, position_index); - return this._refreshElements(); - }; - - JqTreeWidget.prototype.getStateFromStorage = function() { - return this.save_state_handler.getStateFromStorage(); - }; - - JqTreeWidget.prototype.addToSelection = function(node) { - this.select_node_handler.addToSelection(node); - return this._getNodeElementForNode(node).select(); - }; - - JqTreeWidget.prototype.getSelectedNodes = function() { - return this.select_node_handler.getSelectedNodes(); - }; - - JqTreeWidget.prototype.isNodeSelected = function(node) { - return this.select_node_handler.isNodeSelected(node); - }; - - JqTreeWidget.prototype.removeFromSelection = function(node) { - this.select_node_handler.removeFromSelection(node); - return this._getNodeElementForNode(node).deselect(); - }; - - JqTreeWidget.prototype.scrollToNode = function(node) { - var $element, top; - $element = $(node.element); - top = $element.offset().top - this.$el.offset().top; - return this.scroll_handler.scrollTo(top); - }; - - JqTreeWidget.prototype.getState = function() { - return this.save_state_handler.getState(); - }; - - JqTreeWidget.prototype.setState = function(state) { - this.save_state_handler.setState(state); - return this._refreshElements(); - }; - - JqTreeWidget.prototype._init = function() { - JqTreeWidget.__super__._init.call(this); - this.element = this.$el; - this.mouse_delay = 300; - this.is_initialized = false; - if (typeof SaveStateHandler !== "undefined" && SaveStateHandler !== null) { - this.save_state_handler = new SaveStateHandler(this); - } else { - this.options.saveState = false; - } - if (typeof SelectNodeHandler !== "undefined" && SelectNodeHandler !== null) { - this.select_node_handler = new SelectNodeHandler(this); - } - if (typeof DragAndDropHandler !== "undefined" && DragAndDropHandler !== null) { - this.dnd_handler = new DragAndDropHandler(this); - } else { - this.options.dragAndDrop = false; - } - if (typeof ScrollHandler !== "undefined" && ScrollHandler !== null) { - this.scroll_handler = new ScrollHandler(this); - } - if ((typeof KeyHandler !== "undefined" && KeyHandler !== null) && (typeof SelectNodeHandler !== "undefined" && SelectNodeHandler !== null)) { - this.key_handler = new KeyHandler(this); - } - this._initData(); - this.element.click($.proxy(this._click, this)); - this.element.dblclick($.proxy(this._dblclick, this)); - if (this.options.useContextMenu) { - return this.element.bind('contextmenu', $.proxy(this._contextmenu, this)); - } - }; - - JqTreeWidget.prototype._deinit = function() { - this.element.empty(); - this.element.unbind(); - this.key_handler.deinit(); - this.tree = null; - return JqTreeWidget.__super__._deinit.call(this); - }; - - JqTreeWidget.prototype._initData = function() { - if (this.options.data) { - return this._loadData(this.options.data); - } else { - return this._loadDataFromUrl(this._getDataUrlInfo()); - } - }; - - JqTreeWidget.prototype._getDataUrlInfo = function(node) { - var data_url, getUrlFromString, - _this = this; - data_url = this.options.dataUrl || this.element.data('url'); - getUrlFromString = function() { - var data, selected_node_id, url_info; - url_info = { - url: data_url - }; - if (node && node.id) { - data = { - node: node.id - }; - url_info['data'] = data; - } else { - selected_node_id = _this._getNodeIdToBeSelected(); - if (selected_node_id) { - data = { - selected_node: selected_node_id - }; - url_info['data'] = data; - } - } - return url_info; - }; - if ($.isFunction(data_url)) { - return data_url(node); - } else if ($.type(data_url) === 'string') { - return getUrlFromString(); - } else { - return data_url; - } - }; - - JqTreeWidget.prototype._getNodeIdToBeSelected = function() { - if (this.options.saveState) { - return this.save_state_handler.getNodeIdToBeSelected(); - } else { - return null; - } - }; - - JqTreeWidget.prototype._initTree = function(data) { - this.tree = new this.options.nodeClass(null, true, this.options.nodeClass); - if (this.select_node_handler) { - this.select_node_handler.clear(); - } - this.tree.loadFromData(data); - this._openNodes(); - this._refreshElements(); - if (!this.is_initialized) { - this.is_initialized = true; - return this._triggerEvent('tree.init'); - } - }; - - JqTreeWidget.prototype._openNodes = function() { - var max_level; - if (this.options.saveState) { - if (this.save_state_handler.restoreState()) { - return; - } - } - if (this.options.autoOpen === false) { - return; - } else if (this.options.autoOpen === true) { - max_level = -1; - } else { - max_level = parseInt(this.options.autoOpen); - } - return this.tree.iterate(function(node, level) { - if (node.hasChildren()) { - node.is_open = true; - } - return level !== max_level; - }); - }; - - JqTreeWidget.prototype._refreshElements = function(from_node) { - var $element, createFolderLi, createLi, createNodeLi, createUl, doCreateDomElements, escapeIfNecessary, is_root_node, node_element, - _this = this; - if (from_node == null) { - from_node = null; - } - escapeIfNecessary = function(value) { - if (_this.options.autoEscape) { - return html_escape(value); - } else { - return value; - } - }; - createUl = function(is_root_node) { - var class_string; - if (is_root_node) { - class_string = 'jqtree-tree'; - } else { - class_string = ''; - } - return $("
      "); - }; - createLi = function(node) { - var $li; - if (node.isFolder()) { - $li = createFolderLi(node); - } else { - $li = createNodeLi(node); - } - if (_this.options.onCreateLi) { - _this.options.onCreateLi(node, $li); - } - return $li; - }; - createNodeLi = function(node) { - var class_string, escaped_name, li_classes; - li_classes = ['jqtree_common']; - if (_this.select_node_handler && _this.select_node_handler.isNodeSelected(node)) { - li_classes.push('jqtree-selected'); - } - class_string = li_classes.join(' '); - escaped_name = escapeIfNecessary(node.name); - return $("
    • " + escaped_name + "
    • "); - }; - createFolderLi = function(node) { - var button_char, button_classes, escaped_name, folder_classes, getButtonClasses, getFolderClasses; - getButtonClasses = function() { - var classes; - classes = ['jqtree-toggler']; - if (!node.is_open) { - classes.push('jqtree-closed'); - } - return classes.join(' '); - }; - getFolderClasses = function() { - var classes; - classes = ['jqtree-folder']; - if (!node.is_open) { - classes.push('jqtree-closed'); - } - if (_this.select_node_handler && _this.select_node_handler.isNodeSelected(node)) { - classes.push('jqtree-selected'); - } - return classes.join(' '); - }; - button_classes = getButtonClasses(); - folder_classes = getFolderClasses(); - escaped_name = escapeIfNecessary(node.name); - if (node.is_open) { - button_char = _this.options.openedIcon; - } else { - button_char = _this.options.closedIcon; - } - return $("
    • " + button_char + "" + escaped_name + "
    • "); - }; - doCreateDomElements = function($element, children, is_root_node, is_open) { - var $li, $ul, child, _i, _len; - $ul = createUl(is_root_node); - $element.append($ul); - for (_i = 0, _len = children.length; _i < _len; _i++) { - child = children[_i]; - $li = createLi(child); - $ul.append($li); - child.element = $li[0]; - $li.data('node', child); - if (child.hasChildren()) { - doCreateDomElements($li, child.children, false, child.is_open); - } - } - return null; - }; - if (from_node && from_node.parent) { - is_root_node = false; - node_element = this._getNodeElementForNode(from_node); - node_element.getUl().remove(); - $element = node_element.$element; - } else { - from_node = this.tree; - $element = this.element; - $element.empty(); - is_root_node = true; - } - doCreateDomElements($element, from_node.children, is_root_node, is_root_node); - return this._triggerEvent('tree.refresh'); - }; - - JqTreeWidget.prototype._click = function(e) { - var click_target, event, node; - click_target = this._getClickTarget(e.target); - if (click_target) { - if (click_target.type === 'button') { - this.toggle(click_target.node, this.options.slide); - e.preventDefault(); - return e.stopPropagation(); - } else if (click_target.type === 'label') { - node = click_target.node; - event = this._triggerEvent('tree.click', { - node: node - }); - if (!event.isDefaultPrevented()) { - return this._selectNode(node, true); - } - } - } - }; - - JqTreeWidget.prototype._dblclick = function(e) { - var click_target; - click_target = this._getClickTarget(e.target); - if (click_target && click_target.type === 'label') { - return this._triggerEvent('tree.dblclick', { - node: click_target.node - }); - } - }; - - JqTreeWidget.prototype._getClickTarget = function(element) { - var $button, $el, $target, node; - $target = $(element); - $button = $target.closest('.jqtree-toggler'); - if ($button.length) { - node = this._getNode($button); - if (node) { - return { - type: 'button', - node: node - }; - } - } else { - $el = $target.closest('.jqtree-element'); - if ($el.length) { - node = this._getNode($el); - if (node) { - return { - type: 'label', - node: node - }; - } - } - } - return null; - }; - - JqTreeWidget.prototype._getNode = function($element) { - var $li; - $li = $element.closest('li'); - if ($li.length === 0) { - return null; - } else { - return $li.data('node'); - } - }; - - JqTreeWidget.prototype._getNodeElementForNode = function(node) { - if (node.isFolder()) { - return new FolderElement(node, this); - } else { - return new NodeElement(node, this); - } - }; - - JqTreeWidget.prototype._getNodeElement = function($element) { - var node; - node = this._getNode($element); - if (node) { - return this._getNodeElementForNode(node); - } else { - return null; - } - }; - - JqTreeWidget.prototype._contextmenu = function(e) { - var $div, node; - $div = $(e.target).closest('ul.jqtree-tree .jqtree-element'); - if ($div.length) { - node = this._getNode($div); - if (node) { - e.preventDefault(); - e.stopPropagation(); - this._triggerEvent('tree.contextmenu', { - node: node, - click_event: e - }); - return false; - } - } - }; - - JqTreeWidget.prototype._saveState = function() { - if (this.options.saveState) { - return this.save_state_handler.saveState(); - } - }; - - JqTreeWidget.prototype._mouseCapture = function(position_info) { - if (this.options.dragAndDrop) { - return this.dnd_handler.mouseCapture(position_info); - } else { - return false; - } - }; - - JqTreeWidget.prototype._mouseStart = function(position_info) { - if (this.options.dragAndDrop) { - return this.dnd_handler.mouseStart(position_info); - } else { - return false; - } - }; - - JqTreeWidget.prototype._mouseDrag = function(position_info) { - var result; - if (this.options.dragAndDrop) { - result = this.dnd_handler.mouseDrag(position_info); - if (this.scroll_handler) { - this.scroll_handler.checkScrolling(); - } - return result; - } else { - return false; - } - }; - - JqTreeWidget.prototype._mouseStop = function(position_info) { - if (this.options.dragAndDrop) { - return this.dnd_handler.mouseStop(position_info); - } else { - return false; - } - }; - - JqTreeWidget.prototype._triggerEvent = function(event_name, values) { - var event; - event = $.Event(event_name); - $.extend(event, values); - this.element.trigger(event); - return event; - }; - - JqTreeWidget.prototype.testGenerateHitAreas = function(moving_node) { - this.dnd_handler.current_item = this._getNodeElementForNode(moving_node); - this.dnd_handler.generateHitAreas(); - return this.dnd_handler.hit_areas; - }; - - JqTreeWidget.prototype._selectCurrentNode = function() { - var node, node_element; - node = this.getSelectedNode(); - if (node) { - node_element = this._getNodeElementForNode(node); - if (node_element) { - return node_element.select(); - } - } - }; - - JqTreeWidget.prototype._deselectCurrentNode = function() { - var node; - node = this.getSelectedNode(); - if (node) { - return this.removeFromSelection(node); - } - }; - - return JqTreeWidget; - - })(MouseWidget); - - SimpleWidget.register(JqTreeWidget, 'tree'); - - NodeElement = (function() { - function NodeElement(node, tree_widget) { - this.init(node, tree_widget); - } - - NodeElement.prototype.init = function(node, tree_widget) { - this.node = node; - this.tree_widget = tree_widget; - return this.$element = $(node.element); - }; - - NodeElement.prototype.getUl = function() { - return this.$element.children('ul:first'); - }; - - NodeElement.prototype.getSpan = function() { - return this.$element.children('.jqtree-element').find('span.jqtree-title'); - }; - - NodeElement.prototype.getLi = function() { - return this.$element; - }; - - NodeElement.prototype.addDropHint = function(position) { - if (position === Position.INSIDE) { - return new BorderDropHint(this.$element); - } else { - return new GhostDropHint(this.node, this.$element, position); - } - }; - - NodeElement.prototype.select = function() { - return this.getLi().addClass('jqtree-selected'); - }; - - NodeElement.prototype.deselect = function() { - return this.getLi().removeClass('jqtree-selected'); - }; - - return NodeElement; - - })(); - - FolderElement = (function(_super) { - __extends(FolderElement, _super); - - function FolderElement() { - _ref2 = FolderElement.__super__.constructor.apply(this, arguments); - return _ref2; - } - - FolderElement.prototype.open = function(on_finished, slide) { - var $button, doOpen, - _this = this; - if (slide == null) { - slide = true; - } - if (!this.node.is_open) { - this.node.is_open = true; - $button = this.getButton(); - $button.removeClass('jqtree-closed'); - $button.html(this.tree_widget.options.openedIcon); - doOpen = function() { - _this.getLi().removeClass('jqtree-closed'); - if (on_finished) { - on_finished(); - } - return _this.tree_widget._triggerEvent('tree.open', { - node: _this.node - }); - }; - if (slide) { - return this.getUl().slideDown('fast', doOpen); - } else { - this.getUl().show(); - return doOpen(); - } - } - }; - - FolderElement.prototype.close = function(slide) { - var $button, doClose, - _this = this; - if (slide == null) { - slide = true; - } - if (this.node.is_open) { - this.node.is_open = false; - $button = this.getButton(); - $button.addClass('jqtree-closed'); - $button.html(this.tree_widget.options.closedIcon); - doClose = function() { - _this.getLi().addClass('jqtree-closed'); - return _this.tree_widget._triggerEvent('tree.close', { - node: _this.node - }); - }; - if (slide) { - return this.getUl().slideUp('fast', doClose); - } else { - this.getUl().hide(); - return doClose(); - } - } - }; - - FolderElement.prototype.getButton = function() { - return this.$element.children('.jqtree-element').find('a.jqtree-toggler'); - }; - - FolderElement.prototype.addDropHint = function(position) { - if (!this.node.is_open && position === Position.INSIDE) { - return new BorderDropHint(this.$element); - } else { - return new GhostDropHint(this.node, this.$element, position); - } - }; - - return FolderElement; - - })(NodeElement); - - html_escape = function(string) { - return ('' + string).replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"').replace(/'/g, ''').replace(/\//g, '/'); - }; - - _indexOf = function(array, item) { - var i, value, _i, _len; - for (i = _i = 0, _len = array.length; _i < _len; i = ++_i) { - value = array[i]; - if (value === item) { - return i; - } - } - return -1; - }; - - indexOf = function(array, item) { - if (array.indexOf) { - return array.indexOf(item); - } else { - return _indexOf(array, item); - } - }; - - this.Tree.indexOf = indexOf; - - this.Tree._indexOf = _indexOf; - - if (!((this.JSON != null) && (this.JSON.stringify != null) && typeof this.JSON.stringify === 'function')) { - json_escapable = /[\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g; - json_meta = { - '\b': '\\b', - '\t': '\\t', - '\n': '\\n', - '\f': '\\f', - '\r': '\\r', - '"': '\\"', - '\\': '\\\\' - }; - json_quote = function(string) { - json_escapable.lastIndex = 0; - if (json_escapable.test(string)) { - return '"' + string.replace(json_escapable, function(a) { - var c; - c = json_meta[a]; - return (typeof c === 'string' ? c : '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4)); - }) + '"'; - } else { - return '"' + string + '"'; - } - }; - json_str = function(key, holder) { - var i, k, partial, v, value, _i, _len; - value = holder[key]; - switch (typeof value) { - case 'string': - return json_quote(value); - case 'number': - if (isFinite(value)) { - return String(value); - } else { - return 'null'; - } - case 'boolean': - case 'null': - return String(value); - case 'object': - if (!value) { - return 'null'; - } - partial = []; - if (Object.prototype.toString.apply(value) === '[object Array]') { - for (i = _i = 0, _len = value.length; _i < _len; i = ++_i) { - v = value[i]; - partial[i] = json_str(i, value) || 'null'; - } - return (partial.length === 0 ? '[]' : '[' + partial.join(',') + ']'); - } - for (k in value) { - if (Object.prototype.hasOwnProperty.call(value, k)) { - v = json_str(k, value); - if (v) { - partial.push(json_quote(k) + ':' + v); - } - } - } - return (partial.length === 0 ? '{}' : '{' + partial.join(',') + '}'); - } - }; - if (this.JSON == null) { - this.JSON = {}; - } - this.JSON.stringify = function(value) { - return json_str('', { - '': value - }); - }; - } - - SaveStateHandler = (function() { - function SaveStateHandler(tree_widget) { - this.tree_widget = tree_widget; - } - - SaveStateHandler.prototype.saveState = function() { - var state; - state = JSON.stringify(this.getState()); - if (this.tree_widget.options.onSetStateFromStorage) { - return this.tree_widget.options.onSetStateFromStorage(state); - } else if (this.supportsLocalStorage()) { - return localStorage.setItem(this.getCookieName(), state); - } else if ($.cookie) { - $.cookie.raw = true; - return $.cookie(this.getCookieName(), state, { - path: '/' - }); - } - }; - - SaveStateHandler.prototype.restoreState = function() { - var state; - state = this.getStateFromStorage(); - if (state) { - this.setState($.parseJSON(state)); - return true; - } else { - return false; - } - }; - - SaveStateHandler.prototype.getStateFromStorage = function() { - if (this.tree_widget.options.onGetStateFromStorage) { - return this.tree_widget.options.onGetStateFromStorage(); - } else if (this.supportsLocalStorage()) { - return localStorage.getItem(this.getCookieName()); - } else if ($.cookie) { - $.cookie.raw = true; - return $.cookie(this.getCookieName()); - } else { - return null; - } - }; - - SaveStateHandler.prototype.getState = function() { - var open_nodes, selected_node, selected_node_id, - _this = this; - open_nodes = []; - this.tree_widget.tree.iterate(function(node) { - if (node.is_open && node.id && node.hasChildren()) { - open_nodes.push(node.id); - } - return true; - }); - selected_node = this.tree_widget.getSelectedNode(); - if (selected_node) { - selected_node_id = selected_node.id; - } else { - selected_node_id = ''; - } - return { - open_nodes: open_nodes, - selected_node: selected_node_id - }; - }; - - SaveStateHandler.prototype.setState = function(state) { - var open_nodes, selected_node, selected_node_id, - _this = this; - if (state) { - open_nodes = state.open_nodes; - selected_node_id = state.selected_node; - this.tree_widget.tree.iterate(function(node) { - node.is_open = node.id && node.hasChildren() && (indexOf(open_nodes, node.id) >= 0); - return true; - }); - if (selected_node_id && this.tree_widget.select_node_handler) { - this.tree_widget.select_node_handler.clear(); - selected_node = this.tree_widget.getNodeById(selected_node_id); - if (selected_node) { - return this.tree_widget.select_node_handler.addToSelection(selected_node); - } - } - } - }; - - SaveStateHandler.prototype.getCookieName = function() { - if (typeof this.tree_widget.options.saveState === 'string') { - return this.tree_widget.options.saveState; - } else { - return 'tree'; - } - }; - - SaveStateHandler.prototype.supportsLocalStorage = function() { - var testSupport; - testSupport = function() { - var error, key; - if (typeof localStorage === "undefined" || localStorage === null) { - return false; - } else { - try { - key = '_storage_test'; - sessionStorage.setItem(key, true); - sessionStorage.removeItem(key); - } catch (_error) { - error = _error; - return false; - } - return true; - } - }; - if (this._supportsLocalStorage == null) { - this._supportsLocalStorage = testSupport(); - } - return this._supportsLocalStorage; - }; - - SaveStateHandler.prototype.getNodeIdToBeSelected = function() { - var state, state_json; - state_json = this.getStateFromStorage(); - if (state_json) { - state = $.parseJSON(state_json); - return state.selected_node; - } else { - return null; - } - }; - - return SaveStateHandler; - - })(); - - SelectNodeHandler = (function() { - function SelectNodeHandler(tree_widget) { - this.tree_widget = tree_widget; - this.clear(); - } - - SelectNodeHandler.prototype.getSelectedNode = function() { - var selected_nodes; - selected_nodes = this.getSelectedNodes(); - if (selected_nodes.length) { - return selected_nodes[0]; - } else { - return false; - } - }; - - SelectNodeHandler.prototype.getSelectedNodes = function() { - var id, node, selected_nodes; - if (this.selected_single_node) { - return [this.selected_single_node]; - } else { - selected_nodes = []; - for (id in this.selected_nodes) { - node = this.tree_widget.getNodeById(id); - if (node) { - selected_nodes.push(node); - } - } - return selected_nodes; - } - }; - - SelectNodeHandler.prototype.isNodeSelected = function(node) { - if (node.id) { - return this.selected_nodes[node.id]; - } else if (this.selected_single_node) { - return this.selected_single_node.element === node.element; - } else { - return false; - } - }; - - SelectNodeHandler.prototype.clear = function() { - this.selected_nodes = {}; - return this.selected_single_node = null; - }; - - SelectNodeHandler.prototype.removeFromSelection = function(node, include_children) { - var _this = this; - if (include_children == null) { - include_children = false; - } - if (!node.id) { - if (node.element === this.selected_single_node.element) { - return this.selected_single_node = null; - } - } else { - delete this.selected_nodes[node.id]; - if (include_children) { - return node.iterate(function(n) { - delete _this.selected_nodes[node.id]; - return true; - }); - } - } - }; - - SelectNodeHandler.prototype.addToSelection = function(node) { - if (node.id) { - return this.selected_nodes[node.id] = true; - } else { - return this.selected_single_node = node; - } - }; - - return SelectNodeHandler; - - })(); - - DragAndDropHandler = (function() { - function DragAndDropHandler(tree_widget) { - this.tree_widget = tree_widget; - this.hovered_area = null; - this.$ghost = null; - this.hit_areas = []; - this.is_dragging = false; - } - - DragAndDropHandler.prototype.mouseCapture = function(position_info) { - var $element, node_element; - $element = $(position_info.target); - if (this.tree_widget.options.onIsMoveHandle && !this.tree_widget.options.onIsMoveHandle($element)) { - return null; - } - node_element = this.tree_widget._getNodeElement($element); - if (node_element && this.tree_widget.options.onCanMove) { - if (!this.tree_widget.options.onCanMove(node_element.node)) { - node_element = null; - } - } - this.current_item = node_element; - return this.current_item !== null; - }; - - DragAndDropHandler.prototype.mouseStart = function(position_info) { - var offset; - this.refreshHitAreas(); - offset = $(position_info.target).offset(); - this.drag_element = new DragElement(this.current_item.node, position_info.page_x - offset.left, position_info.page_y - offset.top, this.tree_widget.element); - this.is_dragging = true; - this.current_item.$element.addClass('jqtree-moving'); - return true; - }; - - DragAndDropHandler.prototype.mouseDrag = function(position_info) { - var area, can_move_to; - this.drag_element.move(position_info.page_x, position_info.page_y); - area = this.findHoveredArea(position_info.page_x, position_info.page_y); - can_move_to = this.canMoveToArea(area); - if (area) { - if (this.hovered_area !== area) { - this.hovered_area = area; - if (this.mustOpenFolderTimer(area)) { - this.startOpenFolderTimer(area.node); - } - if (can_move_to) { - this.updateDropHint(); - } - } - } else { - this.removeHover(); - this.removeDropHint(); - this.stopOpenFolderTimer(); - } - return true; - }; - - DragAndDropHandler.prototype.canMoveToArea = function(area) { - var position_name; - if (!area) { - return false; - } else if (this.tree_widget.options.onCanMoveTo) { - position_name = Position.getName(area.position); - return this.tree_widget.options.onCanMoveTo(this.current_item.node, area.node, position_name); - } else { - return true; - } - }; - - DragAndDropHandler.prototype.mouseStop = function(position_info) { - this.moveItem(position_info); - this.clear(); - this.removeHover(); - this.removeDropHint(); - this.removeHitAreas(); - if (this.current_item) { - this.current_item.$element.removeClass('jqtree-moving'); - } - this.is_dragging = false; - return false; - }; - - DragAndDropHandler.prototype.refreshHitAreas = function() { - this.removeHitAreas(); - return this.generateHitAreas(); - }; - - DragAndDropHandler.prototype.removeHitAreas = function() { - return this.hit_areas = []; - }; - - DragAndDropHandler.prototype.clear = function() { - this.drag_element.remove(); - return this.drag_element = null; - }; - - DragAndDropHandler.prototype.removeDropHint = function() { - if (this.previous_ghost) { - return this.previous_ghost.remove(); - } - }; - - DragAndDropHandler.prototype.removeHover = function() { - return this.hovered_area = null; - }; - - DragAndDropHandler.prototype.generateHitAreas = function() { - var hit_areas_generator; - hit_areas_generator = new HitAreasGenerator(this.tree_widget.tree, this.current_item.node, this.getTreeDimensions().bottom); - return this.hit_areas = hit_areas_generator.generate(); - }; - - DragAndDropHandler.prototype.findHoveredArea = function(x, y) { - var area, dimensions, high, low, mid; - dimensions = this.getTreeDimensions(); - if (x < dimensions.left || y < dimensions.top || x > dimensions.right || y > dimensions.bottom) { - return null; - } - low = 0; - high = this.hit_areas.length; - while (low < high) { - mid = (low + high) >> 1; - area = this.hit_areas[mid]; - if (y < area.top) { - high = mid; - } else if (y > area.bottom) { - low = mid + 1; - } else { - return area; - } - } - return null; - }; - - DragAndDropHandler.prototype.mustOpenFolderTimer = function(area) { - var node; - node = area.node; - return node.isFolder() && !node.is_open && area.position === Position.INSIDE; - }; - - DragAndDropHandler.prototype.updateDropHint = function() { - var node_element; - if (!this.hovered_area) { - return; - } - this.removeDropHint(); - node_element = this.tree_widget._getNodeElementForNode(this.hovered_area.node); - return this.previous_ghost = node_element.addDropHint(this.hovered_area.position); - }; - - DragAndDropHandler.prototype.startOpenFolderTimer = function(folder) { - var openFolder, - _this = this; - openFolder = function() { - return _this.tree_widget._openNode(folder, _this.tree_widget.options.slide, function() { - _this.refreshHitAreas(); - return _this.updateDropHint(); - }); - }; - return this.open_folder_timer = setTimeout(openFolder, 500); - }; - - DragAndDropHandler.prototype.stopOpenFolderTimer = function() { - if (this.open_folder_timer) { - clearTimeout(this.open_folder_timer); - return this.open_folder_timer = null; - } - }; - - DragAndDropHandler.prototype.moveItem = function(position_info) { - var doMove, event, moved_node, position, previous_parent, target_node, - _this = this; - if (this.hovered_area && this.hovered_area.position !== Position.NONE && this.canMoveToArea(this.hovered_area)) { - moved_node = this.current_item.node; - target_node = this.hovered_area.node; - position = this.hovered_area.position; - previous_parent = moved_node.parent; - if (position === Position.INSIDE) { - this.hovered_area.node.is_open = true; - } - doMove = function() { - _this.tree_widget.tree.moveNode(moved_node, target_node, position); - _this.tree_widget.element.empty(); - return _this.tree_widget._refreshElements(); - }; - event = this.tree_widget._triggerEvent('tree.move', { - move_info: { - moved_node: moved_node, - target_node: target_node, - position: Position.getName(position), - previous_parent: previous_parent, - do_move: doMove, - original_event: position_info.original_event - } - }); - if (!event.isDefaultPrevented()) { - return doMove(); - } - } - }; - - DragAndDropHandler.prototype.getTreeDimensions = function() { - var offset; - offset = this.tree_widget.element.offset(); - return { - left: offset.left, - top: offset.top, - right: offset.left + this.tree_widget.element.width(), - bottom: offset.top + this.tree_widget.element.height() + 16 - }; - }; - - return DragAndDropHandler; - - })(); - - VisibleNodeIterator = (function() { - function VisibleNodeIterator(tree) { - this.tree = tree; - } - - VisibleNodeIterator.prototype.iterate = function() { - var is_first_node, _iterateNode, - _this = this; - is_first_node = true; - _iterateNode = function(node, next_node) { - var $element, child, children_length, i, must_iterate_inside, _i, _len, _ref3; - must_iterate_inside = (node.is_open || !node.element) && node.hasChildren(); - if (node.element) { - $element = $(node.element); - if (!$element.is(':visible')) { - return; - } - if (is_first_node) { - _this.handleFirstNode(node, $element); - is_first_node = false; - } - if (!node.hasChildren()) { - _this.handleNode(node, next_node, $element); - } else if (node.is_open) { - if (!_this.handleOpenFolder(node, $element)) { - must_iterate_inside = false; - } - } else { - _this.handleClosedFolder(node, next_node, $element); - } - } - if (must_iterate_inside) { - children_length = node.children.length; - _ref3 = node.children; - for (i = _i = 0, _len = _ref3.length; _i < _len; i = ++_i) { - child = _ref3[i]; - if (i === (children_length - 1)) { - _iterateNode(node.children[i], null); - } else { - _iterateNode(node.children[i], node.children[i + 1]); - } - } - if (node.is_open) { - return _this.handleAfterOpenFolder(node, next_node, $element); - } - } - }; - return _iterateNode(this.tree, null); - }; - - VisibleNodeIterator.prototype.handleNode = function(node, next_node, $element) {}; - - VisibleNodeIterator.prototype.handleOpenFolder = function(node, $element) {}; - - VisibleNodeIterator.prototype.handleClosedFolder = function(node, next_node, $element) {}; - - VisibleNodeIterator.prototype.handleAfterOpenFolder = function(node, next_node, $element) {}; - - VisibleNodeIterator.prototype.handleFirstNode = function(node, $element) {}; - - return VisibleNodeIterator; - - })(); - - HitAreasGenerator = (function(_super) { - __extends(HitAreasGenerator, _super); - - function HitAreasGenerator(tree, current_node, tree_bottom) { - HitAreasGenerator.__super__.constructor.call(this, tree); - this.current_node = current_node; - this.tree_bottom = tree_bottom; - } - - HitAreasGenerator.prototype.generate = function() { - this.positions = []; - this.last_top = 0; - this.iterate(); - return this.generateHitAreas(this.positions); - }; - - HitAreasGenerator.prototype.getTop = function($element) { - return $element.offset().top; - }; - - HitAreasGenerator.prototype.addPosition = function(node, position, top) { - this.positions.push({ - top: top, - node: node, - position: position - }); - return this.last_top = top; - }; - - HitAreasGenerator.prototype.handleNode = function(node, next_node, $element) { - var top; - top = this.getTop($element); - if (node === this.current_node) { - this.addPosition(node, Position.NONE, top); - } else { - this.addPosition(node, Position.INSIDE, top); - } - if (next_node === this.current_node || node === this.current_node) { - return this.addPosition(node, Position.NONE, top); - } else { - return this.addPosition(node, Position.AFTER, top); - } - }; - - HitAreasGenerator.prototype.handleOpenFolder = function(node, $element) { - if (node === this.current_node) { - return false; - } - if (node.children[0] !== this.current_node) { - this.addPosition(node, Position.INSIDE, this.getTop($element)); - } - return true; - }; - - HitAreasGenerator.prototype.handleClosedFolder = function(node, next_node, $element) { - var top; - top = this.getTop($element); - if (node === this.current_node) { - return this.addPosition(node, Position.NONE, top); - } else { - this.addPosition(node, Position.INSIDE, top); - if (next_node !== this.current_node) { - return this.addPosition(node, Position.AFTER, top); - } - } - }; - - HitAreasGenerator.prototype.handleAfterOpenFolder = function(node, next_node, $element) { - if (node === this.current_node || next_node === this.current_node) { - return this.addPosition(node, Position.NONE, this.last_top); - } else { - return this.addPosition(node, Position.AFTER, this.last_top); - } - }; - - HitAreasGenerator.prototype.handleFirstNode = function(node, $element) { - if (node !== this.current_node) { - return this.addPosition(node, Position.BEFORE, this.getTop($(node.element))); - } - }; - - HitAreasGenerator.prototype.generateHitAreas = function(positions) { - var group, hit_areas, position, previous_top, _i, _len; - previous_top = -1; - group = []; - hit_areas = []; - for (_i = 0, _len = positions.length; _i < _len; _i++) { - position = positions[_i]; - if (position.top !== previous_top && group.length) { - if (group.length) { - this.generateHitAreasForGroup(hit_areas, group, previous_top, position.top); - } - previous_top = position.top; - group = []; - } - group.push(position); - } - this.generateHitAreasForGroup(hit_areas, group, previous_top, this.tree_bottom); - return hit_areas; - }; - - HitAreasGenerator.prototype.generateHitAreasForGroup = function(hit_areas, positions_in_group, top, bottom) { - var area_height, area_top, position, _i, _len; - area_height = (bottom - top) / positions_in_group.length; - area_top = top; - for (_i = 0, _len = positions_in_group.length; _i < _len; _i++) { - position = positions_in_group[_i]; - hit_areas.push({ - top: area_top, - bottom: area_top + area_height, - node: position.node, - position: position.position - }); - area_top += area_height; - } - return null; - }; - - return HitAreasGenerator; - - })(VisibleNodeIterator); - - DragElement = (function() { - function DragElement(node, offset_x, offset_y, $tree) { - this.offset_x = offset_x; - this.offset_y = offset_y; - this.$element = $("" + node.name + ""); - this.$element.css("position", "absolute"); - $tree.append(this.$element); - } - - DragElement.prototype.move = function(page_x, page_y) { - return this.$element.offset({ - left: page_x - this.offset_x, - top: page_y - this.offset_y - }); - }; - - DragElement.prototype.remove = function() { - return this.$element.remove(); - }; - - return DragElement; - - })(); - - GhostDropHint = (function() { - function GhostDropHint(node, $element, position) { - this.$element = $element; - this.node = node; - this.$ghost = $('
    • '); - if (position === Position.AFTER) { - this.moveAfter(); - } else if (position === Position.BEFORE) { - this.moveBefore(); - } else if (position === Position.INSIDE) { - if (node.isFolder() && node.is_open) { - this.moveInsideOpenFolder(); - } else { - this.moveInside(); - } - } - } - - GhostDropHint.prototype.remove = function() { - return this.$ghost.remove(); - }; - - GhostDropHint.prototype.moveAfter = function() { - return this.$element.after(this.$ghost); - }; - - GhostDropHint.prototype.moveBefore = function() { - return this.$element.before(this.$ghost); - }; - - GhostDropHint.prototype.moveInsideOpenFolder = function() { - return $(this.node.children[0].element).before(this.$ghost); - }; - - GhostDropHint.prototype.moveInside = function() { - this.$element.after(this.$ghost); - return this.$ghost.addClass('jqtree-inside'); - }; - - return GhostDropHint; - - })(); - - BorderDropHint = (function() { - function BorderDropHint($element) { - var $div, width; - $div = $element.children('.jqtree-element'); - width = $element.width() - 4; - this.$hint = $(''); - $div.append(this.$hint); - this.$hint.css({ - width: width, - height: $div.height() - 4 - }); - } - - BorderDropHint.prototype.remove = function() { - return this.$hint.remove(); - }; - - return BorderDropHint; - - })(); - - ScrollHandler = (function() { - function ScrollHandler(tree_widget) { - this.tree_widget = tree_widget; - this.previous_top = -1; - this._initScrollParent(); - } - - ScrollHandler.prototype._initScrollParent = function() { - var $scroll_parent, getParentWithOverflow, setDocumentAsScrollParent, - _this = this; - getParentWithOverflow = function() { - var css_value, css_values, parent, scroll_parent, _i, _j, _len, _len1, _ref3, _ref4; - css_values = ['overflow', 'overflow-y']; - scroll_parent = null; - _ref3 = _this.tree_widget.$el.parents(); - for (_i = 0, _len = _ref3.length; _i < _len; _i++) { - parent = _ref3[_i]; - for (_j = 0, _len1 = css_values.length; _j < _len1; _j++) { - css_value = css_values[_j]; - if ((_ref4 = $.css(parent, css_value)) === 'auto' || _ref4 === 'scroll') { - return $(parent); - } - } - } - return null; - }; - setDocumentAsScrollParent = function() { - _this.scroll_parent_top = 0; - return _this.$scroll_parent = null; - }; - if (this.tree_widget.$el.css('position') === 'fixed') { - setDocumentAsScrollParent(); - } - $scroll_parent = getParentWithOverflow(); - if ($scroll_parent && $scroll_parent.length && $scroll_parent[0].tagName !== 'HTML') { - this.$scroll_parent = $scroll_parent; - return this.scroll_parent_top = this.$scroll_parent.offset().top; - } else { - return setDocumentAsScrollParent(); - } - }; - - ScrollHandler.prototype.checkScrolling = function() { - var hovered_area; - hovered_area = this.tree_widget.dnd_handler.hovered_area; - if (hovered_area && hovered_area.top !== this.previous_top) { - this.previous_top = hovered_area.top; - if (this.$scroll_parent) { - return this._handleScrollingWithScrollParent(hovered_area); - } else { - return this._handleScrollingWithDocument(hovered_area); - } - } - }; - - ScrollHandler.prototype._handleScrollingWithScrollParent = function(area) { - var distance_bottom; - distance_bottom = this.scroll_parent_top + this.$scroll_parent[0].offsetHeight - area.bottom; - if (distance_bottom < 20) { - this.$scroll_parent[0].scrollTop += 20; - this.tree_widget.refreshHitAreas(); - return this.previous_top = -1; - } else if ((area.top - this.scroll_parent_top) < 20) { - this.$scroll_parent[0].scrollTop -= 20; - this.tree_widget.refreshHitAreas(); - return this.previous_top = -1; - } - }; - - ScrollHandler.prototype._handleScrollingWithDocument = function(area) { - var distance_top; - distance_top = area.top - $(document).scrollTop(); - if (distance_top < 20) { - return $(document).scrollTop($(document).scrollTop() - 20); - } else if ($(window).height() - (area.bottom - $(document).scrollTop()) < 20) { - return $(document).scrollTop($(document).scrollTop() + 20); - } - }; - - ScrollHandler.prototype.scrollTo = function(top) { - var tree_top; - if (this.$scroll_parent) { - return this.$scroll_parent[0].scrollTop = top; - } else { - tree_top = this.tree_widget.$el.offset().top; - return $(document).scrollTop(top + tree_top); - } - }; - - ScrollHandler.prototype.isScrolledIntoView = function(element) { - var $element, element_bottom, element_top, view_bottom, view_top; - $element = $(element); - if (this.$scroll_parent) { - view_top = 0; - view_bottom = this.$scroll_parent.height(); - element_top = $element.offset().top - this.scroll_parent_top; - element_bottom = element_top + $element.height(); - } else { - view_top = $(window).scrollTop(); - view_bottom = view_top + $(window).height(); - element_top = $element.offset().top; - element_bottom = element_top + $element.height(); - } - return (element_bottom <= view_bottom) && (element_top >= view_top); - }; - - return ScrollHandler; - - })(); - - KeyHandler = (function() { - var DOWN, LEFT, RIGHT, UP; - - LEFT = 37; - - UP = 38; - - RIGHT = 39; - - DOWN = 40; - - function KeyHandler(tree_widget) { - this.tree_widget = tree_widget; - if (tree_widget.options.keyboardSupport) { - $(document).bind('keydown.jqtree', $.proxy(this.handleKeyDown, this)); - } - } - - KeyHandler.prototype.deinit = function() { - return $(document).unbind('keydown.jqtree'); - }; - - KeyHandler.prototype.handleKeyDown = function(e) { - var current_node, key, moveDown, moveLeft, moveRight, moveUp, selectNode, - _this = this; - if ($(document.activeElement).is('textarea,input')) { - return true; - } - current_node = this.tree_widget.getSelectedNode(); - selectNode = function(node) { - if (node) { - _this.tree_widget.selectNode(node); - if (_this.tree_widget.scroll_handler && (!_this.tree_widget.scroll_handler.isScrolledIntoView($(node.element).find('.jqtree-element')))) { - _this.tree_widget.scrollToNode(node); - } - return false; - } else { - return true; - } - }; - moveDown = function() { - return selectNode(_this.getNextNode(current_node)); - }; - moveUp = function() { - return selectNode(_this.getPreviousNode(current_node)); - }; - moveRight = function() { - if (current_node.hasChildren() && !current_node.is_open) { - _this.tree_widget.openNode(current_node); - return false; - } else { - return true; - } - }; - moveLeft = function() { - if (current_node.hasChildren() && current_node.is_open) { - _this.tree_widget.closeNode(current_node); - return false; - } else { - return true; - } - }; - if (!current_node) { - return true; - } else { - key = e.which; - switch (key) { - case DOWN: - return moveDown(); - case UP: - return moveUp(); - case RIGHT: - return moveRight(); - case LEFT: - return moveLeft(); - } - } - }; - - KeyHandler.prototype.getNextNode = function(node, include_children) { - var next_sibling; - if (include_children == null) { - include_children = true; - } - if (include_children && node.hasChildren() && node.is_open) { - return node.children[0]; - } else { - if (!node.parent) { - return null; - } else { - next_sibling = node.getNextSibling(); - if (next_sibling) { - return next_sibling; - } else { - return this.getNextNode(node.parent, false); - } - } - } - }; - - KeyHandler.prototype.getPreviousNode = function(node) { - var previous_sibling; - if (!node.parent) { - return null; - } else { - previous_sibling = node.getPreviousSibling(); - if (previous_sibling) { - if (!previous_sibling.hasChildren() || !previous_sibling.is_open) { - return previous_sibling; - } else { - return this.getLastChild(previous_sibling); - } - } else { - if (node.parent.parent) { - return node.parent; - } else { - return null; - } - } - } - }; - - KeyHandler.prototype.getLastChild = function(node) { - var last_child; - if (!node.hasChildren()) { - return null; - } else { - last_child = node.children[node.children.length - 1]; - if (!last_child.hasChildren() || !last_child.is_open) { - return last_child; - } else { - return this.getLastChild(last_child); - } - } - }; - - return KeyHandler; - - })(); - -}).call(this);