Skip to content

Student - Todo Widget #3402

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 26 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
0bb8780
add skeleton and ui
ndrsszsz May 13, 2025
acf00a8
restructure TodoScreens
ndrsszsz May 15, 2025
1412ab0
update TodoWidgetProvider
ndrsszsz May 15, 2025
1056246
update TodoItem
ndrsszsz May 15, 2025
d5586a6
ui updates
ndrsszsz May 19, 2025
0d87e8c
wednesday progress
ndrsszsz May 21, 2025
dc4e869
routes
ndrsszsz May 23, 2025
815d8f2
rewrite TodoWidgetProvider
ndrsszsz May 23, 2025
426900f
add routing for calendar date button
ndrsszsz May 23, 2025
7246210
Merge branch 'master' into feature/MBL-18777-Todo-Widget
suhaibabsi-inst May 26, 2025
f20e1a7
Fix merge issues
suhaibabsi-inst May 26, 2025
bdfeeb4
Rewrite logic to reactive store. Fix GetPlannables never returning be…
vargaat May 27, 2025
96de7b8
Few enhancements
suhaibabsi-inst May 28, 2025
5354413
Fix scheme issues
suhaibabsi-inst May 28, 2025
b5e67b1
Route interception model
suhaibabsi-inst May 29, 2025
f2122fb
Fix all routing to calendar view
suhaibabsi-inst May 29, 2025
d4d50ee
Organizing things
suhaibabsi-inst Jun 1, 2025
0addbc1
bottom gradient
suhaibabsi-inst Jun 1, 2025
12ca47f
Todo States (Empty, Failure, List) view enhancement
suhaibabsi-inst Jun 2, 2025
5d9b606
Cleaning
suhaibabsi-inst Jun 2, 2025
c56263e
Fix unit tests
suhaibabsi-inst Jun 2, 2025
0e7efc8
Merge branch 'master' into feature/MBL-18777-Todo-Widget
suhaibabsi-inst Jun 2, 2025
3221615
Failure action handling, cleaning
suhaibabsi-inst Jun 2, 2025
42c40e2
Fix compile issues
suhaibabsi-inst Jun 2, 2025
9e5ed46
Fix compile issues
suhaibabsi-inst Jun 2, 2025
2233e99
Update TodoWidgetProvider.swift
suhaibabsi-inst Jun 2, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 66 additions & 0 deletions Core/Core/Common/CommonModels/Router/Route.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
//
// This file is part of Canvas.
// Copyright (C) 2025-present Instructure, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
//

import Foundation

public struct Route {

public enum Segment: Equatable {
case literal(String)
case param(String)
case splat(String)
}

public let template: String
public let segments: [Segment]

public init(_ template: String) {
self.template = template
self.segments = template.split(separator: "/").map { part in
if part.hasPrefix("*") {
return .splat(String(part.dropFirst()))
} else if part.hasPrefix(":") {
return .param(ID.expandTildeID(String(part.dropFirst())))
}
return .literal(String(part))
}
}

public func match(_ url: URLComponents) -> [String: String]? {
var parts = url.path.split(separator: "/")
if parts.count >= 2, parts[0] == "api", parts[1] == "v1" {
parts.removeFirst(2)
}
var params: [String: String] = [:]
for segment in segments {
switch segment {
case .literal(let template):
guard !parts.isEmpty else { return nil } // too short
guard parts.removeFirst() == template else { return nil }
case .param(let name):
guard !parts.isEmpty else { return nil } // too short
params[name] = ID.expandTildeID(String(parts.removeFirst()))
case .splat(let name):
params[name] = parts.joined(separator: "/")
parts = []
}
}
guard parts.isEmpty else { return nil } // too long
return params
}
}
40 changes: 3 additions & 37 deletions Core/Core/Common/CommonModels/Router/RouteHandler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,27 +24,12 @@ public struct RouteHandler {
public typealias ViewFactory = (URLComponents, [String: String], [String: Any]?, AppEnvironment) -> UIViewController?
public typealias DiscardingEnvironmentViewFactory = (URLComponents, [String: String], [String: Any]?) -> UIViewController?

public enum Segment: Equatable {
case literal(String)
case param(String)
case splat(String)
}

public let template: String
public let route: Route
public let factory: ViewFactory
public let segments: [Segment]

public init(_ template: String, factory: @escaping ViewFactory = { _, _, _, _ in nil }) {
self.template = template
self.route = Route(template)
self.factory = factory
self.segments = template.split(separator: "/").map { part in
if part.hasPrefix("*") {
return .splat(String(part.dropFirst()))
} else if part.hasPrefix(":") {
return .param(ID.expandTildeID(String(part.dropFirst())))
}
return .literal(String(part))
}
}

/// This is to avoid large file changes on Routes.
Expand All @@ -57,25 +42,6 @@ public struct RouteHandler {
}

public func match(_ url: URLComponents) -> [String: String]? {
var parts = url.path.split(separator: "/")
if parts.count >= 2, parts[0] == "api", parts[1] == "v1" {
parts.removeFirst(2)
}
var params: [String: String] = [:]
for segment in segments {
switch segment {
case .literal(let template):
guard !parts.isEmpty else { return nil } // too short
guard parts.removeFirst() == template else { return nil }
case .param(let name):
guard !parts.isEmpty else { return nil } // too short
params[name] = ID.expandTildeID(String(parts.removeFirst()))
case .splat(let name):
params[name] = parts.joined(separator: "/")
parts = []
}
}
guard parts.isEmpty else { return nil } // too long
return params
route.match(url)
}
}
10 changes: 5 additions & 5 deletions Core/Core/Common/CommonModels/Router/Router.swift
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ open class Router {
template(for: .parse(url))
}
public func template(for url: URLComponents) -> String? {
handler(for: url)?.template
handler(for: url)?.route.template
}

public func isRegisteredRoute(_ url: URL) -> Bool {
Expand Down Expand Up @@ -193,10 +193,10 @@ open class Router {
return
}

for route in handlers {
if let params = route.match(url) {
if let view = route.factory(url, params, userInfo, env) {
show(view, from: from, options: options, analyticsRoute: route.template)
for handler in handlers {
if let params = handler.match(url) {
if let view = handler.factory(url, params, userInfo, env) {
show(view, from: from, options: options, analyticsRoute: handler.route.template)
}

return // don't fall back if a matched route returns no view
Expand Down
1 change: 1 addition & 0 deletions Core/Core/Common/CommonUI/Fonts/FontExtensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ public extension Font {

static var bold10: Font { Font(UIFont.scaledNamedFont(.bold10)) }
static var bold11: Font { Font(UIFont.scaledNamedFont(.bold11)) }
static var bold12: Font { Font(UIFont.scaledNamedFont(.bold12)) }
static var bold13: Font { Font(UIFont.scaledNamedFont(.bold13)) }
static var bold14: Font { Font(UIFont.scaledNamedFont(.bold14)) }
static var bold15: Font { Font(UIFont.scaledNamedFont(.bold15)) }
Expand Down
4 changes: 3 additions & 1 deletion Core/Core/Common/CommonUI/Fonts/UIFontExtensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ public extension UIFont {
case regular10, regular11Monodigit, regular12, regular13, regular14, regular14Italic, regular15, regular16, regular17, regular20, regular22, regular23, regular24, regular20Monodigit, regular30
case medium10, medium12, medium14, medium16, medium20
case semibold11, semibold12, semibold13, semibold14, semibold16, semibold17, semibold16Italic, semibold18, semibold20, semibold22, semibold23, semibold28, semibold38
case bold10, bold11, bold13, bold14, bold15, bold16, bold17, bold20, bold22, bold24, bold34
case bold10, bold11, bold12, bold13, bold14, bold15, bold16, bold17, bold20, bold22, bold24, bold34
case heavy24
}

Expand Down Expand Up @@ -106,6 +106,8 @@ public extension UIFont {
return scaledFont(.body, for: applicationFont(ofSize: 10, weight: .bold))
case .bold11:
return scaledFont(.body, for: applicationFont(ofSize: 11, weight: .bold))
case .bold12:
return scaledFont(.body, for: applicationFont(ofSize: 12, weight: .bold))
case .bold13:
return scaledFont(.body, for: applicationFont(ofSize: 13, weight: .bold))
case .bold14:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@ public class CoreHostingController<Content: View>: UIHostingController<CoreHosti
NavigationBarStyled,
TestTreeHolder,
DefaultViewProvider,
CoreHostingControllerProtocol {
CoreHostingControllerProtocol,
VisibilityObservedViewController {
// MARK: - UIViewController Overrides
public override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
supportedInterfaceOrientationsValue ?? super.supportedInterfaceOrientations
Expand All @@ -44,6 +45,8 @@ public class CoreHostingController<Content: View>: UIHostingController<CoreHosti
}
}

public private(set) var visibilityObservation = VisibilityObservation()

// MARK: - Support Variables For Overrides
/** The value to be returned by the `supportedInterfaceOrientations` property. Nil reverts to the default behaviour of the UIViewController regarding that property. */
public var supportedInterfaceOrientationsValue: UIInterfaceOrientationMask?
Expand Down Expand Up @@ -96,6 +99,12 @@ public class CoreHostingController<Content: View>: UIHostingController<CoreHosti
public override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
didAppearSubject.send()
visibilityObservation.viewDidAppear()
}

public override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
visibilityObservation.viewDidDisappear()
}

public var didAppearPublisher: AnyPublisher<Void, Never> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@

import UIKit

open class CoreNavigationController: UINavigationController {
open class CoreNavigationController: UINavigationController, VisibilityObservedViewController {
public private(set) var visibilityObservation = VisibilityObservation()

public var remoteLogger = RemoteLogger.shared
public override var prefersStatusBarHidden: Bool {
topViewController?.prefersStatusBarHidden ?? super.prefersStatusBarHidden
Expand Down Expand Up @@ -56,6 +58,16 @@ open class CoreNavigationController: UINavigationController {
self.interactivePopGestureRecognizer?.delegate = self
}

open override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
visibilityObservation.viewDidAppear()
}

open override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
visibilityObservation.viewDidDisappear()
}

// MARK: - Navigation Methods

override public func pushViewController(_ viewController: UIViewController, animated: Bool) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
//
// This file is part of Canvas.
// Copyright (C) 2025-present Instructure, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
//

import UIKit

public protocol VisibilityObservedViewController: UIViewController {
var visibilityObservation: VisibilityObservation { get }
}

extension VisibilityObservedViewController {
public func onAppear(_ block: @escaping () -> Void) {
if visibilityObservation.isVisible {
block()
} else {
visibilityObservation.appearTask = block
}
}
}

public class VisibilityObservation {

fileprivate var appearTask: (() -> Void)?
fileprivate var isVisible: Bool = false

init() {}

func viewDidAppear() {
isVisible = true
appearTask?()
appearTask = nil
}

func viewDidDisappear() {
isVisible = false
}
}

open class BaseVisibilityObservedViewController: UIViewController, VisibilityObservedViewController {
public private(set) var visibilityObservation = VisibilityObservation()

open override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
visibilityObservation.viewDidAppear()
}

open override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
visibilityObservation.viewDidDisappear()
}
}
20 changes: 20 additions & 0 deletions Core/Core/Common/Extensions/Foundation/DateExtensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,26 @@ extension DateFormatter {
}
}

extension Date.ISO8601FormatStyle {
public static var queryDayDateStyle: Self {
return Date
.ISO8601FormatStyle()
.locale(.init(languageCode: .english, languageRegion: .unitedStates))
}
}

extension FormatStyle where Self == Date.ISO8601FormatStyle {
public static var queryDayDateStyle: Self {
Date.ISO8601FormatStyle.queryDayDateStyle
}
}

extension ParseStrategy where Self == Date.ISO8601FormatStyle {
public static var queryDayDateStyle: Self {
Date.ISO8601FormatStyle.queryDayDateStyle
}
}

#if DEBUG
public extension Date {
static func make(calendar: Calendar = Cal.currentCalendar, year: Int, month: Int? = nil, day: Int? = nil, hour: Int? = nil, minute: Int? = nil, second: Int? = nil) -> Date {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,13 @@ public extension URLComponents {
return cleaned
}

func hasOrigin(_ value: String) -> Bool {
if queryItems?.first(where: { $0.name == "origin" })?.value == value {
return true
}
return false
}

var originIsCalendar: Bool {
if queryItems?.first(where: { $0.name == "origin" })?.value == "calendar" {
return true
Expand Down Expand Up @@ -168,6 +175,20 @@ public extension URLComponents {
func queryValue(for queryName: String) -> String? {
queryItems?.first(where: { $0.name == queryName })?.value
}

func settingOrigin(_ origin: String) -> URLComponents {
var components = self
var items = queryItems ?? []

if let oIndex = items.firstIndex(where: { $0.name == "origin" }) {
items[oIndex].value = origin
} else {
items.append(URLQueryItem(name: "origin", value: origin))
}

components.queryItems = items
return components
}
}

public extension CharacterSet {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,9 @@ public extension UISplitViewController {
var detailTopViewController: UIViewController? {
detailNavigationController?.topMostViewController()
}

func resetToRoot(animated: Bool = false) {
masterNavigationController?.popToRootViewController(animated: animated)
detailNavigationController?.setViewControllers([EmptyViewController()], animated: animated)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,7 @@ extension PlannerListViewController: UITableViewDataSource, UITableViewDelegate
private func routeToPlannableDetailsAtUrl(_ url: URL?) {
guard let url else { return }

let to = url.appendingQueryItems(URLQueryItem(name: "origin", value: "calendar"))
let to = url.appendingOrigin("calendar")
env.router.route(to: to, from: self, options: .detail)
}

Expand Down
Loading